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哈哈,大家有更好的方法也欢迎评论区互动呀~

相关推荐
慢半拍iii16 分钟前
JAVA Web —— A / 网页开发基础
前端
gnip35 分钟前
pnpm 的 monorepo架构多包管理
前端·javascript
zolty42 分钟前
基于hiprint的票据定位打印系统开发实践
javascript
新手村领路人2 小时前
Firefox自定义备忘
前端·firefox
乖女子@@@2 小时前
css3新增-网格Grid布局
前端·css·css3
百思可瑞教育2 小时前
使用UniApp实现一个AI对话页面
javascript·vue.js·人工智能·uni-app·xcode·北京百思可瑞教育·百思可瑞教育
伐尘3 小时前
【CE】图形化CE游戏教程通关手册
前端·chrome·游戏·逆向
不想吃饭e3 小时前
在uniapp/vue项目中全局挂载component
前端·vue.js·uni-app
非凡ghost3 小时前
AOMEI Partition Assistant磁盘分区工具:磁盘管理的得力助手
linux·运维·前端·数据库·学习·生活·软件需求
UrbanJazzerati3 小时前
前端入门:margin居中、border、box-radius、transform、box-shadow、mouse事件、preventDefault()
前端·面试