JS进阶--bind原理及实现

“Programmers are in a race with the Universe to create bigger and better idiot-proof programs, while the Universe is trying to create bigger and better idiots. So far the Universe is winning.” —Rich Cook

官方描述

首先我们先来看看MDN上对于bind函数的定义,主要有三个特点:

  1. bind函数会创建一个新函数(称为绑定函数),新函数与原函数具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)
  2. 当生成的新函数被调用执行时,其this值始终指向bind函数的第一个参数且无法改变
  3. bin函数可以接受预设的参数,该参数最终提供给原函数
  4. 新函数也能使用new操作符创建对象,这种行为就像把原函数当成构造器。提供的this值被忽略,同时调用时的参数被提供给模拟函数

用例说明

众所周知,JS中函数(ES6箭头函数除外)的this指向是在函数执行时动态绑定的,函数定义和实际运行时的所处的环境不一样,往往导致未知的bug。 尤其是在React组件中,经常会由于this指向问题导致无法调用到函数,所以通常我们会在constructor函数中使用bind函数来绑定this(当然一般我们都会使用箭头函数来避免此类情况)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var name = "paopaolee"
function test(){
console.log(this.name)
}
var obj = {
name: "leepaopao",
print: test,
}
test(); // paopaolee
obj.print(); // leepaopao
var obj1 = {
name: 'xxxxxx',
print: test.bind(this) // 返回了一个新函数并且this指向window
}
obj1.print(); // paopaolee

动手实现

第一版

目标:实现描述中的特征1、2、3

分析:返回的结果是一个函数;新函数调用时this指向第一个参数, 可以通过call/apply函数解决,call/apply函数的区别在于参数传递,前者需逐一列出,后者可以传递数组;bind函数可接受预设参数最终提供原函数,由于参数不确定,所以选用apply实现。

实现:

1
2
3
4
5
6
7
8
Function.prototype.paopaoleeBind = function (Othat) {
var slice = Array.prototype.slice;
var self = this; //普通调用时, this指向bind调用者
var args = slice.call(arguments, 1);
return function () {
self.apply(Othat, args);
}
}

第二版

目标:第一版中初步实现了前三个特征,当然还有诸多问题,例如如果新函数执行时需要有返回值、第一版中实现了绑定时传参,那新函数调用时传参数呢?以及bind函数调用不正确等等。

优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.paopaoleeBind = function (Othat) {
var slice = Array.prototype.slice;
var toString = Array.portotype.toString;
var self = this;
// 解决: 非法调用
if(typeof self !=== 'function' || toString.call(self) !== '[object Function]'){
throw new TypeError("incorrectly call");
}
var args = slice.call(arguments, 1);
return function () {
// 解决: 新函数执行时return返回值
return self.apply(Othat, args.concat(slice.call(arguments))); // 解决: 新函数执行可接受参数
}
}

第三版

目标: 解决bind函数的第三个特征:

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

也就是说调用bind返回的新函数作为构造函数执行时,bind时指定的对象(参数Othat)会失效,但是传入的参数依然有效。

举个🌰:

1
2
3
4
5
6
7
8
9
10
11
12
13
function female(name, age){
this.favorite = 'shopping';
console.log('xxx', this instanceof female);
console.log(name, age);
}

var boundFunc = female.bind(obj, 'paopaoliu');
var p = new boundFunc(23);

/*****
xxx true
paopaolee 23
*****/

尽管我们执行了 female.bind(obj, 'paopaoliu') ,但最终 this instanceof femaletrue

分析: 要判断新函数调用时是否为new构造函数调用,可以通过this是否为新函数的实例,所以我们把返回的匿名函数换成具名函数,以便判断。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Function.prototype.paopaoleeBind = function (Othat) {
var slice = Array.prototype.slice;
var toString = Array.portotype.toString;
var self = this;
// 解决: 非法调用
if(typeof self !== 'function' || toString.call(self) !== '[object Function]'){
throw new TypeError("incorrectly call");
}
var args = slice.call(arguments, 1);
function boundFunc() {
// 判断this是否为boundFunc实例,如果是说明是new构造函数调用,否则是普通调用
var Othis = this instanceof boundFunc ? this : Othat;
// 解决: 新函数执行时return返回值
return self.apply(Othis, args.concat(slice.call(arguments))); // 解决: 新函数执行可接受参数
}
return boundFunc;
}

用上面的🌰测试下:

1
2
3
4
5
6
7
8
9
10
11
12
function female(name, age){
this.favorite = 'shopping';
console.log('xxx', this instanceof female);
console.log(name, age);
}
var boundFunc = female.paopaoleeBind(null, 'paopaoliu');
var p = new boundFunc(23);

/*****
xxx false
paopaoliu 23
*****/

显然,此时 this 指向的是 boundFunc 的实例。female 的原型链被破坏了;

第四版

目标:重塑原型链
实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.paopaoleeBind = function (Othat) {
var slice = Array.prototype.slice;
var toString = Array.portotype.toString;
var self = this;
// 解决: 非法调用
if(typeof self !=== 'function' || toString.call(self) !== [object Function]){
throw new TypeError("incorrectly call");
}
var args = slice.call(arguments, 1);
function boundFunc() {
// 判断this是否为boundFunc实例,如果是说明是new构造函数调用,否则是普通调用
var Othis = this instanceof boundFunc ? this : Othat;
// 解决: 新函数执行时return返回值
return self.apply(Othis, args.concat(slice.call(arguments))); // 解决: 新函数执行可接受参数
}
boundFunc.prototype = this.portotype;
return boundFunc;
}

上述代码,重塑原型链的做法是直接将boundFunc.prototype = this.prototype,相当于直接用原函数的prototype覆盖掉了boundFunc函数原来的prototype,这样如果我们更改boundFunc.prototype中的属性是也会影响原函数的prototype上的属性。

最终版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Function.prototype.paopaoleeBind = function (Othat) {
var slice = Array.prototype.slice;
var toString = Array.portotype.toString;
var self = this;
// 解决: 非法调用
if(typeof self !=== 'function' || toString.call(self) !== [object Function]){
throw new TypeError("incorrectly call");
}
var args = slice.call(arguments, 1);
function boundFunc() {
// 判断this是否为boundFunc实例,如果是说明是new构造函数调用,否则是普通调用
var Othis = this instanceof boundFunc ? this : Othat;
// 解决: 新函数执行时return返回值
return self.apply(Othis, args.concat(slice.call(arguments))); // 解决: 新函数执行可接受参数
}
// 重塑原型链: 方法一
boundFunc.prototype = Object.create(this.portotype);
// 重塑原型链: 方法二
/****
function fNOP (){}
fNOP.prototype = this.prototype;
fbound.prototype = new fNOP();
****/
return boundFunc;
}