本文介绍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. HashMap 的 put 方法源码分析(JDK 1.8)

    一.HashMap 的 put 方法源码分析(JDK 1.8) 以下是 HashMap 的 put 方法的源码(JDK 1.8): hash(key) 方法 hash(key) 方法用于计算键的哈希值 ...

  2. 微信接龙转Excel

    1.新建Excel表格 2.将微信接龙信息复制至表格 3.选择列 4.选择[数据]->[分列] 5.选择[分隔符号]->[下一步] 6.选择[分隔符号]->[下一步] 这里以[空格] ...

  3. apache配置symfony并隐藏入口文件app.php

    ------------------------------- 参考: 配置Web服务器 apache url路由配置重写 Apache URL重写规则(详解) symfony官网文档 ------- ...

  4. 『Plotly实战指南』--样式定制基础篇

    在数据可视化的世界中,一个精心设计的图表不仅能准确传达信息,还能提升整体的专业性和吸引力. 而Plotly作为Python中强大的可视化库,提供了丰富的样式定制功能,帮助我们轻松实现这一目标. 本文从 ...

  5. 等保2.0>Windows下实现MySQL数据库自动备份

    说明: MySQL数据库安装目录:C:\Program Files\mysql-5.7.37-winx64\mysql-5.7.37-winx64 MySQL数据库存放目录:C:\Program Fi ...

  6. Java开发手册——【强制】所有整型包装类对象之间值的比较,全部使用 equals 方法比较

    Java开发手册说明: 对于 Integer var = ? 在-128 至 127 之间的赋值,Integer 对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的 I ...

  7. 【工具】Vscode翻译插件推荐(不用谷歌翻译api、支持短句英汉互译、支持查词、支持自动补全、不需要浏览器)

    2024/04/24说明:这篇暂时修改为粉丝可见,因为正在冲粉丝量,等到我弄完了粉丝量的要求,我就改回来!不方便看到全文的小伙伴不好意思!! 需求: 1)偶尔需要查英文生词: 2)有时候想不起来中文对 ...

  8. MySql的information_schema.processlist库学习之"如何检测出大数据sql查询"

    1.如何通过MySql检测出大数据sql查询 一般数据库都会存在:information_schema数据库 检测出大数据sql查询[time时间越长说明,数据量越大,要根据公司的限度来衡量,我的思路 ...

  9. RPC实战与核心原理之异步RPC

    异步RPC:压榨单机吞吐量 如何提升单机吞吐量 提升吞吐量,其实关键就两个字:"异步",提高CPU等资源的利用率 调用端如何异步 异步,最常用的方式就是返回 Future 对象的 ...

  10. C# 之静态构造器与静态字段初始化器

    public class Test { /// <summary> /// 静态字段初始化器会在调用静态构造器前运行. /// 如果类型没有静态构造器,字段会在类型被使用前或运行时中更早的 ...