✨前言
最近,作者在深度学习JavaScript的知识,今天给大家分享一篇我学习过程中遇到的难题,手写call!
🔍传统call需要做什么?
让我们先想想,我们平常是怎么使用call的?
var name = "我是全局对象";
function gretting(...args){
console.log(args,arguments[0],arguments[1])
return `hello , Im ${this.name}`;
}
const lj = {
name:'雷军'
}
console.log(gretting.call(lj,19,'北京')); //传入对象
console.log(gretting.call(null,18,'武汉')); //传入null
查看控制台,结果如下:
通常来讲:
- 当call的第一个参数指向对象时,则调用者的this会指定为该对象
- 当call的第一个参数指向null时,则调用者的this会指定为全局
我们这里特意打印了arg和arguments,因为这两点在我们待会的实现中需要仔细考虑
⚠️严格模式下:
当我们开启了严格模式:
'use strict'
var name = "我是全局对象";
function gretting(...args){
console.log(args,arguments[0],arguments[1])
return `hello , Im ${this.name}`;
}
const lj = {
name:'雷军'
}
console.log(gretting.call(lj,19,'北京')); //传入对象
console.log(gretting.call(null,18,'武汉')); //传入null
查看控制台:
你会发现报错了,这是因为
如果call() 的第一个参数是 null 或 undefined,this 不会转换为全局对象,而是直接保持 null 或 undefined,因此,当执行 gretting.call(null, 18, '武汉') 时,this 是 null,访问 this.name 会抛出错误。
✍️手写call
好了 说了那么多,让我们开始手写call吧!温馨提示,手写New涉及到很多js的知识点,有点复杂哦,请慢慢分析!
1.原型方法
我们思考,平常我们在调用call时,是不是以Fn.call(...)的方式,意味着每个函数对象在被调用的时候,都能使用call()方法。
gretting.call(obj, 1, 2, 3) //任意一个函数都能找到call的方法,由于原型链
gretting2.call(null, 'haha', 3) //任意一个函数都能找到call的方法,由于原型链
gretting3.call(obj) //任意一个函数都能找到call的方法,由于原型链
这是因为每个函数对象的原型都是Function这个老大哥,所以在它上面设置prototype方法可以映照到所有的函数对象中。
所以我们手写call需要在所有函数对象的原型Function上面的prototype实现。
但由于这个Function上已经存在了call,我们使用myCall来实现我们自己的手写call
Function.prototype.myCall = function(需要定义的参数) {
需要实现的内容
}
gretting.myCall() //定义了Function的prototype之后,便能在原型链上找到
2.传入参数
接下来思考,对于这个myCall,我们该如何设置形参呢?
我们仍旧需要思考平常使用call的行为,通常来说,我们会传入第一个参数为需要绑定this的对象,后续的参数为函数的参数。
gretting.call(obj, '雷军',18) //第一个参数传入需要绑定this的对象,后续参数按照函数原本的参数传递
function gretting(name,age)
{
...
}
var obj ={
...
}
所以myCall的参数应该按下面的方式设置:
Function.prototype.myCall = function(context, ...args) {
需要实现的内容
}
-
使用context指定需要绑定的this对象
-
使用rest运算符,能够拿到任意数量的函数参数
3. 当context为null或undefined
这一步我们需要处理当context为null或者undefined时的情况,我们要做的是将context赋值为window,也就是全局对象.
Function.prototype.myCall = function(context, ...args) {
if (context === null || context === undefined) {
context = window;
}
}
这一点是由于考虑到我们平常使用call时,指定第一个参数为null或者undefined时的情况
4.this的含义
此时我们要对this进行一个判断:当this不为函数时,则报错。
但你仔细想想,这里Function.prototype.myCall的this是指谁啊?
我们通常判断this是看运行时最后的调用位置决定。
比如gretting.myCall(obj, 1, 2, 3),那么这里myCall里的this此时指的就是gretting函数对象。所以myCall里面的this指向最后的调用位置gretting
知道了这里的this指向谁后,接下来,我们需要做一个判断:判断当前的this类型是否为函数
Function.prototype.myCall = function(context, ...args) {
if (context === null || context === undefined) {
context = window;
}
if (typeof this !== 'function') { //this指向最后的调用位置,我们确保函数使用myCall方法
throw new TypeError('Function.prototype.myCall called on non-function')
}
}
5.更改函数里的this指向
好了,接下来是最关键的步骤,我们要完成call的核心功能:更改函数里的this指向为context!
首先,我们都知道:当隐式调用时,里面的this会指向调用的对象!
举个例子:
var name = "im outer"
const obj = {
name:"im obj",
function fn(){
return this.name;
}
}
console.log(obj.fn()); //隐式调用,指向obj对象中的name:"im obj"
所以我们可以利用这种规则,来实现myCall里面的核心步骤:将greeting函数体里面的this绑定改变为obj
Function.prototype.myCall = function(context, ...args) {
if (context === null || context === undefined) {
context = window;
}
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.myCall called on non-function')
}
const fnKey = Symbol('fn'); //创建一个唯一且不可变的值:为了防止覆盖context对象的key
context[fnKey] = this; //为context添加gretting函数对象
context[fnKey](...args); //context对象调用gretting函数,隐式绑定,将gretting里的this指定为context,完成我们想要的效果
}
gretting(obj,1,2,3)
请仔细理解后三行的代码: 我们利用了隐式绑定的规则来实现更改gretting里的this指向!
- 创建fnKey的目的是为了给context设置一个唯一的key,不会context中其它的key冲突。
- 添加greeting函数对象是隐式绑定的必备条件,确保context对象内有这个函数
- 调用context中的greeting方法,从而实现隐式绑定:当函数调用时,如果前面存在调用它的对象,那么this就会隐式绑定到这个对象上
6.返回对象
最后一步就是要将调用greeting函数后返回的内容给返回,毕竟咱们是在myCall里调用gretting,所以肯定也要获取greeting的返回内容,将其返回。
⭐所以最终的函数代码objectFactory如下:
Function.prototype.myCall = function(context, ...args) {
if (context === null || context === undefined) {
context = window;
}
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.myCall called on non-function')
}
const fnKey = Symbol('fn');
context[fnKey] = this;
const result = context[fnKey](...args); //得到调用后的返回值result
return result; //返回result
}
gretting(obj,1,2,3)
♻️需要注意:记得回收
上面的代码,有一点点小问题,我们设置fnKey以及context[fnKey] = this之后再调用const result = context[fnKey](...args),那么这个时候其实就已经将gretting中的this指向context了,我们此时已经达到了我们的目的,这个时候其实context里面的fnKey键其实就没有用了。
因为我们已经达到了我们的目的,所以需要“过河拆桥”,所以把context里面的fnKey键值给删除。。
const fnKey = Symbol('fn');
context[fnKey] = this;
const result = context[fnKey](...args);
delete context[fnKey]; //这里需要删除fnKey的键值对
return result; //返回result
🎯总结
我们在上面已经完成了整个手写call的过程,接下来可以验证效果:
function gretting(...args) {
return `hello , I am ${this.name}.`;
}
Function.prototype.myCall = function(context, ...args) {
if (context === null || context === undefined) {
context = window;
}
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.myCall called on non-function')
}
const fnKey = Symbol('fn');
context[fnKey] = this;
const result = context[fnKey](...args);
delete context[fnKey];
return result;
}
var obj = {
name:"刘德华",
age:18,
fn: function() {
}
}
console.log(gretting.myCall(obj,1,2,3)); //输出obj里面的name:“刘德华” ,成功实现call绑定效果!
通过这篇文章,我们深入剖析了 call 方法的工作原理,并一步步实现了自己的 myCall。关键点包括:
- 原型链机制:所有函数都能调用
call,是因为Function.prototype定义了它。 - this 绑定原理:利用
context.fn()的隐式绑定规则,动态改变this指向。 - 边界处理:包括
null/undefined转全局对象、非函数调用报错、参数传递等。 - 内存优化:使用
Symbol防止属性冲突,并在调用后删除临时属性。
最终,我们成功实现了一个功能完备的 myCall,并通过测试验证了其正确性。理解这一过程,不仅能加深对 this 和函数调用的认知,也为手写其他高阶函数(如 apply、bind)打下了坚实基础。建议动手实践并尝试扩展,彻底掌握这一核心知识点! 🚀
🌇结尾
本文部分内容参考KYLE SIMPSON的《你不知道的JavaScript(上卷)》
感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生
(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)
作者:3Katrina
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。