本文介绍JavaScript 中的 call 、apply 和 bind 方法的基本使用,使用注意点以及常见的使用场景等,并简单介绍这些方法的实现原理提供对应的源码。
call && apply 方法

call 和 apply 是 JavaScript 中两个重要的常用方法,这两个方法的功能 (作用) 基本上是一样的,都是修改函数内部的 this,并且执行当前函数,如果这个函数是其它对象的成员,那么也可以把它们的功能理解为借用对象的方法并绑定为 this

我们先通过代码来看下call 和 apply的基本使用情况。

// call && apply基本使用
// (1) 修改函数中的 this
// (2) 执行修改了this后的函数 /* 演示代码-01 */
function f1() {
console.log("f1-1-this->", this)
} /* 001-直接调用函数 */
f1();
/* 打印结果:f1-1-this->window */ /* 002-通过 call 和 apply 调用函数 */
f1.call({ name: "zs" });
f1.apply({ name: "zs" });
/* 打印结果:f1-1-this->{ name: "zs" } */
/* 打印结果:f1-1-this->{ name: "zs" } */ /* 演示代码-02 */
function a() {
console.log("a-1-this->", this)
} function b() {
console.log("b-1-this->", this)
} a(); /* a-1-this->window */
b(); /* b-1-this->window */
a.call(b); /* a-1-this->function b */
a.call.call.call(b); /* b-1-this->window */ /* 演示代码-03 */
let o1 = { name: "Yong", showName() { console.log("姓名:" + this.name) } };
let o2 = { name: "Xia" }; // o1.showName(); /* 姓名:Yong */
// o2.showName(); /* 报错:Uncaught TypeError: o2.showName is not a function */ /* 相当于是 o2.showName() */
o1.showName.call(o2); /* 姓名:Xia */
o1.showName.apply(o2); /* 姓名:Xia */

call 和 apply的基本功能一样,但使用时也存在一些差异,体现在两个方面。

  • 参数的传递方式不同,call通过参数列表方式传递,apply则通过数组的方式传递
  • 形参(期望传递的参数)的个数不同,call方法的形参个数为0,而 apply方法的形参个数为1
let o1 = {
name: "Yong",
show() {
console.log("姓名:" + this.name + " Other:", arguments);
}
};
let o2 = { name: "Xia" }; /* (1) 参数的传递方式不同 */
o1.show(); /* 姓名:Yong Other: */
o1.show.call(o2); /* 姓名:Xia Other: */
o1.show.call(o2, 100, 200, "abc"); /* 姓名: Xia Other: Arguments(3)[100, 200, "abc"] */
o1.show.apply(o2, [10, 20, "abc"]); /* 姓名: Xia Other: Arguments(3)[100, 200, "abc"] */ /* (2) 形参个数不同 */
console.log(Function.prototype.call === o1.show.call); /* true */
console.log(Function.prototype.call.length, Function.prototype.apply.length)/* 1,2 */

基于 call 方法和 apply 方法的基本功能和它们的差异,下面试着给出这两个方法的实现原理( 源码 ),因为所有的函数都能够调用这两个方法,因此这两个方法自然应该被实现在Function.prototype上面,内部的实现主要处理两个工作,即修改 this 和 执行函数,在调用并执行函数的时候需要考虑到参数的传递以及它们传递方式的不同。

/* call 原理 */
Function.prototype.call = function(context) {
/* 01-上下文环境的容错处理,如果context是原始类型那么就先包装 */
context = context ? Object(context) : window; /* 02-获取方法并把该方法添加到当前的对象上 */
context.f = this; /* 03-拿到参数列表(剔除了绑定 this的第一个参数) */
let args = [];
for (let i = 1; i < arguments.length; i++) {
args.push(arguments[i]);
} /* 04-调用并执行函数,利用了数组的 toString来处理参数 */
return eval("context.f(" + args + ")");
} /* apply 原理 */
Function.prototype.apply = function(context, args) {
/* 01-上下文环境的容错处理,如果context是原始类型那么就先包装 */
context = context ? Object(context) : window; /* 02-获取方法并把该方法添加到当前的对象上 */
context.f = this; /* 03-如果没有以数组传递参数那么就直接调用并返回*/
if (!args) {
return context.f();
} /* 04-如果以数组传递了参数那么就利用 eval 来执行函数并返回结果 */
return eval("context.f(" + args + ")");
} /* 测试代码 */
let o1 = {
name: "Yong",
show() {
console.log("姓名:" + this.name + " Other:", arguments);
}
}; let o2 = { name: "Xia" };
o1.show.call(o2, 10, 20, 30); /* 姓名:Xia Other: Arguments(3) [10, 20, 30] */
o1.show.apply(o2, [100, 200, 300]); /* 姓名:Xia Other: Arguments(3) [100, 200, 300] */ console.log(Function.prototype.call === o1.show.call); /* true */
console.log(Function.prototype.call.length, Function.prototype.apply.length) /* 1,2 */
bind 方法

在 JavaScript 中,其实现在bind方法用的已经比较少了,我个人的感觉是因为这个方法使用起来相对于 call 或者是 apply 来说会比较麻烦,而且可读性不好,bind方法的功能和 call 很像,它也能过绑定函数中的 this,区别在于该方法并不执行函数,而是把绑定了(修改了) this后的函数返回。

在下面通过一段代码来简单演示bind方法的基本使用。

/* bind 方法的基本使用                */
/* (1) 绑定函数中的 this */
/* (2) 把绑定 this 后的函数返回 */
/* (3) 允许多种传参的方式 */
/* (4) 可以通过 new 来调用目标函数 */
/* (5) 实例化对象能找到原类的原型对象 */ /* 演示代码-01 */
let o1 = {
name: "Yong",
show() {
console.log("姓名:" + this.name + " Other:", arguments);
}
}; let o2 = { name: "Xia" };
let fnc = o1.show.bind(o2); fnc(10, 20, 30); /* 姓名:Xia Other: Arguments(3) [10, 20, 30] */ /* 演示代码02 */
f1.prototype.say = function() { console.log("say ...") } function f1(a, b, c) {
console.log("f1-this->", this, a, b, c);
} function f2() {
console.log("f2-this->", this);
} /* [1] 允许两种传参方式: */
/* 方式1 */
// let F = f1.bind(f2,10,20,30);
// F(); /* f1-this-> ƒ f2() 10,20,30 */ /* 方式2 */
let F = f1.bind(f2, 10);
F(20, 30); /* f1-this-> ƒ f2() 10,20,30 */ /* [2] 通过 new 来调用目标函数 */
/* 注解:实例化的对象 f 构造函数为原先的函数 f1 */
let f = new F(200, 300); /* f1-this-> f1 {} 10 200 300 */
console.log(f); /* f1 {} */ /* [3] 实例化的对象可以找到原先构造函数的原型对象 */
f.say(); /* say ... */

如果仅仅是处理修改函数中的 this 并把函数返回,那么bind方法在实现上会简单很多,似乎只需要像下面这样来在 Function.prototype上面添加一个 bind函数就可以了。

Function.prototype.bind = function(context) {
let that = this;
return function() {
that.call(context);
}
} /* 测试代码 */
function fn1() {
console.log("fn1-", this)
} function fn2() {
console.log("fn2-", this)
} let fn = fn1.bind(fn2);
fn(); /* fn1- ƒ fn2() */

但是如果需要把参数的传递以及构造函数的调用等因素都考虑进去,那么bind方法内部的实现可能就会稍微复杂点,特别是它允许两种方式来传递参数,下面给出最终版本的代码供参考。

/* bind 方法的实现原理 */
Function.prototype.bind = function(context) {
let that = this; /* 获取第一部分参数 : ex 获取 let F = f1.bind(f2, 10); 中的10*/
let argsA = [].slice.call(arguments, 1); /* [10] */ function bindFunc() {
/* 获取第二部分的参数:ex 获取 F(20, 30); 中的 20 和 30 */
let argsB = [].slice.call(arguments); /* [20,30] */
let args = [...argsA, ...argsB];
return that.apply(this instanceof bindFunc ? this : context, args);
} /* 原型处理 */
function Fn() {};
Fn.prototype = this.prototype;
bindFunc.prototype = new Fn(); /* 返回函数 */
return bindFunc;
}

前端开发系列036-基础篇之call && apply的更多相关文章

  1. 从0到1用react+antd+redux搭建一个开箱即用的企业级管理后台系列(基础篇)

    背景 ​ 最近因为要做一个新的管理后台项目,新公司大部分是用vue写的,技术栈这块也是想切到react上面来,所以,这次从0到1重新搭建一个react项目架子,需要考虑的东西的很多,包括目录结构.代码 ...

  2. 前端开发:css基础知识之盒模型以及浮动布局。

    前端开发:css基础知识之盒模型以及浮动布局 前言 楼主的蛮多朋友最近都在学习html5,他们都会问到同一个问题 浮动是什么东西?  为什么这个浮动没有效果?  这个问题楼主已经回答了n遍.今天则是把 ...

  3. ESP8266开发之旅 基础篇① 走进ESP8266的世界

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

  4. ESP8266开发之旅 基础篇② 如何安装ESP8266的Arduino开发环境

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

  5. ESP8266开发之旅 基础篇③ ESP8266与Arduino的开发说明

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

  6. openlayers5-webpack 入门开发系列一初探篇(附源码下载)

    前言 openlayers5-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载 ...

  7. leaflet-webpack 入门开发系列一初探篇(附源码下载)

    前言 leaflet-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载地址 w ...

  8. 【Windows10 IoT开发系列】配置篇

    原文:[Windows10 IoT开发系列]配置篇 Windows10 For IoT是Windows 10家族的一个新星,其针对不同平台拥有不同的版本.而其最重要的一个版本是运行在Raspberry ...

  9. ESP8266开发之旅 基础篇④ ESP8266与EEPROM

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

  10. ESP8266开发之旅 基础篇⑥ Ticker——ESP8266定时库

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

随机推荐

  1. emmy断点调试

    package.cpath = package.cpath .. ';C:/Users/Administrator/AppData/Roaming/JetBrains/IntelliJIdea2021 ...

  2. 使用Python可视化洛伦兹变换

    引言 大家好!今天我们将探讨一个非常有趣且重要的物理概念-洛伦兹变换.它是相对论的核心内容之一,描述了在高速运动下,时间.长度以及其他物理量是如何发生变化的.通过使用 Python 进行可视化,我们不 ...

  3. docker容器运行,交互式与守护式的区别

    一.使用交互式运行容器,容器运行后直接进入到容器内部,退出容器内部后,容器直接关闭

  4. 🎀抓包工具安装-Charles

    简介 Charles 作为一个 HTTP 代理/HTTP 监视器/反向代理工具,允许开发者查看他们的计算机与互联网之间的所有 HTTP 和 HTTPS 通信.工作原理是基于 HTTP 代理的概念,它充 ...

  5. 为什么 Java 中某些新生代和老年代的垃圾收集器不能组合使用?

    为什么 Java 中某些新生代和老年代的垃圾收集器不能组合使用? 在 JVM 中,新生代和老年代的垃圾收集器是分工协作的.然而,并非所有的新生代和老年代垃圾收集器都能任意组合使用,这是由于它们的设计目 ...

  6. 成语答题小程序v3.0

    自从开源成语答题小程序以来不断完善功能,并且不断修复bug,成语答题小程序v3版本完善了很多功能 1.增加了原生模板广告,设置原生模板广告后可以设置首页或答题页是否显示原生模板广告 2.增加了背景设置 ...

  7. [java与https]第一篇、证书杂谈

    一.算法.密钥(对).证书.证书库 令狐冲是个马场老板,这天,他接到店里伙计电话,说有人已经签了租马合同,准备到马场提马,,他二话不说,突突突就去了,到了之后,发现不认识租客. 令狐冲说,你把你租马合 ...

  8. TVM:设计与架构

    本文档适用于想要了解 TVM 架构和/或积极开发项目的开发人员.页面组织如下: 示例编译流程概述了 TVM 将模型的高层描述转换为可部署模块所采取的步骤.要开始使用,请先阅读本节. 逻辑架构组件部分描 ...

  9. Seata源码—5.全局事务的创建与返回处理

    大纲 1.Seata开启分布式事务的流程总结 2.Seata生成全局事务ID的雪花算法源码 3.生成xid以及对全局事务会话进行持久化的源码 4.全局事务会话数据持久化的实现源码 5.Seata Se ...

  10. github常见开源协议概括

    None / No License 默认协议,不允许他人复杂.分发.修改.使用,只能fork下来看 Apache License 2.0 允许个人使用.商业使用.复制.修改.分发,但是出了事作者免责, ...