# 👉 call、apply、bind 的实现

# 三者的联系与区别

  • 三者都会改变函数执行的上下文,即改变函数运行时的 this 指向。

    三者的第一个参数都是改变上下文所指向的对象。当传 null/undefined 会自动指向全局对象 window,传原始值(数字,字符等)会指向它们的自动包装对象。

  • call(Fun, arg1, arg2..)apply(Fun, [..args])的区别在于:
    call 方法接受的是若干个参数的列表,而 apply 方法接受的是一个包含多个参数的数组。

  • bind(Fun, param1, param2, ...)方法与 call/apply 最大的不同是:
    前者返回的是一个绑定上下文的函数,需要手动调用,而后两者是直接执行了函数。

# call/apply 的模拟实现

call、apply 和 bind 是挂在 Function 对象上的三个方法,只有函数才有这些方法。

call/apply 需要做的事情:改变调用函数的 this 指向,执行函数,让指定的对象接受并拥有参数。分步骤来:

  1. 判断指定对象是否为null/undefined,是指向window
  2. 获取若干个参数列表(类数组);
  3. 将调用函数作为指定对象属性 fn 的值,通过此方法改变调用函数重的 this 值;执行 fn 为指定对象增加属性方法,并删除该属性。
    (利用原理:函数作为一个对象的属性,被这个对象自身调用时,this 指向当前对象。)

这里也主要参考了大神冴羽 (opens new window)的文章:

  • call 的模拟实现
Function.prototype._call = function(ctx, ...arguments) {
    // ctx为null/undefined时,this将指向全局对象window
    ctx = ctx || window;

    ctx.fn = this;

    const result = ctx.fn(...arguments);

    // 若入参只有一个ctx, 则需分离开 指定this对象 和 参数列表
    // 获取传入的额外参数 var args = [arguments[1], arguments[2], ...]
    // var args = [];
    // for (var i = 1; i < ctx.length; i++) {
    //     args.push("arguments[" + i + "]");
    // }
    // var result = eval("ctx.fn(" + args + ")");

    delete ctx.fn;
    return result;
};

function bar(name, age) {
    const value = this.value;
    return { value, name, age };
}

var value = "window";

const obj = { value: "obj" };

console.log("test1: ", bar._call(null));
//test1: {value: "window", name: undefined, age, undefined}

console.log("test2: ", bar._call(obj, "obj", 1));
//test2: {value: "obj", name: "obj", age: 1}
  • apply 的模拟实现

apply 和 call 的模拟实现方式差不多,稍有区别的地方是传参的方式为一个包含了多个参数数组。

Function.prototype._apply = function(ctx, argArr) {
    ctx = ctx || window;

    ctx.fn = this;

    let result;

    if (!argArr || !argArr.length) {
        result = context.fn();
    } else {
        result = context.fn(...argArr);
    }

    delete ctx.fn;
    return result;
};

function bar(name, age) {
    const value = this.value;
    return { value, name, age };
}

var value = "window";
const obj = { value: "obj" };

console.log("test1: ", bar._apply(null));
//test1: {value: "window", name: undefined, age, undefined}

console.log("test2: ", bar._apply(obj, ["obj", 1]));
//test2: {value: "obj", name: "obj", age: 1}

# bind 的模拟实现

bind 的模拟实现会较前两者复杂,bind 的特点:

  1. 支持若干个参数,也支持分开传参数;
  2. 会返回一个新的函数(形成闭包)。

先来看看原生 bind 的用法:

// 直接调用
function bar(name, age) {
    return {
        value: this.value,
        name: name,
        age: age,
    };
}

var value = "window";
const foo = {
    value: "foo",
};

const barFoo = bar.bind(foo, "jack");
console.log(barFoo(30));
// { value: "foo", name: "jack", age: 30 }

对于以上的特点,来个 bind 的简易版实现:

Function.prototype._bind = function(ctx, ...bindArgs) {
    ctx = ctx || window;
    const fn = this;
    return function(...args) {
        return fn.apply(ctx, bindArgs.concat(args));
    };
};

以为这就结束了?还没还没......

需特别注意的是:
对于 bind 返回的函数有两种调用方式,直接调用通过 new 的方式

来自于 MDN 关于 bind 的原话

绑定函数自动适应于使用 new 操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前。

原话直白翻译就是我们可以把 bind 返回的函数当做构造函数去用 new 操作符创建实例,但这个时候 bind 传入的 this 指向会失效(因为按照 new 原理来说 this 会被指向新的实例对象),但是传入的参数还是有效的。

// new方式
function Student(name, age) {
    this.habit = "running";
    this.name = name;
    this.age = age;
    console.log({ grade: this.grade, name, age });
}

Student.prototype.job = "study";

var grade = "window";
var grade1 = { grade: "grade1" };
var grade1Student = Student.bind(grade1);

var student1 = grade1Student("jack", 11);
// {grade: "grade1", name: "jack", age: 11}
console.log(student1.job); //Cannot read property 'job' of undefined

var newStudent = new grade1Student("chichi", 10);
// {grade: undefined, name: "chichi", age: 10}

console.log(newStudent);
// Student {habit: "running", name: "chichi", age: 10}

// 对bind返回的函数,直接调用:student1可以获取到 grade 值。
// 对bind返回的函数,new式调用:返回了 undefind。说明绑定的this会因为new时重新被指向新实例而失效。

console.log(
    newStudent.name,
    newStudent.habit,
    newStudent.job,
    newStudent.grade
);
//chichi running study undefined

接着,继续完善,bind 的第二版模拟实现:

Function.prototype._bind = function(ctx, ...bindArgs) {
    // 获取指定的上下文对象,即新的this指向
    ctx = ctx || window;

    // 获取调用函数上下文内容
    const fn = this;

    // 重新绑定指定上下文对象的函数
    const fBind = function(...args) {
        // 1. 当_bind返回的函数,通过new调用时(fBind作为构造器),this 会被指向实例对象
        // 存在关系:实例 instanceof fBind,此时实例对象还能获取到绑定至self的值和原型链上的值

        // 2. 当_bind返回的函数,被直接调用时,this指向ctx/window

        if (this instanceof fBind) {
            return fn.apply(this, bindArgs.concat(args));
        } else {
            return fn.apply(ctx, bindArgs.concat(args));
        }
    };

    // 重新绑定指定上下文对象的函数,需继承调用函数的原型链
    fBind.prototype = Object.create(fn.prototype);

    // 不能直接 fBind.prototype = this.prototype,否则当修改fBind.prototype属性值时,也会影响fn.prototype属性

    return fBind;
};

# 三者的应用场景

  • 数组最大/小值 Math.max.apply(Math, array)
  • 数组合并 [].push.apply(arr1, arr2)
  • 将类数组转成数组 Array.prototype.slice.call(arguments)