学习JS底层--手写call初体验

130 阅读7分钟

✨前言

最近,作者在深度学习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

查看控制台,结果如下:

image.png

通常来讲:

  • 当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

查看控制台:

image.png

你会发现报错了,这是因为

如果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。关键点包括:

  1. 原型链机制:所有函数都能调用 call,是因为 Function.prototype 定义了它。
  2. this 绑定原理:利用 context.fn() 的隐式绑定规则,动态改变 this 指向。
  3. 边界处理:包括 null/undefined 转全局对象、非函数调用报错、参数传递等。
  4. 内存优化:使用 Symbol 防止属性冲突,并在调用后删除临时属性。

最终,我们成功实现了一个功能完备的 myCall,并通过测试验证了其正确性。理解这一过程,不仅能加深对 this 和函数调用的认知,也为手写其他高阶函数(如 applybind)打下了坚实基础。建议动手实践并尝试扩展,彻底掌握这一核心知识点! 🚀


🌇结尾

本文部分内容参考KYLE SIMPSON的《你不知道的JavaScript(上卷)

感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生

(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)

作者:3Katrina
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。