模拟实现 call 和 apply
基础
首先来认识一下 call 和 apply,它们都是 Function.prototype 上的方法,也就是说,所有函数都拥有的方法。
作用都是用来显示绑定函数内部的上下文 this 的指向,区别仅在于两者对参数的处理不同,一个接收参数列表,一个接收参数数组。列出 MDN 的地址:
示例
1 | function b(a, b) { |
所以 call 和 apply 的执行效果其实是一样的,区别就在于接收参数的形式,是参数列表,还是参数数组。
应用场景
- 调用原型方法
1 | // 如果某对象覆盖了原型上某个方法,那么调用该方法一直是走对象上的逻辑,此时如果有需求要走父类逻辑,可通过 call,类似于其他语言的 super |
- 解决参数列表和参数数组问题
1 | var a = [1, 2, 3, 5, 3]; // 对数组数据求最大值 |
- 显示指定函数内的上下文 this 指向
1 | var a = 1; |
模拟实现
Function.prototype.call(thisArg, arg1, arg2, …)
要想模拟实现 call,必须得先掌握几个关键点:
- call 接收的参数形式和含义,及 thisArg 对 null,undefined,基本类型的特殊处理
- call 本质上是函数的另一种调用,只是修改了函数内的 this
这两点是关键,展开讲的话,也就是我们要自己实现这些工作:
- 接收不定长的参数列表,第一个参数 thisArg 表示函数内 this 指向
- 当 thisArg 值为 null 或 undefined 时,在非严格模式下,替换成全局对象,如 window
- 当 thisArg 值为其他基本类型,如 number,boolean 等时,在非严格模式下,自动进行包装对象转换 Object(thisArg)
- 第二个参数开始的剩余参数列表依次传给函数
- 触发函数的执行
- 修改函数的 this 指向第一个参数经过处理后的值
贴代码前,先来大概讲讲各个工作的实现方案:对第一个参数 thisArg 的处理,也就是进行各种判断各种处理即可;获取剩余参数列表,可以用 ES6 的扩展运算符;触发函数执行,也就是调用一下函数即可;
那么,还剩下最后一点,如何模拟实现修改函数内的 this 指向呢?
这就涉及到 this 绑定的各种方式了,文末有推荐文章,感兴趣可以去看看,这里就大概说一说:
- 默认绑定(如普通函数内的 this 默认绑定到 window)
- 隐式绑定(如将函数赋值给某个对象,以对象的方法来调用该函数,this 会绑定到该对象上)
- 显示绑定(call, apply, bind, Reflect.apply)
- new 绑定(当函数和 new 使用时会被当做构造函数,构造函数内部的 this 会绑定到内部新创的对象上)
- 箭头函数的绑定(绑定到箭头函数定义时的上下文)
这五种方式中,可以分成两类:绑定的对象是特定对象或任意对象。其中,隐式绑定和显示绑定属于后者,而我们想要模拟实现 call,自然就不能再使用显示绑定了,那只剩下从隐式绑定方案去解决了。
只要把经过函数挂载到经过处理后的 thisArg 对象上,然后以对象的方法形式调用,就可以完成修改函数内 this 指向的效果了。
1 | Function.prototype.call2 = function(thisArg, ...args) { |
至于如何判断函数内部是否有开启了严格模式,这点就不知道怎么实现了。
Function.prototype.apply(thisArg, [argsArray])
apply 跟 call 本质上是一样的,区别仅在于对参数的接收形式不同,直接看模拟实现的代码:
1 | Function.prototype.apply2 = function(thisArg, args) { |
思考
思考
上面的模拟是否有问题?能否100%模拟?
很难 100% 模拟,我们顶多只能挑一些重要的功能来模拟实现,上面的模拟实现当然也有很多问题,用到 ES6 新特性这点先不讲。其他的问题,比如:
- 没有考虑 Node.js 环境,没有考虑严格模式的处理
- 未发现的坑
这些也都是可以解决的,但处理起来就麻烦一些,可以参考文末的文章。反正,大概清楚 call 和 apply 的工作职责,能把主要的工作模拟实现出来,也就差不多了。不过追求 100% 也是好事,望你加油!