1.如何使用?
我们既然想要模拟实现 call
和 apply
两个方法,那么我们很有必要先了解它们的用法。它们两个的用法也比较简单,这里我就带大家简单复习一遍。
通过代码我们回顾一下。
代码如下:
<script> let obj = { name: '小猪课堂' } function say(age) { console.log("你好:", this.name, '我今年' + age + '岁了'); } say(12); // 你好: 我今年 12 岁了 say.call(obj, 12); // 你好: 小猪课堂 我今年 12 岁了 say.apply(obj, [12]) // 你好: 小猪课堂 我今年 12 岁了 </script>
上段代码非常简单,我们直接调用 say
函数时,函数的 this
指向中是没有找到 name
属性的,我们通过 call
和 apply
方法调用时,将 this
指向了 obj
,而 obj
内部有 name
属性,所以函数中,可以取到 name
属性的值。
看了代码之后我们再来看一下官网对这两个方法的解释,一下印象。
call
官网解释:
call()
方法使用一个指定的this
值和单独给出的一个或多个参数来调用一个函数。
apply
官网解释:
apply()
方法调用一个具有给定this
值的函数,以及以一个数组(或类数组对象)的形式提供的参数。
它们两个的基本上没什么区别,唯一的区别也只有一个,官网也给出了解释。
唯一的区别:
call()
方法的作用和apply()
方法类似,区别就是call()
方法接受的是参数列表,而apply()
方法接受的是一个参数数组。
2.总结特点
知道了这两个方法怎么用之后,我们接下来总结它们的特点,为我们后面的模拟实现提供一个思路
2.1 改变 this
指向
这一点毋庸置疑,我们使用这两个方法就是为了改变 this
指向的,这也是它们两个的共同特点。我们可以将函数原来的 this
指向以后改变后的 this
指向打印出来看看。
代码如下:
<script> let obj = { name: '小猪课堂' } function say() { console.log(this) } say(); // Window {window: Window, self: Window, document: document, name: '', location: Location, …} say.call(obj); // {name: '小猪课堂'} say.apply(obj) // {name: '小猪课堂'} </script>
输出结果:
很明显,直接调用函数 this
指向是指向的 window
全局,使用 call
和 apply
方法后 this
指向的是传入的对象 obj
。
2.2 参数传递
call
和 apply
参数传递的第一个参数都是我们需要将 this
指向的对象,后面的参数则为函数正常应该接收的参数,只不过两个函数中这部分的写法不一致。
call
方法:
第一个参数为 this
指向对象,后面接收一个参数列表,为函数正常接收的参数,示例代码如下:
say.call(obj,12,32);
apply
方法:
apply
方法第一个参数也是接收的 this
指向的对象,只不过后面正常的参数采用数组的形式接收,示例代码如下:
say.apply(obj,[12,32]);
2.3 未指定 this
时
当我们调用 call
和 apply
两个方法时,未传入第一个参数或者传入的参数为 null
或者 undefind
,这种情况下函数的 this
指向会指向哪里呢?
这种情况需要分为严格模式和非严格模式,js
严格模式下,如果未传入第一个参数,或者传入的参数为 null
或者 undefind
,则函数 this
指向为 null
或者 undefind
。在非严格模式下则指向 window
严格模式
代码如下:
say.call(); // undefined say.call(null); // undefined say.call(undefined); // undefined say.apply() // undefined say.apply(null) // null say.apply(undefined) // undefined
输出结果
非严格模式
代码如下:
say.call(); // Window say.call(null); // Window say.call(undefined); // Window say.apply() // Window say.apply(null) // Window say.apply(undefined) // Window
输出结果:
2.4 自动封装对象
当我们 call
和 apply
方法第一个参数传输的不是对象类型时,那么 this
指向将会指向传入的值类型的包装对象,当然,除了 null
和 undefined
之外。
代码如下:
say.call(12); // Number {12} say.call('小猪课堂'); // String {'小猪课堂'} say.call(true); // Boolean {true} say.apply(12); // Number {12} say.apply('小猪课堂'); // String {'小猪课堂'} say.apply(true); // Boolean {true}
输出结果:
3.实现 call
方法
我们知道了 call
方法和 apply
的用法以及它们有什么特点,那么接下来就需要针对这些特点一一想办法去实现它们,我们首先来实现 call
方法。
3.1 完成 this
指向
改变 this 指向是这两个方法的核心功能,只要我们搞明白了如何改变改变 this
指向,那就解决了一大半的问题。
回顾代码:
<script> let obj = { name: '小猪课堂' } function say(age) { console.log("你好:", this.name, '我今年' + age + '岁了'); } say(12); // 你好: 我今年 12 岁了 say.call(obj, 12); // 你好: 小猪课堂 我今年 12 岁了 say.apply(obj, [12]) // 你好: 小猪课堂 我今年 12 岁了 </script>
解决思路:
我们的目标是将函数内部的 this
指向 obj
。再学习 js
的时候,我们可能知道这么一句话,谁是调用者,this
指向就指向谁,那么我们是否可以试想一下,如果是 obj
调用函数 say
的话,那么函数内部 this
指向是不是就指向了 obj
呢?
思路代码:
<script> let obj = { name: '小猪课堂', say: function (age) { console.log("你好:", this.name, '我今年' + age + '岁了'); } } obj.say(12); // 你好: 小猪课堂 我今年 12 岁了 </script>
上段代码中我们没有使用 call
或者 apply
方法,也将函数内部的 this
指向了 obj
。
有了上面得思路后,我们就可以先实现一个简单得 call
方法了。
自定义 customCall
方法:
Function.prototype.customCall = function (context) { const fnKey = Symbol(); // 函数键名,使用 symbo 不会重复 context[fnKey] = this; // 将函数赋值给对象中的 fnKey 属性 const res = context[fnKey](); // 执行函数 delete context[fnKey]; // 删除 context 对象中的该属性,避免越来越多 return res // 返回结果 }
上段代码中有几点需要注意:
- 使用
symbol
是为了避免属性名重复,因为我们obj
对象中可能已经有say
属性名的存在了。 - 将函数赋值给属性的时候,我们直接使用了
this
,因为这个时候的this
本来就是原来的函数,比如我们调用say.call()
,这个时候call
方法中的额this
其实就是say
。 - 执行完函数后需要及时删除掉,因为下次我们调用
customCall
方法是还会生成新的属性。
3.2 this
指向全局
当 call
方法传入的第一个参数为 null
或者 undefind
时,我们需要将 this
指向到全局,修改一下代码。
代码如下:
Function.prototype.customCall = function (context) { if (context === null || context === undefined) { context = globalThis; // this 指向全局 } const fnKey = Symbol(); // 函数键名,使用 symbo 不会重复 context[fnKey] = this; // 将函数赋值给对象中的 fnKey 属性 const res = context[fnKey](); // 执行函数 delete context[fnKey]; // 删除 context 对象中的该属性,避免越来越多 return res // 返回结果 }
3.3 传入参数
call
方法可以传入很多参数的,我们也需要实现接收参数,修改代码。
代码如下:
Function.prototype.customCall = function (context, ...args) { if (context === null || context === undefined) { context = globalThis; // this 指向全局 } const fnKey = Symbol(); // 函数键名,使用 symbo 不会重复 context[fnKey] = this; // 将函数赋值给对象中的 fnKey 属性 const res = context[fnKey](...args); // 执行函数, ...args 解构参数 delete context[fnKey]; // 删除 context 对象中的该属性,避免越来越多 return res // 返回结果 }
这里就添加了一个...args
,...args
可以将我们的可变参数解构为一个数组。
3.4 值类型包装对象(最终版)
当传入的 context
是值类型时,我们需要将它改编为对应的包装对象,修改代码、
代码如下:
Function.prototype.customCall = function (context, ...args) { if (context === null || context === undefined) { context = globalThis; // this 指向全局 } if (typeof context !== 'object') { context = new Object(context); // 值类型变为它的包装对象 } const fnKey = Symbol(); // 函数键名,使用 symbo 不会重复 context[fnKey] = this; // 将函数赋值给对象中的 fnKey 属性 const res = context[fnKey](...args); // 执行函数, ...args 解构参数 delete context[fnKey]; // 删除 context 对象中的该属性,避免越来越多 return res // 返回结果 }
4.实现 apply
方法
我们实现了 call
方法后,apply
方法那就手到擒来了,因为这两个方法就一个区别,接收的正餐参数的格式不一样而已,修改一个 customCall
方法代码即可。
代码如下:
Function.prototype.customApply = function (context, args) { if (context === null || context === undefined) { context = globalThis; // this 指向全局 } if (typeof context !== 'object') { context = new Object(context); // 值类型变为它的包装对象 } const fnKey = Symbol(); // 函数键名,使用 symbo 不会重复 context[fnKey] = this; // 将函数赋值给对象中的 fnKey 属性 const res = context[fnKey](...args); // 执行函数, ...args 解构参数 delete context[fnKey]; // 删除 context 对象中的该属性,避免越来越多 return res // 返回结果 }
上段代码中我们只改了一个地方,就是把接收的...args
参数改为了 args
。
5.测试
代码编写完了,接下来测试一下是否满足我们的需求。
代码如下:
<script> let obj = { name: '小猪课堂', } function say(age) { console.log("你好:", this.name, '我今年' + age + '岁了'); } // 自定义 call 和 apply 方法 Function.prototype.customCall = function (context, ...args) { if (context === null || context === undefined) { context = globalThis; // this 指向全局 } if (typeof context !== 'object') { context = new Object(context); // 值类型变为它的包装对象 } const fnKey = Symbol(); // 函数键名,使用 symbo 不会重复 context[fnKey] = this; // 将函数赋值给对象中的 fnKey 属性 const res = context[fnKey](...args); // 执行函数, ...args 解构参数 delete context[fnKey]; // 删除 context 对象中的该属性,避免越来越多 return res // 返回结果 } Function.prototype.customApply = function (context, args) { if (context === null || context === undefined) { context = globalThis; // this 指向全局 } if (typeof context !== 'object') { context = new Object(context); // 值类型变为它的包装对象 } const fnKey = Symbol(); // 函数键名,使用 symbo 不会重复 context[fnKey] = this; // 将函数赋值给对象中的 fnKey 属性 const res = context[fnKey](...args); // 执行函数, ...args 解构参数 delete context[fnKey]; // 删除 context 对象中的该属性,避免越来越多 return res // 返回结果 } say.customCall(obj, 32); // 你好: 小猪课堂 我今年 32 岁了 say.customCall(); // 你好: 我今年 undefined 岁了 say.customApply(obj, [32]); // 你好: 小猪课堂 我今年 32 岁了 say.customApply(); // 你好: 我今年 undefined 岁了 </script>
测试出来这两个方法基本是满足我们要求的。
总结
实现 call
和 apply
方法实际很简单,总共也就 10
行代码左右,只要我们知道了其中的原理,这段代码就是信手拈来。
如果觉得文章太繁琐或者没看懂,可以观看视频: 小猪课堂