徐漂漂

☝️ 一个工作 2 年并无建树的前端搬砖师

0%

apply/call/bind 一网打尽

首先,这三个方法是用来改变 this 指向的,接下来我们看一下它们的异同。

1. apply

  • 调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.apply(A, arguments); 即 A 对象应用 B 对象的方法。
  • 要注意的是第一个参数,如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,而其他原始值则会被相应的包装对象(wrapper object)所替代。

1.1 如何实现一个apply

回顾一下 apply 的效果,我们可以大致按以下思路走

  1. 实现第一个参数的功能,改变 this 指向
  2. 实现第二个参数的功能。第二个参数是作为调用函数的参数
  3. 返回值:使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。

接下来,我们按以上思路来实现一下。

1.1.1 第一步,绑定 this

1
2
3
4
5
6
f.apply(o);

// 与下面代码的功能类似(假设对象o中预先不存在名为m的属性)。
o.m=f; //将f存储为o的临时方法
o.m(); //调用它,不传入参数
delete o.m;//将临时方法删除

(以上代码摘录自犀牛书)
依样画葫芦,我们可以这么写:

1
2
3
4
5
6
7
Function.prototype.apply = function (context) {
// context 就是需要绑定的对象,相当于上面的 o
// this 就是调用了 apply 的函数,相当于 f
context.__fn = this // 假设原先没有__fn
context.__fn()
delete context.__fn
}

1.1.2 第二步,给函数传递参数

接下来我们想办法实现一下 apply 的第二个参数。其实我最快想到的是 ES6 的方法。用... 直接展开就行了。不过 apply 才 ES3😂,还是再想想老的办法吧。

难点是这个数组的长度是不确定的,也就是说我们没办法很准确地给函数一个个传参。我们所能做的处理也就是把arguments转成字符串形式'arguments[1], arguments[2], ...'。那么如何让字符串能运行起来呢??答案就是 eval

稍稍总结一下, 目前想到的 2 种方法

  1. es6。context.__fn(...arguments)
  2. 把 arguments 转换成string,放到 eval 里面运行 eval('context.__fn('+ 'arguments[1], arguments[2]' +')')

以下是第二种思路的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.apply = function (context, others) {
// context 就是需要绑定的对象,相当于上面的 o
// this 就是调用了 apply 的函数,相当于 f
context.__fn = this // 假设原先没有__fn

var args = [];
// args: 'others[0], others[1], others[2], ...'
for (var i = 0, len = others.length; i < len; i++) {
args.push('others[' + i + ']');
}

eval('context.__fn(' + args.toString() + ')')

delete context.__fn
}

1.1.3 第三步,返回值

返回函数调用后的结果就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Function.prototype.apply = function (context, others) {
// context 就是需要绑定的对象,相当于上面的 o
// this 就是调用了 apply 的函数,相当于 f
context.__fn = this // 假设原先没有__fn

var result;

var args = [];
// args: 'others[0], others[1], others[2], ...'
for (var i = 0, len = others.length; i < len; i++) {
args.push('others[' + i + ']');
}

result = eval('context.__fn(' + args.toString() + ')')

delete context.__fn
}

1.1.4 更进一步,严格模式下的 this

我们之前有提到:第一个参数,如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,而其他原始值则会被相应的包装对象(wrapper object)所替代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Function.prototype.apply = function (context, others) {

if (typeof argsArray === 'undefined' || argsArray === null) {
context = window
}

// context 是一个 object
context = new Object(context)

// context 就是需要绑定的对象,相当于上面的 o
// this 就是调用了 apply 的函数,相当于 f
context.__fn = this // 假设原先没有__fn

var result;

var args = [];
for (var i = 0, len = others.length; i < len; i++) {
args.push('others[' + i + ']');
}

result = eval('context.__fn(' + args.toString() + ')')

delete context.__fn
}

1.1.5 再进一步,确保 __fn 不存在

我们之前的代码都是建立在 __fn 不存在的情况下,那么万一存在呢?因此我们接下来就要找一个 context 中没有存在过的属性。
🤔我们很快可以想到 ES6 的 symbol。

1
2
3
// 像这样
var __fn = new Symbol()
context[__fn] = this

🤔如果不用 ES6,那么另一种方法,是根据 这篇文章中提到的,自己用 Math.random() 模拟实现独一无二的 key。面试时可以直接用生成时间戳即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 生成 UUID 通用唯一识别码
// 大概生成 这样一串 '18efca2d-6e25-42bf-a636-30b8f9f2de09'
function generateUUID(){
var i, random;
var uuid = '';
for (i = 0; i < 32; i++) {
random = Math.random() * 16 | 0;
if (i === 8 || i === 12 || i === 16 || i === 20) {
uuid += '-';
}
uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random))
.toString(16);
}
return uuid;
}
// 简单实现
// '__' + new Date().getTime();

如果这个key万一这对象中还是有,为了保险起见,可以做一次缓存操作(就是先把之前的值保存起来)

1
2
3
4
5
6
7
8
// 像这样
var originalvalue = context.__fn
var hasOriginalValue = context.hasOwnProperty('__fn')
context.__fn = this

if(hasOriginalValue){
context.__fn = originalvalue;
}

2. call

  • 和 apply 的作用是一样的,只是 call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组。

  • 例如 func.apply(obj, [1,2]) 相当于 func.call(obj, 1, 2)

2.1 实现一个 call

思路和 apply 一样。唯一区别就在于参数形式。我们按照 call 的要求来处理参数就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Function.prototype.apply = function (context) {
// context 就是需要绑定的对象,相当于上面的 o
// this 就是调用了 apply 的函数,相当于 f
context.__fn = this // 假设原先没有__fn

var result;

var args = [];
// 我们从 arguments[1] 开始拼就好了
for (var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}

result = eval('context.__fn(' + args.toString() + ')')

delete context.__fn
}

3. bind

我们常将 bind 和以上两个方法区分开,是因为 bind 是 ECMAScript 5 中的方法,且除了将函数绑定至一个对象外还多了一些特点。

  • bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的初始参数,供调用时使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func.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

我们还是先大致思考一下该怎么做:

  1. 实现第一个参数的功能,改变 this 指向。这个和 apply/call 是一样的。
  2. 返回值:返回一个新的函数。
  3. 实现其它参数。其它参数将作为新函数的初始参数,供调用时使用。这个和 call 有些相似。
  4. 使用 new 操作符时,应该忽略第一个参数

后续的步骤我会用 apply/call 来实现bind。如果不想直接用 apply/call,也可以按照上文先实现一个 apply/call。

3.1.1 第一步,返回一个绑定了 this 的新函数

1
2
3
4
5
6
Function.prototype.bind = function (context) {
var self = this;
return function () {
return self.apply(context);
}
}

3.1.2 第二步,给新函数设定初始参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.bind = function (context) {
var self = this;

// 获取 bind 函数从第二个参数到最后一个参数
var initialArgs = Array.prototype.slice.call(arguments, 1);

// 返回一个绑定好 this 的新函数
return function () {
// 这个是调用新函数时传入的参数
var boundArgs = Array.prototype.slice.call(arguments);
// 最终的参数应该是初始参数+新函数的参数
return self.apply(context, args.concat(bindArgs));
}
}

3.1.3 第三步,作为构造函数调用时,忽略要绑定的 this

这里的难点是怎么知道是由 new 调用的。
先说一下答案吧

1
2
3
4
// 假如有以下函数
function Person () {
console.log(this)
}

对于 var gioia = new Person() 来说
使用 new 时,this 会指向 gioia,并且 gioia 是 Person 的实例。
因此,如果 this instance Person,就说明是 new 调用的

new 这一部分这里先不展开讲,有兴趣的可以看一下 JavaScript深入之new的模拟实现
接下来我们可以写代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Function.prototype.bind = function (context) {
var self = this;

// 获取 bind 函数从第二个参数到最后一个参数
var initialArgs = Array.prototype.slice.call(arguments, 1);

// 返回一个绑定好 this 的新函数
function Bound() {
// 这个是调用新函数时传入的参数
var boundArgs = Array.prototype.slice.call(arguments);
// 最终的参数应该是初始参数+新函数的参数
return self.apply(this instance Bound ? this : context, args.concat(bindArgs));
}

Bound.prototype = this.prototype
return Bound
}

4. 参考文章