首先,这三个方法是用来改变 this 指向的,接下来我们看一下它们的异同。
1. apply
- 调用一个对象的一个方法,用另一个对象替换当前对象。例如:
B.apply(A, arguments)
; 即 A 对象应用 B 对象的方法。 - 要注意的是第一个参数,如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,而其他原始值则会被相应的包装对象(wrapper object)所替代。
1.1 如何实现一个apply
回顾一下 apply 的效果,我们可以大致按以下思路走
- 实现第一个参数的功能,改变 this 指向
- 实现第二个参数的功能。第二个参数是作为调用函数的参数
- 返回值:使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。
接下来,我们按以上思路来实现一下。
1.1.1 第一步,绑定 this
1 | f.apply(o); |
(以上代码摘录自犀牛书)
依样画葫芦,我们可以这么写:
1 | Function.prototype.apply = function (context) { |
1.1.2 第二步,给函数传递参数
接下来我们想办法实现一下 apply 的第二个参数。其实我最快想到的是 ES6 的方法。用...
直接展开就行了。不过 apply 才 ES3😂,还是再想想老的办法吧。
难点是这个数组的长度是不确定的,也就是说我们没办法很准确地给函数一个个传参。我们所能做的处理也就是把arguments
转成字符串形式'arguments[1], arguments[2], ...'
。那么如何让字符串能运行起来呢??答案就是 eval
!
稍稍总结一下, 目前想到的 2 种方法
- es6。
context.__fn(...arguments)
- 把 arguments 转换成string,放到 eval 里面运行
eval('context.__fn('+ 'arguments[1], arguments[2]' +')')
以下是第二种思路的代码:
1 | Function.prototype.apply = function (context, others) { |
1.1.3 第三步,返回值
返回函数调用后的结果就行:
1 | Function.prototype.apply = function (context, others) { |
1.1.4 更进一步,严格模式下的 this
我们之前有提到:第一个参数,如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,而其他原始值则会被相应的包装对象(wrapper object)所替代
1 | Function.prototype.apply = function (context, others) { |
1.1.5 再进一步,确保 __fn 不存在
我们之前的代码都是建立在 __fn
不存在的情况下,那么万一存在呢?因此我们接下来就要找一个 context
中没有存在过的属性。
🤔我们很快可以想到 ES6 的 symbol。
1 | // 像这样 |
🤔如果不用 ES6,那么另一种方法,是根据 这篇文章中提到的,自己用 Math.random() 模拟实现独一无二的 key。面试时可以直接用生成时间戳即可。
1 | // 生成 UUID 通用唯一识别码 |
如果这个key万一这对象中还是有,为了保险起见,可以做一次缓存操作(就是先把之前的值保存起来)
1 | // 像这样 |
2. call
和 apply 的作用是一样的,只是
call()
方法接受的是一个参数列表,而apply()
方法接受的是一个包含多个参数的数组。例如
func.apply(obj, [1,2])
相当于func.call(obj, 1, 2)
2.1 实现一个 call
思路和 apply 一样。唯一区别就在于参数形式。我们按照 call 的要求来处理参数就可以了:
1 | Function.prototype.apply = function (context) { |
3. bind
我们常将 bind 和以上两个方法区分开,是因为 bind 是 ECMAScript 5 中的方法,且除了将函数绑定至一个对象外还多了一些特点。
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的初始参数,供调用时使用。
1
2
3
4
5
6
7
8
9func.apply(obj, [1,2])
// 相当于
func.call(obj, 1, 2)
// 相当于
var boundFun = func.bind(obj, 1, 2)
boundFun()
// 也可以这样
var boundFun = func.bind(obj, 1)
boundFun(2)绑定函数也可以使用 new 运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的 this 值会被忽略,但前置参数仍会提供给模拟函数。
3.1 实现一个 bind
我们还是先大致思考一下该怎么做:
- 实现第一个参数的功能,改变 this 指向。这个和 apply/call 是一样的。
- 返回值:返回一个新的函数。
- 实现其它参数。其它参数将作为新函数的初始参数,供调用时使用。这个和 call 有些相似。
- 使用 new 操作符时,应该忽略第一个参数
后续的步骤我会用 apply/call 来实现bind。如果不想直接用 apply/call,也可以按照上文先实现一个 apply/call。
3.1.1 第一步,返回一个绑定了 this 的新函数
1 | Function.prototype.bind = function (context) { |
3.1.2 第二步,给新函数设定初始参数
1 | Function.prototype.bind = function (context) { |
3.1.3 第三步,作为构造函数调用时,忽略要绑定的 this
这里的难点是怎么知道是由 new 调用的。
先说一下答案吧
1 | // 假如有以下函数 |
对于
var gioia = new Person()
来说
使用 new 时,this 会指向 gioia,并且 gioia 是 Person 的实例。
因此,如果this instance Person
,就说明是 new 调用的
new 这一部分这里先不展开讲,有兴趣的可以看一下 JavaScript深入之new的模拟实现
接下来我们可以写代码了:
1 | Function.prototype.bind = function (context) { |