Article 十二月 14, 2020

深入 JavaScript 之 模拟 call函数!!!

Words count 5.7k Reading time 5 mins. Read count 0

前言

js中有不少比较难以理解的概念,比如 js原型继承 。我曾经很早的时候就看过js原型方面的知识,并在当时写了一篇 博客 作为记录,很显然当时的我只是死记硬背。最近我利用空闲的时间将一些相对比较深入的js概念和用法重新学习,并新建了一个专栏 深入javascript 用于记录和分享。以下来介绍如何手动模拟实现一个 call 函数:

原生 call 表现形式

var foo = 'windowFoo'
const obj = {
    foo: 'objFoo',
    getName: function (a, b) {
        console.log(this.foo, a, b)
    }
}

const obj2 = { foo: 'obj2Foo' }

obj.getName(1, 2)                 // objFoo 1, 2
obj.getName.call(obj2, 1, 2)      // obj2Foo 1, 2
obj.getName.call(null, 1, 2)      // windowFoo 1, 2
obj.getName.call(undefined, 1, 2) // windowFoo 1, 2
obj.getName.call(false, 1, 2)     // undefined 1, 2

如何你对于 call 函数用法不太了解,建议先看以下我的这篇博客 深入 JavaScript 之 call函数 用法 ,在了解它的用法和行为后再来模拟它的实现会更得心应手。

根据上述的代码,我们可以得到它的表现形式如下:

  • 改变一个函数内部的 this 指向
  • 第一个参数为其他的特定值时,this 指向会转成不同的值(这里指代 nullundefined 等值)
  • 以参数列表的形式将后续参数传给函数

将上面的三种表现形式实现,便几乎实现了一个 call 函数的模拟了,下面我们来一步一步实现。

改变一个函数内部的 this 指向

首先我们需要知道的是 call 函数是 Function.prototype 上的方法,于是我们可以这样写:

const obj = {
    name: 'objName',
    getThis: function () {
        console.log(this)
    }
}
const obj2 = { name: 'obj2Name', }

obj.getThis()   // { name: 'objName', getThis: f () }

Function.prototype.call2 = function (context) {
    context.fn = this
    context.fn()
    delete context.fn
}

obj.getThis.call2(obj2)     // { name: 'obj2Name' }

context.fn = this 这里的 this 就是 call2 函数的调用者 getThis 方法,通过赋值给 context.fn ,于是 getThis 函数 内部 this 值 被改为了 fn 的调用者 。以上代码非常简单、易于理解,实现第二个表现形式就更简单了。

第一个参数为其他值时,this 指向会转成不同的值

当第一个参数是 nullundefined 时,函数内部的 this 值会转为 window。当它是其他的值时,它的 this 表现形式类似于一个空对象,其实加个判断就行了。为了更贴近原生表现形式,这里这样写:

Function.prototype.call2 = function (context) {
    function getContext(target) {
        return (target === null || target === undefined) ? window : Object(target)
    }

    context = getContext(context)
    context.fn = this
    context.fn()
    delete context.fn
}

以上几乎 100% 模拟了原生表现,如下:

obj.getThis.call2(null)         // window
obj.getThis.call2(undefined)    // window
obj.getThis.call2(true)         // Boolean(true)

obj.getThis.call(null)         // window
obj.getThis.call(undefined)    // window
obj.getThis.call(true)         // Boolean(true)

以参数列表的形式将后续参数传给函数

采用 ES6 的话就非常简单:

Function.prototype.call2 = function (context) {
    // 截取 context 之后的参数
    const args = [].slice.call(arguments, 1)

    function getContext(target) {
        return (target === null || target === undefined) ? window : Object(target)
    }

    context = getContext(context)
    context.fn = this
    const result = context.fn(...args)
    delete context.fn
    // 用于返回函数的返回值
    return result
}

本篇实现主要参考 该博客 实现的,这位大佬通过 eval 执行一个拼接参数后的函数字符串 来实现以上功能,如果你们感兴趣可以去他博客看看,我这边为了偷懒就直接用 ES6 了😴。

完整代码

var name = 'windowName'

const obj = {
    name: 'objName',
    getName: function (a, b) {
        console.log(this.name, a, b)
        return a + b
    }
}
const obj2 = { name: 'obj2Name' }

Function.prototype.call2 = function (context) {
    // 获取取 context 之后的参数
    const args = []

    for (let i = 1; i < arguments.length; i++) {
        args.push(arguments[i])
    }

    function getContext(target) {
        return (target === null || target === undefined) ? window : Object(target)
    }

    context = getContext(context)
    context.fn = this
    const result = context.fn(...args)
    // 执行完毕删除 fn,避免给 context 对象增加额外属性
    delete context.fn
    // 用于返回函数的返回值
    return result
}
const num = obj.getName.call2(obj2, 1, 2)   // obj2Name 1 2
console.log(num)                            // 3
obj.getName.call2(null, 1, 2)               // windowName 1 2
obj.getName.call2(undefined, 1, 2)          // windowName 1 2
obj.getName.call2(true, 1, 2)               // undefined 1 2

其实还是很简单的,大多数看似复杂的事物只要有序地拆分为若干个小块后,实现起来也是水到渠成~

注意:请在浏览器环境下执行以上代码,不然打印可能会不一致

参考

冴羽的博客 - JavaScript深入之call和apply的模拟实现

0%