2.1 this
javascript总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定,而非函数被声明时的环境。
2.11 this的指向
(1)作为对象调用
(2)作为普通函数调用
(3)构造器调用
(4)Function.prototype.call或Function.prototype.apply调用
1.作为对象的方法调用
当函数作为对象的方法被调用时,this指向对象:
var obj={
a:1,
getA:function(){
alert(this===obj);//输出true
alert(this.a);//输出1
}
}
obj.getA();
2.作为普通函数调用
当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式,此时的this总是指向全局对象。在浏览器的javascript里,这个全局对象是window对象。
window.name="globalName";
var getName=function(){
return this.name;
}
console.log(getName());
callback函数
div节点的事件函数内部,有一个局部的callback方法,callback被作为普通函数调用时,callback内部的this指向了window,但我们往往是想让它指向该div节点。此时有一个简单的解决方案,可以用一个变量保存一个div节点的引用:
window.id="window";
document.getElementById("div").onclick=function(){
var that=this;//保存div的引用
var callback=function(){
alert(that.id);//输出div
}
callback();
}
3.构造器调用
javascript没有类,但是可以从构造器中创建对象,同时也提供了new运算符,使构造器更像是一个类。
除了宿主提供的一些内置函数,大部分的javascript函数都可以当做构造器使用。构造器的外表和普通函数一模一样,它们的区别在于被调用的方式。当做new运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的this就指向返回的这个对象。
var MyClass=function(){
this.name="seven";
}
var obj=new MyClass();
alert(obj.name);//输出seven
var MyClass=function(){
this.name="seven";
return{//显示的返回一个对象
name:"anne"
}
}
var obj=new MyClass();
alert(obj.name);//输出anne
4.Function.prototype.call和Function.prototype.apply
call和apply方法能很好的体现javascript的函数语言特性,在javascript中,几乎每一次编写函数式语言风格都离不开call和apply。跟普通的函数调用相比,用Function.prototype.call或Function.prototype.apply可以动态的改变传入的this.
var obj1={
name:"seven",
getName:function(){
return this.name;
}
}
var obj2={
name:"anne"
};
console.log(obj1.getName());//输出seven
console.log(obj1.getName.call(obj2));//输出anne
2.1.1 丢失的this
var obj={
myName:"seven",
getName:function(){
return this.myName;
}
}
console.log(obj.getName());//输出:seven
var getName2=obj.getName();
console.log(getName2());//输出undefined
当调用obj.getName时,getName方法是作为对象的属性被调用。this指向obj对象。
当用另外一个变量getName2来引用obj.getName,并且调用getName2,此时是普通函数的调用方式,this指向全局window,所以程序执行是undefined.
var getId=document.getElementById;
getId("div1");
document.h方法内部实现需要用到this,这个this本来被期望指向document,当getElementId方法作为document对象的属性被调用时,方法内部的this确实是指向document的。
但当用getId来引用document.getElementById之后,再调用getId,此时就成了普通函数调用,函数内部的this指向了window,而不是原来的document。
我们可以把apply将document当做this传入getId函数,帮助修正this:
var getId=document.getElementById;
getId("div1");
document.getElementById=(function(func){
return function(){
return func.apply(document,arguments);
}
})(document.getElementById);
var getId=document.getElementById;
var div=getId("div1");
alert(div.id);//输出div1
2.2 call和apply
Function原型有两个方法,它们是Function.prototype.call和Function.prototype.apply。
2.2.1 call和apply的区别
它们的作用一样,区别是在于传入参数的形式不同。
apply接受两个参数,第一个参数指定了函数体内this对象的指向,第二个参数为一个带下标签的集合,这个集合可以为数组,也可以为类数组,apply方法把这个集合中的元素作为参数传递给被调用的函数:
var func=function(a,b,c){
alert([a,b,c]);
}
func.apply(null,[1,2,3]);//输出1,2,3
call传入的参数数量是不固定的,跟apply相同的是,第一个参数也是代表函数体内this的指向。从第二个参数开始往后,每个参数被依次传入函数:
var func=function(a,b,c){
alert([a,b,c]);
}
func.call(null,[1,2,3]);//输出1,2,3
当调用一个函数的时候,javascript的解释器并不会计较形参和实参的数量、类型以及顺序上的区别,javascript的参数内部就是用一个数组来表示的,只要用apply推进去就可以啦。
call是包裹在apply语法上的一颗语法糖,如果我们明确知道函数接受多少个参数,而且想一目了然地表达形参和实参的对应关系,那么也可用call来传送参数。
如果我们传入的第一个参数是null,函数体内的this会指向默认的宿主对象,在浏览器则是window.
有时候我们使用call和apply的目的不在于指向this,而是另有用处,比如借用其他对象的方法。那么我们可以传入null来代替某个具体对象。
例如:Math.max.apply(null,[1,2,3,4,5])//输出5
2.2.2 call和apply的用途
1.改变this的指向
var obj1={
name:"seven"
};
var obj2={
name:"anne"
};
window.name="window";
var getName=function(){
alert(this.name);
}
getName();
getName.call(obj1);//输出seven
getName.call(obj2);//输出anne
在实际开发中,经常会遇到this指向被不经意改变的场景,比如有一个div节点,div节点的onclick事件中的this本来是指向这个div的:
document.getElementById("div1").onclick=function(){
alert(this.id);//输出div1
}
假如该事件有一个内部函数func,在事件内部调用func函数时,func函数体内的this就直接指向了window,而不是我们预期的div,见如下代码:
document.getElementById("div1").onclick=function(){
alert(this.id);//输出div1
var func=function(){
alert(this.id);//输出undefined
}
func();
}
这个时候我们使用call来修正this的场景。
document.getElementById("div1").onclick=function(){
alert(this.id);//输出div1
var func=function(){
alert(this.id);//输出div1
}
func.call(this);
}
2.Function.prototype.bind
大部分的浏览器都实现了内置的Function.prototype.bind,用来指定函数内部的this指向,即使没有原生的Function.prototype.bind来实现,我们来模拟一个也不是难事,代码如下:
Function.prototype.bind=function(context){
var self=this;//保存原函数
return function(){//返回一个新的函数
return self.apply(context,arguments);
//执行新的函数的时候,会把之前传入的context
//当做新的函数体内this
}
};
var obj={
name:"sven"
};
var func=function(){
alert(this.name);//输出seven
}.bind(obj);
func();
我们通过Function.prototype.bind来包装func函数,并且传入一个对象context当做参数,这个context对象就是我们想要修正的this对象。
在Function.prototype.bind内部实现中,我们先把func函数的引用保存起来,然后返回一个新的函数。当我们在将来执行func函数时,实际上先指向的是刚刚返回的这函数。在新的函数内部,
self.apply(context,arguments)这句代码才是执行原来的func函数,并且指定context对象为func函数体内的this.
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:"seven"
}
var func=function(a,b,c,d){
alert(this.name);//输出seven
alert([a,b,c,d]);//输出1,2,3,4
}.bind(obj,1,2);
func(3,4);
3.借用其他对象的方法
借用方法的第一步场景是“借用构造函数”,通过这种技术,可以实现一些类似继承的场景:
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("SVEN");
console.log(b.getName());//输出seven
函数的参数列表arguments是一个类数组对象,虽然它也有下标,但它并非是真正的数组,所以不能像数组一样,进行排序操作或者往集合里添加一个新的元素。这种情况下,我们常常会借用Array.prototype对象上的方法。比如想往arguments中添加一个新的元素,通常会借用
Array.prototype.push:
(function(){
Array.prototype.push.call(arguments,3);
console.log(arguments);//输出1,2,3
})(1,2);
在操作arguments的时候,我们经常非常频繁的找Array.prototype对象借用方法。
想把arguments转成真正的数组的时候,可以借用Array.prototype.slice方法;想截去arguments列表中的头一个元素时,又可以借用Array.prototype.shift方法。
Array.prototype.push实际上是一个属性复制的过程,把参数按照下标一次添加到被push的对象上面,顺便修改了这个对象的length属性,至于被修改的对象是谁,到底是数组还是类数组对象,这并不重要。
由此可以判断,我们可以将“任意”对象传入Array.prototype.push;
var a={};
Array.prototype.push.call(a,"first");
alert(a.length);//输出1
alert(a[0]);//first
而在低版本的IE浏览器中执行,必须显式的给对象a设置length属性:
var a={
length:0
};
借用Array.prototype.push方法的对象还要满足一下两个条件:
(1)对象本身要可以存储属性
(2)对象的length属性可读写
比如number类型的属性不可能借用到Array.prototype.push方法: