1. call 和 apply的区别
Function.prototype.call 和 Function.prototype.apply都是非常常用的方法,它们的作用一模一样,区别仅在于传入参数形式的不同。
apply接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可能为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数:
var func = function (x,y,z) {
console.log([x,y,z]);
// [1,2,3]
};
func.apply(null,[1, 2, 3]);
在这段代码中,参数1、2、3被放在数组中一起传入func函数,它们分别对应func参数列表中的x、y、z。
call传入的参数数量不固定,跟apply相同的是,第一个参数也是代表函数体内的this指向,从第二个参数开始往后,每个参数被依次传入函数:
var func = function (x,y,z) {
console.log([x,y,z]);
// [1,2,3]
};
func.call(null,1, 2, 3);
当调用一个函数时,JavaScript的解释器并不会计较形参和实参在数量、类型以及顺序上的区别,JavaScript的参数在内部就是用一个数组来表示的,从这个意义上来说,apply比call的使用率更高,我们不必关心具体有多少参数被传入函数,只要用apply一起推过去就完事了。
call是包装在apply上面的一颗语法糖,如果我们明确地知道函数接受多少个参数,而且想一目了然的表达形参和实参的对应关系,那么也可以用call来传递参数。
当使用call 或者 apply 的时候,如果我们传入的第一个参数为null,函数体内的this会指向默认的宿主对象,在浏览器中为window。
var func = function (x, y, z) {
console.log(this==window);
// true
};
func.call(null, 1, 2, 3);
但如果是在严格模式下,函数体内的this还是为null。
var func = function (x, y, z) {
console.log(this===null);
// true
};
func.call(null, 1, 2, 3);
有时候我们使用call或者apply的目的不在于指定this指向,而是另有有途,比如借用其也对象的方法,那么我们可以传入null来代替某个具体的对象:
var num = Math.max.apply(null,[1,2,3,4,5,6]);
console.log(num);
// 6
2. call和apply的用途
1. 临时改变this的指向,这是它们最常见的用途。下代码可以用来说明:
var user1= {
name:"Augus"
};
var user2 = {
name:"Yuki"
};
window.name="Window";
var getName = function(){
console.log(this.name);
};
getName();
// Window
getName.call(user1);
// Augus
getName.call(user2);
// Yuki
当执行getName.call(user1)这行代码时,getName函数体内的this就指向user1对象,所以此处的:
var getName = function(){
console.log(this.name);
};
实际相当于:
var getName = function(){
console.log(user1.name);
};
在实际开发中,经常会遇到this指向被不经意改变的场景,比如有一个div的节点,div的节点的onclick事件中的this本来是指向这个div的。
document.getElementById("div").onclick = function(){
console.log(this.id);
// div
};
假如该事件函数中有一个内部函数func,在事件内部调用这个函数时,func函数体内的this就指向了window,而不是我们预期的div,请看如下代码:
document.getElementById("div").onclick = function(){
console.log(this.id);
// div
var func = function(){
console.log(this.id);
// undefined
};
func();
};
这个时候我们可以用call来修正func函数的指向this,使其依然指向div。
document.getElementById("div").onclick = function(){
console.log(this.id);
// div
var func = function(){
console.log(this.id);
// div
};
func.call(this);
};
另外在本博客的"JavaScript中this的理解"也用apply来修正this,代码如下:
<div id="user">我是一个用户</div>
<script>
document.getElementById=(function(func){
console.log(func);
// document.getElementById()
return function(){
return func.apply(document,arguments);
};
})(document.getElementById);
var getId = document.getElementById;
var user = getId("user");
console.log(user.id);
// user
</script>
2. 永久绑定的 this 的 bind
大部分高级浏览器都实现了内置的Function.prototype.bind,用来指定函数内部的this指向,如下所示:
Function.prototype.bind = function(context){
// 保存原函数,即调用时的函数
var that = this;
return function(){
// 为调用函数传入指定的this对象及函数
return that.apply(context,arguments);
};
};
var user = {
name:"Augus"
};
var func = function(){
console.log(this.name);
// Augus
}.bind(user);
func();
我们通过Function.prototype.bind来包装func函数,并且传入一个对象context作为参数,这个context对象就是我们想修正的this对象。
在Function.prototype.bind的内部实现中,我们先通过 var that = this 这行代码把func函数的引用保存起来,然后返回一个新的函数。当我们在执行func这个函数时,实际上先执行的是这个刚刚返回的新函数。在新函数的内部,that.apply(context , arugments) 这行代码才是执行的原来的func函数,并且指定context对象为func函数体内的this。
这是一个简化版的Function.prototype.bind实现,通常我们会把它实现的更为复杂一点,使得可以往函数中预定义一些参数。
Function.prototype.bind = function(){
// 保存原函数,即调用时的函数
var that = this;
console.log(arguments);
// Arguments(3) [{…}, 1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// 需要绑定this的上下文
var context = [].shift.call(arguments);
console.log(context);
// {name:"Augus"}
// 剩余的参数转成数组
var args = [].slice.call(arguments);
console.log(args);
// [1,2]
// 返回一个新的函数
return function(){
console.log(arguments);
// Arguments(2) [3, 4, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// 执行新函数的时候,会把之前传入的context当作新函数体内的this
// 并且组合两次分别传入的参数,作为新函数的参数
return that.apply(context,
[].concat.call(
args,
[].slice.call(arguments)
)
);
};
};
var user = {name:"Augus"};
var func = function(a,b,c,d){
console.log(this.name);
// Augus
console.log([a,b,c,d]);
// [1,2,3,4]
}.bind(user,1,2);
func(3,4);
3. 改变this借用其他对象的方法
方法的第一种场景是"借用构造函数",通过这种技术,可以实现一些类似继承的效果,代码如下:
var userA = function(name){
this.name = name;
};
var userB = function(){
userA.apply(this,arguments);
};
userB.prototype.getName = function(){
return this.name;
};
var user = new userB("Augus");
console.log(user.getName());
// Augus
借用的第二种方法运用的场景跟我们的关系更加的密切。
函数的参数列表arguments是一个类数组对象,虽然它也有"下标",但它并不是真正的数组,所以也不能像数组一样,进行排序操作或者往集合里添加一个新的元素,这种情况下,我们常常会借用Array.prototype对象上的方法。 比如想往arguments中添加一个新的元素,通常会借用Array.prototype.push。
(function(){
Array.prototype.push.call(arguments,3);
console.log(arguments);
// Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
})(1,2);
在操作arguments的时候,我们经常会频繁地找Array.prototype对象借用方法。
想把arguments转成真正的数组的时候,可以借用Array.prototype.slice的方法;想截取arguments列表中的头一个元素时,以可以借用Array.prototype.shift方法。那么这种机制的内部实现原理是什么呢?以Array.prototype.push为例,看看V8引擎中是如何实现的。
function ArrayPush(){
// 被push对象的length
var n=TO_UIN32(this.length);
// push参数的个数
var m=%_ArgumentsLength();
for(var i=0;i<m;i++){
// 复制元素
this[i+n]= %_Arguments(i);
};
this.length = n+m;
return this.length;
}
通过这段代码可以看到,Array.prototype.push 实际上是一个属性复制的过程,把参数按照下标依次添加到被push的对象上面,顺便修改了这个对象的length属性,至于修改的对象是数组还是类数组对象并不重要。
可以看出来,Array.prototype.push并不是数组的专属,对象也可以借用。
var obj = {};
Array.prototype.push.call(obj,'user');
console.log(obj.length);
// 1
console.log(obj[0]);
// user
上述代码在大部分浏览器中都可以正常跑通,如果在低版本的IE浏览中执行,必须显示的给对象obj设置length属性:
var obj = {} ; obj.length=0 ;
大家都在知道,在JavaScritp中一切皆对象,但并不是所有的对象都可以借用其它对象的方法,就像我和马云都是中国人,但我却不可能向他借到钱一样,以Arry.prototype.push方法为例,要借用到此方法,必须要满足两个条件:
1. 对象本身要可以存取属性,像number和str类型的数字是绝对不可能借到这个方法的。
2. 对象本身的length属性要可写,如果借用此方法的对象是一个function,就会产生报错。