|

浅谈bind、call和apply的异同及其原理

在 JavaScript 中,修改一个函数的 this 指向,一般有两种方法,一种是通过参数传输,将需要指向的对象传参进去,另一种则是使用 Function 的原型函数,如 bindcallapply

本文将着重介绍 Function 的原型的三个重要函数,即 bind、call 和 apply。并且会尝试手动实现这三个函数,以便探究其原理。

Function 对象

Function 对象是 JavaScript 中非常常用的一个对象,我们定义的任何一个函数,都是 Function 对象的实例。

平时在函数中使用的 arguments 对象也是 Function 原因的一个熟悉。

我们通常调用一个函数:

function a() {
    console.log(this, 'a')
};
function b(b) {
    console.log(b)
}
a.call(b, 'b params');
// 输出 [Function: b] 'a'

以上都很好理解。

bind

bind 函数绑定在 Function.prototype 上,所以是函数的函数。

bind 返回的是一个修改过的函数的引用,所以还需要在调用引用函数才能得到真正的函数返回值。

试着实现一下bind:

function bind(fun, context) {
    if (typeof fun !== 'function') {
        throw "fun must be a function";
    }
    context = context ? Object(context) : window;
    let args = Array.prototype.slice.call(arguments).slice(2);
    // 或者用下面的展开运算符
    // let args = [...arguments].slice(2);
    return function() {
        return fun.apply(context, args.concat([...arguments]))
    }
}

上面实现了一个简单的 bind 方法,这个是一个方法,而不是原型方法,所以这个方法也拥有 bind 方法。调用增方法可是这样的:

function a() {
    console.log(this, 'a')
};
function b(b) {
    console.log(b)
}
let resbind = bind(a, b, 'b params');
resbind();
// 输出 [Function: b] 'a'

原生的 bind 只需要绑定到 prototype 即可。

Function.prototype.bind(context) {
    context = context ? Object(context) : window;
    let self = this;
    let args = Array.prototype.slice.call(arguments).slice(2);
    // 或者用下面的展开运算符
    // let args = [...arguments].slice(2);
    return function() {
        return self.apply(context, args.concat([...arguments]))
    }
}

上面的代码引出了 apply,下面看 apply 。

apply

与 bind 不同的是,apply 方法不是返回引用函数,而是直接调用函数,得到返回值。

我们如果要实现 apply 函数,就不能通过 bind 来修改 this 指向了,这样只会产生循环递归,不会得到结果。所以这里就引出了另一个问题,即 JavaScript 中的 this 指向问题。

默认情况下,全局函数的 this 指向了 window,所以在 bind 中才会有这一句:

context = context ? Object(context) : window;

context 是引用上下文,我们让其默认值为 window,这是符合定义的。

初次之外,如果需要修改一个函数的 this 指向,就只能通过其他上下文的形式来实现了,举个例子:

let person = {
    name: "Tome",
    getName: function () {
        return this.name;
    }
}

persion.getName();
// Tom

请看这个例子中的 this ,person 对象的 this 其所在的函数是 getName,而 getName 所在的上下文是 person 对象,所以 this.name 才是可以访问到的。

基于上面的原理来实现 apply 。

function apply(fun, context) {
    if (typeof fun !== "function") {
        throw "fun must be a function";
    }
    context = context ? Object(context) : window;
    // 先将函数绑定到上下文环境中
    context.fn = fun;
    let result = context.fn(Array.prototype.slice.call(arguments).slice(2));
    // 使用完后删除函数
    delete context.fn;
    return result;
}

测试一下:

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
let slice = apply(Array.prototype.slice, arr, [0, 4]);
console.log(slice);
// 输出 [1, 2, 3, 4]

可以看到输出结果是正确的。

在 JavaScript 中,this 的指向其实就是其所在的上下文环境。在不使用 bind、apply 和 call 方法的情况下,若想修改 this 指向,只能修改其上下文。

call

call 方法与 apply 本质是一样的,只是调用时的传参不同,apply 的参数必须是数组,而 call 的传参是需要直接写入的,可以使用展开运算符。

function call(fun, context) {
    if (typeof fun !== "function") {
        throw "fun must be a function";
    }
    context = context ? Object(context) : window;
    // 先将函数绑定到上下文环境中
    context.fn = fun;
    // call 方法这里传参与 apply 有所不同
    let result = context.fn(...arguments[2]);
    // 使用完后删除函数
    delete context.fn;
    return result;
}

总结

bindapplycall 都是为了改变函数的 this 指向,它们都是函数的函数,bind 返回一个函数引用,apply 和 call 返回函数调用结果。apply 和 call 只是传参形式不同,本质并无区别。

最后需要了解,JavaScript 中的 this 指向的本质还是跟运行时的上下文有关,所以修改 this 指向的本质就是修改其上下文。

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注