js基础:手写call、apply、bind函数

前言

大家好,我是热爱前端的luckyCover,今天分享几道手写题,希望大家都能轻松拿下。

在手写前,你需要掌握this指向问题,了解call、apply、bind属性的含义。如果你还不了解,可以看我的上一篇文章:我不允许你还不了解this、call、apply、bind- luckyCover掘金

手写call

手写call函数前,我们来捋一下书写步骤:

  1. 函数名:尽可能贴合原属性名,最好带有自定义的含义(如myCall、customCall等)
  2. 参数:第一个参数是上下文对象,第二个参数开始可能有不确定个参数个数(即参数可能不止一个)
  3. 函数体:上下文绑定及函数调用
javascript 复制代码
Function.prototype.myCall = function(context, ...args) {
    // 生成唯一的键,方便后续函数执行完进行删除操作
    const key = Symbol();
    // 给当前上下文对象绑定this
    context[key] = this;
    // 调用函数并传递参数,一般的函数会有返回值,我们还是用res接收一下
    const res = context[key](...args);
    // 调用结束即刻删除当前引用,防止上下文对象中存在该函数
    delete context[key];
    // 返回返回值
    return res;
}

写完测试下吧~

javascript 复制代码
function myFn(a, b, c, d, e) {
    console.log(`${this.myName}`, a, b, c, d, e);
}

const obj = {
    myName: 'yy',
}

myFn.myCall(obj, 1, 2, 3, 4, 5); // 参数逐个传递 输出结果:yy, 1, 2, 3, 4, 5
console.log(obj); // { myName: 'yy' }

手写apply

applycall的区别就在第二个参数上,步骤:

  1. 函数名:myApply
  2. 参数:第一个参数是上下文对象,第二个参数是一个类数组
  3. 函数体:上下文绑定及函数调用
javascript 复制代码
Function.prototype.myApply = function(context, args) {
    // 生成唯一的键,和上边call相同作用
    const key = Symbol();
    // 给当前上下文对象绑定this
    context[key] = this;
    // 参数args是一个数组,我们需要将其展开
    const res = context[key](...args);
    // 调用结束即刻删除当前引用,防止上下文对象中存在该函数
    delete context[key];
    // 返回返回值
    return res;
}

测试如下:

javascript 复制代码
function myFn(a, b, c, d, e) {
    console.log(`${this.myName}`, a, b, c, d, e);
}

const obj = {
    myName: 'yy',
}

myFn.myApply(obj, [1, 2, 3, 4, 5]); // 以数组形式传递 输出结果:yy, 1, 2, 3, 4, 5
console.log(obj); // { myName: 'yy' }

手写bind

bindcall的唯一区别在于bind不会立即执行函数,而是返回一个绑定好上下文的新函数,我们看下步骤:

  1. 函数名:myBind
  2. 参数:第一个参数是上下文对象,第二个参数往后逐个传递(和call传参相同)
  3. 函数体:上下文绑定及函数调用,返回新的函数
javascript 复制代码
Function.prototype.myBind = function(context, ...args) {
    // 保存this(即函数本身)
    let fn = this;
    return function(...args1) {
        let allArgs = [...args, ...args1];
        // 判断是否为new的构造函数
        if(new.target) {
            return new fn(...allArgs);
        } else {
            fn.call(context, ...allArgs); // 需展开数组中每个元素
            // fn.apply(context, allArgs); // 调用apply情况下,直接传递allArgs数组,无需展开
        }
    } 
}

测试myBind函数:

javascript 复制代码
function myFn(a, b, c, d, e) {
    console.log(`${this.myName}`, a, b, c, d, e);
}

const obj = {
    myName: 'yy',
}

const newFn = myFn.myBind(obj, 1, 2, 3, 4, 5)
newFn(); // 输出:yy, 1, 2, 3, 4, 5 
console.log(obj);  // { myName: 'yy' }

下面我为大家提供不一样的写法,但是实现效果还是一样滴,大家根据自己情况选择即可。

不同的写法

call

javascript 复制代码
Function.prototype.myCall = function(context) {
    // 类型校验
    if(typeof this !== 'function') {
        throw new Error('不是函数哦')
    }
    // 保存函数于上下文对象中
    context.fn = this
    // 整理参数
    arguments[1] ? context.fn(...[].slice.call(arguments, 1)) : context.fn()
    // 删除上下文对象中的fn函数
    delete context.fn
}

简单解释下这一条表达式含义:

text 复制代码
arguments[1] ? context.fn(...[].slice.call(arguments, 1)) : context.fn()
  1. arguments:传递给当前函数的参数,可能不止一个
  2. arguments[1]:函数的第二个参数,我们知道第一个参数是context(上下文对象)
  3. ...[].slice.call(arguments, 1):其实arguments就是call函数的上下文对象,就相当是arguments.slice(1),我们知道arguments指的是函数接收的所有参数(它是个数组),slice(1)中的1表示截取数组元素的起始索引(索引默认从0开始),所以是从第二个参数开始截取一直到数组末尾。而且slice方法会返回一个新数组,那么这个新数组的元素可能就放到了代码里最前边的[]中,并且我们将该数组展开(...[])并作为参数传递给context.fn()
  4. 如果没有第一个参数,即arguments[1]为空,那么直接调用函数,context.fn(),没有传递参数,默认的上下文对象就为window

相信理解了call函数及其参数的作用,上边代码多看几遍都能看得懂的,不理解就先写出代码,再反复看,实操还是最重要滴😊。

apply

javascript 复制代码
Function.prototype.myApply = function(context) {
    // 类型校验
    if(typeof this !== 'function') {
        throw new Error('不是函数哦')
    }
    // 保存函数于上下文对象中
    context.fn = this
    // 整理参数
    arguments[1] ? context.fn(...arguments[1]) : context.fn()
    // 删除上下文对象中的fn函数
    delete context.fn
}
text 复制代码
arguments[1] ? context.fn(...arguments[1]) : context.fn()

我们知道apply的第二个参数是个数组,那么也不需要往后截取所有参数了,直接将数组展开作为参数传递即可。

bind

javascript 复制代码
Function.prototype.myApply = function(context) {
    // 类型校验
    if(typeof this !== 'function') {
        throw new Error('不是函数哦')
    }
    // 整理参数
    let arg = arguments[1] ? [].slice.call(arguments, 1) : []
    return (...newArgs) => {
       this.apply(context, arg.concat(newArgs))
       // this.call(context, ...arg.concat(newArgs))
    }
    // 删除上下文对象中的fn函数
    delete context.fn
}
text 复制代码
let arg = arguments[1] ? [].slice.call(arguments, 1) : []

[].slice.call(arguments, 1),这里和上边call的写法是一样的,只是前边没有加三个点...,此时返回的新数组是未展开的,因为后边要返回一个新的函数调用新函数时可能会传递新的参数,那么就要在新的函数里边将参数进行合并。至于新函数里边使用call还是apply来调用都可以,使用call来调用时记得合并完数组要将其展开

结语

以上就是本篇的所有内容,喜欢的请点点赞支持下luckyCover哈哈,大家有更好的方法也欢迎评论区互动呀~

相关推荐
老虎06277 分钟前
JavaWeb前端03(Ajax概念及在前端开发时应用)
前端·javascript·ajax
Aphasia31121 分钟前
性能优化之重绘和重排
前端·面试
Python私教23 分钟前
yggjs_react使用教程 v0.1.1
前端·react.js·前端框架
Jinuss25 分钟前
Vue3源码reactivity响应式篇之Map、Set等代理处理详解
前端·vue.js·vue3
用纸拆浪26 分钟前
❤️❤️组件踩坑日记:vxe-table-select下拉表格异步加载时的数据回显问题
前端
小鸡脚来咯27 分钟前
react速成
前端·javascript·react.js
Juchecar30 分钟前
JavaScript 和 Vue3 中 for...in 与 for...of 的区别
前端·vue.js
剽悍一小兔31 分钟前
React15.x版本 子组件调用父组件的方法,从props中拿的,这个方法里面有个setState,结果调用报错
前端·javascript·react.js
神笔码农nice32 分钟前
VUE从入门到精通二:ref、reactive、computed计算属性、watch监听、组件之间的通信
前端·javascript·vue.js
柯南二号39 分钟前
【前端】React回调函数解析
前端·react.js·前端框架