【前端面试5】手写Function原型方法

文章目录

前言

大家好,今天是冲击中级工程师第五天,今天不打卡八股,我们一起来手写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

总结

今天面试题就到这里,让我们每天学习几分钟,一起成长。管他难易,获得成长就好。明天见。

相关推荐
庖丁解牛9 小时前
第二课:把代码保存成网页 + 认识变量 (2)
前端
代码猎人9 小时前
怎么开发一个Vue3项目
前端
梦里寻码9 小时前
从 0 到 1 用 NestJS + LangChain 搭一套可售卖的 SaaS AI 客服:挂件一行代码接入,知识库秒变 SEO 帮助中心
前端·后端·github
爱喝水的小周9 小时前
实验十一 Vuex 实现王者荣耀后台比赛管理系统
前端·css·html
三十而立洋9 小时前
前端模块化的演进:从全局变量到 Import Maps
前端·javascript·前端工程化
橘猫走江湖9 小时前
浮动塌陷复现失败:父元素为什么一直不塌
前端·css
小凯在掘金9 小时前
一文搞懂 Map、Set、WeakMap、WeakSet
javascript·面试
用户1733598075379 小时前
纯前端 PDF 按书签拆分避坑指南:拆分后章节对不上?
前端·javascript·vue.js
мо仙堡杠把子ご灬9 小时前
Flex 自适应布局:告别 calc,用一个 height-auto 搞定
前端·javascript
环境栈笔记9 小时前
指纹浏览器检查代理失败怎么排查?先分清代理、网络和检测服务
前端·人工智能·后端·自动化