Call 和 apply 实现原理

278 阅读5分钟

写在前面

大家都知道在Javascript中,call和apply都是改变作用域,只是传参方式不同而已。今天,我们就要对于call和apply其中实现原理来深入了解一下。毕竟只有知其然,并知其所以然,才能下次遇到各类问题才会不慌。其实JavaScript中能改变作用域的还有一个很重要的bind,我们在学好了call和apply之后,再来探究bind

call 和 apply 都是构造函数Function原型上的方法

call

Function.prototype.call()

在 MDN 中解释是: call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数

对于MDN给出的这句解释,不知道大家是不是一眼就领会了其中的精髓,反正我第一次看的时候一脸懵逼。于是,我只能一点点分析call这个方法。MDN上,不止给了这一句话解释,还给了个Demo,我们一起来看下。

function Product (name, price) {
    this.name = name
    this.price = price
}

function Food (name, price) {
    Product.call(this, name, price)
    this.category = 'food'
}
console.log(new Food('cheese', 5).name)     // 'cheese'

MDN上这个Demo,用call实现了一个继承。这还是我们之前在继承那篇中分析过的构造函数继承。其核心就是通过改变作用域的方式来在新的作用域内实例了属性和方法。

这样我们大概就能理解到,call实际上就是想我们在执行某一个fn方法的时候,传入了个一个obj对象,然后将fn方法的作用域对象替换为obj。实际上就是fn内部的所有this作用域内的属性都到obj里面去实例化

function callTest () {
    console.log(this.value)
}

let tempObj = {
    value: 'chencc'
}

tempObj.fn = callTest
tempObj.fn()        // 'chencc'

大家看到这段代码是不是大概知道call方法的实现思想了。这里如果我们在非严格模式下,callTest这个方法如果直接执行的化,这个this就会指向全局,全局并没有定义value,那么输出的只能是undefined。但是这里,我们将callTest方法赋值给tempObj对象的fn。当执行tempObj的fn方法时。这里this就指向的是tempObj。大家应该都就看过这么一句话,谁调用这个方法,那么this就指向这个对象。所以,这里这里this.value的值就是chencc。

大家对于this了解还不够深刻的,后面我在写一篇单独分析this。

分析到这里,我们来尝试看一下call的具体实现

Function.prototype.myCall = function (context) {
    // 判断this类别是不是function
    // 因为myCall是在构造函数Function原型上的方法
    // 所以实例化的方法才能继承到myCall方法
    if (typeof this !== 'function') {
        throw new TypeError('not function')
    }
    // 传入的context是否为空,为空则作用域还是设置为全局
    context = context || window
    // 给传入的 obj 对象添加一个属性,并给属性赋值调用的这个方法
    context.fn = this
    // 获取传入参数,因为call传入的是一个个参数,所以arguments解构后
    // 直接将第一个传入作用域对象的参数去除,就是实际需要的传参
    let arg = [...arguments].slice(1)
    // 直接执行这个方法,并将参数传入
    let result = context.fn(...arg)
    // 删除给 传入context 添加的临时属性
    delete context.fn
    return result
}

上面就是一个call方法的实现核心了,对于下面的apply,我们就不需要重复分析了。大家如果用过这两个方法,也都应该知道,唯一的区别也就是传入参数不同。一会我也会在下面实现一个

apply

Function.prototype.apply()

在 MDN 中解释是:apply() 方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数

具体实现apply

Function.prototype.myCall = function (context) {
    // 判断this类别是不是function
    // 因为myCall是在构造函数Function原型上的方法
    // 所以实例化的方法才能继承到myCall方法
    if (typeof this !== 'function') {
        throw new TypeError('not function')
    }
    // 传入的context是否为空,为空则作用域还是设置为全局
    context = context || window
    // 给传入的 obj 对象添加一个属性,并给属性赋值调用的这个方法
    context.fn = this
    // 这里唯一和call方法不一样的地方
    // call 传入的是一个个参数 (context, arg1, arg2, arg3)
    // apply 传入参数是 (context, [arg1, arg2, arg3]) 第二个参数是数组
    // 这里直接判断传入第二个参数数组有没有,有直接执行传入数组解构
    // 没有则直接执行这个方法
    let result = arguments[1] ? context.fn(...arguments[1) : context.fn()
    // 删除给 传入context 添加的临时属性
    delete context.fn
    return result
}

例子

分析了 apply 和 call 的具体实现方式后,我们也要知道 call 和 apply,可以干什么。

const numbers = [5, 6, 2, 3, 7]
// 第一个参数传入 null,是因为没有对象去调用这个方法。只是需要这个方法计算
const max = Math.max.apply(null, numbers)

// 这里Math.max 常规使用方式是 Math.max(5, 6, 2, 3, 7)
// 因为这里 Math.max 不支持数组,可以用这个方式传入数组
 
console.log(max)        // 7

const min = Math.min.apply(null, numbers)
console.log(min)        // 2

这里只是写了一个简单的使用Math中的取最大和最小值的方法,apply和call的核心作用就是改变作用域,也就是this指向。灵活使用好这两个方法,可以在平时开发中搞出很多黑科技。

备注:call 和 apply 的方法都会使函数立即执行的,有时候也是可以用来调用函数