call、apply、bind详细解析
语法
MDN中对三种方法进行了详细的说明:
func.call(thisArg,arg1,arg2,...)
thisArg:可选,在func函数运行时使用的this值。在非严格模式下,指定为null或undefined时自动指向全局对象。
arg1,arg2,...:指定的参数列表
func.apply(thisArg,[argsArray])
thisArg:与call中一致。
argsArray:可选,一个数组或者类数组对象,其中的数组元素将作为单独的参数传给func函数。如果该参数值为null或undefined,则表示不需要传入参数。
func.bind(thisArg,arg1,arg2,...)
参数与call一致
可以发现,三种方法各自有相似的地方,又各自有不同之处。具体差别在哪里我们总结一下:
共同点:
-
三个都是挂在Function原型上的方法,只有函数才能调用。
-
作用都是用来改变函数执行时的this指向。
区别:
1.call和apply的区别:
call和apply的唯一区别就是其传给函数的参数形式不同,call的参数是一串参数组成的序列,apply的参数是一个数组或者类数组,其实就相当于把call的参数放到了数组中。
2.call和bind的区别:
- call和bind的语法除了方法名不一样外,其他的一模一样。但是区别在于其返回不一样。call的返回和apply一样,都是返回调用指定this和参数的函数执行的结果;而bind的返回则是原函数的拷贝,并拥有制定的this值和初始参数。
- call在改变this上下文后立即执行函数,bind则是返回拷贝的函数,原函数不执行,因而后面还需单独调用函数执行。
应用
1. 改变作用域(改变this指向)
在《JavaScript高级程序设计》中,对call和apply用途是这样描述的:这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。也就是俗称的改变this指向。
这一应用也是call、apply、bind最重要最常用的应用了。通过改变this指向,可以借用其他对象上的方法,实现代码复用。
例子如下:
window.name = "zhang"
function Test(){
console.log(this.name);
}
let obj = {
name: 'joury'
}
Test() //zhang
Test.call(obj) //joury
Test.bind(obj)() //joury
对bind来说,bind()会创建一个this改变的不会立即执行的绑定函数,这个函数也可以用来作为构造函数来用。
2.判断数据类型
其实这种应用主要靠的也是改变this指向来实现的。大家都知道typeof运算符可以判断数据类型,但是typeof用来判断一些基本数据类型还算方便,在引用类型上判断就不那么灵光。我们先回顾一下typeof的返回:
- undefined ---如果这个值未定义
- boolean ---如果这个值是布尔值
- string ---如果这个值是字符串
- number ---如果这个值是数值
- object ---如果这个值是对象或null
- function ---如果这个值是函数
可以看到,typeof的返回值在null上有些令人迷惑,但是技术上也并无错误,毕竟null被认为是一个空对象引用,但是在严格判断上未免不尽人意。
为了更好的判断数据类型,我们可以采用Object.prototype.toString.call()的方法,call的这种改变this指向的功能则可以实现判断多种数据类型,几乎可以判断所有类型的数据。
注意:Object.prototype.toString.call()返回的是[Object 类型]的格式,为了单独提取出类型字符串,一般采用Object.prototype.toString.call().slice(8,-1)将类型字符串截取出来。
这种方式可以判断String、Number、Boolean、Null、Undefined、Object、Array、Function、Date、RegExp、Map、Set、HTMLDivElement(DOM元素)、WeakMap、Window、Error、Arguments等数据类型,相比typeof强大很多。
3.使Math方法可以直接跟数组
在进行数据分析、比较时,大家一般都把数据放在一个数组中,数组自身的方法就有很多,但是有些方法就不方便操作,比如获取最大值最小值等,需要使用Math.max等方法,但是Math.max方法后面跟的是数值,不能跟数组,怎么办呢?
刚才讲过apply传递参数刚好是数组类型,同时还可以改变this指向,因此可以采用Math.max.apply(Math,array)的方法来对数组进行操作。
let arr = [1,2,3,5,7,9];
let max = Math.max.apply(Math,arr);
console.log(max);//9
这里多说一句,在利用Math.max来操作数组上,ES6新加了spread操作符也可以做到,spread操作符是由三个点组成(...),很像Rest操作符,但是意义相反,是为了展开操作,将可迭代的对象进行展开。
let arr1 = [1,5,9,6,4,7];
let arr2 = [2,0,8,...arr1,10]; //[2,0,8,1,5,9,6,4,7,10]
let max = Math.max(...arr2); //10
4.指定默认参数(偏函数)
这个主要是bind的应用,用来初始化部分参数。
function mul(a,b) {
return a * b;
};
let double = mul.bind(null,2);
let triple = mul.bind(null,3);
alert(double(4)); // = mul(2, 4) = 6
alert( triple(4) ); // = mul(3, 4) = 12
当然,偏函数也可以用call、apply来实现,但是为了不立即执行,一般会采用bind来创建绑定函数。偏函数可以引出 “柯里化” 这一概念,在此文中不细述。
5.多重嵌套函数this保存
在多重嵌套的函数中,我们经常会遇到保存上级作用域this的情况,尤其是在绑定事件、定时器中。一般都采用let That = this之类的方式来保存this,但是学过bind之后,可以采用另一种方法来保存:
//常用的用变量保存this的方法
btn.click(function(){
let _this = this;
lis.each(function(){
this; //lis
_this; //btn
})
})
//用bind保存this的方法
btn.click(function(){
lis.each(function(){
this; //btn
}).bind(this)()
})
手写call、apply、bind
1.手写call
以下方法来自于OBKoro1大佬的js基础-面试官想知道你有多理解call,apply,bind?[不看后悔系列]
//call是函数的一个方法,所以需要写在Function的原型上,而不是单纯写一个具有call功能的函数。
Function.prototype.myCall = function (context, ...arr) {
if (context === null || context === undefined) {
// 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
context = window
} else {
context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
}
const specialPrototype = Symbol('特殊属性Symbol') // 用于临时储存函数
context[specialPrototype] = this; // 函数的this指向隐式绑定到context上
let result = context[specialPrototype](...arr); // 通过隐式绑定执行函数并传递参数
delete context[specialPrototype]; // 删除上下文对象的属性
return result; // 返回函数执行结果
};
2.手写apply
待补充,稍后更新