真懂了?自定义实现 Javascript 中的 call、apply 和 bind 方法

自定义实现 Javascript 中的 call、apply 和 bind 方法

通常情况下,在调用函数时,函数内部的 this 的值是访问该函数的对象。利用 callapplybind,你可以在调用现有函数时将任意值分配给 this,而无需先将函数作为属性附加到对象上。这使得你可以将一个对象的方法用作通用的实用函数。

call 方法

call 方法的原始使用方式:

function sayName(hello) {
    console.log(hello, this.name);
}
const p1 = {
    name: "HuFei"
};
sayName.call(p1, "welcome ");

上面的示例将输出:welcome HuFei 。

分析 call 函数的基本原理,就会注意到以下几点:

  1. 调用函数调用的原型改变了它的指向,即上面的函数调用变成了p1.name
  2. 无论我们传递给 sayName.call 的参数是什么,都应作为arg1、arg2、…传递给原来的 sayName
  3. 不会对 p1sayName 函数产生副作用,即 call 函数不会以任何方式修改原始的 p1sayName

尝试实现自定义 call 函数的第一步,将自定义的 myCall 追加到 Function 的原型对象上面。

Function.prototype.myCall = function(newThis) {
    newThis.fnName = this;
    const result = newThis.fnName();
    delete newThis.fnName;
    return result;
};

然后我们呢就可以像下面这样改变 sayNamethis 指向了。

sayName.myCall(p1, 'welcome ');

这么写存在一个问题,就是函数第一个参数后面的参数无法进行传递,所以需要对 myCall 做一些改动。

想要传递参数,需要借助 JavaScript 中的 eval 函数。

eval 函数会将传入的字符串当做 JavaScript 代码进行执行。传入的字符串可以是 JavaScript 表达式、语句或语句序列;表达式可以包括变量和现有对象的属性

Function.prototype.myCall = function(newThis, ...args) {
    newThis.fnName = this;
    const argsStr = [];
    args.forEach((x, i) => [
        argsStr.push(`argsStr[${i}]`)
    ])
    const result = eval("newThis.fnName(" + args.toString() + ")");
    delete newThis.fnName;
    return result;
};

但是上面的实现,还有一个问题,假如newThis 本身已经存在属性 fnName,那么 myCall 方法最终会删除 newThis 原有的 fnName

所以该怎么办呢?

可以生成一个uuid,用于新的属性。

Function.prototype.myCall = function(newThis, ...args) {
    const key = String(parseInt(1000 * Math.random())) + Date.now();
    newThis[key] = this;
    const argsStr = [];
    args.forEach((x, i) => [
        argsStr.push(`args[${i}]`)
    ])
    const result = eval("newThis[key](" + argsStr.toString() + ")");
    delete newThis[key];
    return result;
};

这时候可以借助 ES6 引入的 Symbol 特性。

Function.prototype.myCall = function(newThis, ...args) {
    const key = Symbol('fnName');
    newThis[key] = this;
    const argsStr = [];
    args.forEach((x, i) => [
        argsStr.push(`args[${i}]`)
    ])
    const result = eval("newThis[key](" + argsStr.toString() + ")");
    delete newThis[key];
    return result;
};

Symbolsfor...in 迭代中不可枚举。另外,Object.getOwnPropertyNames() 不会返回 symbol 对象的属性,但是你能使用 Object.getOwnPropertySymbols() 得到它们。

虽然不能完全避免副作用,但对 Object.getOwnPropertySymbols() 的应用 必然比不上 for...inObject.keys 那样普遍。

apply 方法

这个函数与 call() 几乎完全相同,只是函数参数在 call() 中逐个作为列表传递,而在 apply() 中它们会组合在一个对象中,通常是一个数组——例如:
func.call(this, "eat", "bananas")
等价于
func.apply(this, ["eat", "bananas"])

所以,它的自定义实现跟 call 相比,差别不大(只在第一行啊)。

Function.prototype.myApply = function(newThis, args) {
    const key = Symbol('fnName');
    newThis[key] = this;
    const argsStr = [];
    args.forEach((x, i) => [
        argsStr.push(`args[${i}]`)
    ])
    const result = eval("newThis[key](" + argsStr.toString() + ")");
    delete newThis[key];
    return result;
};

bind 方法

相比 callapplyFunction 原型对象上的 bind() 方法会创建一个新函数。当调用该新函数时,它会调用原始函数并将其 this 指向设定为给定的值。同时,还可以传入一系列指定的参数,这些参数会插入到调用新函数时传入的参数的前面。

bind 方法有以下几个特点:

  1. 创建并返回一个新函数,可称之为绑定函数。该绑定函数封装了原始函数对象;
Function.prototype.myBind = function(newThis) {
    if (typeof this !== "function") {
        throw new Error(this + " cannot be bound as it's not callable");
    }
    const fn = Symbol('key')
    newThis[fn] = this;
    return function boundFunction() {
        return newThis[fn]();
    };
};

const person = {
    lastName: "Fei"
};

function fullName(salutaion, firstName) {
    console.log(salutaion, firstName, this.lastName);
}

const bindFullName = fullName.myBind(person, "Mr");

bindFullName("Hu ");

如果我们运行上述代码,得到的输出结果是:undefined undefined Fei
因此,如果你仔细观察一下,参数 “Mr” 是在创建绑定函数 bindFullName 时提供的,而参数 “Hu” 是在调用 bindFullName 时提供的,而 bindFullName 又会调用目标函数。因此,当我们调用带有参数 "Hu " 的 bindFullName 时,参数 “Mr” 被添加到参数列表中。

让我们尝试在自己的 myBind 方法中实现同样的功能。

Function.prototype.myBind = function (newThis, ...boundArguments) {
    if (typeof this !== "function") {
        throw new Error(this + "cannot be bound as it's not callable");
    }
    const key = Symbol('key')
    newThis[key] = this;
    return function boundFunction(...args) {
        const argsStr = [];
        boundArguments.forEach((x, i) => {
            argsStr.push(`boundArguments[${i}]`)
        })
        args.forEach((x, i) => [
            argsStr.push(`args[${i}]`)
        ])
        return eval("newThis[key](" + argsStr.toString() + ")");
    };
};

绑定完成了吗?还没有,我们还漏掉了 MDN 定义中 thisArg 的绑定。

如果使用 new 运算符构造绑定函数,则忽略该值。

这意味着,当使用new 操作符调用绑定函数时,我们需要忽略在创建绑定函数时传递的这个值。以上面的例子 new bindFullName("Hu") 为例,输出结果应该是:Mr Hu undefined。

所以,此时应当判断当点函数是作为普通函数调用还是作为构造函数调用。

Function.prototype.myBind = function (newThis, ...boundArguments) {
    if (typeof this !== "function") {
        throw new Error(this + "cannot be bound as it's not callable");
    }
    const key = Symbol('key')
    newThis[key] = this;
    return function boundFunction(...args) {
        const argsStr = [];
        boundArguments.forEach((x, i) => {
            argsStr.push(`boundArguments[${i}]`)
        })
        args.forEach((x, i) => [
            argsStr.push(`args[${i}]`)
        ])
        const isConstructor = this instanceof boundFunction;
        if (isConstructor) {
            // new operator
            return 
        } else {
            return eval("newThis[key](" + argsStr.toString() + ")");
        }
    };
};

MDN 有一个很好的 bind polyfill,可以解决 new 操作符的问题。它的基本思路是设置一个中间构造函数 fNOP,以便绑定函数和 bind() 函数调用在同一个原型链上,因为使用 new 操作符调用绑定函数涉及到原型链的传递。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/557991.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第四百六十七回

文章目录 1. 知识回顾2. 使用方法3. 示例代码4. 内容总结 我们在上一章回中介绍了"OverlayEntry组件简介"相关的内容,本章回中将介绍OverlayEntry组件的用法.闲话休提,让我们一起Talk Flutter吧。 1. 知识回顾 我们在上一章回中介绍了Overlay…

高通 Android 12 源码编译aidl接口

最近在封装系统sdk接口 于是每次需要更新aidl接口 ,传统方式一般使用make update-api或者修改Android.mk文件,今天我尝试使用Android.bp修改 ,Android 10之前在Android.mk文件修改,这里不做赘述。下面开始尝试修改,其实…

CTFHub(web sql注入)(二)

布尔盲注 盲注原理: 将自己的注入语句使用and与?id1并列,完成注入 手工注入: 爆库名长度 首先通过折半查找的方法,通过界面的回显结果找出数据库名字的长度,并通过相同的方法依次找到数据库名字的每个字符、列名…

ROS 2边学边练(29)-- 使用替换机制

前言 启动文件用于启动节点、服务和执行流程。这组操作可能有影响其行为的参数。替换机制可以在参数中使用,以便在描述可重复使用的启动文件时提供更大的灵活性。替换是仅在执行启动描述期间评估的变量,可用于获取特定信息,如启动配置、环境变…

2024年哪一款洗地机好用?四大热门主流机型分享

传统的拖地方式必须是拖一会就得清洗一遍拖把,如果房屋面积大,中途得经历无数次换清水的过程,而且拖地是得频繁得弯腰用力气,顽固的污渍还需要来回反复拖几遍,甚至要蹲下身子手动抹布清洁,真的是费时费力。…

【科研入门】评价指标AUC原理及实践

评价指标AUC原理及实践 目录 评价指标AUC原理及实践一、二分类评估指标1.1 混淆矩阵1.2 准确率 Accuracy定义公式局限性 1.3 精确率 Precision 和 召回率 Recall定义公式 1.4 阈值定义阈值的调整 1.5 ROC与AUC引入定义公式理解AUC算法 一、二分类评估指标 1.1 混淆矩阵 对于二…

脾虚百病生,出现这3种情况,说明是脾虚了,简单2步养出好脾胃~

中医认为脾胃为后天之本,人体通过脾胃来消化吸收营养物质,脾主运化水谷精微、运化水湿,脾主肌肉,脾主生血、统血,为气血生化之源,是人体气机升降的枢纽。 脾虚百病生 李东垣在《脾胃论》说:“内…

Python CSV数据处理工具库之clevercsv使用详解

概要 CSV(Comma-Separated Values)是一种常见的数据格式,用于存储和传输表格数据。Python clevercsv库是一个强大的CSV数据处理工具,提供了丰富的特性和功能,帮助用户高效处理CSV文件。 安装 要安装Python clevercsv库,可以使用pip工具进行安装: pip install cleverc…

mysql 重复单号 统计

任务: 增加重复件统计分析: 统计展示选择时间范围内重复1次、重复2次、重复3次、重复4次、重复5次及以上的数据量 17、统计出现的重复次数 增加重复件统计分析: 统计展示选择时间范围内重复1次、重复2次、重复3次、重复4次、重复5次及以上的数…

Scala 04 —— 函数式编程底层逻辑

函数式编程 底层逻辑 该文章来自2023/1/14的清华大学交叉信息学院助理教授——袁洋演讲。 文章目录 函数式编程 底层逻辑函数式编程假如...副作用是必须的?函数的定义函数是数据的函数,不是数字的函数如何把业务逻辑做成纯函数式?函数式编程…

【Linux系统】地址空间 Linux内核进程调度队列

1.进程的地址空间 1.1 直接写代码&#xff0c;看现象 1 #include<stdio.h>2 #include<unistd.h>3 4 int g_val 100;5 6 int main()7 {8 int cnt 0;9 pid_t id fork();10 if(id 0)11 {12 while(1)13 {14 printf(&…

牛客Linux高并发服务器开发学习第三天

静态库的使用(libxxx.a) 将lession04的文件复制到lession05中 lib里面一般放库文件 src里面放源文件。 将.c文件转换成可执行程序 gcc main.c -o app main.c当前目录下没有head.h gcc main.c -o app -I ./include 利用-I 和head所在的文件夹&#xff0c;找到head。 main.c…

进程控制相关

进程终止 进程终止时&#xff0c;操作系统要释放对应进程申请的相关内核数据结构和对应的代码和数据。其不本质就是释放进程申请的系统资源。 进程终止的常见方式&#xff1a; 1、代码运行完毕且结果正确。 2、代码运行完毕但结果不正确。 3、代码没运行完&#xff0c;进程…

【Entity Framework】闲话EF中批量配置

【Entity Framework】闲话EF中批量配置 文章目录 【Entity Framework】闲话EF中批量配置一、概述二、OnModelCreating中的批量配置元数据API的缺点 三、预先约定配置忽略类型默认类型映射预先约定配置的限制约定添加新约定替换现有约定约定实现注意事项 四、何时使用每种方法进…

通过实例学C#之ArrayList

介绍 ArrayList对象可以容纳若干个具有相同类型的对象&#xff0c;那有人说&#xff0c;这和数组有什么区别呢。其区别大概可以分为以下几点&#xff1a; 1.数组效率较高&#xff0c;但其容量固定&#xff0c;而且没办法动态改变。 2.ArrayList容量可以动态增长&#xff0c;但…

使用go和消息队列优化投票功能

文章目录 1、优化方案与主要实现代码1.1、原系统的技术架构1.2、新系统的技术架构1.3、查看和投票接口实现1.4、数据入库MySQL协程实现1.5、路由配置1.6、启动程序入口实现 2、压测结果2.1、设置Jmeter线程组2.2、Jmeter聚合报告结果&#xff0c;支持11240/秒吞吐量2.3、Jmeter…

vue 一键更换主题颜色

这里提供简单的实现步骤&#xff0c;具体看自己怎么加到项目中 我展示的是vue2 vue3同理 在 App.vue 添加 入口处直接修改 #app { // 定义的全局修改颜色变量--themeColor:#008cff; } // 组件某些背景颜色需要跟着一起改变&#xff0c;其他也是同理 /deep/ .ant-btn-primar…

『FPGA通信接口』汇总目录

Welcome 大家好&#xff0c;欢迎来到瑾芳玉洁的博客&#xff01; &#x1f611;励志开源分享诗和代码&#xff0c;三餐却无汤&#xff0c;顿顿都被噎。 &#x1f62d;有幸结识那个值得被认真、被珍惜、被捧在手掌心的女孩&#xff0c;不出意外被敷衍、被唾弃、被埋在了垃圾堆。…

【Linux学习】Linux编辑器-vim使用

这里写目录标题 1. &#x1f320;vim的基本概念&#x1f320;2. vim的基本操作&#x1f320;3.vim异常处理&#x1f320;4. vim正常模式的相关命令&#x1f320;5. vim末&#xff08;底&#xff09;行模式相关命令 vi/vim都是多模式编辑器&#xff0c;不同的是vim是vi的升级版本…

开发与产品的战争之自动播放视频

开发与产品的战争之自动播放视频 起因 产品提了个需求&#xff0c;对于网站上的宣传视频&#xff0c;进入页面就自动播放。但是基于我对chromium内核的一些浅薄了解&#xff0c;我当时就给拒绝了: “浏览器不允许”。&#xff08;后续我们浏览器默认都是chromium内核的&#…