本内容参考 mqyqingfeng 和 MDN 上关于 bind 的实现,进一步深入了解其本质。欢迎学习,不断进步~

看到一个特有意思的话,贴上方便理解:

猫吃鱼,狗吃肉,奥特曼打小怪兽。

有天狗想吃鱼了

猫.吃鱼.call(狗,鱼)

狗就吃到鱼了

猫成精了,想打怪兽

奥特曼.打小怪兽.call(猫,小怪兽)

call

call和apply类似,都是立即执行函数,除了参数列表不一样其他都差不多,主要看你的参数是什么形式

// 简化打印
var log = console.log.bind(console);

// 每个函数都是构造函数 Function 的实例对象,
// bar.call或者bar.apply函数中的this自然也是指向该实例对象bar的
// 模拟call方法,首先传入一个参数,考虑参数的边界值,null的话为window;
// 将子函数当做需要继承的函数一个对象属性,此处的bar作为一个foo的属性;
// 执行完了删除即可;
/**
  var foo = {
      value: 1,
      bar: function() {
        console.log(this.value)
    }
};
**/
Function.prototype.call2 = function(context) {
    var context = context || window;
    context.fn = this;

    // 通过从arguments对象中循环取值放到数组中,解决不定参数问题
    var args = [];
    for (var i = 1; i < arguments.length; i++) {
        args.push(`arguments[${ i }]`)
    }
    // eval 方式待优化
    var result = eval(`context.fn(${args})`); 
    delete context.fn;
    return result;
}

// test
var value = 2;
var foo = {
    value: 1
}

function bar(name, age) {
    log(this.value, name, age) 
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar.call(null); // 2
bar.call2(foo, '小米', 19)

apply

var log = console.log.bind(console);
Function.prototype.apply2 = function(context, arr) {
    var context = context || window;
    context.fn = this;

    var result;
    if (!arr) result = context.fn();
    else {
        var args = [];
        for (var i = 0; i < arr.length; i++) {
            args.push(`arr[${i}]`);
        }
        result = eval(`context.fn(${args})`)
    }

    delete context.fn
    return result;
}

// test
var foo = {
    value: 1
}
function bar() {
    log(this.value, [].slice.call(arguments))

}

bar.apply2(foo, ['kevin', '123']);
}

bind

直接调用bind我们发现会返回一个新函数,这就是它和call & apply不一样的地方,当我不想改变上下文环境立即执行,而是想在回调中执行的时候,bind的方法是你最好的选择。另外bind和call一样,参数是列表形式传递。

// 参考MDN中bind的写法
// 引入了Array.prototype.slice(), Array.prototype.concat(), Function.prototype.call() 和 Function.prototype.apply()
// 返回一个函数、传入不定参数
// 一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
// 也就是说当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效
var log = console.log.bind(console); 
Function.prototype.bind2 = function (context) {
	// 检测调用bind方法的必须是函数
        // !this instanceof Function 写成这种是不是可以
    if (typeof this !== "function") {
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    // 获取bind函数从第二个参数到最后一个参数,这里和call的处理是一致的
    var args = Array.prototype.slice.call(arguments, 1);

	// 直接将fBound.prototype = this.prototype,我们直接修改fBound.prototype的时候,也会影响函数的prototype值被修改。我们通过一个空函数来进行中转
    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
        // 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
        // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    // fNOP.prototype = this.prototype; 就是将 this.prototype 原型对象作为 fNOP.prototype 的原型对象,
    // 也就是 this.prototype 和 fNOP.prototype 指向同一个对象。
    // 像 var f = new fNOP(); 之后找原型链上的属性,就是通过 f.__proto__,因为 f.__proto__ == fNOP.prototype == this.prototype
    // 就会去 this.prototype 上找属性了
    fNOP.prototype = this.prototype;

    fBound.prototype = new fNOP();

    return fBound;
}


var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    return 2333
}

var bindFoo = bar.bind2(foo, 'jiangshan');

var obj = new bindFoo();

log(obj) // {habit: 'shopping'}