this 、 call 和 apply
this
跟别的语言大相径庭的是,JavaScript的 this 总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。
this 的指向
-
作为对象的方法调用。
当函数作为对象的方法被调用时, this 指向该对象。
var obj = {
a: 1,
getA: function() {
console.log(this === obj); //true
return this.a; //1
}
}
console.log(obj.getA());
-
作为普通函数调用。
this 总是指向全局对象window。
window.name = 'globalName';
var getName = function(){
return this.name;
};
console.log( getName() ); // 输出:globalName
-
构造器调用。
当用 new 运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的
this
就指向返回的这个对象。
var MyClass = function() {
this.name = 'steven';
return { // 显式地返回一个对象
name: 'anne'
}
};
var obj = new MyClass();
console.log("使用构造器调用:" + obj.name); // 输出:anne
-
Function.prototype.call 或 Function.prototype.apply 调用。
可以动态地改变传入函数的
this
。
var obj1 = {
name: 'steven',
getName: function() {
return this.name;
}
};
var obj2 = {
name: 'anne'
}
console.log(obj1.getName.call(obj2));// 输出:anne
丢失的 this
一个例子:使用getId
变量来代替document.getElementById
var getId = function(id){
return document.getElementById(id);
};
console.log(getId('div1').id)
如果getId
直接 来引用 document.getElementById 之后
,再调用getId
,此时就成了普通函数调用,函数内部的 this
指向了window
,而不是原来的 document
,会出现报错,情况如下:
var getId = document.getElementById;
console.log(getId('div1').id); //Uncaught TypeError: Illegal invocation (非法调用)
我们可以尝试利用 apply 把 document 当作 this 传入 getId 函数,帮助“修正” this:
var getId = (function(obj) {
return function() {
return obj.apply(document, arguments);
}
})(document.getElementById);
console.log(getId('div1').id)
call和apply
call和apply 的区别
Function.prototype.call 和 Function.prototype.apply 都是非常常用的方法。它们的作用一模一样,区别仅在于传入参数形式的不同。
-
apply 接受两个参数:
- 第一个参数指定了函数体内 this 对象的指向
- 第二个参数为一个带下标的集合(数组或类数组),
apply
方法把这个集合中的元素作为参数传递给被调用的函数
var func = function( a, b, c ){ alert ( [ a, b, c ] ); // 输出 [ 1, 2, 3 ] }; func.apply( null, [ 1, 2, 3 ] );//1参为null,函数体内的 this 会指向默认的宿主对象,即window
-
call 传入的参数数量不固定
- 第一个参数指定了函数体内 this 对象的指向
- 从第二个参数开始往后,每个参数被依次传入函数
var func = function( a, b, c ){ alert ( [ a, b, c ] ); // 输出 [ 1, 2, 3 ] }; func.call( null, 1, 2, 3 );//1参为null,函数体内的 this 会指向默认的宿主对象,即window
当调用一个函数时,JavaScript 的解释器并不会计较形参和实参在数量、类型以及顺序上的区别,JavaScript的参数在内部就是用一个数组来表示的。从这个意义上说,
apply
比call
的使用率更高,我们不必关心具体有多少参数被传入函数,只要用 apply 一股脑地推过去就可以了。当使用
call
或者apply
的时候,如果我们传入的第一个参数为null
,函数体内的 this 会指向默认的宿主对象,在浏览器中则是window
,但如果是在严格模式下,函数体内的this
还是为null
。有时候我们使用
call
或者apply
的目的不在于指定this
指向,而是另有用途,比如借用其他对象的方法。那么我们可以传入null
来代替某个具体的对象。
call 和 apply 的用途
改变 this 指向
var obj1 = {
name: 'steven'
};
var obj2 = {
name: 'anne'
};
window.name = 'window';
var getName = function(name) {
console.log(this.name);
};
getName(); //'window'
getName.apply(obj1); //'steven'
getName.apply(obj2); //'anne'
在执行getName.apply(obj1)
时, getName
函数体内的 this
就指向 obj1
对象,实际相当于
var getName = function(name) {
console.log(obj1.name);
};
一个点击的实例场景:
document.getElementById('div1').onclick = function() {
var _that = this; //需要额外申明一个中转变量来存储对象的this
var foo = function() {
alert(_that.id);
}
return foo();
};
在使用apply
或call
后:
document.getElementById('div1').onclick = function() {
var foo = function() {
alert(this.id);
}
foo.apply(this);
return foo();
};
Function.prototype.bind
大部分现代浏览器都实现了内置的 Function.prototype.bind
,用来指定函数内部的 this 指向,如果没有的话模拟起来不困难:
Function.prototype.bind = function(context) {
var self = this; //保存原函数
return function() {
// 这句代码才是执行原来的 func 函数,并且指定 context对象为 func 函数体内的 this ,也是我们想修正的 this 对象
return self.apply(context, arguments);
}
}
var obj = {
name: 'steven'
};
var func = function() {
alert(this.name); //输出'steven'
}.bind(obj);
func();
再稍微复杂一些,使得可以往func
函数内预先填写一些参数
Function.prototype.bind = function(){
var self = this, // 保存原函数
context = [].shift.call( arguments ), // 需要绑定的 this 上下文
args = [].slice.call( arguments ); // 剩余的参数转成数组
return function(){ // 返回一个新的函数
return self.apply( context, [].concat.call( args, [].slice.call( arguments ) ) );
// 执行新的函数的时候,会把之前传入的 context 当作新函数体内的 this
// 并且组合两次分别传入的参数,作为新函数的参数
}
};
var obj = {
name: 'sven'
};
var func = function( a, b, c, d ){
alert ( this.name ); // 输出:sven
alert ( [ a, b, c, d ] ) // 输出:[ 1, 2, 3, 4 ]
}.bind( obj, 1, 2 );
func( 3, 4 );
借用其他对象的方法
- 场景一:鸠占鹊巢
var A = function(name) {
this.name = name;
};
var B = function() {
A.apply(this, arguments);
};
B.prototype.getName = function() {
return this.name;
}
var b = new B('steven');
console.log(b.getName());
- 场景二:往
arguments
中添加一个新的元素,通常会借用Array.prototype.push
(function(){
Array.prototype.push.call(arguments,3);
console.log(arguments);
})(1,2,5,c)
在操作 arguments
的时候,我们经常非常频繁地找Array.prototype
对象借用方法,例如:
- 想把 arguments 转成真正的数组的时候,可以借用
Array.prototype.slice
方法。 - 想截去arguments 列表中的头一个元素时,可以借用
Array.prototype.shift
方法。
我们甚至可以把“任意”对象传入 Array.prototype.push
,但是对象要满足以下两个条件:
- 对象本身要可以存取属性
- 对象的 length 属性可读写