19

手写一个 JavaScript 的 apply、call 函数 - 开发二三事 - SegmentFault 思否

 4 years ago
source link: https://segmentfault.com/a/1190000020871501
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在 JavaScript 中 call、apply 用来干嘛?

之前我有提到过 call、apply 主要用来改变函数运行时的执行环境(this),对于执行环境(this)以及 call、apply 用法不了解的,可以参考之前的文章:JavaScript 函数作用域、执行环境(this)、call、apply、bind 的用法

call、apply 函数实现分析:

call 函数实现分析:

语法实现:
fun.call(context, arg0, arg1, arg2 ...)
参数分析:
context:指定 fun 函数运行时的执行环境(this 值),一般不指定默认为null,此时指向全局对象(window || global)。
arg0, arg1, arg2 ...:fun 运行时的具体参数。

通过分析,我们可以发现,fun 使用 call 方法运行时,会使 fun 的 this 绑定到传入的 context 对象中,然后拿着之后的参数运行 fun 函数。与 初始的 fun() 的区别在于,改变了 this 的指向。那么如何改变 this 指向呢。我们之前都见过这样一个例子:

const obj = {
    name: 'obj',
    sayName: function() {
        console.log(this.name)
    }
}
obj.sayName() // obj

可以看到,对象内部方法在执行时 this 是指向 对象本身的。那么我们可以利用这一点,来将要执行 call 方法的 fun 的 this 指向到对应的对象上,即让 fun 变成 context 的一个属性方法,执行完毕后,再将该方法 delete 掉,以去掉 context 对于 fun 的影响,代码实现如下:

Function.prototype.call_test = function(context) {
   // 当 context 未定义或定义为 null 时,将 context 指向 window
   context = context || window
   // 将要执行的 fun(即this) 变成给 context 的 属性方法
   context._callFn = this
   context._callFn() // 执行属性方法(即将 函数的 this 强行关联到 context 对象上)
   delete context._callFn // 删除 对象关联的属性方法,以去除后续 对 fun 执行的影响
}

const obj2 = {
    name: 'test'
}

obj.sayName.call_test(obj2) // test

可以看到,我们写的 call_test 方法成功模实现了 call 改变 this 的功能,接下来还要去处理函数运行时参数的问题,每个函数体有一个内置的 arguments 变量,用来存储函数应用时的参数,我们可以使用 arguments 变量来获取函数运行时的具体参数:

Function.prototype.call_test = function(context) {
   // 当 context 未定义或定义为 null 时,将 context 指向 window
   context = context || window
   // 将要执行的 fun(即this) 变成给 context 的 属性方法
   context._callFn = this
   
   const args = [...arguments].slice(1) // 首先把 arguments 类数组变成数组,再拿到去除第一个参数的新数组
   
   context._callFn(...args) // 执行包含参数的属性方法,将参数数组展开为一个个参数
   delete context._callFn // 删除 对象关联的属性方法,以去除后续 对 fun 执行的影响
}

const person = {
    name: 'person',
    say(age) {
        console.log(this)
        console.log(`我叫${this.name}我今年${age}`)
    }
}

const person1 = {
    name: 'person1'
}

person.say.call_test(person1, 22) // 我叫person1我今年22

浏览器 console 跑了一遍, 666~

apply 实现分析:

语法实现:
fun.call(context, [args])
参数分析:
context:指定 fun 函数运行时的执行环境(this 值),一般不指定默认为 null,此时指向全局对象(window || global)。
[args]:fun 运行时的参数数组。

其实 apply 方法跟 call 方法实现没有多大区别,唯一就在于对待 参数 的处理行为,apply 方法要求参数以数组或类数组的形式传递(ES5 之后支持类数组对象),具体实现如下:

Function.prototype.apply_test = function(context) {
   // 当 context 未定义或定义为 null 时,将 context 指向 window
   context = context || window
   // 将要执行的 fun(即this) 变成给 context 的 属性方法
   context._callFn = this
   
   const args = [...arguments].slice(1) // 首先把 arguments 类数组变成数组,再拿到去除第一个参数的新数组
   
   context._callFn(args) // 执行包含参数的属性方法
   delete context._callFn // 删除 对象关联的属性方法,以去除后续 对 fun 执行的影响
}

const person = {
    name: 'person',
    say(age) {
        console.log(this)
        console.log(`我叫${this.name}我今年${age}`)
    }
}

const person1 = {
    name: 'person1'
}

person.say.call_test(person1, [22]) // 我叫person1我今年22(所以参数较少是,还是用 call 看着舒服点哈~)

至此,我们完成了 call、apply 方法的手动实现。实现的难点在于,要想到将函数设置成 context 的属性方法,来实现 执行环境(this) 的绑定,调用完成后需要delete该属性,以移除影响,不想删的童鞋可以在浏览器里试一试,会出现啥奇葩情况,在这里不展开了。这里只是给大家提供个简单的实现思路,到实际应用中还是要考虑其他问题的,这里暂时也不做展开。感兴趣的可以了解下还有那些问题,比如不让用 ES6 语法怎么办?比如 ‘_callFn’ 被占用了怎么办等等,思考是个好习惯,希望我们的拿来主义少一些。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK