模拟实现 JS 的 bind 方法

为什么要实现一个 bind 函数?

bind() 函数在 ECMA-262 第五版才被加入
它可能无法在所有浏览器上运行,为了世界和平,必要的时候我们要手动实现它

现有 bind 函数的功能?

改造之前要清楚现有 bind() 函数做了哪些事儿

MDN 上找到一些关于它的定义

bind () 方法创建一个新的函数,在调用时设置 this 关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项

语法 function.bind(thisArg[, arg1[, arg2[, ...]]])

函数会创建一个新绑定函数,它包装了原函数对象
ceshiFn.bind(myObject)

绑定函数也可以使用 new 运算符构造,此时提供的 this 值会被忽略,但前置参数(arg1,arg2)仍会提供给模拟函数

1
2
var Fn = ceshiFn.bind(myObject,1,2)
new Fn()

此时 myObject 被忽略 但是 参数依然会传递给 ceshiFn 令其初始化

参数:

  • thisArg:当被绑定的函数被调用时,将它的 this 关键字设置为 thisArg
  • arg1,arg2: 被调用时,这些参数将传递给被绑定的方法

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

继续探索 bind 函数的功能

1
2
3
4
5
6
var obj = {};
console.log('obj',obj)
console.log(typeof Function.prototype.bind) // bind
console.log(typeof Function.prototype.bind()) //bind
console.log(Function.prototype.bind.name) //bind
console.log(Function.prototype.bind().name) // bound

由此我们可以得到得出以下结论

  1. bindFunction 原型链中 Function.prototype 的一个属性,每个函数都可以调用它
  2. bind 本身是一个函数名为 bind 的函数,返回值是一个名为 bound 的函数

下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var obj = {
name: 'yishu',
};
function original(a, b){
console.log(this.name);
console.log([a, b]);
return false;
}
var bound = original.bind(obj, 1);
var boundResult = bound(2); // 'yishu', [1, 2]

console.log(boundResult); // false
console.log(original.bind.name); // 'bind'
console.log(original.bind.length); // 1
console.log(original.bind().length); // 2 返回original函数的形参个数
console.log(bound.name); // 'bound original'
console.log((function(){}).bind().name); // 'bound '
console.log((function(){}).bind().length); // 0
  1. bind 是函数可被传参数,返回值 bound 也是函数,也可以传参数
  2. bind() 绑定的函数的 this 关键字是 bind() 的第一个参数
  3. 传递 bind 的其他参数被接收处理了,bind() 之后返回的函数 bound 函数的参数也被接收处理了,也就是说被合并处理了
  4. 并且 bind() 后的 namebound + 空格 + 调用bind的函数名。如果是匿名函数则是 bound + 空格
  5. bind 后的返回值函数 bound,执行后返回值是原函数(original)的返回值
  6. bind 函数形参(即函数的 length)是 1。bind 后返回的 bound 函数形参根据绑定的函数原函数(original)形参个数确定

到这里 我们根据得出的结论 就可以模拟一个简单版本的 bind 函数了

核心功能的 bindFn 函数

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
26
27
28
29
30
31
32
Function.prototype.bindFn = function(thisArg){

//保证是一个函数调用了bindFn函数
if (typeof this != 'function'){
throw new TypeError(this + 'must be a function');
}

//保存除了thisArg之外的其他形参 转成数组
console.log('bindFn',arguments)
let arg = [].slice.call(arguments ,1);
var self = this;

var bound = function(){
// bind返回的函数 的参数转成数组
var boundArgs = [].slice.call(arguments);
// apply修改this指向,把两个函数的参数合并传给self函数,并执行self函数,返回执行结果
return self.apply(thisArg, arg.concat(boundArgs));
}
return bound;
}

// 测试
var obj = {
name: 'yishu',
age:18
};
function original(){
console.log([].slice.call(arguments))
}
var bound = original.bindFn(obj,3,4,5)
bound(7); // [3, 4, 5, 7]

到这里基本上把 bind 的核心功能写完了,也能够适用大部分场景了 bindFn 只能能做到的只是永久地绑定指定的 this ,但是我们发现 MDN 上关于 bind函数描述 还有一种情况,那就是当你使用 new操作符调用绑定函数时
是这么说的

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

我们可以通过一个实例来看原生的 bind 对于使用 new 的情况是怎么样的

1
2
3
4
5
6
7
8
9
var obj = {
name: 'yishu',
};
function original(){
console.log('this', this.name,[].slice.call(arguments));
}
var bound = original.bind(obj,1);
bound(2,3) //this yishu (3) [1, 2, 3]
new bound(2,3) //this undefined (3) [1, 2, 3]s

此时 this 指向了 new bound() 生成的新对象,所以找不到 nameyishu 的值了,但是参数依然传递的

结论

  • bind 原先指向 obj 的失效了,其他参数有效。
  • new bound 的返回值是以 original 原函数构造器生成的新对象。original 原函数的 this 指向的就是这个新对象

我们看到 又涉及到 new 操作了,写过关于模拟 new 的文章
简单摘要 new 做了什么

1
2
3
4
5
1.创建了一个全新的对象。
2.这个对象会被执行[[Prototype]](也就是__proto__)链接。
3.生成的新对象会绑定到函数调用的this。
4.通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。
5.如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。

所以 ,当使用 new 调用的时候,bind 的返回值函数 bound 内部要模拟实现 new 实现的操作,似曾相识了

bindFn 函数的升级

区分是否是 new 调用 当使用 new 调用需要在 bind 函数返回值函数里实现模拟 new 的操作

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Function.prototype.bindFn = function bind(thisArg){
if(typeof this !== 'function'){
throw new TypeError(this + ' must be a function');
}
// 存储调用bind的函数本身
var self = this;
// 去除thisArg的其他参数 转成数组
var args = [].slice.call(arguments, 1);
var bound = function(){
// bind返回的函数 的参数转成数组
var boundArgs = [].slice.call(arguments);
var finalArgs = args.concat(boundArgs);
// new 调用时,其实this instanceof bound判断也不是很准确。es6 new.target就是解决这一问题的。
//new.target属性允许你检测函数或构造方法是否是通过new运算符被调用的
if(new.target){ //检测函数或构造方法是否是通过new运算符被调用的
// 这里是实现上文描述的 new 的第 1, 2, 4 步
// 1.创建一个全新的对象
// 2.并且执行[[Prototype]]链接
// 4.通过`new`创建的每个对象将最终被`[[Prototype]]`链接到这个函数的`prototype`对象上。
// self可能是ES6的箭头函数,没有prototype,所以就没必要再指向做prototype操作。
if(self.prototype){
// ES5 提供的方案 Object.create()
// bound.prototype = Object.create(self.prototype);
// 但既然是模拟ES5的bind,那浏览器也基本没有实现Object.create()
// 所以采用 MDN ployfill方案 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create
function Empty(){}
Empty.prototype = self.prototype;
bound.prototype = new Empty();
}
// 这里是实现上文描述的 new 的第 3 步
// 3.生成的新对象会绑定到函数调用的`this`。
var result = self.apply(this, finalArgs);
// 这里是实现上文描述的 new 的第 5 步
// 5.如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),
// 那么`new`表达式中的函数调用会自动返回这个新的对象。
var isObject = typeof result === 'object' && result !== null;
var isFunction = typeof result === 'function';
if(isObject || isFunction){
return result;
}
return this;
}
else{
//不使用new操作符时
// apply修改this指向,把两个函数的参数合并传给self函数,并执行self函数,返回执行结果
return self.apply(thisArg, finalArgs);
}
};
return bound;
}

总结

  • bind 是 Function 原型链中的 Function.prototype 的一个属性,它是一个函数,修改 this 指向,合并参数传递给原函数,返回值是一个新的函数。
  • bind 返回的函数可以通过 new 调用,这时提供的 this 的参数被忽略,指向了 new 生成的全新对象。内部模拟实现了 new 操作符。
  • bindFn
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
26
27
28
29
30
31
// 最终版 删除注释 详细注释版请看上文
Function.prototype.bind = Function.prototype.bind || function bind(thisArg){
if(typeof this !== 'function'){
throw new TypeError(this + ' must be a function');
}
var self = this;
var args = [].slice.call(arguments, 1);
var bound = function(){
var boundArgs = [].slice.call(arguments);
var finalArgs = args.concat(boundArgs);
if(new.target){
if(self.prototype){
function Empty(){}
Empty.prototype = self.prototype;
bound.prototype = new Empty();
}
var result = self.apply(this, finalArgs);
var isObject = typeof result === 'object' && result !== null;
var isFunction = typeof result === 'function';
if(isObject || isFunction){
return result;
}
return this;
}
else{
return self.apply(thisArg, finalArgs);
}
};
return bound;
}