一、前言
手写 call
和apply
源码算是中高级前端去面试最常考的知识点之一了,主要不是它有多难,而是这几行代码每一行都是一个JavaScript
核心知识点,你对其中一个不熟悉根本就做不到完全理解 call
和apply
的实现原理。这一篇文章直接把 call
和apply
源码涉及到的 JavaScript
核心知识点分别拆分讲解清楚,然后再去教你手写 call
和apply
源码,确保完全去理解里面每一行代码,这样做是真的深刻学会这个知识,而不是死记硬背(面试官随便指一行一问就穿帮了)。
手写
call
和apply
你必须掌握下面这些前置知识点,下面我将会分别讲解说明,然后再去手写call源码:1、会
call
和apply
的用法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, ...)
看个例子:
javascript
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() 方法
这部分之前空闲写过一篇文章介绍过可以直接跳过去看看:通过学会Object.defineProperty的用法彻底理解vue2响应式原理
(3)熟练理解各种情况下 JavaScript 里面 this的指向
这部分之前空闲写过一篇文章介绍过可以直接跳过去看看: 总结出五种场景下JavaScript中的this指向
(4)什么是es6中参数 ...args
简单说明:es6新增的用法,用于收集剩余的参数,
且必须是最后一个
如下所示,无论你传入多少个参数,都能通过...args
解构出来,一般用于不确定传入参数个数的情况下使用, 但必须放在最后。
scss
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, 适合在绝对不能出现重复变量的场景下使用
ini
// 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。
javascript
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 方法。
细节优化一、
上面例子中最简单的call这就实现了,但有不少东西是需要优化的,例如传过来的不是一个对象test而是一个 undefined、null或者其它数据类型呢????
那就把所有类型都转成Object数据类型
就行啦(下面代码第二行)。
注意: this 在浏览器指向 window 但在node
里面指向global, globalThis
作用是自动识别环境,在浏览器是 window 在 node 是 global。
javascript
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)
看看打印出来的结果,哪怕传入的是一个数字,最后也会转成一个对象,这就实现了传入的内容永远都是对象。
细节优化二、
上述代码强行给
test
造一个fn
,然后触发fn
来改变 this 指向,但如果fn
重名了怎么办???作为一个公用的方法,调用频率非常大,能做到万无一失的办法只有一个,就是用Symbol让这个属性永远唯一
(如下第三行)。
javascript
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)
看看效果
细节优化三、
(1)根据上面效果打印出来看看除了需要用到属性a和b外,还有
Symbol(key)
这个东西,理论上Symbol(key)
只是用来触发改变this方向根本不可能需要操作它,那么我们把Symbol(key)
变成只读是不是更好???(如下第五行设置为只读)(2)完成
cxt[key](...args)
调用之后需要把值返回(如下第9和第25行)(3)
cxt[key]
只是用来临时执行,用完就删除(如下第10行)
看代码:一个完整的 call 就手写完成了,代码不多就是波及的知识点非常多,功底差一点都写不出来。
javascript
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行处)。
javascript
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基础肯定差不了。周末空闲创作的文章如果哪里写错了大佬们可以指点一下。