实现原生 bind 函数

bind() 方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind() 方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

看 MDN 上关于bind的用法如下:

  1. 创建绑定函数
    bind() 最简单的用法是创建一个函数,不管怎么调用,函数都有同样的 this 值。
  2. 偏函数
    bind() 的另一个最简单的用法是使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为bind()的参数写在 this 后面。
    例如:
    1
    2
    3
    4
    5
    6
    7
    function addArguments(arg1, arg2) {
    return arg1 + arg2
    }
    var result1 = addArguments(1, 2);
    // 创建一个函数,它拥有预设的第一个参数
    var addThirtySeven = addArguments.bind(null, 37);
    var result2 = addThirtySeven(5); // 37 + 5 = 42

上述37就是预设参数,函数 addThirtySeven 被调用时传入的参数会和之前的预设参数(37)一起传入 addThirtySeven 中。

  1. 作为构造函数使用的绑定函数。
    返回的函数可以作为构造函数使用(也就是可以用 new 创建)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function Point(x, y) {
    this.x = x;
    this.y = y;
    }

    Point.prototype.toString = function() {
    return this.x + ',' + this.y;
    };

    var emptyObj = {};
    var YAxisPoint = Point.bind(emptyObj, 0/*x*/);

    // 可以用 new 创建
    var axisPoint = new YAxisPoint(5);
    axisPoint.toString(); // '0, 5'

    axisPoint instanceof Point; // true
    axisPoint instanceof YAxisPoint; // true

    // 仍然能作为一个普通函数来调用
    YAxisPoint(13);
    emptyObj.x + ',' + emptyObj.y; // '0,13'

所以要是实现一个bind函数需要实现以下几个点:

  1. 创建一个新函数,替换 this 并返回新函数。
  2. 可以设置预设参数并不会丢失预设参数。
  3. 可以当做构造函数调用且有正确的this指向。

首先用猴子补丁

1
Function.prototype.bind = Function.prototype.bind || bind;

实现第一点:创建一个函数并返回新函数,将传入的第一个参数作为函数的 this 指向,使用 apply 或 call 来实现。

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

举例:

1
2
3
4
5
6
7
8
9
function fn() {
console.log(this); // 指向 obj
return Array.prototype.slice.call(arguments);
}
var obj = {
a: 1
}
fn.bind(obj, 1, 2)(); // []
fn.bind(obj, 1, 2)(3); // [3]

实现了绑定指定的 this 并返回新函数,也看到传入别的参数会丢失。接着实现第二点:可以设置预设参数,并且将预设参数以及之后调用的参数按顺序传入原函数。

1
2
3
4
5
6
7
8
9
function bind (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1); // arguments是类数组,把类数组对象转化成数组,因为apply里传的参数是数组。因为第一个arguments第一个参数是context,所以需要截取后面的参数。
return function() {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs); // 将 bind 传入的参数和调用返回函数传入的参数一起传入原函数中
return self.apply(context, finalArgs)
}
}

举例:

1
2
3
4
5
6
7
function fn() {
return Array.prototype.slice.call(arguments) ;
}
// args是[1,2] innerArgs是[]
fn.bind(null, 1, 2)(); // [1,2]
// args是[1,2] innerArgs是[3]
fn.bind(null, 1, 2)(3); // [1,2,3]

第三点:bind 绑定的函数可以当做构造函数来使用。
那现在如果用 new 创建的话会是什么情况呢?
举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Point(x, y) {
this.x = x;
this.y = y;
}
var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 0);

var axisPoint = new YAxisPoint(5);
axisPoint.x // undefined
axisPoint.y // undefined

emptyObj.x // 0
emptyObj.x // 5

axisPoint instanceof Point; // false
axisPoint instanceof YAxisPoint; // true axisPoint是YAxisPoint的实例,但不是Point的实例

可以看到,用 new 创建对象实例时,并没有将构造函数的 this 指向实例化对象,bind 绑定的还是 emptyObj,目前和普通函数调用没有区别。
new YAxisPoint()时又做了什么呢?var a = new A(),new 一个对象主要做了这些操作:

  1. 创建一个对象。
  2. 将对象的 [[prototype]] 属性指向构造函数的原型对象上(一定是构造函数的原型对象)。
  3. 把新对象绑定到构造函数的 this。
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

所以,如果用 new 调用的时候,执行第二步会将新对象的 [[prototype]] 属性指向构造函数(YAxisPoint)的原型对象,所以axisPoint instanceof YAxisPoint是 true,而构造函数 YAxisPoint() 是

1
2
3
4
5
return function() {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return self.apply(context, finalArgs)
}

context 是调用 bind 时传进去的 empty,调用 Point 函数的还是 empty,也就是说上下文指向的是传入的empty,并没有把新对象 axisPoint 绑定上,所以会发现

1
2
3
4
emptyObj.x // 0
emptyObj.x // 5
axisPoint.x // undefined
axisPoint.y // undefined

所以我们现在要做的是,如果是 new 创建的对象,需要把调用函数的 this 指向新对象(也就是构造函数 this,看下面分析),那怎么判断是 new 调用的构造函数呢?我们来打印一下调用时 this 的指向。加上 new 调用的判断:

1
2
3
4
5
6
7
8
9
10
11
function bind (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var resultFunc = function() {
console.log(this); // 打印this
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return self.apply(context, finalArgs)
}
return resultFunc;
}

发现如果是普通函数调用,this 指向的是全局的 Window 对象 Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …},如果是 new 调用,this 指向的是 resultFunc 这个函数resultFunc {}
再反过来看 new 一个对象的过程第三步:把新对象绑定到构造函数的 this,也正好说明了这点:new调用时,this指向的是构造函数的this。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function bind (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var resultFunc = function() {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
if (this instanceof resultFunc) {
return self.apply(this, finalArgs)
} else {
return self.apply(context, finalArgs)
}
}
return resultFunc;
}

看到axisPoint可以正常赋值,但是axisPoint instanceof Point为 false

1
2
3
axisPoint.x // 0
axisPoint.y // 5
axisPoint instanceof Point // false

还需要将构造函数的原型指向原本调用函数 self 的原型,所以需要加上resultFunc.prototype = self.prototype。而一般实现这种继承都需要加入一个中间函数,否则改变 resultFunc.prototype 会改变 self.prototype 的值。所以最终代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function bind (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var resultFunc = function() {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
if (this instanceof resultFunc) {
return self.apply(this, finalArgs)
} else {
return self.apply(context, finalArgs)
}
}
var F = function () {};
F.prototype = self.prototype;
resultFunc.prototype = new F();
return resultFunc;
}