js 实现call和apply方法,超详细思路分析

壹 ❀ 引
我在 五种绑定策略彻底弄懂this 一文中,我们提到call,apply,bind属于显示绑定,这三个方法都能直接修改this指向。其中call与apply比较特殊,它们在修改this的同时还会直接执行方法,而bind只是返回一个修改完this的boundFunction并未执行,那么今天我们来讲讲如果通过JavaScript模拟实现call与apply方法。
贰 ❀ 关于call与apply
贰 ✿ 壹 call与apply区别
除了都能改变this指向并执行函数,call与apply唯一区别在于参数不同,具体如下:
var fn = function (arg1, arg2) {
// do something
};
fn.call(this, arg1, arg2); // 参数散列
fn.apply(this, [arg1, arg2]) // 参数使用数组包裹
call第一参数为this指向,后续散列参数均为函数调用所需形参,而在apply中这些参数被包裹在一个数组中。
贰 ✿ 贰 使用场景
call与apply在日常开发中非常实用,我们在此列举几个实用的例子。
检验数据类型:
function type(obj) {
var regexp = /\s(\w+)\]/;
var result = regexp.exec(Object.prototype.toString.call(obj))[1];
return result;
};
console.log(type([123]));//Array
console.log(type('123'));//String
console.log(type(123));//Number
console.log(type(null));//Null
console.log(type(undefined));//Undefined
数组取最大/小值:
var arr = [11, 1, 0, 2, 3, 5];
// 取最大
var max1 = Math.max.call(null, ...arr);
var max2 = Math.max.apply(null, arr);
// 取最小
var min1 = Math.min.call(null, ...arr);
var min2 = Math.min.apply(null, arr);
console.log(max1); //11
console.log(max2); //11
console.log(min1); //0
console.log(min2); //0
函数arguments类数组操作:
var fn = function () {
var arr = Array.prototype.slice.call(arguments);
console.log(arr); //[1, 2, 3, 4]
};
fn(1, 2, 3, 4);
关于这两个方法实用简单说到这里,毕竟本文的核心主旨是手动实现call与apply方法,我们接着说。
叁 ❀ 实现一个call方法
我们从一个简单的例子解析call方法
var name = '时间跳跃';
var obj = {
name: '听风是风'
};
function fn() {
console.log(this.name);
};
fn(); //时间跳跃
fn.call(obj); //听风是风
在这个例子中,call方法主要做了两件事:
- 修改了
this指向,比如fn()默认指向window,所以输出时间跳跃 - 执行了函数
fn
叁 ✿ 壹 改变this并执行方法
先说第一步改变this怎么实现,其实很简单,只要将方法fn添加成对象obj的属性不就好了。所以我们可以这样:
//模拟call方法
Function.prototype.call_ = function (obj) {
obj.fn = this; // 此时this就是函数fn
obj.fn(); // 执行fn
delete obj.fn; //删除fn
};
fn.call_(obj); // 听风是风
注意,这里的call_是我们模拟的call方法,我们来解释模拟方法中做了什么。
- 我们通过
Function.prototype.call_的形式绑定了call_方法,所以所有函数都可以直接访问call_。 fn.call_属于this隐式绑定,所以在执行时call_时内部this指向fn,这里的obj.fn = this就是将方法fn赋予成了obj的一条属性。obj现在已经有了fn方法,执行obj.fn,因为隐式绑定的问题,fn内部的this指向obj,所以输出了听风是风。- 最后通过
delete删除了obj上的fn方法,毕竟执行完不删除会导致obj上的属性越来越多。
叁 ✿ 贰 传参
我们成功改变了this指向并执行了方法,但仍有一个问题待解决,call_无法接受参数。
其实也不难,我们知道函数有一个arguments属性,代指函数接收的所有参数,它是一个类数组,比如下方例子:
Function.prototype.call_ = function (obj) {
console.log(arguments);
};
fn.call_(obj, 1, 2, 3);// [{name:'听风是风'},1,2,3...]
很明显arguments第一位参数是我们需要让this指向的对象,所以从下标1开始才是真正的函数参数,这里就得对arguments进行加工,将下标1之后的参数剪切出来。
有同学肯定就想到了arguments.splice,前面说了arguments并非数组,所以不支持Array方法。没关系,不是还有Array.prototype.slice.call(arguments)吗,转一次数组再用。很遗憾,我们现在是在模拟call方法,也不行。那就用最保险的for循环吧,如下:
Function.prototype.call_ = function (obj) {
var args = [];
// 注意i从1开始
for (var i = 1, len = arguments.length; i < len; i++) {
args.push(arguments[i]);
};
console.log(args);// [1, 2, 3]
};
fn.call_(obj, 1, 2, 3);
数组也不能直接作为参数传递给函数,有同学可能想到array.join字符拼接方法,这也存在一个问题,比如我们是希望传递参数1 2 3三个参数进去,但经过join方法拼接,它会变成一个参数"1,2,3",函数此时接受的就只有一个参数了。
所以这里我们不得不借用恶魔方法eval,看个简单的例子:
var fn = function (a, b, c) {
console.log(a + b + c);
};
var arr = [1, 2, 3];
fn(1, 2, 3);//6
eval("fn(" + arr + ")");//6
你一定有疑问,为什么这里数组arr都不分割一下,fn在执行时又如何分割数组呢?其实eval在执行时会将变量转为字符串,这里隐性执行了arr.toString()。来看个有趣的对比:
console.log([1, 2, 3].toString()); //"1,2,3"
console.log([1, 2, 3].join(',')); //"1,2,3"
可以看出``eval帮我们做了数组处理,这里就不需要再使用join方法了,因此eval("fn(" + arr + ")")可以看成eval("fn(1,2,3)")`。
我们整理下上面的思路,改写后的模拟方法就是这样:
var name = '时间跳跃';
var obj = {
name: '听风是风'
};
function fn(a, b, c) {
console.log(a + b + c + this.name);
};
//模拟call方法
Function.prototype.call_ = function (obj) {
var args = [];
// 注意i从1开始
for (var i = 1, len = arguments.length; i < len; i++) {
args.push(arguments[i]);
};
obj.fn = this; // 此时this就是函数fn
eval("obj.fn(" + args + ")"); // 执行fn
delete obj.fn; //删除fn
};
fn.call_(obj, "我的", "名字", "是");
可以了吗?很遗憾,这段代码会报错。因为我们传递的后三个参数都是字符串。在args.push(arguments[i])这一步我们提前将字符串进行了解析,这就导致eval在执行时,表达式变成了eval("obj.fn(我的,名字,是)");设想一下我们普通调用函数的形式是这样obj.fn("我的","名字","是"),所以对于eval而言就像传递了三个没加引号的字符串,无法进行解析。
不信我们可以传递三个数字,比如:
fn.call_(obj, 1,2,3); // 6听风是风
因为数字不管加不加引号,作为函数参数都是可解析的,而字符串不加引号,那就被认为是一个变量,而不存在我的这样的变量,自然就报错了。
怎么办呢?其实我们可以在args.push(arguments[i])这里先不急着解析,改写成这样:
args.push("arguments[" + i + "]");
遍历完成的数组args最终就是这个样子["arguments[1]","arguments[2]","arguments[3]"],当执行eval时,arguments[1]此时确实是作为一个变量存在不会报错,于是被eval解析成了一个真正的字符传递给了函数。
所以改写后的call_应该是这样:
var name = '时间跳跃';
var obj = {
name: '听风是风'
};
function fn(a, b, c) {
console.log(a + b + c + this.name);
};
//模拟call方法
Function.prototype.call_ = function (obj) {
var args = [];
// 注意i从1开始
for (var i = 1, len = arguments.length; i < len; i++) {
args.push("arguments[" + i + "]");
};
obj.fn = this; // 此时this就是函数fn
eval("obj.fn(" + args + ")"); // 执行fn
delete obj.fn; //删除fn
};
fn.call_(obj, "我的", "名字", "是"); // 我的名字是听风是风
叁 ✿ 叁 考虑特殊this指向
我们知道,当call第一个参数为undefined或者null时,this默认指向window,所以上面的方法还不够完美,我们进行最后一次改写,考虑传递参数是否是有效对象:
var name = '时间跳跃';
var obj = {
name: '听风是风'
};
function fn(a, b, c) {
console.log(a + b + c + this.name);
};
//模拟call方法
Function.prototype.call_ = function (obj) {
//判断是否为null或者undefined,同时考虑传递参数不是对象情况
obj = obj ? Object(obj) : window;
var args = [];
// 注意i从1开始
for (var i = 1, len = arguments.length; i < len; i++) {
args.push("arguments[" + i + "]");
};
obj.fn = this; // 此时this就是函数fn
eval("obj.fn(" + args + ")"); // 执行fn
delete obj.fn; //删除fn
};
fn.call_(obj, "我的", "名字", "是"); // 我的名字是听风是风
fn.call_(null, "我的", "名字", "是"); // 我的名字是时间跳跃
fn.call_(undefined, "我的", "名字", "是"); // 我的名字是时间跳跃
那么到这里,对于call方法的模拟就完成了。
肆 ❀ 实现一个apply方法
apply方法因为接受的参数是一个数组,所以模拟起来就更简单了,理解了call实现,我们就直接上代码:
var name = '时间跳跃';
var obj = {
name: '听风是风'
};
function fn(a, b, c) {
console.log(a + b + c + this.name);
};
//模拟call方法
Function.prototype.apply_ = function (obj, arr) {
obj = obj ? Object(obj) : window;
obj.fn = this;
if (!arr) {
obj.fn();
} else {
var args = [];
// 注意这里的i从0开始
for (var i = 0, len = arr.length; i < len; i++) {
args.push("arr[" + i + "]");
};
eval("obj.fn(" + args + ")"); // 执行fn
};
delete obj.fn; //删除fn
};
fn.apply_(obj, ["我的", "名字", "是"]); // 我的名字是听风是风
fn.apply_(null, ["我的", "名字", "是"]); // 我的名字是时间跳跃
fn.apply_(undefined, ["我的", "名字", "是"]); // 我的名字是时间跳跃
伍 ❀ 总
上述代码总有些繁杂,我们来总结下这两个方法:
// call模拟
Function.prototype.call_ = function (obj) {
//判断是否为null或者undefined,同时考虑传递参数不是对象情况
obj = obj ? Object(obj) : window;
var args = [];
// 注意i从1开始
for (var i = 1, len = arguments.length; i < len; i++) {
args.push("arguments[" + i + "]");
};
obj.fn = this; // 此时this就是函数fn
var result = eval("obj.fn(" + args + ")"); // 执行fn
delete obj.fn; //删除fn
return result;
};
// apply模拟
Function.prototype.apply_ = function (obj, arr) {
obj = obj ? Object(obj) : window;
obj.fn = this;
var result;
if (!arr) {
result = obj.fn();
} else {
var args = [];
// 注意这里的i从0开始
for (var i = 0, len = arr.length; i < len; i++) {
args.push("arr[" + i + "]");
};
result = eval("obj.fn(" + args + ")"); // 执行fn
};
delete obj.fn; //删除fn
return result;
};
如果允许使用ES6,使用拓展运算符会简单很多,实现如下:
// ES6 call
Function.prototype.call_ = function (obj) {
obj = obj ? Object(obj) : window;
obj.fn = this;
// 利用拓展运算符直接将arguments转为数组
let args = [...arguments].slice(1);
let result = obj.fn(...args);
delete obj.fn
return result;
};
// ES6 apply
Function.prototype.apply_ = function (obj, arr) {
obj = obj ? Object(obj) : window;
obj.fn = this;
let result;
if (!arr) {
result = obj.fn();
} else {
result = obj.fn(...arr);
};
delete obj.fn
return result;
};
那么到这里,关于call与apply模拟实现全部结束。bind实现存在部分不同,我另起了一篇文章,详情请见js 手动实现bind方法,超详细思路分析!
这篇文章也是第一篇我使用markdown书写的文章,为了统一样式,我也专门修改了博客样式。
参考
js 实现call和apply方法,超详细思路分析的更多相关文章
- JS中 call() 与apply 方法
1.方法定义 call方法: 语法:call([thisObj[,arg1[, arg2[, [,.argN]]]]]) 定义:调用一个对象的一个方法,以另一个对象替换当前对象. 说明: call ...
- 面向对象的js编程 Call和apply方法
JavaScript中有一个call和apply方法,其作用基本相同,但也有略微的区别. 一.方法定义 1.call 方法 语法:call([thisObj[,arg1[, arg2[, [,.arg ...
- js中 call() 和 apply() 方法的区别和用法详解
1.定义 每个函数都包含俩个非继承而来的方法:call() 和 apply() call 和 apply 可以用来重新定义函数的的执行环境,也就是 this 的指向:call 和 apply 都是 ...
- js中Function的apply方法与call方法理解
最近在使用jQuery的$.each方法时很,突然想到$.each($('div'),function(index,entity){});中的这个index和entity是哪冒出来的,而且可有可无的, ...
- Spring Boot 日志配置方法(超详细)
默认日志 Logback : 默认情况下,Spring Boot会用Logback来记录日志,并用INFO级别输出到控制台.在运行应用程序和其他例子时,你应该已经看到很多INFO级别的日志了. 从上图 ...
- VS2015ASP.NET MVC5项目中Spring.NET配置方法(超详细)
首先,在ASP.NET MVC5项目右键,如下图所示,选择“管理Nuget程序包...” 然后,在弹出的页面的搜索框中输入“spring.web”,在返回结果中选择Spring.Web和Spring. ...
- Windows 2016 无域故障转移群集部署方法 超详细图文教程 (二)
上一章我们配置了一台设备,接着根据那个配置,配置其它设备.这里我配置了三台设备: 创建故障转移群集,并添加设备. 之前的操作都是每台服务器都要做的,而这个操作,只需要任选一台去做即可,我这里选d1 1 ...
- Windows 2016 无域故障转移群集部署方法 超详细图文教程 (一)
故障转移群集是一个很实用的功能,而windows在2016版本开始,终于支持不用域做故障转移群集. 在群集中,我们可以设定一个"群集IP" 而客户端只需要根据这个"群集I ...
- (企业面试部分)超详细思路讲解SQL语句的查询实现,及数据的创建。
企业面试部分详细的SQL问题,思路讲解 第一步:创建数据库表,及插入数据信息 --Student(S#,Sname,Sage,Ssex) 学生表 CREATE TABLE student( sno ) ...
- 超详细思路讲解SQL语句的查询实现,及数据的创建。
最近一直在看数据库方面的问题,总结了一下SQL语句,这是部分详细的SQL问题,思路讲解: 第一步:创建数据库表,及插入数据信息 --Student(S#,Sname,Sage,Ssex) 学生表 CR ...
随机推荐
- python 设计模式 开篇 第1篇
1. 设计模式的定义 软件设计中普遍存在的 反复出现的 各种问题 所提出的解决方案 首先,什么是软件设计? 系统开发 项目开始的时候 需要做 需求分析 软件系统设计 软件建模 类的设计 接口的设计 等 ...
- [kubernetes]服务健康检查
前言 进程在运行,但是不代表应用是正常的,对此pod提供的探针可用来检测容器内的应用是否正常.k8s对pod的健康状态可以通过三类探针来检查:LivenessProbe.ReadinessProbe和 ...
- 【LINT】cpplint 分析笔记
cpplint 分析笔记 · [前提得看下google规范] @2022-1-13 20:44:48 error message formate: [filename] [linenum] [mess ...
- 文心一言 VS 讯飞星火 VS chatgpt (185)-- 算法导论14.1 2题
二.用go语言,对于图 14-1中的红黑树 T 和关键字 x.key 为35的结点x,说明执行 OS-RANK(T,x) 的过程. 文心一言: 在红黑树中,OS-RANK(T, x) 是一个操作,用于 ...
- OpenKruise :Kubernetes背后的托底
本文分享自华为云社区<OpenKruise核心能力和工作原理>,作者:可以交个朋友. 一. 诞生背景 Kubernetes 自身提供的应用部署管理功能,无法满足大规模应用场景的需求,例如应 ...
- 【ES系列】(一)简介与安装
首发博客地址 首发博客地址 系列文章地址 教学视频 为什么要学习 ES? 强大的全文搜索和检索功能:Elasticsearch 是一个开源的分布式搜索和分析引擎,使用倒排索引和分布式计算等技术,提供了 ...
- [转帖]超好用的自带火焰图的 Java 性能分析工具 Async-profiler 了解一下
https://cloud.tencent.com/developer/article/1554194 火焰图 如果你经常遇到 Java 线上性能问题束手无策,看着线上服务 CPU 飙升一筹莫展,发现 ...
- [转帖]top指令如何查询指定用户的所有进程?
有两种方法. 1.top指令与字符串查询指令通过通道配合查询. 指令: "-d 5"指的是每五秒刷新一次进程列表,"grep"是字符串查询指令,它可以将含 ...
- Harbor镜像仓库的导出与整理之二
Harbor镜像仓库的导出与整理之二 背景 前几天参照大神的blog进行了一下harbor的镜像列表的获取与下载. 当时发现一个很诡异的问题. 实际上镜像仓库里面的镜像很多. 但是导出和列表里面的却很 ...
- Linux 环境下 node 以及 jit 的简单环境配置说明
注意事项: 1. 注意这个包需要实时更新. 2. 更新时需要按照目录覆盖文件. 3. 谁负责谁治理的态度, 有更新,需要完整的进行覆盖安装. 建议先删除旧文件, 替换新文件. 4. 为了简单起见, 直 ...