公司动态
JavaScript 核心
一、数据类型基本类型 VS 引用类型基本类型:String/Number/Boolean/Null/Undefined/Symbol/BigInt存在栈中值传递赋值是深拷贝互不影响引用类型Object(包含对象 {}、数组 []、函数 function、Date、RegExp)栈存地址、堆存数据引用传递修改会相互影响赋值是浅拷贝堆VS栈栈存放基本类型、函数执行上下文空间小、自动回收堆存放引用类型空间大、垃圾回收强类型语言VS弱类型语言强类型语言禁止隐式类型转换类型严格、变量类型固定、更健壮如Java、C、TypeScript弱类型语言允许自动隐式转换类型随时可变、易出 BUG如JS 。解释性语言VS编译型语言编译型语言整体编译、生成可执行文件.exe、运行快、平台绑定、编译期报错。C解释型语言逐行翻译、跨平台、开发灵活、运行慢、运行时报错。JS / TS包装类型包装类型就是 JS 为基本类型String/Number/Boolean临时创建的对象使其能调用属性和方法用完即销毁因此基本类型无法添加属性而包装对象可以。内置对象(自带、不用自己定义就能直接用的对象分为三类标准内置对象、包装对象、全局对象。)基础Object Array Function包装String Number Boolean Symbol BigInt工具Math JSON Date RegExp高级Promise Map Set WeakMap WeakSet Error检测方式typeof检测基本类型。null 会误判为 object数组、对象、正则等都返回 object无法区分instanceof基于原型链检测引用类型(原理instanceof 用于判断某个对象是否属于某个构造函数的实例递归查找实例的 _proto_ 原型链判断是否能找到构造函数的 prototype。实现循环获取实例的隐式原型与构造函数显式原型对比找到返回 true到原型链顶端返回 falseObject.prototype.toString.call ()最精准可判断所有类型返回[object Type]constructor容易被修改原型不稳定Array.isArray()专门判断数组undefined VS null1.undefined 表示变量已声明但未赋值null 表示赋值为空代表 “空对象”。2.typeof undefined 为 undefinedtypeof null 为 object原因是 JS 最初实现中会用类型标签标识类型null 在内存中表示为空指针二进制全 0而对象的类型标签恰好也是 000导致 typeof 误判这是一个历史遗留 bug3.转为数字时 undefined 是 NaNnull 是 04.安全获取 undefined 推荐使用 void 0因为 undefined 不是关键字可能被局部变量覆盖隐式类型转换运算 遇到字符串转拼接否则转数字 - * / %一律转数字比较 优先转数字条件判断 if /while 转布尔对象先 valueOf 再 toString 转原始值。BigInt因为Number 类型数值有精度上限无法安全表示和运算超大整数因此提出 BigInt 提案用于表示任意精度的大整数。NaNtypeof NaN 的结果是 numberNaN 是 JavaScript 中唯一一个不等于自身的值isNaN()先把值转为Number再判断是不是 NaNNumber.isNaN() 先判断是不是Number再判断是不是 NaN更准确Object.is() 与、 的区别1. 会隐式转换比较值2. 比较类型值但对 NaN、±0 判断怪异NaN NaN → false0 -0 → true3.Object.is 同 但修复了 两个怪异点Object.is(NaN, NaN) → trueObject.is(0, -0) → false|| 和 的返回值|| 返回第一个真值全假返回最后一个 返回第一个假值全真返回最后一个。两者都返回操作数本身而非布尔值。精度缺失原因JavaScript 采用 IEEE 双精度浮点数标准0.1 和 0.2 在二进制中是无限循环小数存储时会被截断产生精度丢失导致相加不等于 0.3。解决方法1.转成整数运算后再转回小数先乘倍数变成整数运算后再除回去2.使用 toFixed 四舍五入模板字符串用反引号支持直接换行、${} 嵌入变量 / 表达式新增方法includes(str)是否包含指定字符串startsWith(str)是否以指定字符串开头endsWith(str)是否以指定字符串结尾repeat(n)重复 n 次padStart(len, str)前面填充padEnd(len, str)后面填充trimStart() / trimEnd()去除开头 / 结尾空格trim()去除首尾空格conststrhello world;str.includes(world);// truestr.startsWith(hello);// truestr.endsWith(d);// true*.repeat(3);// ***123.padStart(5,0);// 00123123.padEnd(5,0);// 12300数组1.增删改会改变原数组push末尾添加 → 返回新长度pop末尾删除 → 返回被删元素unshift开头添加 → 返回新长度shift开头删除 → 返回被删元素splice(start, count, …items)删除 / 插入 / 替换slice(start, end)截取sort排序reverse反转fill填充concat拼接多个数组join转字符串2.遍历不改变原数组for…in遍历索引 keyfor…of遍历元素值forEach只遍历不能链式无返回map遍历并返回一个新数组可以链式调用常用于数据加工filter过滤返回符合条件的新数组find找第一个符合条件元素找不到返回 undefinedfindIndex找第一个符合条件索引findLast / findLastIndex从后找reduce 累计 arr.reduce((prev,cur) prevcur, 0)some有一个满足就 trueevery全部满足才 trueindexOf从前找索引includes判断是否包含某一项返回布尔值keys / values / entriesfor (const [i,val] of arr.entries()){}3.静态Array.isArray判断是否数组Array.from类数组 → 真数组for…in VS for…offor…in 遍历键名/ 索引key/index能遍历对象会遍历原型链适合对象for…of 遍历值value不能遍历普通对象不遍历原型链适合数组 / Set / Map / 类数组 / 字符串可迭代对象。普通对象不可迭代不能直接用 for…of借助 Object.keys / values / entries 转为数组即可遍历for (const [key, value] of Object.entries(obj)) {}Map VS WeakMap VS Object特性ObjectMapWeakMap键的类型字符串 或 Symbol任意类型对象键的顺序部分有序按插入顺序遍历–长度Object.keys(obj).lengthmap.size无size原型有无无遍历不可直接迭代需先获取键原生可迭代for…of支持 forEach不可遍历性能频繁增删性能一般频繁增删性能更优–引用方式强引用内存泄漏风险较高弱引用可 GC 回收内存泄漏风险极低Map 键类型任意、有序、可直接遍历、高频操作性能更好强引用容易内存泄漏适合动态、键为复杂类型且需要频繁增删 / 维护顺序的键值对场景。Object 键仅限String 或 Symbol、有原型属性、可直接 JSON.stringify适合存储静态的结构化数据WeakMap 键只能是对象是弱引用会被垃圾回收不可遍历、无 size主要用于避免内存泄漏JSON 是一种轻量级、纯文本的数据交换格式是纯字符串键必须双引号常见用途前后端接口数据传输配置文件本地存储 localStorage深拷贝JSON.parse(JSON.stringify(obj))二、深拷贝 vs 浅拷贝浅拷贝定义只拷贝第一层属性引用类型共享地址实现Object.assign 和扩展运算符都是浅拷贝。const newObj {…obj}语法简洁只能用于对象字面量Object.assign(target, obj1, obj2)会修改第一个目标对象并支持一次性合并多个源对象。深拷贝定义完全拷贝所有层级互不影响实现JSON.parse(JSON.stringify(obj))、递归扩展运算符1.数组 / 对象浅拷贝与合并2.函数传参(将数组直接作为函数的多个参数传入)如求max、min3.将类数组转为真数组Array.from()/…const nodeList document.querySelectorAll(‘div’)const nodeArr […nodeList]4.解构快速获取剩余元素解构解构赋值 快速从数组 / 对象中提取值赋值给变量数组解构按位置顺序取值对象解构按属性名匹配都支持默认值、嵌套、剩余参数//默认值const [a, b 20] [10] // a 10, b 20//嵌套const { user: { name } } { user: { name: ‘zs’ } } // name ‘zs’//rest 参数 …变量名用于获取函数的多余参数将其整合为一个数组必须放在参数末尾参数个数不确定时使用特性argumentsrest 参数类型类数组对象不是真数组真正的数组可直接用 map/filter获取获取函数所有传入的参数获取形参之外的剩余参数写法无需声明需要通过 … 显式声明类数组对象定义可以通过下标访问、有 length 属性但不能直接调用数组方法。常见如 arguments、DOM 集合。转换Array.from(arrayLike) / […arrayLike]原因arguments 是设计时只需要索引和 length所以是轻量级类数组对象遍历可用 for 循环、for…of、转数组三、原型链每个函数有 prototype 用于挂载公共属性方法节省内存每个对象都有 _proto_ 指向构造函数的 prototype对象访问属性会通过 proto 逐层向上查找这条链式结构就是原型链原型链最终指向 Object.prototype终点为 nullJS 借助原型与原型链实现继承和方法复用。原型修改在原 prototype 上追加 / 修改方法不破坏原型关系新旧实例通用constructor 正常原型重写直接覆盖整个 prototype 为新对象旧实例失效、constructor 丢失、原原型方法清空需手动挂载 constructor开发中优先使用原型追加修改原型链指向1.实例 _proto_ -自身构造函数的 prototype2.所有引用类型的原型-Object.prototypePerson.prototype._proto_ → Object.prototypeFunction.prototype._proto_ → Object.prototype3.Object.prototype._proto_ null4.构造函数的 _proto_ - Function.prototypeFunction._proto_ → Function.prototypeObject._proto_ → Function.prototype获得对象非原型链上的属性Object.keys (obj) 返回可枚举的属性名数组需包含不可枚举属性用 Object.getOwnPropertyNames (obj)若还需包含 Symbol 键用 Reflect.ownKeys (obj)。new实现原理创建空对象、关联原型将实例的 _proto_ 指向构造函数的 prototype、将构造函数的 this 指向实例执行函数并返回新对象或返回函数返回的对象。返回值规则构造函数无返回值 → 返回新对象构造函数返回原始值 → 返回新对象构造函数返回对象 → 返回该对象覆盖新对象箭头函数VS普通函数箭头函数没有 prototype、没有自己的 this继承外层 this不能修改 this也没有内部构造方法不能被 new 实例化否则会抛出类型错误。普通函数 this 动态绑定指向调用它的对象用 call/apply/bind 可以改变 this。可做构造函数new 生成实例有 prototype。四、变量提升原因变量提升是为了解决函数相互调用和 JS 编译执行阶段的设计会导致变量未赋值就可访问并得到 undefined容易出错。解决var 有提升默认 undefined、无块级作用域、可重复、挂全局let 有块级作用域、存在暂时性死区、不重复const 同 let但必须初始化赋值且栈内容不可改基础类型不能修改引用类型可以修改内部属性但不能改指向函数提升只提升声明尾调用就是函数最后一步是返回另一个函数的调用结果后面不能再有其他操作尾递归是特殊尾调用。引擎可做尾调用优化复用栈帧无论递归多少次调用栈深度始终为 1不会溢出。CommonJSrequire/module.exports、同步加载运行时解析先执行完模块代码再拿到导出结果、浅拷贝、不支持树摇ESMimport/export、异步加载编译时解析适合前端。引用绑定外部导入的值自动更新、支持静态分析和 Tree-Shaking打包可删除未使用代码优化体积。同都实现模块化、作用域隔离、代码复用use strict 是严格模式限制未声明变量、修正 this 指向this undefined、禁止怪异语法、强制代码规范减少隐性 bug提升代码安全性与引擎优化能力五、 闭包内层函数 外层函数变量 闭包函数嵌套内层函数引用外层变量且被外部引用外层执行完毕变量不销毁作用实现变量私有化外部可访问内部变量缓存、柯里化、防抖、节流缺点内存泄漏局部变量仍存在需手动释放引用六、作用域 作用域链作用域变量可访问范围。分为全局页面关闭时销毁、函数、块级作用域GC作用域链变量的查找规则。先在当前作用域查找找不到就逐级向外层父作用域查找一直查到全局作用域。这种层层向上查找变量的链式结构就是作用域链在函数定义时就已确定var 函数作用域let/const 有块级作用域执行上下文是 JS 引擎为代码运行创建的抽象环境代表代码当前正在执行的环境包含了 This 指向、变量存储的变量对象以及访问上级的作用域链。它分为创建确定 This、构建 VO、建立作用域链和执行代码运行两个阶段。会创建全局、函数执行上下文。一个程序只有 一个 全局上下文页面关闭时销毁。可以有 无数个函数执行上下文但同一时间只有一个在运行函数执行完毕后上下文销毁内部变量被垃圾回收。多个执行上下文通过 调用栈 (Call Stack) 来管理遵循后进先出 (LIFO) 原则。this 由函数调用方式决定而非定义位置1.箭头函数无自身 this继承外层作用域的 this2.new 绑定this 指向实例对象3.call/apply/bind 硬绑定fn.call(thisArg, 参数1, 参数2)、fn.apply(thisArg, [参数数组])同立即执行this 绑定为传入对象返回结果和新函数异传参形式不同call 接收零散参数apply 接收数组参数fn.bind(obj)不执行返回新函数永久绑定 this不会被修改4.谁调用指向谁5.函数非严格模式独立函数直接调用this → window / 全局对象严格模式独立调用this → undefined七、 防抖、节流防抖触发后 n 秒内不再触发才执行/多次触发只执行最后一次搜索框输入等你不动了我再执行节流固定时间内只执行一次/多次触发只执行一次滚动、拖拽不管点多快我按固定节奏来// 防抖 设置定时器频繁触发就清空重计时functiondebounce(fn,delay){lettimernull;returnfunction(...args){// 每次触发都清空定时器重新计时if(timer)clearTimeout(timer);//有则清除timersetTimeout((){//没有就添加fn.apply(this,args);timernull;//使用完清除},delay);};}//Lodash 实现npm i lodashconstdebounceSearch_.debounce(函数,延迟毫秒,[配置])//常用配置//leading: true 立即执行之后等待间隔//trailing: true 默认结束后延迟执行// 取消debounceSearch.cancel();// 节流记录时间戳间隔到了才允许执行。functionthrottle(fn,interval){letlastTime0;returnfunction(...args){constnowDate.now();if(now-lastTimeinterval){fn.apply(this,args);lastTimenow;}};}//Lodash 实现constthrottleScroll_.throttle(函数,300);// 绑定滚动事件window.addEventListener(scroll,throttleScroll);React 中必须用 useRef 存防抖 / 节流函数函数组件每次渲染都会重新创建函数实例不加 useRef 防抖节流会失效//搜索框防抖import{useState,useRef}fromreact;import_fromlodash;exportdefaultfunctionSearchInput(){const[keyword,setKeyword]useState();// useRef 缓存防抖函数实例constdebounceFnuseRef(_.debounce((val){// 发起接口请求console.log(请求搜索,val);},500)).current;consthandleChange(e){constvale.target.value;setKeyword(val);debounceFn(val);};returninput value{keyword}onChange{handleChange}placeholder输入搜索/;}}滚动节流import{useEffect,useRef}fromreact;import_fromlodash;exportdefaultfunctionScrollBox(){constthrottleFnuseRef(_.throttle((){console.log(滚动位置,window.scrollY);},200)).current;useEffect((){window.addEventListener(scroll,throttleFn);return(){// 组件卸载取消监听 取消节流window.removeEventListener(scroll,throttleFn);throttleFn.cancel();};},[]);returndiv style{{height:200vh}}滚动页面测试/div;}八、垃圾回收机制1.主要回收算法引用计数每个对象维护一个引用计数器计数器为 0 立即回收。互相引用内存泄漏标记清除从根节点标记可达对象清除不可达现代浏览器主流2.分代回收新生代存放短期使用、生命周期短的对象空间小、回收频繁、速度快算法复制回收采用两个等大空间From / To存活对象复制转移清空原空间老生代长期存活、常驻对象空间大、回收频率低算法标记清除 标记压缩避免内存碎片3.内存泄漏意外全局变量未声明变量挂载 window闭包滥用内层长期引用外层变量无法释放定时器 / 事件监听 未销毁DOM 元素脱离文档仍被 JS 引用循环引用旧引用计数缺陷现代标记清除已解决全局缓存无限累加第三方组件未销毁4.如何避免内存泄漏合理使用 let/const禁用隐式全局定时器、监听事件页面销毁时手动清除闭包合理使用不用的引用手动置空 obj nullDOM 操作后及时解绑引用九、转换普通字符串 ↔ 数组 split() / join()对象/数组 ↔ JSON字符串 JSON.stringify/ JSON.parse对象 - 数组 keys()/values()/entries()十、事件简要介绍事件代理以及什么时候使用事件代理发生在事件处理流程的哪个阶段有什么好处事件代理就是利用事件冒泡机制把事件统一绑定在父元素上由父元素代理子元素触发事件。就是说我们将事件添加到本来要添加事件的父节点将事件委托给父节点来触发处理函数这通常会在 这通常会使用在大量的同级元素需要添加同一类事件的时候比如一个动态的非常多的列表需要为每个列表项都添加 点击事件这时可以使用事件代理通过判断e.target.nodeName来判断发生的具体元素从而判断是否是在 列表项中触发这样的好处是可以减少事件绑定同时动态的DOM结构仍然可以监听代码更简洁。事件代理发生在冒泡阶段。如何阻止事件冒泡和默认事件标准的DOM对象中可以使用事件对象的stopPropagation()方法来阻止事件冒泡 默认事件的话通过事件对象的preventDefault()方法来阻止。如何实现一个前端模板引擎手写简易模板引擎支持变量 条件 循环functionrender(tpl,data){// 替换变量tpltpl.replace(/{{\s*([\w.])\s*}}/g,(_,key){returnkey.split(.).reduce((o,k)o?.[k],data)||;});// 替换条件tpltpl.replace(/{{if\s(.?)}}(.*?){{\/if}}/gs,(_,exp,code){returneval((${exp}))?code:;});// 替换循环tpltpl.replace(/{{each\s(.?)}}(.*?){{\/each}}/gs,(_,arrExp,code){constarreval(arrExp);returnarr.map(itemcode.replace(/{{item}}/g,item)).join();});returntpl;}如何实现懒加载懒加载就是根据用户的浏览需要加载内容这种技术常常用来加载图片的时候使用。我们判断用户是否即将浏览到底部之后进行加载内容 这时候可能会需要加载大量的内容可以使用fragment来优化一下因为大部分是使用滑动和滚轮来触发的因此很有可能会不断触发可以使用函数节流做一个优化防止用户不断触发。functionlazyLoad(){constimgsdocument.querySelectorAll(img[data-src])imgs.forEach(img{constrectimg.getBoundingClientRect()// 图片进入视口if(rect.topwindow.innerHeight){img.srcimg.dataset.src img.removeAttribute(data-src)}})}// 滚动 节流window.addEventListener(scroll,_.throttle(lazyLoad,200))constobservernewIntersectionObserver(entries{entries.forEach(en{if(en.isIntersecting){constimgen.target img.srcimg.dataset.src observer.unobserve(img)}})})document.querySelectorAll(img[data-src]).forEach(imgobserver.observe(img))