JavaScript中bind方法的实现

在讨论bind方法前,咱们能够先看一个例子:javascript

var getElementsByTagName = document.getElementsByTagName;
getElementsByTagName('body');

这样在浏览器(这里使用的是chrome)执行会报错:
图片描述java

缘由也显而易见:上面的getElementsByTagName方法是document.getElementsByTagName的引用,可是在执行时this指向了globalwindow对象,而不是document对象。chrome

解决办法也很简单,使用callbind方法来改变this数组

var getElementsByTagName = document.getElementsByTagName;
getElementsByTagName.call(document, 'body');

浏览器

var getElementsByTagName = document.getElementsByTagName;
getElementsByTagName.bind(document)('body');

上述两种解决办法也能够看出callbind的区别:call方法是直接执行,而bind方法是返回一个新函数。闭包

实现

因为bind方法是从ES5才开始引入的,不是全部浏览器都支持,为了实现兼容,须要本身实现bind方法。app

咱们先来看看bind方法的定义:函数

bind方法会建立一个新函数。当这个新函数被调用时, bind的第一个参数将做为它运行时的 this(该参数不能被重写), 以后的一序列参数将会在传递的实参前传入做为它的参数。
新函数也能使用 new操做符建立对象:这种行为就像把原函数当成构造器,提供的 this值被忽略。

初步思路

  1. 由于bind方法不是当即执行函数,须要返回一个待执行的函数,这里能够利用闭包:return function(){}
  2. 做用域绑定:可使用applycall方法来实现;
  3. 参数传递:因为参数的不肯定性,须要用apply传递数组;

根据上述思路,咱们先来实现一个简单的customBind方法;测试

Function.prototype.customBind = function (context) {
    var self = this,
        /**
         * 因为参数的不肯定性,咱们用 arguments 来处理
         * 这里的 arguments 只是一个类数组对象,能够用数组的 slice 方法转化成标准格式数组
         * 除了做用域对象 self 之外,后面的全部参数都须要做为数组进行参数传递
         */
        args = Array.prototype.slice.call(arguments, 1);
    // 返回新函数
    return function() {
        // 做用域绑定
        return self.apply(context, args);
    }
};

测试第一版

var testFn = function(obj, arg) {
    console.log('做用域对象属性值:' + this.value);
    console.log('绑定函数时参数对象属性值:' + obj.value);
    console.log('调用新函数参数值:' + arg);
}
var testObj = {
    value: 1
};
var newFn = testFn.customBind(testObj, {value: 2});
newFn('hello world');

// 执行结果:
// 做用域对象属性值:1
// 绑定函数时参数对象属性值:2
// 调用新函数参数值:undefined

从测试执行结果能够看出,上面已经实现了做用域绑定,可是返回新函数newFn不支持传参,只能在testFn绑定时传参。
由于咱们最终须要使用的是newFn,因此咱们须要让newFn支持传参。this

动态参数

咱们来继续改造

Function.prototype.customBind = function (context) {
    var fn = this,
        args = Array.prototype.slice.call(arguments, 1);
    return function() {
        // 将新函数执行时的参数 arguments 所有数组化,而后与绑定时传参 arg 合并
        var newArgs = Array.prototype.slice.call(arguments);
        return fn.apply(context, args.concat(newArgs));
    }
};

测试动态参数

var testFn = function(obj, arg) {
    console.log('做用域对象属性值:' + this.value);
    console.log('绑定函数时参数对象属性值:' + obj.value);
    console.log('调用新函数参数值:' + arg);
}
var testObj = {
    value: 1
};
var newFn = testFn.customBind(testObj, {value: 2});
newFn('hello world');

// 执行结果:
// 做用域对象属性值:1
// 绑定函数时参数对象属性值:2
// 调用新函数参数值:hello world

能够看出,绑定时传的参数和新函数执行时传的参数是合并在一块儿造成完整参数的。

原型链

咱们再回到bind方法的定义第二条:新函数也能使用new操做符建立对象。
说明绑定后的新函数被new实例化以后,须要继承原函数的原型链方法,且绑定过程当中提供的this被忽略(继承原函数的this对象),可是参数仍是会使用。因此咱们须要一个中转的函数将原型链传递下去。

首先咱们须要明确new实例化过程,好比说var a = new b()

  1. 建立一个空对象a = {},而且this变量引用指向到这个空对象a
  2. 继承被实例化函数的原型:a.__proto__ = b.prototype
  3. 被实例化方法bthis对象的属性和方法将被加入到这个新的this引用的对象中:b的属性和方法被加入的a里面;
  4. 新建立的对象由this所引用:b.call(a)

接下来咱们实现原型链。

Function.prototype.customBind = function (context) {
    var self = this,
        args = Array.prototype.slice.call(arguments, 1);
    // 建立中转函数
    var cacheFn = function() {};
    var newFn =  function() {
        var newArgs = Array.prototype.slice.call(arguments);
        /**
         * 这里的 this 是指调用时的执行上下文
         * 若是是 new 操做,须要绑定 new 以后做用域,this 指向新的实例对象
         */
        return self.apply(this instanceof cacheFn ? this : context, args.concat(newArgs));
    };

    // 中转原型链
    cacheFn.prototype = self.prototype;
    newFn.prototype = new cacheFn();

    return newFn;
};

测试原型链

function Point(x, y) {
  this.x = x;
  this.y = y;
}

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

var YAxisPoint = Point.customBind({}, 0);
var axisPoint = new YAxisPoint(5);
axisPoint.toString();   // "0,5"

axisPoint instanceof Point; // true
axisPoint instanceof YAxisPoint; // true
new Point(1, 2) instanceof YAxisPoint; // true
相关文章
相关标签/搜索