深浅拷贝 浅拷贝 浅拷贝是指创建一个新对象,该对象的属性值与原对象相同,但对于引用类型的属性,仍然共享同一个引用。
实现原理 检查输入是否为对象类型 根据原对象类型(数组或普通对象)创建新对象 遍历原对象的自有属性,将属性值直接复制到新对象 1 2 3 4 5 6 7 8 9 10 function shallowCopy (obj ) { if (typeof obj !== 'object' ) return obj; let newObj = Array .isArray(obj) ? [] : {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = obj[key]; } } return newObj; }
使用场景 当对象属性都是基本类型时 当需要快速创建对象副本,且不关心引用类型属性的共享问题时 当对象结构简单,没有嵌套引用类型时 深拷贝 深拷贝是指创建一个新对象,该对象的所有属性(包括嵌套的引用类型属性)都是原对象的副本,不共享引用。
1. 极简版深拷贝 使用 JSON 序列化和反序列化实现深拷贝,简单易用,但有局限性。
1 2 3 function deepClone (obj ) { return JSON .parse(JSON .stringify(obj)); }
局限性 :
不能处理函数、正则表达式、Date 等特殊对象 不能处理循环引用 会丢失 undefined 和 Symbol 类型的属性 2. 简单版深拷贝 只考虑普通对象和数组,不处理特殊对象和循环引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 function deepClone (target ) { if (typeof target === 'object' && target !== null ) { let cloneTarget = Array .isArray(target) ? [] : {}; for (const key in target) { if (target.hasOwnProperty(key)) { cloneTarget[key] = deepClone(target[key]); } } return cloneTarget; } else { return target; } };
3. 完整版深拷贝 考虑了循环引用、特殊对象和各种数据类型的深拷贝实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 function deepClone (obj = {}, map = new Map () ) { if (typeof obj !== "object" || obj === null ) return obj; if (map.get(obj)) return map.get(obj); if (obj instanceof Date ) return new Date (obj); if (obj instanceof RegExp ) return new RegExp (obj.source, obj.flags); if (obj instanceof Function ) return obj; let result = Array .isArray(obj) ? [] : {}; map.set(obj, result); for (const key in obj) { if (obj.hasOwnProperty(key)) { result[key] = deepClone(obj[key], map); } } const symbolKeys = Object .getOwnPropertySymbols(obj); for (const symbolKey of symbolKeys) { result[symbolKey] = deepClone(obj[symbolKey], map); } return result; }
深拷贝的使用场景 当对象包含嵌套的引用类型属性时 当需要完全独立的对象副本,避免修改原对象时 当对象结构复杂,需要递归处理时 性能考虑 深拷贝的性能开销较大,特别是对于大型对象 对于频繁操作的场景,考虑使用浅拷贝或其他优化策略 对于特定场景,可以使用更高效的深拷贝库,如 lodash.cloneDeep 事件总线(发布订阅模式) 事件总线是一种实现组件间通信的设计模式,通过发布和订阅事件来实现松耦合的通信机制。
实现原理 维护一个事件缓存对象,存储事件名称和对应的回调函数列表 提供 on 方法注册事件监听器 提供 off 方法移除事件监听器 提供 emit 方法触发事件,执行对应的回调函数 优化实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 class EventEmitter { constructor ( ) { this .events = new Map (); } on (eventName, callback ) { if (typeof callback !== 'function' ) { throw new TypeError ('Callback must be a function' ); } if (!this .events.has(eventName)) { this .events.set(eventName, []); } this .events.get(eventName).push(callback); return this ; } once (eventName, callback ) { if (typeof callback !== 'function' ) { throw new TypeError ('Callback must be a function' ); } const onceCallback = (...args ) => { callback(...args); this .off(eventName, onceCallback); }; onceCallback.originalCallback = callback; return this .on(eventName, onceCallback); } off (eventName, callback ) { if (!this .events.has(eventName)) { return this ; } const callbacks = this .events.get(eventName); const index = callbacks.findIndex(fn => fn === callback || fn.originalCallback === callback ); if (index > -1 ) { callbacks.splice(index, 1 ); if (callbacks.length === 0 ) { this .events.delete(eventName); } } return this ; } emit (eventName, ...args ) { if (!this .events.has(eventName)) { return this ; } const callbacks = [...this.events.get(eventName)]; callbacks.forEach(callback => { try { callback(...args); } catch (error) { console .error(`Error in event ${eventName} callback:` , error); } }); return this ; } clear ( ) { this .events.clear(); return this ; } listenerCount (eventName ) { if (!this .events.has(eventName)) { return 0 ; } return this .events.get(eventName).length; } eventNames ( ) { return Array .from(this .events.keys()); } } const eventBus = new EventEmitter();const fn1 = function (name, age ) { console .log(`${name} ${age} ` ); }; const fn2 = function (name, age ) { console .log(`hello, ${name} ${age} ` ); }; eventBus.on('greet' , fn1); eventBus.on('greet' , fn2); eventBus.once('greetOnce' , (name ) => { console .log(`Hello ${name} , this is a once event` ); }); eventBus.emit('greet' , '布兰' , 12 ); eventBus.emit('greetOnce' , '布兰' ); eventBus.emit('greetOnce' , '布兰' ); eventBus.off('greet' , fn1); eventBus.emit('greet' , '布兰' , 12 ); eventBus.clear(); eventBus.emit('greet' , '布兰' , 12 );
事件总线的使用场景 组件间通信:特别是在非父子组件之间 跨模块通信:不同模块之间通过事件进行解耦 事件驱动架构:基于事件的系统设计 异步操作处理:通过事件通知异步操作的完成 优势 松耦合:发布者和订阅者之间没有直接依赖 可扩展性:可以轻松添加新的事件和监听器 灵活性:支持多对多的通信模式 可维护性:事件名称作为通信的契约,使代码更清晰 解析 URL 参数为对象 解析 URL 参数为对象是前端开发中常见的需求,用于获取 URL 中的查询参数并转换为可操作的对象格式。
实现原理 从 URL 中提取查询字符串部分 将查询字符串按 & 分割为参数数组 遍历参数数组,处理每个参数的键值对 对参数值进行解码和类型转换 处理重复键的情况,将其转换为数组 优化实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 function parseUrlParams (url ) { if (!url) { url = window .location.href; } const queryStringMatch = url.match(/\?([^#]+)/ ); if (!queryStringMatch) { return {}; } const paramsStr = queryStringMatch[1 ]; const paramsArr = paramsStr.split('&' ); const paramsObj = {}; paramsArr.forEach(param => { if (!param) return ; const [key, value] = param.split('=' ); const decodedKey = decodeURIComponent (key); let decodedValue = value !== undefined ? decodeURIComponent (value) : true ; if (typeof decodedValue === 'string' && /^\d+(\.\d+)?$/ .test(decodedValue)) { decodedValue = parseFloat (decodedValue); } if (typeof decodedValue === 'string' ) { if (decodedValue.toLowerCase() === 'true' ) { decodedValue = true ; } else if (decodedValue.toLowerCase() === 'false' ) { decodedValue = false ; } else if (decodedValue.toLowerCase() === 'null' ) { decodedValue = null ; } else if (decodedValue.toLowerCase() === 'undefined' ) { decodedValue = undefined ; } } if (paramsObj.hasOwnProperty(decodedKey)) { if (Array .isArray(paramsObj[decodedKey])) { paramsObj[decodedKey].push(decodedValue); } else { paramsObj[decodedKey] = [paramsObj[decodedKey], decodedValue]; } } else { paramsObj[decodedKey] = decodedValue; } }); return paramsObj; } const url = 'https://example.com?name=布兰&age=12&active=true&score=95.5&tags=js&tags=html&tags=css' ;const params = parseUrlParams(url);console .log(params);
使用场景 获取 URL 中的查询参数 处理用户通过 URL 传递的配置信息 实现页面间的数据传递 构建和解析 API 请求参数 注意事项 处理 URL 编码:使用 decodeURIComponent 解码参数值 处理类型转换:将数字、布尔值等字符串转换为对应类型 处理重复键:将重复的参数键转换为数组 处理边缘情况:如空参数、无值参数等 现代替代方案 在现代浏览器中,可以使用 URLSearchParams API 来更方便地解析 URL 参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function parseUrlParamsModern (url ) { const urlObj = new URL(url || window .location.href); const params = new URLSearchParams(urlObj.search); const result = {}; for (const [key, value] of params.entries()) { if (result.hasOwnProperty(key)) { if (Array .isArray(result[key])) { result[key].push(value); } else { result[key] = [result[key], value]; } } else { result[key] = value; } } return result; }
性能考虑 对于简单的 URL 参数解析,原生实现和 URLSearchParams 都有良好的性能 对于复杂的 URL 参数处理,URLSearchParams 提供了更简洁和可靠的 API 在需要兼容旧浏览器的情况下,使用原生实现 字符串模板 字符串模板是一种将数据动态插入到字符串中的技术,常用于生成 HTML、邮件内容、配置文件等。
实现原理 使用正则表达式匹配模板中的占位符(如 {{variable}}) 查找数据对象中对应的值 替换模板中的占位符为实际值 递归处理所有占位符,直到模板中没有占位符为止 优化实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 function renderTemplate (template, data ) { const reg = /\{\{([^}]+)\}\}/g ; return template.replace(reg, (match, key ) => { const trimmedKey = key.trim(); const keys = trimmedKey.split('.' ); let value = data; for (const k of keys) { if (value === undefined || value === null ) { break ; } value = value[k]; } return value !== undefined && value !== null ? value : '' ; }); } const template = '我是{{name}},年龄{{age}},性别{{sex}},邮箱{{contact.email}}' ;const person = { name: '布兰' , age: 12 , contact: { email: 'bran@example.com' } }; const result = renderTemplate(template, person);console .log(result);
高级实现(支持条件和循环) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 function advancedRender (template, data ) { template = template.replace(/\{\{if\s+([^}]+)\}\}(.*?)\{\{\/if\}\}/g s, (match, condition, content ) => { try { const evalFn = new Function ('data' , `return (${condition.trim()} );` ); const result = evalFn(data); return result ? content : '' ; } catch (e) { console .error('Error evaluating condition:' , e); return '' ; } }); template = template.replace(/\{\{each\s+([^\s]+)\s+as\s+([^}]+)\}\}(.*?)\{\{\/each\}\}/g s, (match, arrayName, itemName, content ) => { const array = data[arrayName.trim()]; if (!Array .isArray(array)) { return '' ; } return array.map((item, index ) => { return content.replace(new RegExp (`\\{\\{${itemName.trim()} \\}\\}` , 'g' ), item) .replace(new RegExp (`\\{\\{${itemName.trim()} \.index\\}\\}` , 'g' ), index); }).join('' ); }); template = template.replace(/\{\{([^}]+)\}\}/g , (match, key ) => { const trimmedKey = key.trim(); const keys = trimmedKey.split('.' ); let value = data; for (const k of keys) { if (value === undefined || value === null ) { break ; } value = value[k]; } return value !== undefined && value !== null ? value : '' ; }); return template; } const advancedTemplate = ` <h1>{{name}}</h1> {{if age >= 18}} <p>成年人</p> {{else}} <p>未成年人</p> {{/if}} <h2>兴趣爱好</h2> {{each hobbies as hobby}} <li>{{hobby}}</li> {{/each}} ` ;const userData = { name: '布兰' , age: 12 , hobbies: ['编程' , '读书' , '运动' ] }; const advancedResult = advancedRender(advancedTemplate, userData);console .log(advancedResult);
使用场景 生成动态 HTML 内容 生成邮件模板 生成配置文件 生成报表或文档 注意事项 处理未定义的值:当数据对象中没有对应键时,应提供默认值或空字符串 处理嵌套属性:支持 {{user.name}} 这样的嵌套属性访问 防止 XSS 攻击:如果模板内容来自用户输入,需要进行适当的转义 性能考虑:对于频繁渲染的场景,考虑使用更高效的模板引擎 现代替代方案 在现代 JavaScript 中,可以使用模板字面量(Template Literals)来实现简单的字符串模板:
1 2 3 4 5 6 7 8 9 10 11 12 function renderWithTemplateLiterals (data ) { return `我是${data.name} ,年龄${data.age} ,性别${data.sex || '' } ` ; } const person = { name: '布兰' , age: 12 }; console .log(renderWithTemplateLiterals(person));
对于更复杂的场景,可以使用专业的模板引擎,如 Handlebars、Mustache 或 EJS 等。
图片懒加载 图片懒加载是一种优化技术,用于延迟加载页面上的图片,只有当图片进入视口时才会加载,从而提高页面加载速度和减少带宽使用。
实现原理 监听滚动事件,检测图片是否进入视口 当图片进入视口时,将 data-src 属性的值赋给 src 属性 加载完成后从列表中移除图片,避免重复处理 所有图片加载完成后移除事件监听器,减少性能消耗 优化实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 function imgLazyLoad (selector = 'img' , options = {} ) { const { root = null , rootMargin = '0px' , threshold = 0 } = options; let imgList = [...document.querySelectorAll(selector)]; let length = imgList.length; if (length === 0 ) return ; if ('IntersectionObserver' in window ) { const observer = new IntersectionObserver((entries ) => { entries.forEach((entry ) => { if (entry.isIntersecting) { const img = entry.target; if (img.dataset.src) { img.src = img.dataset.src; img.classList.add('lazy-loaded' ); observer.unobserve(img); length--; if (length === 0 ) { observer.disconnect(); } } } }); }, { root, rootMargin, threshold }); imgList.forEach((img ) => observer.observe(img)); } else { let count = 0 ; const loadImages = function ( ) { let deleteIndexList = []; imgList.forEach((img, index ) => { const rect = img.getBoundingClientRect(); if (rect.top < window .innerHeight && rect.bottom >= 0 ) { if (img.dataset.src) { img.src = img.dataset.src; img.classList.add('lazy-loaded' ); deleteIndexList.push(index); count++; if (count === length) { window .removeEventListener('scroll' , loadImages); window .removeEventListener('resize' , loadImages); window .removeEventListener('orientationchange' , loadImages); } } } }); imgList = imgList.filter((_, index ) => !deleteIndexList.includes(index)); }; const debouncedLoad = function (func, wait ) { let timeout; return function ( ) { clearTimeout (timeout); timeout = setTimeout (func, wait); }; }; const debouncedLoadImages = debouncedLoad(loadImages, 100 ); window .addEventListener('scroll' , debouncedLoadImages); window .addEventListener('resize' , debouncedLoadImages); window .addEventListener('orientationchange' , debouncedLoadImages); loadImages(); } } imgLazyLoad('img[data-src]' );
使用场景 长页面包含大量图片时 移动端页面,减少初始加载时间和流量消耗 图片库或相册页面 电商网站的商品列表页 注意事项 为图片设置占位符,避免页面布局跳动 确保 data-src 属性包含完整的图片 URL 考虑添加加载动画,提升用户体验 对于关键图片,不建议使用懒加载 现代替代方案 在现代浏览器中,可以使用原生的 loading 属性来实现图片懒加载:
1 <img src ="placeholder.jpg" data-src ="real-image.jpg" loading ="lazy" alt ="描述" >
loading 属性支持三个值:
lazy:延迟加载图片,直到它进入视口eager:立即加载图片auto:由浏览器决定是否延迟加载性能考虑 Intersection Observer 比传统的滚动监听性能更好,因为它是浏览器原生实现的 防抖处理可以减少滚动事件的触发频率,提高性能 图片加载完成后及时移除事件监听器,避免内存泄漏 对于大量图片的页面,考虑使用虚拟滚动技术 参考:图片懒加载
函数防抖 函数防抖(Debounce)是一种优化技术,用于限制高频事件的执行频率。当事件被触发后,等待一段时间再执行回调函数,如果在这段时间内事件再次被触发,则重新计时。
实现原理 维护一个计时器,记录上次触发事件的时间 当事件触发时,清除之前的计时器并重新设置 只有当事件停止触发一段时间后,才会执行回调函数 简单版实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function debounce (func, wait ) { var timeout; return function ( ) { var context = this ; var args = arguments ; clearTimeout (timeout) timeout = setTimeout (function ( ) { func.apply(context, args) }, wait); } }
最终版实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 function debounce (func, wait, immediate ) { var timeout, result; var debounced = function ( ) { var context = this ; var args = arguments ; if (timeout) clearTimeout (timeout); if (immediate) { var callNow = !timeout; timeout = setTimeout (function ( ) { timeout = null ; }, wait) if (callNow) result = func.apply(context, args) } else { timeout = setTimeout (function ( ) { result = func.apply(context, args) }, wait); } return result; }; debounced.cancel = function ( ) { clearTimeout (timeout); timeout = null ; }; return debounced; }
使用场景 搜索框输入:用户输入时不立即搜索,等待用户停止输入后再执行 窗口 resize 事件:调整窗口大小时,等待调整完成后再执行布局计算 按钮点击:防止用户重复点击提交表单 滚动事件:滚动时不立即触发计算,等待滚动停止后再执行 使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const searchInput = document .getElementById('search' );const searchDebounced = debounce(function (e ) { console .log('搜索:' , e.target.value); }, 300 ); searchInput.addEventListener('input' , searchDebounced); const resizeDebounced = debounce(function ( ) { console .log('窗口大小:' , window .innerWidth, 'x' , window .innerHeight); }, 200 ); window .addEventListener('resize' , resizeDebounced);const submitBtn = document .getElementById('submit' );const submitDebounced = debounce(function ( ) { console .log('提交表单' ); }, 1000 , true ); submitBtn.addEventListener('click' , submitDebounced);
函数节流 函数节流(Throttle)是一种优化技术,用于限制函数的执行频率,确保在一定时间内只执行一次。
实现原理 记录上次执行函数的时间 当事件触发时,检查当前时间与上次执行时间的差 如果时间差大于等于指定的时间间隔,则执行函数并更新上次执行时间 时间戳实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function throttle (func, wait ) { var previous = 0 ; return function ( ) { var now = +new Date (); var context = this ; var args = arguments ; if (now - previous > wait) { func.apply(context, args); previous = now; } } }
定时器实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function throttle (fn, delay ) { let wait = false ; return (...args ) => { if (wait) { return ; } fn(...args); wait = true ; setTimeout (() => { wait = false ; }, delay); }; }
最终版实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 function throttle (func, wait, options ) { var timeout, context, args, result; var previous = 0 ; if (!options) options = {}; var later = function ( ) { previous = options.leading === false ? 0 : new Date ().getTime(); timeout = null ; func.apply(context, args); if (!timeout) context = args = null ; }; var throttled = function ( ) { var now = new Date ().getTime(); if (!previous && options.leading === false ) previous = now; var remaining = wait - (now - previous); context = this ; args = arguments ; if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout (timeout); timeout = null ; } previous = now; func.apply(context, args); if (!timeout) context = args = null ; } else if (!timeout && options.trailing !== false ) { timeout = setTimeout (later, remaining); } }; throttled.cancel = function ( ) { clearTimeout (timeout); previous = 0 ; timeout = null ; } return throttled; }
使用场景 滚动事件:滚动时定期执行某些操作,如更新滚动位置、加载更多内容 鼠标移动:跟踪鼠标位置,但不需要实时更新 游戏中的动画:限制动画更新频率,提高性能 API 请求:限制请求频率,避免过多请求导致服务器压力 使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const scrollThrottled = throttle(function ( ) { console .log('滚动位置:' , window .scrollY); }, 100 ); window .addEventListener('scroll' , scrollThrottled);const mouseMoveThrottled = throttle(function (e ) { console .log('鼠标位置:' , e.clientX, e.clientY); }, 50 ); document .addEventListener('mousemove' , mouseMoveThrottled);const apiRequestThrottled = throttle(function (query ) { console .log('请求 API:' , query); }, 1000 ); apiRequestThrottled('query1' ); apiRequestThrottled('query2' ); apiRequestThrottled('query3' );
防抖与节流的区别 防抖 :事件触发后等待一段时间执行,若期间再次触发则重新计时节流 :事件触发后立即执行,然后在指定时间内不再执行使用场景 :防抖适用于需要等待用户操作完成后再执行的场景,节流适用于需要定期执行的场景参考:JavaScript 专题之跟着 underscore 学防抖 参考:JavaScript 专题之跟着 underscore 学节流
函数柯里化 函数柯里化(Currying)是一种函数式编程技术,将接受多个参数的函数转换为一系列接受单个参数的函数。
实现原理 接收一个函数和部分参数 返回一个新函数,该函数接收剩余参数 当所有参数都收集完毕后,执行原始函数 基本实现 1 2 3 4 5 6 7 8 9 10 11 12 function curry (fn ) { let judge = (...args ) => { if (args.length == fn.length) return fn(...args) return (...arg ) => judge(...args, ...arg) } return judge }
使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function add (a, b, c ) { return a + b + c } let addCurry = curry(add)console .log(addCurry(1 )(2 )(3 )); console .log(addCurry(1 , 2 )(3 )); console .log(addCurry(1 )(2 , 3 )); function createUser (name, age, email ) { return { name, age, email }; } const createUserCurry = curry(createUser);const createAdultUser = createUserCurry('_' , 18 );const createAdultJohn = createAdultUser('John' );console .log(createAdultJohn('john@example.com' ));
应用场景 参数复用 :当某些参数在多次调用中重复时,可以通过柯里化固定这些参数延迟执行 :将函数的执行推迟到所有参数都收集完毕函数组合 :便于函数之间的组合和链式调用类型检查 :可以在每个参数传入时进行类型检查高级实现(支持占位符) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function curryWithPlaceholder (fn, placeholder = '_' ) { return function curried (...args ) { const filteredArgs = args.slice(0 , fn.length).filter(arg => arg !== placeholder); if (filteredArgs.length >= fn.length) { return fn(...filteredArgs); } return function (...nextArgs ) { const combinedArgs = args.map(arg => arg === placeholder && nextArgs.length > 0 ? nextArgs.shift() : arg ); return curried(...combinedArgs, ...nextArgs); }; }; } const add = (a, b, c ) => a + b + c;const addCurry = curryWithPlaceholder(add);console .log(addCurry(1 )(2 )(3 )); console .log(addCurry('_' , 2 )(1 )(3 )); console .log(addCurry(1 , '_' , 3 )(2 ));
偏函数 偏函数(Partial Application)是一种函数式编程技术,将一个函数的部分参数固定,返回一个接受剩余参数的新函数。
实现原理 接收一个函数和部分参数 返回一个新函数,该函数接收剩余参数 当调用新函数时,将固定参数和新参数合并后传递给原始函数 基本实现 1 2 3 4 5 6 7 8 9 10 11 function partial (fn, ...args ) { return (...arg ) => { return fn(...args, ...arg) } }
支持占位符的实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function partial (fn, ...args ) { return (...arg ) => { let fullArgs = [...args]; let argIndex = 0 ; for (let i = 0 ; i < fullArgs.length && argIndex < arg.length; i++) { if (fullArgs[i] === '_' ) { fullArgs[i] = arg[argIndex++]; } } if (argIndex < arg.length) { fullArgs = fullArgs.concat(arg.slice(argIndex)); } return fn(...fullArgs); } }
使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 function add (a, b, c ) { return a + b + c } let partialAdd = partial(add, 1 )console .log(partialAdd(2 , 3 )); function clg (a, b, c ) { console .log(a, b, c) } let partialClg = partial(clg, '_' , 2 )partialClg(1 , 3 ); function fetchData (url, method, headers, data ) { console .log(`Fetching ${url} with ${method} ` , { headers, data }); } const get = partial(fetchData, '_' , 'GET' );const getWithHeaders = partial(get, '_' , { 'Content-Type' : 'application/json' });getWithHeaders('https://api.example.com/data' );
应用场景 固定默认参数 :为函数设置默认参数,减少重复代码简化函数调用 :将复杂函数简化为更具针对性的函数函数适配 :将一个函数的接口适配到另一个函数的接口参数预填充 :在函数调用前预先填充一些参数柯里化与偏函数的区别 柯里化 :将多参数函数转换为一系列单参数函数,每次只接收一个参数偏函数 :固定函数的部分参数,返回一个接收剩余参数的新函数执行时机 :柯里化在所有参数收集完毕后执行,偏函数在调用时立即执行现代 JavaScript 中的应用 在现代 JavaScript 中,可以使用箭头函数和展开运算符来简化偏函数的创建:
1 2 3 4 5 6 7 8 9 const add = (a, b, c ) => a + b + c;const add5 = (b, c ) => add(5 , b, c);const add10 = add.bind(null , 10 );console .log(add5(2 , 3 )); console .log(add10(2 , 3 ));
JSONP JSONP(JSON with Padding)是一种跨域请求技术,利用 script 标签不受同源策略限制的特性来实现跨域数据获取。
实现原理 创建一个 script 标签,将请求 URL 设置为其 src 属性 URL 中包含回调函数名称,服务器将数据包装在该回调函数中返回 当 script 标签加载完成时,浏览器会执行返回的 JavaScript 代码,调用回调函数并传入数据 优化实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 const jsonp = ({ url, params, callbackName = `jsonp_${Date .now()} _${Math .random().toString(36 ).substr(2 , 9 )} ` , timeout = 5000 } ) => { return new Promise ((resolve, reject ) => { const generateUrl = () => { let dataSrc = '' ; for (let key in params) { if (params.hasOwnProperty(key)) { dataSrc += `${encodeURIComponent (key)} =${encodeURIComponent (params[key])} &` ; } } dataSrc += `callback=${callbackName} ` ; return `${url} ?${dataSrc} ` ; }; const scriptEle = document .createElement('script' ); scriptEle.src = generateUrl(); scriptEle.type = 'text/javascript' ; scriptEle.async = true ; const timeoutId = setTimeout (() => { reject(new Error ('JSONP request timeout' )); cleanup(); }, timeout); window [callbackName] = data => { resolve(data); cleanup(); }; scriptEle.onerror = () => { reject(new Error ('JSONP request failed' )); cleanup(); }; const cleanup = () => { clearTimeout (timeoutId); if (scriptEle.parentNode) { scriptEle.parentNode.removeChild(scriptEle); } delete window [callbackName]; }; document .body.appendChild(scriptEle); }); };
使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 jsonp({ url: 'https://api.example.com/weather' , params: { city: 'Beijing' , unit: 'celsius' }, timeout: 3000 }) .then(data => { console .log('天气数据:' , data); }) .catch(error => { console .error('请求失败:' , error); });
优缺点 优点 :兼容性好,支持所有浏览器,包括旧版本浏览器缺点 :只能用于 GET 请求,存在安全风险(如 XSS 攻击),无法处理错误状态码AJAX AJAX(Asynchronous JavaScript and XML)是一种在不重新加载整个页面的情况下,与服务器交换数据并更新部分页面的技术。
实现原理 创建 XMLHttpRequest 对象 配置请求参数(方法、URL、是否异步等) 发送请求 监听请求状态变化,处理响应 优化实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 const ajax = ({ url, method = 'GET' , params, data, headers = {}, timeout = 10000 } ) => { return new Promise ((resolve, reject ) => { if (params) { const searchParams = new URLSearchParams(); for (const key in params) { if (params.hasOwnProperty(key)) { searchParams.append(key, params[key]); } } url += `?${searchParams.toString()} ` ; } const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP' ); xhr.timeout = timeout; xhr.onreadystatechange = function ( ) { if (xhr.readyState !== 4 ) return ; let responseData; try { responseData = JSON .parse(xhr.responseText); } catch (e) { responseData = xhr.responseText; } if (xhr.status >= 200 && xhr.status < 300 ) { resolve({ data: responseData, status: xhr.status, statusText: xhr.statusText }); } else { reject({ error: new Error (`HTTP error! status: ${xhr.status} ` ), status: xhr.status, statusText: xhr.statusText, data: responseData }); } }; xhr.onerror = function ( ) { reject(new Error ('Network error occurred' )); }; xhr.ontimeout = function ( ) { reject(new Error ('Request timeout' )); }; xhr.open(method, url, true ); for (const key in headers) { if (headers.hasOwnProperty(key)) { xhr.setRequestHeader(key, headers[key]); } } if (!headers['Content-Type' ] && data) { if (typeof data === 'object' && data !== null ) { xhr.setRequestHeader('Content-Type' , 'application/json' ); data = JSON .stringify(data); } else { xhr.setRequestHeader('Content-Type' , 'application/x-www-form-urlencoded' ); } } xhr.send(data); }); }; const get = (url, options = {} ) => ajax({ ...options, url, method : 'GET' });const post = (url, data, options = {} ) => ajax({ ...options, url, method : 'POST' , data });const put = (url, data, options = {} ) => ajax({ ...options, url, method : 'PUT' , data });const del = (url, options = {} ) => ajax({ ...options, url, method : 'DELETE' });
使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 get('https://api.example.com/users' , { params: { page : 1 , limit : 10 }, headers: { 'Authorization' : 'Bearer token123' } }) .then(response => { console .log('用户列表:' , response.data); }) .catch(error => { console .error('请求失败:' , error); }); post('https://api.example.com/users' , { name: 'John' , email: 'john@example.com' }) .then(response => { console .log('创建用户成功:' , response.data); }) .catch(error => { console .error('请求失败:' , error); });
现代替代方案 在现代 JavaScript 中,fetch API 是 AJAX 的更现代替代方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const fetchData = (url, options = {} ) => { const defaultOptions = { headers: { 'Content-Type' : 'application/json' } }; const mergedOptions = { ...defaultOptions, ...options }; return fetch(url, mergedOptions) .then(response => { if (!response.ok) { throw new Error (`HTTP error! status: ${response.status} ` ); } return response.json(); }); }; fetchData('https://api.example.com/users' ) .then(data => console .log('用户列表:' , data)) .catch(error => console .error('请求失败:' , error));
优缺点 优点 :支持多种请求方法 可以发送和接收各种数据格式 支持错误处理和超时设置 现代浏览器支持良好 缺点 :旧浏览器不支持(需要 polyfill) 实现相对复杂 注意事项 跨域问题 :需要服务器设置 CORS 头安全问题 :避免发送敏感信息,使用 HTTPS性能问题 :对于频繁请求,考虑使用缓存错误处理 :总是处理可能的错误情况new 运算符 new 运算符用于创建一个构造函数的实例对象,它会执行以下操作:
实现原理 创建一个新的空对象 将新对象的原型指向构造函数的 prototype 属性 将构造函数的 this 绑定到新对象上 执行构造函数代码 如果构造函数返回一个对象,则返回该对象;否则返回新创建的对象 实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function objectFactory (Constructor, ...args ) { var obj = new Object (); obj.__proto__ = Constructor.prototype; var ret = Constructor.apply(obj, args); return typeof ret === 'object' ? ret || obj : obj; };
使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 function Person (name, age ) { this .name = name; this .age = age; } Person.prototype.sayHello = function ( ) { console .log(`Hello, my name is ${this .name} , I'm ${this .age} years old.` ); }; let p = objectFactory(Person, '布兰' , 12 );console .log(p); p.sayHello(); function PersonWithReturn (name, age ) { this .name = name; return { age: age, greeting: `Hello, I'm ${age} years old.` }; } let p2 = objectFactory(PersonWithReturn, '布兰' , 12 );console .log(p2); console .log(p2.name); function PersonWithNullReturn (name, age ) { this .name = name; this .age = age; return null ; } let p3 = objectFactory(PersonWithNullReturn, '布兰' , 12 );console .log(p3);
注意事项 构造函数必须是函数,如果不是函数会抛出错误 如果构造函数返回一个对象(包括数组、函数等),则返回该对象 如果构造函数返回 null 或基本类型,则返回新创建的对象 新对象的原型链会指向构造函数的 prototype instanceof instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
实现原理 获取左侧对象的原型 检查该原型是否等于右侧构造函数的 prototype 属性 如果是,返回 true;否则,继续向上查找原型链 如果查找到原型链末端(null)仍未找到,则返回 false 实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function instanceOf (left, right ) { const prototype = right.prototype; let proto = Object .getPrototypeOf(left); while (true ) { if (proto === null ) return false ; if (proto === prototype) { return true ; } proto = Object .getPrototypeOf(proto); } }
使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function Person ( ) {}function Animal ( ) {}const person = new Person();const animal = new Animal();console .log(instanceOf(person, Person)); console .log(instanceOf(person, Object )); console .log(instanceOf(person, Animal)); console .log(instanceOf(animal, Animal)); console .log(instanceOf(animal, Object )); const arr = [];const obj = {};console .log(instanceOf(arr, Array )); console .log(instanceOf(arr, Object )); console .log(instanceOf(obj, Object )); console .log(instanceOf(obj, Array )); function fn ( ) {}console .log(instanceOf(fn, Function )); console .log(instanceOf(fn, Object ));
注意事项 instanceof 只能用于对象,不能用于基本类型instanceof 检查的是原型链,而不是直接比较类型可以通过修改对象的原型链来改变 instanceof 的结果 在多个全局环境下(如 iframe),instanceof 可能会出现意外结果,因为不同环境的构造函数是不同的对象 与 typeof 的区别 typeof 用于判断基本类型,返回字符串(如 ‘string’, ‘number’, ‘boolean’, ‘object’, ‘function’, ‘undefined’, ‘symbol’)instanceof 用于判断对象是否是某个构造函数的实例,返回布尔值typeof null 返回 ‘object’,这是一个历史遗留问题typeof [] 返回 ‘object’,无法区分数组和普通对象实际应用 检查对象类型,确保传入的参数类型正确 实现多态,根据对象类型执行不同的操作 验证继承关系,确保对象符合预期的接口 Object.create()Object.create() 方法创建一个新对象,使用指定的原型对象和属性来初始化。
实现原理 检查传入的原型对象是否为对象或 null 创建一个空构造函数 F 将构造函数 F 的 prototype 指向传入的原型对象 使用构造函数 F 创建一个新实例 如果提供了属性描述符对象,则使用 Object.defineProperties 添加属性 如果原型为 null,则创建一个没有原型的对象 实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 Object .create2 = function (proto, propertyObject = undefined ) { if (typeof proto !== 'object' && typeof proto !== 'function' ) { throw new TypeError ('Object prototype may only be an Object or null.' ); } function F ( ) {} F.prototype = proto; const obj = new F(); if (propertyObject !== undefined ) { Object .defineProperties(obj, propertyObject); } if (proto === null ) { obj.__proto__ = null ; } return obj; };
使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 const person = { name: '布兰' , age: 12 , sayHello: function ( ) { console .log(`Hello, my name is ${this .name} ` ); } }; const student = Object .create(person, { grade: { value: '6th' , writable: true , enumerable: true , configurable: true } }); console .log(student.name); console .log(student.grade); student.sayHello(); const emptyObj = Object .create(null );console .log(emptyObj); console .log(emptyObj.toString); function Parent (name ) { this .name = name; } Parent.prototype.sayName = function ( ) { console .log(`My name is ${this .name} ` ); }; function Child (name, age ) { Parent.call(this , name); this .age = age; } Child.prototype = Object .create(Parent.prototype, { constructor : { value: Child, writable: true , enumerable: false , configurable: true }, sayAge: { value: function ( ) { console .log(`I'm ${this .age} years old` ); }, writable: true , enumerable: true , configurable: true } }); const child = new Child('布兰' , 12 );child.sayName(); child.sayAge();
应用场景 实现继承 :通过指定原型对象来实现对象间的继承关系创建特殊对象 :创建没有原型的对象,避免原型链污染属性描述符控制 :通过第二个参数精确控制新对象的属性原型链隔离 :创建一个干净的对象,只包含指定的属性和方法Object.assign()Object.assign() 方法用于将所有可枚举的自有属性从一个或多个源对象复制到目标对象,返回目标对象。
实现原理 检查目标对象是否为 null 或 undefined 将目标对象转换为对象类型 遍历所有源对象 遍历每个源对象的自有可枚举属性 将属性值复制到目标对象 返回目标对象 实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 Object .assign2 = function (target, ...source ) { if (target == null ) { throw new TypeError ('Cannot convert undefined or null to object' ); } let ret = Object (target); source.forEach(function (obj ) { if (obj != null ) { for (let key in obj) { if (obj.hasOwnProperty(key)) { ret[key] = obj[key]; } } } }); return ret; };
使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 const target = { a : 1 , b : 2 };const source1 = { b : 3 , c : 4 };const source2 = { d : 5 };const result = Object .assign(target, source1, source2);console .log(result); console .log(target); const user = { name: '布兰' , age: 12 }; const address = { city: '北京' , district: '朝阳区' }; const contact = { email: 'bran@example.com' , phone: '1234567890' }; const userInfo = Object .assign({}, user, address, contact);console .log(userInfo);const original = { a : 1 , b : { c : 2 } };const clone = Object .assign({}, original);console .log(clone); console .log(clone.b === original.b); function createUser (options ) { const defaultOptions = { name: '匿名' , age: 18 , active: true }; return Object .assign({}, defaultOptions, options); } const user1 = createUser({ name : '布兰' , age : 12 });console .log(user1); const user2 = createUser({ active : false });console .log(user2);
注意事项 浅拷贝 :Object.assign() 只进行浅拷贝,对于嵌套对象,只会复制引用目标对象修改 :目标对象会被修改,返回的是修改后的目标对象可枚举属性 :只复制可枚举的自有属性属性覆盖 :如果多个源对象有相同的属性,后面的会覆盖前面的类型转换 :目标对象会被转换为对象类型,如果是基本类型会被包装应用场景 对象合并 :将多个对象合并为一个对象克隆 :创建对象的浅拷贝默认值设置 :为函数参数设置默认值属性复制 :将一个对象的属性复制到另一个对象配置合并 :合并默认配置和用户配置JSON.stringify()JSON.stringify() 方法将 JavaScript 对象或值转换为 JSON 字符串。
实现原理 处理基本类型:根据类型进行相应的转换 处理对象类型:处理 null、Date、RegExp 等特殊对象 处理数组:递归处理每个元素 处理普通对象:递归处理每个属性 处理循环引用:检测并抛出错误 处理 toJSON 方法:如果对象有 toJSON 方法,使用其返回值 实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 function jsonStringify (data, replacer, space ) { function getReplacedValue (key, value ) { if (typeof replacer === 'function' ) { return replacer(key, value); } else if (Array .isArray(replacer)) { return replacer.includes(key) ? value : undefined ; } return value; } function getIndent (level ) { if (space === undefined ) return '' ; if (typeof space === 'number' ) { return ' ' .repeat(Math .min(space, 10 )); } else if (typeof space === 'string' ) { return space.substring(0 , 10 ); } return '' ; } const seen = new WeakSet (); function stringify (data, key, level = 0 ) { const dataType = typeof data; const replacedValue = getReplacedValue(key, data); if (replacedValue === undefined ) { return undefined ; } if (dataType !== 'object' || data === null ) { if (replacedValue === null ) { return 'null' ; } else if (typeof replacedValue === 'boolean' ) { return String (replacedValue); } else if (typeof replacedValue === 'number' ) { return isNaN (replacedValue) || !isFinite (replacedValue) ? 'null' : String (replacedValue); } else if (typeof replacedValue === 'string' ) { return '"' + replacedValue.replace(/[\"\\\b\f\n\r\t]/g , function (char ) { const escapeMap = { '"' : '\\"' , '\\' : '\\\\' , '\b' : '\\b' , '\f' : '\\f' , '\n' : '\\n' , '\r' : '\\r' , '\t' : '\\t' }; return escapeMap[char]; }) + '"' ; } else if (typeof replacedValue === 'symbol' || typeof replacedValue === 'function' || typeof replacedValue === 'undefined' ) { return undefined ; } return String (replacedValue); } if (seen.has(replacedValue)) { throw new TypeError ('Converting circular structure to JSON' ); } seen.add(replacedValue); try { if (typeof replacedValue.toJSON === 'function' ) { return stringify(replacedValue.toJSON(), key, level); } if (Array .isArray(replacedValue)) { const indent = getIndent(level); const nextIndent = getIndent(level + 1 ); const items = replacedValue.map((item, index ) => { const itemStr = stringify(item, String (index), level + 1 ); return itemStr === undefined ? 'null' : nextIndent + itemStr; }); return indent + '[' + (items.length ? '\n' + items.join(',\n' ) + '\n' + indent : '' ) + ']' ; } const indent = getIndent(level); const nextIndent = getIndent(level + 1 ); const keys = Object .keys(replacedValue); const properties = keys.map(key => { const valueStr = stringify(replacedValue[key], key, level + 1 ); if (valueStr === undefined ) return undefined ; return nextIndent + '"' + key + '": ' + valueStr; }).filter(Boolean ); return indent + '{' + (properties.length ? '\n' + properties.join(',\n' ) + '\n' + indent : '' ) + '}' ; } finally { seen.delete(replacedValue); } } const result = stringify(data, '' ); return result === undefined ? undefined : String (result); }
使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 console .log(jsonStringify(1 )); console .log(jsonStringify('hello' )); console .log(jsonStringify(true )); console .log(jsonStringify(null )); console .log(jsonStringify(undefined )); console .log(jsonStringify(NaN )); console .log(jsonStringify(Infinity )); const obj = { name: '布兰' , age: 12 , hobbies: ['编程' , '读书' ], address: { city: '北京' }, birth: new Date ('2010-01-01' ), regex: /\d+/ , sayHello: function ( ) { console .log('Hello' ); }, undefinedProp: undefined , symbolProp: Symbol ('test' ) }; console .log(jsonStringify(obj));console .log(jsonStringify(obj, function (key, value ) { if (key === 'age' ) return value + 1 ; if (key === 'address' ) return undefined ; return value; })); console .log(jsonStringify(obj, ['name' , 'age' , 'hobbies' ]));console .log(jsonStringify(obj, null , 2 ));
注意事项 循环引用 :会抛出 TypeError 错误特殊值处理 :NaN、Infinity、-Infinity 会被转换为 “null”函数和Symbol :会被忽略(对象中)或返回 undefined(单独序列化时)undefined :会被忽略(对象中)或返回 undefined(单独序列化时)Date对象 :会调用 toJSON() 方法,转换为 ISO 日期字符串RegExp对象 :会被转换为 {}toJSON方法 :如果对象有 toJSON() 方法,会使用其返回值JSON.parse()JSON.parse() 方法将 JSON 字符串转换为 JavaScript 对象。
实现原理 验证 JSON 字符串格式 将 JSON 字符串转换为 JavaScript 对象 处理reviver函数(如果提供) 实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 function jsonParse (jsonString, reviver ) { if (typeof jsonString !== 'string' ) { throw new TypeError ('JSON.parse() expects a string argument' ); } jsonString = jsonString.trim(); if (jsonString === '' ) { throw new SyntaxError ('Unexpected end of JSON input' ); } const rx_one = /^[\],:{}\s]*$/ ; const rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g ; const rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g ; const rx_four = /(?:^|:|,)(?:\s*\[)+/g ; if (!rx_one.test( jsonString.replace(rx_two, "@" ) .replace(rx_three, "]" ) .replace(rx_four, "" ) )) { throw new SyntaxError ('Unexpected token in JSON at position' ); } const obj = (new Function ('return ' + jsonString))(); if (typeof reviver === 'function' ) { function walk (key, value ) { if (value && typeof value === 'object' ) { for (const k in value) { if (Object .prototype.hasOwnProperty.call(value, k)) { const newVal = walk(k, value[k]); if (newVal !== undefined ) { value[k] = newVal; } else { delete value[k]; } } } } return reviver(key, value); } return walk('' , obj); } return obj; }
使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const jsonStr = '{"name":"布兰","age":12,"hobbies":["编程","读书"]}' ;const obj = jsonParse(jsonStr);console .log(obj); const jsonStr2 = '{"name":"布兰","age":12,"birth":"2010-01-01"}' ;const obj2 = jsonParse(jsonStr2, function (key, value ) { if (key === 'age' ) return value + 1 ; if (key === 'birth' ) return new Date (value); return value; }); console .log(obj2); const jsonArrayStr = '[1, 2, 3, "hello", true, null]' ;const array = jsonParse(jsonArrayStr);console .log(array);
注意事项 格式验证 :JSON 字符串必须符合严格的 JSON 格式安全问题 :使用 eval 或 new Function 解析 JSON 存在安全风险,应确保输入的 JSON 字符串来自可信来源reviver函数 :可以用来转换解析后的值,如将日期字符串转换为 Date 对象错误处理 :格式不正确的 JSON 字符串会抛出 SyntaxError 错误现代实现 在现代 JavaScript 中,JSON.parse() 是浏览器原生实现的,性能更好且更安全。但了解其实现原理有助于理解 JSON 格式和 JavaScript 对象之间的转换过程。
参考:实现 JSON.stringify 参考:JSON.parse 三种实现方式
函数原型方法 call 使用一个指定的 this 值和一个或多个参数来调用一个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Function .prototype.call2 = function (context ) { var context = context || window ; const fnSymbol = Symbol ('fn' ); context.fnSymbol = this ; var args = []; for (var i = 1 , len = arguments .length; i < len; i++) { args.push('arguments[' + i + ']' ); } var result = eval ('context.fnSymbol(' + args +')' ); delete context.fnSymbol; return result; }
1 2 3 4 5 6 7 8 9 10 11 12 Function .prototype.call3 = function (context ) { if (typeof this !== 'function' ) { throw new TypeError ('Error' ); } context = context || window ; context.fn = this ; const args = [...arguments].slice(1 ); const result = context.fn(...args); delete context.fn; return result; }
apply apply 和 call 一样,唯一的区别就是 call 是传入不固定个数的参数,而 apply 是传入一个数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Function .prototype.apply2 = function (context, arr ) { var context = context || window ; const fnSymbol = Symbol ('fn' ); context.fnSymbol = this ; var result; if (!arr) { result = context.fn(); } else { var args = []; for (var i = 0 , len = arr.length; i < len; i++) { args.push('arr[' + i + ']' ); } result = eval ('context.fnSymbol(' + args + ')' ) } delete context.fnSymbol return result; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Function .prototype.myApply = function (context ) { if (typeof this !== 'function' ) { throw new TypeError ('Error' ); } context = context || window ; context.fn = this ; let result; if (arguments [1 ]) { result = context.fn(...arguments[1 ]); } else { result = context.fn(); } delete context.fn; return result; }
bind bind 方法会创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
bind() 除了 this 外,还可传入多个参数; bing 创建的新函数可能传入多个参数; 新函数可能被当做构造函数调用; 函数可能有返回值; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Function .prototype.bind2 = function (context ) { var self = this ; var args = Array .prototype.slice.call(arguments , 1 ); var fNOP = function ( ) {}; var fBound = function ( ) { var bindArgs = Array .prototype.slice.call(arguments ); return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs)); } fNOP.prototype = this .prototype; fBound.prototype = new fNOP(); return fBound; }
1 2 3 4 5 6 7 8 9 10 11 12 Function .prototype.myBind = function (context ) { if (typeof this !== 'function' ) { throw new TypeError ('Error' ); } var _this = this ; var args = Array .prototype.slice.call(arguments , 1 ); return function ( ) { var bindArgs = Array .prototype.slice.call(arguments ); _this.apply(context, args.concat(bindArgs)); } }
数组原型方法 数组原型方法是 JavaScript 中常用的数组操作方法,下面我们来实现一些核心的数组原型方法。
forEach forEach 方法对数组的每个元素执行一次提供的函数。
实现原理 检查调用对象是否为 null 或 undefined 检查回调函数是否为函数 将调用对象转换为对象 获取数组长度(确保为正整数) 遍历数组,对每个存在的元素调用回调函数 实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 Array .prototype.forEach2 = function (callback, thisArg ) { if (this == null ) { throw new TypeError ('this is null or not defined' ); } if (typeof callback !== "function" ) { throw new TypeError (callback + ' is not a function' ); } const O = Object (this ); const len = O.length >>> 0 ; let k = 0 ; while (k < len) { if (k in O) { callback.call(thisArg, O[k], k, O); } k++; } };
使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const arr = [1 , 2 , 3 , 4 , 5 ];arr.forEach2((item, index, array ) => { console .log(`Item ${index} : ${item} ` ); }); const obj = { multiplier : 2 };arr.forEach2(function (item ) { console .log(item * this .multiplier); }, obj);
map map 方法创建一个新数组,其结果是该数组中的每个元素都调用一次提供的函数后的返回值。
实现原理 检查调用对象和回调函数 创建一个新数组 遍历原数组,对每个元素调用回调函数,并将结果存入新数组 返回新数组 实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 Array .prototype.map2 = function (callback, thisArg ) { if (this == null ) { throw new TypeError ('this is null or not defined' ); } if (typeof callback !== "function" ) { throw new TypeError (callback + ' is not a function' ); } const O = Object (this ); const len = O.length >>> 0 ; const res = new Array (len); let k = 0 ; while (k < len) { if (k in O) { res[k] = callback.call(thisArg, O[k], k, O); } k++; } return res; };
使用示例 1 2 3 4 5 6 const arr = [1 , 2 , 3 , 4 , 5 ];const doubled = arr.map2(item => item * 2 );console .log(doubled); const squared = arr.map2(item => item * item);console .log(squared);
filter filter 方法创建一个新数组,包含通过所提供函数实现的测试的所有元素。
实现原理 检查调用对象和回调函数 创建一个空数组 遍历原数组,对每个元素调用回调函数 如果回调函数返回 true,则将该元素添加到新数组 返回新数组 实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 Array .prototype.filter2 = function (callback, thisArg ) { if (this == null ) { throw new TypeError ('this is null or not defined' ); } if (typeof callback !== "function" ) { throw new TypeError (callback + ' is not a function' ); } const O = Object (this ); const len = O.length >>> 0 ; const res = []; let k = 0 ; while (k < len) { if (k in O) { if (callback.call(thisArg, O[k], k, O)) { res.push(O[k]); } } k++; } return res; };
使用示例 1 2 3 4 5 6 const arr = [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ];const evenNumbers = arr.filter2(item => item % 2 === 0 );console .log(evenNumbers); const greaterThan5 = arr.filter2(item => item > 5 );console .log(greaterThan5);
some some 方法测试数组中是否至少有一个元素通过了由提供的函数实现的测试。
实现原理 检查调用对象和回调函数 遍历数组,对每个元素调用回调函数 如果回调函数返回 true,则立即返回 true 如果遍历完所有元素都没有返回 true,则返回 false 实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 Array .prototype.some2 = function (callback, thisArg ) { if (this == null ) { throw new TypeError ('this is null or not defined' ); } if (typeof callback !== "function" ) { throw new TypeError (callback + ' is not a function' ); } const O = Object (this ); const len = O.length >>> 0 ; let k = 0 ; while (k < len) { if (k in O) { if (callback.call(thisArg, O[k], k, O)) { return true ; } } k++; } return false ; };
使用示例 1 2 3 4 5 6 const arr = [1 , 2 , 3 , 4 , 5 ];const hasEven = arr.some2(item => item % 2 === 0 );console .log(hasEven); const hasGreaterThan10 = arr.some2(item => item > 10 );console .log(hasGreaterThan10);
reduce reduce 方法对数组中的每个元素执行一个由您提供的 reducer 函数,将其结果汇总为单个返回值。
实现原理 检查调用对象和回调函数 处理初始值 遍历数组,对每个元素调用回调函数,更新累加器 返回累加器 实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 Array .prototype.reduce2 = function (callback, initialValue ) { if (this == null ) { throw new TypeError ('this is null or not defined' ); } if (typeof callback !== "function" ) { throw new TypeError (callback + ' is not a function' ); } const O = Object (this ); const len = O.length >>> 0 ; let k = 0 ; let acc; if (arguments .length > 1 ) { acc = initialValue; } else { while (k < len && !(k in O)) { k++; } if (k >= len) { throw new TypeError ('Reduce of empty array with no initial value' ); } acc = O[k++]; } while (k < len) { if (k in O) { acc = callback(acc, O[k], k, O); } k++; } return acc; };
使用示例 1 2 3 4 5 6 7 8 9 const arr = [1 , 2 , 3 , 4 , 5 ];const sum = arr.reduce2((acc, item ) => acc + item, 0 );console .log(sum); const product = arr.reduce2((acc, item ) => acc * item, 1 );console .log(product); const max = arr.reduce2((acc, item ) => Math .max(acc, item));console .log(max);
flat flat 方法创建一个新数组,所有子数组元素递归地拼接到新数组中。
实现原理 检查调用对象 处理深度参数 递归遍历数组,将元素添加到结果数组中 如果元素是数组,则继续递归 实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 Array .prototype.flat2 = function (depth = 1 ) { if (this == null ) { throw new TypeError ('this is null or not defined' ); } const O = Object (this ); const len = O.length >>> 0 ; const depthNum = Number (depth); const actualDepth = depthNum > 0 ? Math .floor(depthNum) : 0 ; const result = []; function flatten (array, currentDepth ) { for (let i = 0 ; i < array.length; i++) { const item = array[i]; if (Array .isArray(item) && currentDepth < actualDepth) { flatten(item, currentDepth + 1 ); } else { result.push(item); } } } flatten(O, 0 ); return result; }; function flattenDeep (arr ) { return arr.reduce((acc, val ) => Array .isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), [] ); }
使用示例 1 2 3 4 5 6 7 8 9 const arr1 = [1 , 2 , 3 , [4 , 5 , [6 , 7 ]]];const flatOnce = arr1.flat2();console .log(flatOnce); const flatTwice = arr1.flat2(2 );console .log(flatTwice); const flatDeep = flattenDeep(arr1);console .log(flatDeep);
性能考虑 forEach、map、filter :时间复杂度为 O(n),其中 n 是数组长度some :时间复杂度最好为 O(1)(找到符合条件的元素),最坏为 O(n)reduce :时间复杂度为 O(n)flat :时间复杂度为 O(n),其中 n 是所有层级元素的总数注意事项 所有方法都不会修改原数组(除了回调函数可能修改) 所有方法都会跳过稀疏数组中的空槽位 回调函数接收三个参数:当前元素、索引、原数组 可以通过 thisArg 参数设置回调函数的 this 值 对于大型数组,应考虑性能影响,避免在回调函数中执行复杂操作 参考:forEach#polyfill