学会call和apply源码涉及的JS知识点后再去手写源码,让你深刻理解实现原理

发布于:2024-05-07 ⋅ 阅读:(18) ⋅ 点赞:(0)

一、前言

手写 callapply源码算是中高级前端去面试最常考的知识点之一了,主要不是它有多难,而是这几行代码每一行都是一个JavaScript 核心知识点,你对其中一个不熟悉根本就做不到完全理解 callapply 的实现原理。这一篇文章直接把 callapply 源码涉及到的 JavaScript 核心知识点分别拆分讲解清楚,然后再去教你手写 callapply 源码,确保完全去理解里面每一行代码,这样做是真的深刻学会这个知识,而不是死记硬背(面试官随便指一行一问就穿帮了)。

手写 callapply 你必须掌握下面这些前置知识点,下面我将会分别讲解说明,然后再去手写call源码:

1、会 callapply的用法

2、了解 object.defineproperty() 方法

3、熟练理解各种情况下 JavaScript 里面 this的指向

4、知道什么是es6中参数 ...args

5、Symbol数据类型

二、手写call和apply必须会的前置知识点讲解

(1)call和apply的用法

想要学手写call和apply原理,那必需会用call()这个东西啊,下面先说明使用语法,再展示例子。

语法: functionName.call(myThis, arg1, arg2, ...)

  • functionName :要调用的函数名称。
  • myThis :函数执行时的上下文(即想要this指向的地方)。
  • arg1 , arg2 , ...:要传递给函数的参数列表。

apply和call唯一区别是apply传参是数组,语法functionName.call(myThis, arg1, arg2, ...)

看个例子

const testObj = {
  getName: function(city, country) {
    return this.firstName + ' ' + this.lastName + ', ' + city + ', ' + country;
  }
};

const testObj1 = {
  firstName: '天天鸭',
  lastName: '不是'
};

// 打印结果都是: 天天鸭 不是, 一名, 产品
console.log(testObj.getName.call(testObj1, '一名', '产品')); 
console.log(testObj.getName.apply(testObj1, ['一名', '产品'])); 

解释: 定义了一个 testObj 对象,其中包含一个 getName 方法用来返回一段信息。 通过使用 call() 方法调用 testObj 对象的getName方法,我们可以使 this 指向 testObj1,并传入不同的参数。

(2)object.defineproperty() 方法

这部分之前空闲写过一篇文章介绍过可以直接跳过去看看:

(3)熟练理解各种情况下 JavaScript 里面 this的指向

这部分之前空闲写过一篇文章介绍过可以直接跳过去看看:

(4)什么是es6中参数 ...args

简单说明:es6新增的用法,用于收集剩余的参数,且必须是最后一个

如下所示,无论你传入多少个参数,都能通过...args解构出来,一般用于不确定传入参数个数的情况下使用, 但必须放在最后。

const test = (a, b, ...args) => {
  console.log(a)     // 1
  console.log(b)     // 3
  console.log(args)  // [5, 6, 9]
}

test(1, 3, 5, 6, 9)

(5)Symbol数据类型

大家所熟知的JavaScript有6中数据类型,分别是:

  • String 字符串类型
  • Number 数字类型
  • Object 对象类型
  • Boolean 布尔值类型
  • Null 空值
  • Undefined 未定义

但ES6给我们带来一种全新的数据类型:Symbol

简单说明:Symbol的作用是创建唯一的,不会与其他任何值相等的、不可被修改的值。

如下所示,哪怕是内容一样,在判定上也会认定为 false, 适合在绝对不能出现重复变量的场景下使用

// Symbol没有参数的情况
let s1 = Symbol();
let s2 = Symbol();

console.log(s1 === s2)  // false

// Symbol有参数且值相同的情况
let g1 = Symbol('aa');
let g2 = Symbol('aa');

console.log(g1 === g2)  // false

三、手写call源码

如果上面几个 JavaScript 知识点你都理解了,那么看懂 call 源码完全没有问题,既然看懂了自然就会记住实现思路,不用死记硬背也能形成长期记忆。下面先看看最简单的call版本。

解释:testCall就是我自己写的call方法,method.testCall(test, 3, 4)去调用并且把 this 指向test 方法。

实现思路:核心代码在testCall里面,method 调用 testCall,所以 testCall 这里this指的是 method 方法,而cxt 传过来指的是 test对象,想要指向 test 很简单啊,把 this 放到cxt里面随便一个属性例如fn里面cxt.fn(),然后cxt.fn(...args)触发fn(),那么谁触发就指向谁自然this就指向了test。

Function.prototype.testCall = function(cxt, ...args){
// 核心代码:把this放到想要指向的cxt实例某个属性中,然后执行属性使this指向cxt
  cxt.fn = this  
  cxt.fn(...args)
}

function method(a, b){
  console.log(this, a, b)
  return a + b
}

const test = {
  a: 1,
  b: 2
}

method.testCall(test, 3, 4)

上述代码的打印结果,this 指向了想要指向的 test 方法。

image.png

细节优化一、

上面例子中最简单的call这就实现了,但有不少东西是需要优化的,例如传过来的不是一个对象test而是一个 undefined、null或者其它数据类型呢????那就把所有类型都转成Object数据类型就行啦(下面代码第二行)。

注意: this 在浏览器指向 window 但在node里面指向global, globalThis作用是自动识别环境,在浏览器是 window 在 node 是 global。

Function.prototype.testCall = function(cxt, ...args){
  cxt = cxt === undefined || cxt === null ? globalThis:Object(cxt)
  cxt.fn = this
  cxt.fn(...args)
}

function method(a, b){
  console.log(this, a, b)
  return a + b
}

method.testCall(123, 3, 4)

看看打印出来的结果,哪怕传入的是一个数字,最后也会转成一个对象,这就实现了传入的内容永远都是对象。

image.png

细节优化二、

上述代码强行给test造一个fn,然后触发fn来改变 this 指向,但如果fn重名了怎么办???作为一个公用的方法,调用频率非常大,能做到万无一失的办法只有一个,就是用Symbol让这个属性永远唯一(如下第三行)。

Function.prototype.testCall = function(cxt, ...args){
  cxt = cxt === undefined || cxt === null ? globalThis:Object(cxt)
  const key = Symbol('key')
  cxt[key] = this
  cxt[key](...args)
}

function method(a, b){
  console.log(this, a, b)
  return a + b
}

const test = {
  a: 1,
  b: 2
}

method.testCall(test, 3, 4)

看看效果

image.png

细节优化三、

(1)根据上面效果打印出来看看除了需要用到属性a和b外,还有Symbol(key)这个东西,理论上Symbol(key)只是用来触发改变this方向根本不可能需要操作它,那么我们把Symbol(key)变成只读是不是更好???(如下第五行设置为只读)

(2)完成cxt[key](...args)调用之后需要把值返回(如下第9和第25行)

(3)cxt[key]只是用来临时执行,用完就删除(如下第10行)

看代码:一个完整的 call 就手写完成了,代码不多就是波及的知识点非常多,功底差一点都写不出来。

Function.prototype.testCall = function(cxt, ...args){
  cxt = cxt === undefined || cxt === null ? globalThis:Object(cxt)
  const key = Symbol('key')
  const that = this
  Object.defineProperty(cxt, key, {
    enumerable: false,     // 只读
    value: that,           // 值
  });
  const res = cxt[key](...args)
  delete cxt[key]    // 这是临时的,用完就删除
  return res         // 把返回值返回
}

function method(a, b){
  console.log(this, a, b)
  return a + b
}

const test = {
  a: 1,
  b: 2
}

method.testCall(test, 3, 4)
console.log(method.testCall(test, 3, 4))  // 返回值:7

四、手写apply源码

思路和写call简直就一样,唯一区别是需要传数组并且判断当前传的是不是数组(第4行处)。

Function.prototype.testCall = function(cxt, isArr){

   // 判断传入的参数是否为数组
  if (isArr && !Array.isArray(isArr)) {
    throw new TypeError("传入的第二个参数必须是数组");
  }

  cxt = cxt === undefined || cxt === null ? globalThis:Object(cxt)
  const key = Symbol('key')
  const that = this
  Object.defineProperty(cxt, key, {
    enumerable: false,     // 只读
    value: that,           // 值
  });
  const res = cxt[key](...isArr)
  delete cxt[key]    // 这是临时的,用完就删除
  return res         // 把返回值返回
}

function method(a, b){
  console.log(this, a, b)
  return a + b
}

const test = {
  a: 1,
  b: 2
}

method.testCall(test, [3, 4])

小结:

想要能深刻理解并且自己手写一个call或者apply难点不是实现过程的本身。而且要涉及到很多其它的知识点,如果面试一个前端开发能理解里面每一行代码,那这个前端的JS基础肯定差不了。周末空闲创作的文章如果哪里写错了大佬们可以指点一下。