Javascript中的this详解

*本文代码中使用的 function dump(v,title), 是一个自己写的输出变量描述的函数。

如果你准备在JavaScript中写类,那么你必需对关键字 this 的使用有深刻的理解:

这是JavaScript手册中对 this的简单解释:

this:指当前对象。

对于 JScript 的客户版本,如果在其他所有对象的上下文之外使用 this,则它指的是 window 对象。

在全局环境下的 this

通常我们认为,Javascript 不在任何函数,不在任何类中,直接书写运行的代码的环境,称为直接运行环境,或者全局环境。实际上这个环境并不是一个真正意义上全局环境,

运行下面的代码:

dump(this,'Global This');

我们可以得到输出结果:

Global This: object – [object Window]

[The window Object]

可以看到,在这个环境下,this 等于 浏览器的原生对象 window ,它是原生类 Window 的实例。

事实上,当我们在浏览器中运行JavaScript代码时,windows 是所有代码的根节点,所有的代码都是存储并挂接于这个节点的,当然多窗口的环境下会有多个window对象。

function Car(){

}
dump(Car);
dump(window.Car);

输出结果:

■ function function Car() {  …

■ function function Car() { …

可以看到 Car 和 window.Car 是完全相同的东西

在类中使用 this

现在我们来看看当使用类的时候,this会发生什么样的变化。下面是一段典型的类定义代码,我们来逐步分析:

function Car(color,doors){
    //用构造模式定义属性
    this.color = color;
    this.doors = doors;
    this.dispThis = function(){
    	dump(this,'this');
    };
    dump(this,'this');
}

//原型方法
Car.prototype.dispThis2 = function(){
	dump(this,'this');
};


//静态方法
Car.dispThis3 = function(){
	dump(this,'this');
};

aCar = new Car('red',4);
aCar.dispThis();
aCar.dispThis2();
Car.dispThis3();
Car('red',4);

输出结果:

 

this: object – [object Object]                                                       //构造函数 new Car()

color ■ string red
doors ■ number 4
dispThis ■ function function () { dump(this, "this"); }…
dispThis2 ■ function function () { dump(this, "this"); }…

this: object – [object Object]                                                        //构造方法 aCar.dispThis();

color ■ string red
doors ■ number 4
dispThis ■ function function () { dump(this, "this"); }…
dispThis2 ■ function function () { dump(this, "this"); }…

this: object – [object Object]                                                       //原型方法 aCar.dispThis2()

color ■ string red
doors ■ number 4
dispThis ■ function function () { dump(this, "this"); }…
dispThis2 ■ function function () { dump(this, "this"); }…

 
this: function function Car(color, doors) { this.co…               //静态方法 Car.dispThis3()

this: object – [object Window]                                                   //直接运行构造函数 Car()

[The window Object]

 

可以看到在构造函数、构造方法、原型方法中的都可以获取到this对象,它们指向同一个实例对象aCar,该对象由 new Car() 语句生成。

Car的静态方法 Car.dispThis4(其实就是Car的一个直接的function属性)中,this 就是Car 本身,

当不使用new 关键字,而直接运行类的构造函数Car(其实现在它就是一个普通函数)时,在函数内部 this 就是当前运行环境的window对象

注意:比较一下 aCar.dispThis() 和 静态方法 Car.dispThis3() 输出结果就可以发现:CarCar 生成的对象是2个完全不同的概念;Car是一个函数对象,继承与原生的 Function ;而Car的实例aCar则是一个单纯的对象,它继承与JavaScript原生的 Object

比较一下:

 

 

  代码 结果
1 aCar.dispThis(); this = aCar
2 aCar.dispThis2(); this = aCar
3 Car.dispThis3(); this = Car
4 new Car(); this = aCar
5 Car(); this = window

通过这个表格特别是1、2、3行,我们可以清晰的得到一个规律:this 其实就等于调用代码时的调用的节点,即:aCar. 和 Car. 。这个调用节点也就是参考文档中所提到的上下文环境

那么如何解释4、5的结果呢?

new Car();使用了一个特殊关键字 new ,这个关键字的作用就是生成一个新的对象,并赋值给 this 。所以 this = aCar

Car();没有通过任何节点调用,实际上通过了默认节点window 来调用,通过上一节我们也可以看出 Car 实际上就是 window.Car 。所以 this = window

总结和结论:

JavaScript中所有的东西都是对象,包括数字、字符、函数以及对象的属性,函数总是存在(挂接)于某个对象中(默认存在于window对象),并通过某个对象调用的。在函数中总是有一个可用的的 this 变量,脚本引擎对 this 的解析是动态的,运行时的;它指向函数被调用时所挂接的对象。

我们可以通过一系列函数对象来进一步明晰这个过程,这些函数对象我们有时也称之为类的静态方法,实际上类的静态方法本身也是一个类,也是一个对象,明白了这一点,你就可以深刻理解Javascript的对象机制了。

function Car(){
	this.name = 'car';
	e(this);
};

Car.Wheel = function(){
	this.name = 'wheel';
	e(this);

};

Car.Wheel.Size = function(){
	this.name = 'wheel.size';
	e(this);
}

Car();
Car.Wheel();
Car.Wheel.Size();

输出结果

■ object – [object Window]
■ function function Car() { this.name = "car"; …
■ function function () { this.name = "wheel"; …

 

  代码 结果
1 Car(); this = window
2 Car.Wheel(); this = Car
3 Car.Wheel.Size(); this = Car.Wheel

当 this 遇到事件

当我们在事件函数中使用到 this 时,this 有时会很不听话,经常会取值为意料之外的对象。先来看一段示例代码:

我们定义一个类的方法函数 Car.prototype.show();

function Car(){
	this.name = 'Class Car';
}


Car.prototype.show = function(){
	echo('On Car.show');
	dump(this,'this');
};


aCar = new Car;

然后使用两个按钮的分别用两种方式触发 aCar.show;

<input id="btn1" type="button" value="Press Me call car.show"/>
<input id="btn2" type="button" value="Press Me call car.show #2"/>

<script type='text/javascript'>
   document.getElementById("btn1").onclick = aCar.show;
   document.getElementById("btn2").onclick = function(){aCar.show();} 
</script>

 

输出结果

//btn1 click

this: object – [object HTMLInputElement]

tagName ■ string INPUT
value ■ string Press Me call car.show
id ■ string btn1
className ■ string
…….

//btn2 click

this: object – [object Object]

name ■ string Class Car
show ■ function function () { echo("On Car.show"); …

 

我们看到btn1click事件中 this 的输出结果是 Input元素,

btn2Click事件中,this 的输出结果是 aCar

为什么会出现这一结果呢?

造成不同的主要原因是事件的绑定方式的不同,我们来看看事件的绑定过程:

btn1使用 document.getElementById("btn1").onclick = aCar.show  这条语句进行了事件绑定。
我们把 btn1.click 指向(引用)了 aCar.show,当事件触发时 btn1.click 被调用(回调),也就是调用了 aCar.show  的代码;看起来一切正常,唯一需要注意的是: aCar.show  的内部代码不是以节点名称 aCar 来调用的,而是以节点 btn1 来调用的,也就是说此时 show()  的运行时节点是 btn1,那么 this 也就成为了 btn1

btn2 使用 document.getElementById("btn1").onclick = function(){aCar.show();}
和btn1唯一的区别就是它使用了一个匿名函数把 aCar.show();包裹起来了。就是这样简单一个包裹引起根本性的变化,当事件触发时 btn1.click 被调用(回调),运行代码并不直接是 aCar.show ,而是通过匿名函数又再次调用 aCar.show ;此时调用的节点是 aCar ,所以 this = aCar 。

有些文章用事件函数的复制和引用来解释btn1和btn2结果的不同,这种解释实际是错误的,流于表象的,虽然很多文章还做了很多图来解释… JavaScript 的所有赋值语句都应该理解为引用,这又是另外一个话题,不扯远了…

我们来进一步比较不同事件函数绑定方式所带来的 this 的变化:

function Car(){
	this.name = 'Class Car';
}


Car.prototype.show = function(){
	dump(this,'this');
};


aCar = new Car;

element.onclick = aCar.show;                       // this = element
element.addEventListener('click',aCar.show, false) // this = element

element.onclick = function () {aCar.show();}      // this = aCare
lement.attachEvent('onclick',aCar.show)           // this = aCare
<element onclick="aCar.show();">                  // this = aCare

element.onclick = function () {this.style.color = '#cc0000';}     //this = element
<element onclick="this.style.color = '#cc0000';">                 //this = element


总结:this 指向函数被调用时所挂接的对象。当事件函数被调用时,this 总是等于事件所在的节点,因此我们应该使用匿名函数作为事件函数,并在匿名函数中二次调用可能需要的其它函数。以此来避免可能的 this 混乱。

现在我们来看一段示范代码,这是一段典型的既能获取函数本身所在节点,又能获取触发对象的代码模式

function Car(){
	this.name = 'Class Car';
}

Car.prototype.doSomethin = function(obj){
  dump(this,'this');  //this = aCar
  dump(obj,'obj');    //obj = element
};

aCar = new Car();

<element onclick="aCar.doSomething(this)">                //方法1
element.onclick = function() {aCar.doSomething(this)}     //方法2

在类中进行事件绑定操作

当我们在类代码中进行事件绑定操作时,对this的使用会遇到新的困难,我们知道所有的事件函数中 this 都必然等于事件函数所在的对象(即上例中的btn1),那么我们如何在一个类中书写一段匿名事件函数,并在匿名事件函数中调用当前类的对象呢?

看代码:

<input id="btn1" type="button" value="Press Me call car.show"/>

<script type='text/javascript'><!--

function Car(){
	this.name = 'Class Car';
}
Car.prototype.showThis = function(obj){
	dump(this,'this');
	dump(obj,'obj');
};
Car.prototype.bind = function(){
	document.getElementById("btn1").onclick = function(){ this.showThis(this); };
};

aCar = new Car;
aCar.bind();

--></script>

如果你看到这里对 this 已经有了深刻的理解,那么就会发现这段代码实际使用的话会出错的,问题就出在这一行代码:

document.getElementById("btn1").onclick = function(){this.showThis(this);};

这行代码的本身是在aCar.bind中运行,然后这行代码又写了一个匿名函数:function(){this.showThis(this);} 。匿名函数中我们希望调用当前对象 aCar 的方法 showThis ,同时又想把事件触发对象btn1 传递给 showThis。我们用了2个 this ,这里的 this 到底应该等于什么呢?两个 this 都应该等于 btn1 ,因为这个匿名函数将会以节点 btn1.click 来运行的。this.showThis 将会无法调用到我们希望的目标 aCar.showThis ;

那么如何改写这段代码,来正确的调用aCar.showThis 呢?似乎目前只有这个方法,使用一个临时变量来传递 this :

var _this = this;

document.getElementById("btn1").onclick = function(obj){_this.showThis(obj);};

其实这样代码看起来有点让人难以理解,如果想更深入的解释这段代码,就需要进一步解释JavaScript的编译和解释运行机制,我对此也没有深入的研究,但是可以推测出的原因是:JavaScript的脚本引擎对变量的编译和解释是根据代码书写环境(书写节点)来进行的,而对this 似乎总是根据运行环境(运行节点)来赋值的。

回调函数和 this 使用的总结

关于 this 的使用似乎很让人头晕,特别是当 this 和事件和类混在一起使用的时候,如果你需要一个简单的方法来处理这个,我们可以归纳出几条规则

  1. 事件处理函数又可以称为回调函数(CallBack Function),回调函数不仅仅用于界面事件(Click,Moueover等等)的处理,很多异步处理需要大量的回调函数。
  2. 总是使用匿名函数作为回调函数,并在匿名函数中再调用其它命名函数,使用 event = function(){ somefunction(); }  的方式,不要使用 event = somefunction 的方式
  3. 命名并传递默认参数,同时也可以把回调函数中的 this 传递给其它被调用函数。如:event = function(params){ somefunction(this,params); }
  4. 如果在类中书写匿名回调函数,并且需要在函数中调用当前类的当前对象,使用一个临时变量来引用 function(){_this.showThis();};

最后来看看常见的Jquery的事件绑定方式:

<input id="btn1" type="button" value="Press Me call car.show"/>
<input id="btn2" type="button" value="Press Me call car.show #2"/>

<script type='text/javascript'><!--


function Car(){
	this.name = 'Class Car';
}
Car.prototype.show = function(){
	echo('On Car.show');
	dump(this,'this');
};
aCar = new Car;

$("#btn1").click(function(){aCar.show()});  //通过匿名函数2次调用 this = aCar
$("#btn2").click(aCar.show);                // this = btn2

--></script>

输出结果

On Car.show //这个是引用方式 this = aCar

this: object – [object Object]

name ■ string Class Car
show ■ function function () { echo("On Car.show"); …

On Car.show //这个是复制方式 this = btn2

this: object – [object HTMLInputElement] 

tagName ■ string INPUT
value ■ string Press Me call car.show #2
id ■ string btn2
className ■ string
…….

这篇文章有一个评论

  1. 第 kometo页

    一年多没深入写Javascript的深入代码来,这几天有个项目涉及JS的内容,忘记了不少,然后找半天资料,最后发现还是自己写的这篇最全面,最具有指导意义,哈哈,意淫一下~~~

发表评论