壹.2.4 深入理解 call、apply、bind
上一篇我们知道,call、apply、bind都是和this指向有关的,这三个方法是JavaScript内置对象Function的原型的方法。相当一部分前端工程师对它们的理解仍旧比较浅显,所谓具备JavaScript基础扎实,是绕不开这些基础又常用的知识点的,这次让我们来深入理解并掌握它们。
壹.2.4.1 基本介绍
1. 语法
func.call(thisArg, param1, param2, ...)//func是个函数
func.apply(thisArg, [param1,param2,...])
func.bind(thisArg, param1, param2, ...)返回值:
call / apply:返回func 执行的结果 ;
bind:返回func的拷贝,并拥有指定的this值和初始参数。
参数:
thisArg(可选):
func的this指向thisArg对象;非严格模式下:若
thisArg指定为null,undefined,则func的this指向window对象;严格模式下:
func的this为undefined;值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象,如 String、Number、Boolean。
param1,param2(可选): 传给func的参数。
如果param不传或为 null/undefined,则表示不需要传入任何参数.
apply第二个参数为类数组对象,数组内各项的值为传给
func的参数。
2. 必须是函数才能调用call/apply/bind
call、apply 和 bind 是挂在Function对象上的三个方法,只有函数才有这些方法。只要是函数就可以调用它们,比如: Object.prototype.toString就是个函数,我们经常看到这样的用法:Object.prototype.toString.call(data)。
3. 作用
改变函数执行时的this指向,目前所有关于它们的运用,都是基于这一点来进行的。
4. 如何不弄混call和apply
弄混这两个方法的不在少数,不要小看这个问题,记住下面的这个方法就好了。双a记忆法:apply是以a开头,它传给func的参数是类Array对象(类数组对象),也是以a开头的。
call与apply的唯一区别
传给func的参数写法不同:
apply是第2个参数,这个参数是一个类数组对象:传给
func参数都写在数组中。call从第2~n的参数都是传给
func的。
call/apply与bind的区别
执行:
call/apply改变了函数的
this的指向并马上执行该函数;bind则是返回改变了
this指向后的函数,不执行该函数。
返回值:
call/apply 返回
func的执行结果;bind返回
func的拷贝,并指定了func的this指向,保存了func的参数。
返回值这段在下方bind应用中有详细的示例解析。
壹.2.4.2 什么是类数组?
先说数组,这我们都熟悉。它的特征有:可以通过索引(index)调用,如 array[0];具有长度属性length;可以通过 for 循环或forEach方法,进行遍历。
那么,类数组是什么呢?顾名思义,就是具备与数组特征类似的对象。比如,下面的这个对象,就是一个类数组。
类数组 arrayLike 可以通过下标进行调用,具有length属性,同时也可以通过 for 循环进行遍历。
类数组,还是比较常用的,只是我们平时可能没注意到。比如,我们获取 DOM 节点的方法,返回的就是一个类数组;再比如,在一个函数体中使用 arguments 获取到的所有参数,也是一个类数组。
但是需要注意的是:类数组无法使用 forEach、splice、push 等数组原型链上的方法,毕竟它不是真正的数组。那么类数组想使用数组原型链上的方法,该怎么办呢?请继续往下看。
壹.2.4.3 call/apply/bind的核心理念:借用方法
什么是借用方法,让我们打个比方。
生活中:
平时没时间做饭的我,周末想给家人炖个牛肉火锅尝尝。但是没有适合的锅,而我又不想出去买,所以就向邻居借了一个锅来用,这样既达到了目的,又节省了开支,一举两得。
程序中:
A对象有个方法,B对象因为某种原因也需要用到同样的方法,那么这时候我们是单独为 B 对象扩展一个方法呢,还是借用一下 A 对象的方法呢?
当然是借用 A 对象的方法更便捷,既达到了目的,又节省了内存。
这就是call/apply/bind的核心理念:借用方法。借助已实现的方法,改变方法中数据的this指向,减少重复代码,节省内存。
还记得刚才的类数组么?如果它想借用 Array 原型链上的slice方法,可以这样:
以此类推,domNodes 就可以应用 Array 下的其他方法了。
壹.2.4.4 应用场景
这些应用场景,多加体会就可以发现它们的理念都是:借用方法。
1. 判断数据类型
Object.prototype.toString用来判断类型再合适不过,借用它我们几乎可以判断所有类型的数据:
2. 类数组对象借用数组的方法
因为类数组对象不是真正的数组,所以没有数组类型上自带的一些方法,所以我们需要去借用数组的方法。
比如借用数组的push方法:
3. apply获取数组最大值最小值
apply直接传递数组当作要调用方法的参数,也省去了展开数组这一步,比如使用Math.max、Math.min来获取数组的最大值/最小值。
4. 继承
ES5的继承也都是通过借用父类的构造方法来实现父类方法/属性的继承:
类似的应用场景还有很多,就不再一一列举了。关键在于它们借用方法的理念,若不理解的话可以多看几遍。
5. call、apply应该用哪个?
call,apply的效果完全一样,它们的区别也在于
参数数量/顺序确定就用call,参数数量/顺序不确定的话就用apply。
考虑可读性:参数数量不多就用call,参数数量比较多的话,把参数整合成数组,使用apply。
参数集合已经是一个数组的情况,用apply,比如上文的获取数组最大值/最小值。
参数数量/顺序不确定的话就用apply,比如以下示例:
6. bind的应用场景
保存函数参数
首先来看下一道经典的面试题:
造成这个现象的原因是等到setTimeout异步执行时,i已经变成6了。那么如何使他输出: 1,2,3,4,5呢?可以通过bind来巧妙实现。
实际上这里也用了闭包,我们知道bind会返回一个函数,这个函数也是闭包(下一篇会深度介绍“闭包”的相关知识)。
它保存了函数的this指向、初始参数,每次i的变更都会被bind的闭包存起来,所以输出1-5。具体细节,下面有个手写bind方法,详细阅读一下就能搞明白。
回调函数this丢失问题
这是一个常见的问题,下面是我在开发VSCode插件处理webview通信时,遇到的真实问题,一开始以为VSCode的API哪里出问题,调试了一番才发现是this指向丢失的问题。
回调函数this为何会丢失?显然声明的时候不会出现问题,执行回调函数的时候也不可能出现问题。问题出在传递回调函数的时候:
因为传递过去的this.handleMessage是函数内存地址,没有附带上下文对象,也就是说该函数this.handleMessage没有绑定它的this指向。
既然知道问题了,那我们只要绑定回调函数的this指向为PageA就解决问题了。
回调函数this丢失的解决方案
(1)bind绑定回调函数的this指向:
这是典型bind的应用场景, 绑定this指向,用做回调函数。
(2)用箭头函数绑定this指向:
箭头函数的this指向定义的时候外层第一个普通函数的this,在这里指的是class类:PageA
壹.2.4.5 面试题:用原生JavaScript实现call/apply、bind
在一线互联网公司的面试中,手写实现call、apply、bind(特别是bind)一直是比较高频的面试题,在这里我们也一起来实现一下这几个函数。
1. 实现call
实现思路
参考call的语法规则,需要设置一个参数
thisArg,也就是this的指向;将
thisArg封装为一个Object;通过为
thisArg创建一个临时方法,这样thisArg就是调用该临时方法的对象了,会将该临时方法的this隐式指向到thisArg上(参考上一篇文章《壹.2.3 彻底搞懂this》);执行
thisArg的临时方法,并传递参数;删除临时方法,返回方法的执行结果。
认真读一下注释,基本就能理解了,主要还是用到判断this指向的规则。如果读完还是不明白,可以加我的微信(见本书封面)交流。附带更正一下,其中第11行:
当以非构造函数形式被调用时,
Object等同于new Object()。 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object
多讲一下如何正确地判断thisArg:
上面代码判断thisArg的写法是经过调整后的严谨写法,因为之前笔者发现很多前端工程师判断参数thisArg,只是简单的以是否为false来判断,比如:
经过测试,以下三种情况,thisArg都会意外地绑定到window上:
所以应该严谨一点儿判断,如下:
2. 实现Apply
实现思路
传递给函数的参数处理,不太一样,其他部分跟call一样;
apply接受第二个参数为类数组对象, 这里用了《JavaScript权威指南》一书中判断是否为类数组对象的方法。
3. 实现bind
手写实现bind是一线互联网企业中的一个高频的面试题,如果面试的中高级前端,只是能说出它们的区别、用法并不能让你脱颖而出,对bind的理解要有足够的深度,才能得到面试官的赞许。
实现思路
(1)拷贝调用函数:
调用函数,也即调用
myBind的函数,用一个变量临时储存它;使用
Object.create复制调用函数的prototype给funcForBind;
(2)返回拷贝的函数funcForBind;
(3)调用拷贝的函数funcForBind:
new调用判断:通过instanceof判断函数是否通过new调用,来决定绑定的context;通过
call绑定this、传递参数;返回调用函数的执行结果。
这里给出一种基于ES6的实现(基于老版ES的实现方式也不少,但没有ES6的spread和rest操作符实现简洁):
上面的代码第7行可能不好理解,二次传参(secondParams)是说什么?举个例子:
上面第8行调用myBind之后,会返回一个新函数,然后再给这个新函数传入参数,就是二次传参。
最后更新于
这有帮助吗?