JavaScript 中的 bind 函数

bind() 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体。当新函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。一个绑定函数也能使用 new 操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

语法

1
fun.bind(thisArg[, arg1[, arg2[, ...]]])

参数

thisArg : 当绑定函数被调用时,该函数会作为原函数运行时的 this 指向。当使用 new 操作符调用绑定函数时,该参数无效。

arg1, arg2, ... : 当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。

返回值

返回由指定的 this 值和初始化参数改造的原函数拷贝

使用

根据介绍可以知道 bind() 函数有以下功能:

  1. 返回一个新函数,新函数与原函数具有相同的函数体
  2. 新函数的 this 值被绑定到 bind() 函数的第一个参数,并且不能被重写
  3. 可以接受预设的参数提供给原函数
  4. 可以使用 new 操作符创建对象
1
2
3
4
5
6
7
const person = {
name: 'A',
getName(prefix, suffix) {
return `${prefix} ${this.name} ${suffix}`;
},
};
console.log(person.getName('Best', 'in the world')); // Best A in the world

对于 person 对象的 getName() 函数我们使用 bind() 更改其 this 指向:

1
2
3
const getNameBind1 = person.getName.bind({ name: 'B' });
console.log(getNameBind1('Best', 'in the world')); // Best B in the world
console.log(getNameBind1.call({ name: 'C' }, 'Best', 'in the world')); // Best B in the world

上例中,我们将 this 指向对象 { name: 'B' } ,因此 getNameBind1() 函数的 this.name 值应该为 B 。同时 bind() 函数绑定的 this 值不能被重写,我们使用 call() 函数继续改变其 this 的指向,但是从输出看到其值仍为 bind() 函数绑定的值。

1
2
const getNameBind2 = person.getName.bind({ name: 'C' }, 'Best');
console.log(getNameBind2('in the world')); // Best C in the world

上例中我们提前传入了参数 prefix ,从 getNameBind2() 的使用中可以看到能成功输出 prefix 以及 suffix

1
2
3
4
5
6
7
8
9
10
11
12
13
function AddResult(x, y, z) {
this.result = x + y + z;
}

AddResult.prototype.getResult = function() {
return this.result;
};

const AddOneAndTwoResult = AddResult.bind(null, 1, 2);
const addResult = new AddOneAndTwoResult(3);
console.log(addResult); // AddResult { result: 6 }
console.log(addResult.result); // 6
console.log(addResult.getResult()); // 6

上例中我们定义了一个 AddResult 的构造函数,我们成功传入了部分参数并使用 new 操作符创建了 AddResult 对象,同时实例可以使用构造函数上的方法。

实现

返回新函数并更改 this 指向

bind() 函数的调用者是函数,因此我们需要重写 Function.prototype.bind 。对于返回新函数很简单,我们只要 return 一个新函数即可。而更改 this 指向使用 call() 或者 apply() 都可以实现。需要注意的是 call() 或者 apply() 也是 Function.prototype 上的方法,也就是说调用 call() 或者 apply() 需要一个函数方法。根据 this 的指向特性,因此我们需要借用最外层函数的 this ,此处的 this 为调用 bind() 函数的函数。

1
2
3
4
5
6
Function.prototype.bind = function(thisArg) {
const self = this;
return function() {
return self.call(thisArg);
};
};

对于上面的代码当然可以使用箭头函数简化一下:

1
2
3
Function.prototype.bind = function(thisArg) {
return () => this.call(thisArg);
};

我们对前面的 person 函数再进行测试一下:

1
2
3
const getNameBind1 = person.getName.bind({ name: 'B' });
console.log(getNameBind1('Best', 'in the world')); // undefined B undefined
console.log(getNameBind1.call({ name: 'C' }, 'Best', 'in the world')); // undefined B undefined

可以看到我们并没有处理参数的情况。

参数处理

bind() 函数可以接受预设的参数提供给原函数。在 bind() 函数调用时第一个参数为 thisArg ,其他的为原函数的参数项。此外在返回的函数也可以传入参数。因此解决的方式也很简单,在返回的函数处将参数整合并调用。

1
2
3
4
5
6
7
Function.prototype.bind = function(thisArg, ...args) {
return (...argsNested) => this.call(thisArg, ...args, ...argsNested);
};

const getNameBind1 = person.getName.bind({ name: 'B' });
console.log(getNameBind1('Best', 'in the world')); // Best B in the world
console.log(getNameBind1.call({ name: 'C' }, 'Best', 'in the world')); // Best B in the world

现在我们实现的 bind() 函数已经可以处理参数的情况了。

构造函数

new 操作符后跟的函数必须为构造函数,而如果返回箭头函数的话,将不能使用 new 操作符。这是因为箭头函数不能用作构造函数,并且箭头函数没有 prototype 属性。此外 new 操作符会覆盖 bind 绑定的 this 对象,也就是说 new 操作符会将 this 绑定到 new 操作符新创建的对象上。因此我们需要将 call() 调用中的 this 更换为第一个 return 的函数中的 this

1
2
3
4
5
6
7
8
9
Function.prototype.bind = function(thisArg, ...args) {
const self = this;
return function(...argsNested) {
return self.call(this, ...args, ...argsNested);
};
};

console.log(addResult); // { result: 6 }
console.log(addResult.result); // 6

此时我们实现了 bind() 函数的可以使用 new 操作符的功能,但是这里的 this 绑定到了返回函数中,而一般的使用是需要将 this 绑定到 bind() 函数的第一个参数的。那么如何来区分函数的使用方式呢?

区分 bind() 函数的调用方式

bind() 函数的一般使用方式和 new 操作符的使用关键区别在于 new 操作符。

当代码 new Foo(...) 执行时,会发生以下事情:

  1. 一个继承自 Foo.prototype 的新对象被创建。
  2. 使用指定的参数调用构造函数 Foo ,并将 this 绑定到新创建的对象。
  3. 由构造函数返回的对象就是 new 表达式的结果。

因此这也就是我们之前介绍 bind() 函数功能时,new 操作符返回的对象可以使用构造函数上方法的原因。为了更好的表述我们用具名函数来重写一下。

1
2
3
4
5
6
7
Function.prototype.bind = function(thisArg, ...args) {
const self = this;
function fBound(...argsNested) {
return self.call(this, ...args, ...argsNested);
}
return fBound;
};

也就是说我们返回 fBound 函数需要有原构造函数的 prototype 属性,这里最简单的实现也就是继承。于是我们将 fBound 函数继承原函数的 prototype 属性,并在 fBound 函数调用时判断此处的 this 是否有原函数的 prototype 属性,如果有则说明是 new 的调用方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
Function.prototype.bind = function(thisArg, ...args) {
const self = this;
function fBound(...argsNested) {
if (this instanceof fBound) {
return self.call(this, ...args, ...argsNested);
}
return self.call(thisArg, ...args, ...argsNested);
}
if (this.prototype) {
fBound.prototype = Object.create(this.prototype);
}
return fBound;
};

此时我们已经实现了 bind() 函数的全部功能。

与原 bind() 函数的不同

需要注意的是我们实现的 bind() 函数与 polyfill 的实现一致,而与原生函数有所不同。

  • 这部分实现创建的函数有 prototype 属性。(正确的绑定函数没有的)
  • 这部分实现创建的绑定函数所有的 length 属性并不是同ECMA-262标准一致的:它的 length 是0,而在实际的实现中根据目标函数的 length 和预先指定的参数个数可能会返回非零的 length。
1
2
3
4
5
6
7
const AddOneAndTwoResult = AddResult.bind(null, 1, 2);
// polyfill
console.log(AddOneAndTwoResult.prototype); // AddResult {}
console.log(AddOneAndTwoResult.length); // 0
// 原生
console.log(AddOneAndTwoResult.prototype); // undefined
console.log(AddOneAndTwoResult.length); // 1

原生的 bind() 函数返回的 fBound() 函数使用了 Function() 构造函数声明了一个函数,因此使得函数的 length 为正确值,而不是 0。具体可查看 es5-shim 实现


参考资料: