一个合格的中级前端工程师须要掌握的 28 个 JavaScript 技巧

前言

文中代码对应的详细注释和具体使用方法都放在个人 github 上,源代码在底部链接node

1.判断对象的数据类型

使用 Object.prototype.toString 配合闭包,经过传入不一样的判断类型来返回不一样的判断函数,一行代码,简洁优雅灵活(注意传入 type 参数时首字母大写)webpack

不推荐将这个函数用来检测可能会产生包装类型的基本数据类型上,由于 call 始终会将第一个参数进行装箱操做,致使基本类型和包装类型没法区分git

2. 循环实现数组 map 方法

使用方法:将 selfMap 注入到 Array.prototype 上(下面数组的迭代方法同理)github

值得一提的是,map 的第二个参数为第一个参数回调中的 this 指向,若是第一个参数为箭头函数,那设置第二个 this 会由于箭头函数的词法绑定而失效web

另外就是对稀疏数组的处理,经过 hasOwnProperty 来判断当前下标的元素是否存在与数组中(感谢评论区的朋友)算法

3. 使用 reduce 实现数组 map 方法

4. 循环实现数组 filter 方法

5. 使用 reduce 实现数组 filter 方法

6. 循环实现数组的 some 方法

执行 some 方法的数组若是是一个空数组,最终始终会返回 false,而另外一个数组的 every 方法中的数组若是是一个空数组,会始终返回 truechrome

7. 循环实现数组的 reduce 方法

由于可能存在稀疏数组的关系,因此 reduce 须要保证跳过稀疏元素,遍历正确的元素和下标(感谢@神三元的提供的代码)编程

8. 使用 reduce 实现数组的 flat 方法

由于 selfFlat 是依赖 this 指向的,因此在 reduce 遍历时须要指定 selfFlat 的 this 指向,不然会默认指向 window 从而发生错误json

原理经过 reduce 遍历数组,遇到数组的某个元素还是数组时,经过 ES6 的扩展运算符对其进行降维(ES5 可使用 concat 方法),而这个数组元素可能内部还嵌套数组,因此须要递归调用 selfFlat数组

同时原生的 flat 方法支持一个 depth 参数表示降维的深度,默认为 1 即给数组降一层维度

传入 Inifity 会将传入的数组变成一个一维数组

原理是每递归一次将 depth 参数减 1,若是 depth 参数为 0 时,直接返回原数组

9. 实现 ES6 的 class 语法

ES6 的 class 内部是基于寄生组合式继承,它是目前最理想的继承方式,经过 Object.create 方法创造一个空对象,并将这个空对象继承 Object.create 方法的参数,再让子类(subType)的原型对象等于这个空对象,就能够实现子类实例的原型等于这个空对象,而这个空对象的原型又等于父类原型对象(superType.prototype)的继承关系

而 Object.create 支持第二个参数,即给生成的空对象定义属性和属性描述符/访问器描述符,咱们能够给这个空对象定义一个 constructor 属性更加符合默认的继承行为,同时它是不可枚举的内部属性(enumerable:false)

而 ES6 的 class 容许子类继承父类的静态方法和静态属性,而普通的寄生组合式继承只能作到实例与实例之间的继承,对于类与类之间的继承须要额外定义方法,这里使用 Object.setPrototypeOf 将 superType 设置为 subType 的原型,从而可以从父类中继承静态方法和静态属性

10. 函数柯里化

使用方法:

柯里化是函数式编程的一个重要技巧,将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术

函数式编程另外一个重要的函数 compose,可以将函数进行组合,而组合的函数只接受一个参数,因此若是有接受多个函数的需求而且须要用到 compose 进行函数组合,就须要使用柯里化对准备组合的函数进行部分求值,让它始终只接受一个参数

借用冴羽博客中的一个例子

11. 函数柯里化(支持占位符)

使用方法:

经过占位符能让柯里化更加灵活,实现思路是,每一轮传入的参数先去填充上一轮的占位符,若是当前轮参数含有占位符,则放到内部保存的数组末尾,当前轮的元素不会去填充当前轮参数的占位符,只会填充以前传入的占位符

12. 偏函数

使用方法:

偏函数和柯里化概念相似,我的认为它们区别在于偏函数会固定你传入的几个参数,再一次性接受剩下的参数,而函数柯里化会根据你传入参数不停的返回函数,直到参数个数知足被柯里化前函数的参数个数

Function.prototype.bind 函数就是一个偏函数的典型表明,它接受的第二个参数开始,为预先添加到绑定函数的参数列表中的参数,与 bind 不一样的是,上面的这个函数一样支持占位符

13. 斐波那契数列及其优化

利用函数记忆,将以前运算过的结果保存下来,对于频繁依赖以前结果的计算可以节省大量的时间,例如斐波那契数列,缺点就是闭包中的 obj 对象会额外占用内存

另外使用动态规划比前者的空间复杂度更低,也是更推荐的解法

14. 实现函数 bind 方法

实现函数的 bind 方法核心是利用 call 绑定 this 指向,同时考虑了一些其余状况,例如

  • bind 返回的函数被 new 调用做为构造函数时,绑定的值会失效而且改成 new 指定的对象
  • 定义了绑定后函数的 length 属性和 name 属性(不可枚举属性)
  • 绑定后函数的 prototype 需指向原函数的 prototype(真实状况中绑定后的函数是没有 prototype 的,取而代之在绑定后的函数中有个 内部属性 [[TargetFunction]] 保存原函数,当将绑定后函数做为构造函数时,将建立的实例的 __proto__ 指向 [[TargetFunction]] 的 prototype,这里没法模拟内部属性,因此直接声明了一个 prototype 属性

15. 实现函数 call 方法

原理就是将函数做为传入的上下文参数(context)的属性执行,这里为了防止属性冲突使用了 ES6 的 Symbol 类型

16. 简易的 CO 模块

使用方法:

run 函数接受一个生成器函数,每当 run 函数包裹的生成器函数遇到 yield 关键字就会中止,当 yield 后面的 promise 被解析成功后会自动调用 next 方法执行到下个 yield 关键字处,最终就会造成每当一个 promise 被解析成功就会解析下个 promise,当所有解析成功后打印全部解析的结果,衍变为如今用的最多的 async/await 语法

17. 函数防抖

leading 为是否在进入时当即执行一次,原理是利用定时器,若是在规定时间内再次触发事件会将上次的定时器清除,即不会执行函数并从新设置一个新的定时器,直到超过规定时间自动触发定时器中的函数

同时经过闭包向外暴露了一个 cancel 函数,使得外部能直接清除内部的计数器

18. 函数节流

和函数防抖相似,区别在于内部额外使用了时间戳做为判断,在一段时间内没有触发事件才容许下次事件触发,同时新增了 trailing 选项,表示是否在最后额外触发一次

19. 图片懒加载

getBoundClientRect 的实现方式,监听 scroll 事件(建议给监听事件添加节流),图片加载完会从 img 标签组成的 DOM 列表中删除,最后全部的图片加载完毕后须要解绑监听事件

intersectionObserver 的实现方式,实例化一个 IntersectionObserver ,并使其观察全部 img 标签

当 img 标签进入可视区域时会执行实例化时的回调,同时给回调传入一个 entries 参数,保存着实例观察的全部元素的一些状态,好比每一个元素的边界信息,当前元素对应的 DOM 节点,当前元素进入可视区域的比率,每当一个元素进入可视区域,将真正的图片赋值给当前 img 标签,同时解除对其的观察

20. new 关键字

21. 实现 Object.assign

Object.assign 的原理能够参考我另一篇博客

22. instanceof

原理是递归遍历 right 参数的原型链,每次和 left 参数做比较,遍历到原型链终点时则返回 false,找到则返回 true

23. 私有变量的实现

使用 Proxy 代理全部含有 _ 开头的变量,使其不可被外部访问

经过闭包的形式保存私有变量,缺点在于类的全部实例访问的都是同一个私有变量

另外一种闭包的实现,解决了上面那种闭包的缺点,每一个实例都有各自的私有变量,缺点是舍弃了 class 语法的简洁性,将全部的特权方法(访问私有变量的方法)都保存在构造函数中

经过 WeakMap 和闭包,在每次实例化时保存当前实例和全部私有变量组成的对象,外部没法访问闭包中的 WeakMap,使用 WeakMap 好处在于当没有变量引用到某个实例时,会自动释放这个实例保存的私有变量,减小内存溢出的问题

24. 洗牌算法

早前的 chrome 对于元素小于 10 的数组会采用插入排序,这会致使对数组进行的乱序并非真正的乱序,即便最新的版本 chrome 采用了原地算法使得排序变成了一个稳定的算法,对于乱序的问题仍没有解决

经过洗牌算法能够达到真正的乱序,洗牌算法分为原地和非原地,图一是原地的洗牌算法,不须要声明额外的数组从而更加节约内存占用率,原理是依次遍历数组的元素,将当前元素和以后的全部元素中随机选取一个,进行交换

25. 单例模式

经过 ES6 的 Proxy 拦截构造函数的执行方法来实现的单例模式

26. promisify

使用方法:

promisify 函数是将回调函数变为 promise 的辅助函数,适合 error-first 风格(nodejs)的回调函数,原理是给 error-first 风格的回调不管成功或者失败,在执行完毕后都会执行最后一个回调函数,咱们须要作的就是让这个回调函数控制 promise 的状态便可

这里还用了 Proxy 代理了整个 fs 模块,拦截 get 方法,使得不须要手动给 fs 模块全部的方法都包裹一层 promisify 函数,更加的灵活

27. 优雅的处理 async/await

使用方法:

无需每次使用 async/await 都包裹一层 try/catch ,更加的优雅,这里提供另一个思路,若是使用了 webpack 能够编写一个 loader,分析 AST 语法树,遇到 await 语法,自动注入 try/catch,这样连辅助函数都不须要使用

28. 发布订阅 EventEmitter

经过 on 方法注册事件,trigger 方法触发事件,来达到事件之间的松散解耦,而且额外添加了 once 和 off 辅助函数用于注册只触发一次的事件以及注销事件

29. 实现 JSON.stringify(附加)

使用 JSON.stringify 将对象转为 JSON 字符串时,一些非法的数据类型会失真,主要表现以下

  • 若是对象含有 toJSON 方法会调用 toJSON
  • 在数组中
    1. 存在 Undefined/Symbol/Function 数据类型时会变为 null
    1. 存在 Infinity/NaN 也会变成 null
  • 在对象中
    1. 属性值为 Undefined/Symbol/Function 数据类型时,属性和值都不会转为字符串
    1. 属性值为 Infinity/NaN ,属性值会变为 null
  • 日期数据类型的值会调用 toISOString
  • 非数组/对象/函数/日期的复杂数据类型会变成一个空对象
  • 循环引用会抛出错误

另外 JSON.stringify 还能够传入第二第三个可选参数,有兴趣的朋友能够深刻了解

实现代码较长,这里我直接贴上对应源代码地址 JSON.stringify

源代码

源代码,欢迎 star,之后有后续的技巧会第一时间添加新的内容

参考资料

JavaScript 专题之函数柯里化

JavaScript专题之函数组合

JavaScript 专题之函数记忆

ES6 系列之私有变量的实现

JavaScript专题之乱序

相关文章
相关标签/搜索