手写 call 的魔法课堂:让函数学会“见风使舵”的奇妙之旅

76 阅读4分钟

今天在代码的海洋里遨游时,我意外发现了一个有趣的魔法道具——call!它能让函数像变色龙一样改变自己的this属性。下面是我的魔法笔记,记录了如何亲手打造这根"魔法指挥棒"。

第一章:函数的"变脸"艺术

想象一下,函数是个有原则的演员,平时只认自己的导演(this)。但有了call,它就能瞬间切换剧组:

function gretting() {
  return `hello,I am ${this.name}`;
}

const lj = { name: '雷军' };
const trump = { name: 'Trump' };

console.log(gretting.call(lj)); // "hello,I am 雷军"
console.log(gretting.call(trump)); // "hello,I am Trump"

这简直是函数界的"变脸"绝活!但更神奇的是callapplybind这三胞胎的不同性格:

  • call:急性子,立即执行,参数一个个传 (obj, 1, 2, 3)
  • apply:同样急性子,但喜欢把参数打包 (obj, [1, 2, 3])
  • bind:慢性子,先记下配置稍后执行 const newFn = fn.bind(obj) 大家想了解一下具体用法,可以看看我的这篇文章JavaScript中的this指向:从懵圈到豁然开朗的奇幻之旅 相信看完能加深对这三胞胎用法的印象

我们知道了callapplybind这三胞胎的不同用法,那么我们怎么手写一个挂载到Function原型链上面的函数呢?接下来我就来讲讲我的方法

第二章:打造我们的魔法棒 myCall

现在进入重头戏——亲手锻造myCall魔法棒!核心步骤就像制作汉堡:

Function.prototype.myCall = function (context, ...args) {
  // 步骤1:处理空导演(context为null/undefined时指向window)
  if (context == null) context = window;
  
  // 步骤2:安全检测(确保调用者是个函数)
  if (typeof this !== 'function') {
    throw new TypeError('魔法棒只能用于函数!');
  }
  
  // 步骤3:创建唯一ID防止冲突(Symbol的妙用)
  const fnKey = Symbol('fn');
  
  // 步骤4:给导演临时加戏(将函数绑定到context)
  context[fnKey] = this;
  
  // 步骤5:执行函数并传递参数
  const result = context[fnKey](...args);
  
  // 步骤6:清理现场(删除临时属性)
  delete context[fnKey];
  
  return result;
};

重点魔法解析

Symbol防冲突:就像给临时演员戴上面具

```
const fnKey = Symbol('fn'); // 创建唯一钥匙
context[fnKey] = this; // 安全挂载函数
```

这是es6新增的数据类型,避免覆盖导演原有的道具(比如原本的context.fn属性)

Rest参数妙用:魔法师的收纳袋

```
步骤3:创建唯一ID防止冲突(Symbol的妙用)
function (context, ...args) // 收集所有剩余参数
context[fnKey](...args) // 展开参数传递
```

这是rest运算符,比传统的`arguments`更优雅,自动转成真数组,可以使用数组的方法

我们可以传参看看结果
 var name = 'zhangsan'
        function gretting(...args) {
            console.log(args, arguments[0], arguments[1]);
            // 将类数组转换成数组
            // console.log([...arguments], Array.from(arguments));
            return `hello,I am ${this.name}`
        }

        console.log(gretting(1, 2, 3));

可以看到打印了一个数组,魔法师收纳袋的作用

image.png

灵活转变this

const fnKey = Symbol('fn');
  
  // 步骤4:给导演临时加戏(将函数绑定到context)
  context[fnKey] = this;
  
  // 步骤5:执行函数并传递参数
  const result = context[fnKey](...args);

我们该如何灵活的玩转this呢?我们要改变函数的this指向,我们需要怎么便捷地做到呢》 我们来在我们写得call函数里面改写一下

Function.prototype.myCall = function (context, ...args) {
            console.log('/////');
            if (context === null || context === undefined) {
                context = window
            }
            //  this? gretting
            console.log(this);

            if (typeof this !== 'function') {
                throw new TypeError('Function.prototype.mycall called on non-function')
            }
         }
       gretting.myCall()  

可以看到结果此时是this是指向我们的函数的,我们可以想到对象的方法的this会指向对象的,我们是不是可以把它作为我们需要指向对象的方法呢,这样我们的this就会指向我们需要指向的对象了, image.png

这个时候我们需要用到es6新增的Symbol数据类型,因为其的唯一,可以帮我避免覆盖原先对象中存在的属性或者方法 context[fnKey] = this由于this就是我们的函数 gretting,我们就这样把其绑定到我们需要指向的对象方法了,这样gretting被context调用时,this就会指向我们所指定的对象

const result = context[fnKey](...args)最后通过对象调用方法,并返回,这样我们就实现了改变this指向,打印我们所指定对象的name

删除临时属性:魔法界的"事了拂衣去"

```
delete context[fnKey]; // 消除痕迹
```

避免污染context对象,做有素质的魔法师,我们需要删除这个属性方法

第三章:当导演缺席时的神奇规则

call遇到nullundefined时,不同模式下有不同表现: 载

// 非严格模式下:自动找大老板window
gretting.call(null); // "hello,I am Trump"

// 严格模式下:保持原则
"use strict";
gretting.call(null); // TypeError: 导演不能为空!

在我们的myCall中,我们选择非严格模式的行为:

if (context == null) context = window;

第四章:魔法实战演示

看我们的魔法棒如何指挥函数:

var obj = { name: '刘老板' };

console.log(gretting.myCall(obj, 1, 2, 3));
// 输出: 
/////             (我们的魔法信号)
hello,I am 刘老板

有趣的是函数内部的argumentsrest参数的区别:

function test(...args) {
  console.log(args);        // 真数组 [1, 2, 3]
  console.log(arguments);   // 类数组 {0:1, 1:2, 2:3}
}

第五章:魔法原理深度解析

  1. 原型链的魔法底座

    Function.prototype.myCall = ...
    

    所有函数都继承自Function.prototype,所以才能实现任何函数.myCall()的调用方式

  2. this的双重身份

    • gretting.myCall()时,myCall内部的this指向gretting
    • context[fnKey]()时,函数内部的this指向context
  3. 类数组的变身术
    传统转换方式:

    // 两种将arguments转数组的方法
    [...arguments] 
    Array.from(arguments)
    

第六章:魔法界的三大禁忌

  1. 忌忘记类型检查

    if (typeof this !== 'function') // 防止非函数调用
    
  2. 忌留下魔法痕迹

    delete context[fnKey] // 务必清理临时属性
    
  3. 忌处理context不当

    context = context || window // 处理边界情况
    

魔法总结:三大核心技巧

  1. Symbol护盾
    使用唯一属性键防止冲突,就像魔法师用专属咒语避免法术干扰

  2. 参数双通道
    ...args收参和传参的优雅处理,如同魔法阵的能量传递

  3. 环境清理术
    执行后立即删除临时属性,体现专业魔法师的素养

image.png

通过今天的学习,我深刻理解了JavaScript中this绑定的魔法机制。原来每个函数体内都住着一个变色龙,而call/apply就是训练它的魔法指令。最重要的是,我们亲手打造的myCall魔法棒不仅能用,还理解了背后的每一条魔法符文!