文章目录
前言
大家好,今天是冲击中级工程师第五天,今天不打卡八股,我们一起来手写Function原型,也就是(apply,call,bind)。
1.手写call
Function.prototype.call 是一个用于显式指定函数执行时 this 值的方法。它通过将第一个参数作为 thisArg,按照 ECMAScript 规范对其进行 ToObject 处理,然后以该 this 绑定调用目标函数,并将后续参数逐个传入。call 会立即执行函数,并返回函数的执行结果。其核心作用是控制 this 绑定,而不是改变函数本身。
那么为何要手写他呢?手写他可以帮助验证我们是否真正理解了 JS 的 this 绑定机制、函数调用模型和语言底层抽象。并且,这也是面试常考题目。
开始吧,我们先来看原生的call定义:
javascript
/**
* Calls a method of an object, substituting another object for the current object.
* @param thisArg The object to be used as the current object.
* @param argArray A list of arguments to be passed to the method.
*/
call(this: Function, thisArg: any, ...argArray: any[]): any;
根据官方说的:
call是一个对象方法,支持其它对象来调用当前对象。
参数thisArg是指当前需要使用的对象
参数argArray是指传递的参数,注意官方这个描述不严谨,他说参数是一个list,实际上是多个参数,这个描述更像是apply的。
通俗来说就是,可以使用call来借用不属于自己的方法,前面一个传递的是上下文,后面传递的是参数。因此手写需要完成的就是:
接收上下文,多参数->绑定this->执行函数
直接上代码:
javascript
Function.prototype.myCall = function(context, ...args) {
if(typeof this !== 'function') {
throw new Error('Function.prototype.myCall - context is not callable');
}
context = context ?? globalThis
context = Object(context)
const fnSymbol = Symbol('fn')
context[fnSymbol] = this
const result = context[fnSymbol](...args)
delete context[fnSymbol]
return result
}
这里为什么使用symbol,是为了给一个唯一标识,保证临时挂载函数属性时不发生属性名冲突,从而避免破坏原对象或被原对象属性覆盖。
注意:??操作符是ES2020(ECMAScript 2020 / ES11)的新特性,之前的版本是没有的,请使用三目运算符或者||(一定要测试好边界)进行实现。
下面测试一下,和原生做个对比:
javascript
const person = {
name: 'John',
greet: function(greeting, question) {
return `${greeting}, my name is ${this.name}, ${question}`
}
}
定义如上对象(为什么greet要传递两个参数是为了apply预留,后文将会说到),然后执行下面代码进行对比:
javascript
console.log(person.greet.call({ name: 'Alice' }, 'Hello', 'How are you?'));
console.log(person.greet.myCall({ name: 'Alice' }, 'Hello', 'How are you?'));
输出:
powershell
Hello, my name is Alice, How are you?
Hello, my name is Alice, How are you?
结果一致,初步测试通过。
2.手写apply
Function.prototype.apply和功能一致,只是参数传递不同,但是还是看看在es文档中的描述:
javascript
/**
* Calls the function, substituting the specified object for the this value of the function, and the specified array for the arguments of the function.
* @param thisArg The object to be used as the this object.
* @param argArray A set of arguments to be passed to the function.
*/
apply(this: Function, thisArg: any, argArray?: any): any;
这里就不过多解释,es5文档可能多年未更新,看起来不太准确,我们只需要知道和call一致,只不过apply传参是数组而已。
那么手写只需要将参数变为数组即可:
javascript
Function.prototype.myApply = function(context, args) {
if(typeof this !== 'function') {
throw new Error('Function.prototype.myApply - context is not callable');
}
context = context ?? globalThis
context = Object(context)
const fnSymbol = Symbol('fn')
context[fnSymbol] = this
const result = context[fnSymbol](...args)
delete context[fnSymbol]
return result
}
测试一下:
javascript
console.log(person.greet.myApply(person, ['Hello', 'How are you?']));
console.log(person.greet.apply(person, ['Hello', 'How are you?']));
sehll
Hello, my name is John, How are you?
Hello, my name is John, How are you?
结果也是一致的。
3.手写bind
Function.prototype.bind 用于创建一个新的函数,并将指定的 this 值永久绑定到该函数,同时可以预置部分参数。与 call 和 apply 不同,bind 不会立即执行函数,而是返回一个带有固定 this 和部分参数的新函数,该函数在之后被调用时会以绑定的 this 和合并后的参数列表执行。这常用于回调函数中保持 this 不丢失,以及实现函数的部分应用(partial application)。
依旧是看其官方文档描述:
javascript
/**
* For a given function, creates a bound function that has the same body as the original function.
* The this object of the bound function is associated with the specified object, and has the specified initial parameters.
* @param thisArg An object to which the this keyword can refer inside the new function.
* @param argArray A list of arguments to be passed to the new function.
*/
bind(this: Function, thisArg: any, ...argArray: any[]): any;
描述:
给定一个函数,创建一个与原函数体相同的新绑定函数。
绑定函数的this对象与指定对象关联,并具有指定的初始参数。
参数thisArg,和上面一样,实际上就是上下文
参数argArray,是一个参数数组,用于传递参数
手写需要注意的细节就是,除了原函数的参数,还需要传递参数,并且还要能够执行,这样的话就需要注意参数的传递了:
javascript
Function.prototype.myBind = function(context, ...args) {
if(typeof this !== 'function') {
throw new Error('Function.prototype.myBind - context is not callable');
}
context = context ?? globalThis
context = Object(context)
const fnSymbol = Symbol('fn')
context[fnSymbol] = this
return function(...newArgs) {
const result = context[fnSymbol](...args, ...newArgs)
delete context[fnSymbol]
return result
}
}
测试一下:
javascript
const boundGreet = person.greet.myBind({ name: 'Charlie' }, 'Hey')('How have you been?')
console.log(boundGreet)
const boundGreetOrigin = person.greet.bind({ name: 'Charlie' }, 'Hey')('How have you been?')
console.log(boundGreetOrigin)
结果:
shell
Hey, my name is Charlie, How have you been?
Hey, my name is Charlie, How have you been?
加餐, 实现函数的柯里化
使用apply来实现柯里化,先搜集参数,收集满了之后直接执行:
javascript
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
};
}
以这个简单加法为例:
javascript
function sum(a, b, c) {
return a + b + c
}
测试:
javascript
const curriedSum = curry(sum)
console.log(curriedSum(1)(2)(3))
console.log(curriedSum(1, 2)(3))
console.log(curriedSum(1)(2, 3))
运行结果:
shell
6
6
6
总结
今天面试题就到这里,让我们每天学习几分钟,一起成长。管他难易,获得成长就好。明天见。