前段时间去听了一个同事的knowledge share session,他大致给我们讲了一些写javascript需要注意的一些细节。session本身并没有什么问题,但是后来大家由此引申出来的讨论引发了我的思考。
这些讨论的内容有点杂乱,边边角角都涉及一点。我花了些时间,写了一些testcase,试图找出其中的规律,并希望对大家有所帮助。
先来看一个简单的例子:
这段代码可以说是最简单的javascript了,首先定义了一个名为test的object,一个名为test2的funciton,一个名为test3的function。
那么此时我们应该如何使用这三个变量呢?最简单的办法:直接引用。
而事实上,我们也可以用window这个包在他们外层的全局object名来引用他们, 即: window.test或者window.test2()。
那么,能不能用this来访问这些变量呢? 理论上讲,this现在应该就是指代window object才对。于是我试着这样访问this.test2().结果显示,成功。
第二个例子开始有点复杂:
在test中,只有一种定义语法是对的: variable: value,如果我想用var t1 = 1;这种方式的话,就会出现语法错误。
当我在test里面定义一个方法时,这个方法就从属于test object, 那么它内部的this就指向了test。所以myTest方法返回的值就应该是1,而不是11.如果我去掉this的话,则正好相反。
而当我尝试去获得t2的时候,却发现t2为undefine,也就是说t2是无法使用test中的其它变量来初始化的。
我试着把this改成test,结果却是test undefine。我把t2的初始化改成t2: test.myTest(),结果也是一样的,test undefine。
而我把之改成t2: this.myTest(),报出来的错又变成了this.myTest() undefine,哪怕我把该方法的定义放到了t2的前面。
由此可以得出的结论是,在定义一个对象时,该对象里面的对象是不能够获得this对象的,相应的,该对象里的object则没有问题。
因此,该对象可以改成这样:
通过这种方法,test里的t2就可以通过调用被成功的初始化了。
看起来似乎很完美...
真的是这样吗?我们真的是没有办法获取this类型么? 我们刚刚对于报错的发现:使用this和使用test得到的错误是不一样的!也许此时的this,根本就不是test。
得到的结果是25.
是不是觉得很吃惊? 此时的this居然指向的是test的外层,即window。另一方面,我们认为此时的test对象尚未创建完毕,因此无法访问。
我们必须为这种怪异的行为提供一个具有概念完整性的解释。
以下是我的理解,因此而造成的一切后果,本人概不负责...
在任何情况下,this都是指向该对象的容器的父亲(容器这个概念是我自己发明的,其本质也是一个对象),对于某个对象,如t1或者t2,他们的容器是test,所以this便指向test的父亲,即window,而window是已经预定义好的。 然而对于某个函数,如myTest,它本身就是容器,而它的父亲则是test,那么当你位于myTest容器内时,其中的this便指向这个容器的父亲test。这就解决了第一个问题:this的指向问题。
而javascript是解释性的语句,它是边读源码边解释的。当它遇到一个函数的定义时,暂时是不解释的,而是直接把source code保存下来。这就造成了定义变量的时候无法使用test,而调用函数(此时test已经定义好了)时则没有问题。
因此当js解释器遇到test时,整个过程是这样的:
1. 解释test,开始定义一个新对象。
2. 解释t1,放入该对象中。
3. 解释t2,尝试寻找test对象,因为test对象还在定义中,因此无法找到。
4. 解释myTest,是个方法,直接把代码拷贝到对象中。
5. 完成test对象的定义。
所以此时你再去调用myTest的时候,test对象已经定义完全,可以正确使用了。
接下来,我们看看function test2() {}这种定义形式:
我们之前魔咒一般的代码,在这里便轻松搞定,具体就不解释了,本身也是很简单的。
同样,根据前面的例子,可以猜出,此时的this指向的是window:
虽然我们知道,在js中,函数也是对象,但是像这种方法定义的函数,是无法获得其内部定义的局部变量的。不过有意思的是,我们还是可以写这样的代码:
此时,你可以把test2当作一个方法来使用test2();又或者当作一个对象来使用test2.t22,都是可以访问的。唯一要记住的就是,在test2中this指向的是window而不是test2.
那么如果我在这个方法里面定义对象或者方法,它的this是不是就是指test2这个function呢?
结果显示,我们是可以访问的t3的,而t4和t6中的this,指向的居然也是window。
我们可以这样理解,当我们把test2当作函数来使用的时候,js解释器就一句一句执行该函数,而test2中的this显然是指向window的,所以test2里面的一切,也都指向window
了。而对于该环境中的任何变量,也都是可以直接访问到的了。
如果我们的猜想正确的话,如果我们把test2当作对象来使用,得到的应该就是另一种结果:
我们发现,t4.t5是可以访问的,而t6()的返回值则是undefine。这又是为什么呢?
其实也很简单,我们只要记住,变量是定义时环境,而函数则是执行是环境。这样我们就不会困惑了,因为对于t5而言,他就是定义是环境,那么this其实是指向当前大括号这个容器的父亲,也就是window。
而函数则要到运行时才会解释,而在运行时,它的this应该是指向test2.
说到这里,我只能说js实在是太绕了,写js也是一件非常痛苦的事情。
但是我们仍然需要继续下去,我们来看最后一个case:
基本而言这个函数和上一个仅仅是在形式上有一些区别,我并没有发现他们在使用上的差别。根据js几本比较重要的书籍上的介绍,都是推荐使用这种函数定义方法,因为这样可以“提醒用户函数也是对象”。
最后想提一点是,在函数中定义一个变量,一定要记得加var,否则就会出现下面这种情况:
t8已经变成了一个全局的变量。
这些讨论的内容有点杂乱,边边角角都涉及一点。我花了些时间,写了一些testcase,试图找出其中的规律,并希望对大家有所帮助。
先来看一个简单的例子:
var test={
};
function test2() {
};
var test3 = function() {
}
这段代码可以说是最简单的javascript了,首先定义了一个名为test的object,一个名为test2的funciton,一个名为test3的function。
那么此时我们应该如何使用这三个变量呢?最简单的办法:直接引用。
而事实上,我们也可以用window这个包在他们外层的全局object名来引用他们, 即: window.test或者window.test2()。
那么,能不能用this来访问这些变量呢? 理论上讲,this现在应该就是指代window object才对。于是我试着这样访问this.test2().结果显示,成功。
第二个例子开始有点复杂:
var test={
t1: 1,
t2: this.t1,
myTest: function() {
var t1 = 11;
return this.t1;
}
};
在test中,只有一种定义语法是对的: variable: value,如果我想用var t1 = 1;这种方式的话,就会出现语法错误。
当我在test里面定义一个方法时,这个方法就从属于test object, 那么它内部的this就指向了test。所以myTest方法返回的值就应该是1,而不是11.如果我去掉this的话,则正好相反。
而当我尝试去获得t2的时候,却发现t2为undefine,也就是说t2是无法使用test中的其它变量来初始化的。
我试着把this改成test,结果却是test undefine。我把t2的初始化改成t2: test.myTest(),结果也是一样的,test undefine。
而我把之改成t2: this.myTest(),报出来的错又变成了this.myTest() undefine,哪怕我把该方法的定义放到了t2的前面。
由此可以得出的结论是,在定义一个对象时,该对象里面的对象是不能够获得this对象的,相应的,该对象里的object则没有问题。
因此,该对象可以改成这样:
var test={
t1: 1,
t2: 0,
myTest: function() {
var t1 = 11;
test.t2 = test.t1;
return this.t2;
}
};
通过这种方法,test里的t2就可以通过调用被成功的初始化了。
看起来似乎很完美...
真的是这样吗?我们真的是没有办法获取this类型么? 我们刚刚对于报错的发现:使用this和使用test得到的错误是不一样的!也许此时的this,根本就不是test。
var g = 25;
var test={
t1: 1,
t2: this.g,
};
alert(test.t2);
得到的结果是25.
是不是觉得很吃惊? 此时的this居然指向的是test的外层,即window。另一方面,我们认为此时的test对象尚未创建完毕,因此无法访问。
我们必须为这种怪异的行为提供一个具有概念完整性的解释。
以下是我的理解,因此而造成的一切后果,本人概不负责...
在任何情况下,this都是指向该对象的容器的父亲(容器这个概念是我自己发明的,其本质也是一个对象),对于某个对象,如t1或者t2,他们的容器是test,所以this便指向test的父亲,即window,而window是已经预定义好的。 然而对于某个函数,如myTest,它本身就是容器,而它的父亲则是test,那么当你位于myTest容器内时,其中的this便指向这个容器的父亲test。这就解决了第一个问题:this的指向问题。
而javascript是解释性的语句,它是边读源码边解释的。当它遇到一个函数的定义时,暂时是不解释的,而是直接把source code保存下来。这就造成了定义变量的时候无法使用test,而调用函数(此时test已经定义好了)时则没有问题。
因此当js解释器遇到test时,整个过程是这样的:
var test={
t1: 1,
t2: this.t1,
myTest: function() {
var t1 = 11;
return this.t1;
}
};
1. 解释test,开始定义一个新对象。
2. 解释t1,放入该对象中。
3. 解释t2,尝试寻找test对象,因为test对象还在定义中,因此无法找到。
4. 解释myTest,是个方法,直接把代码拷贝到对象中。
5. 完成test对象的定义。
所以此时你再去调用myTest的时候,test对象已经定义完全,可以正确使用了。
接下来,我们看看function test2() {}这种定义形式:
function test2() {
var t3 = 1;
var t4 = t3;
return t4;
};
我们之前魔咒一般的代码,在这里便轻松搞定,具体就不解释了,本身也是很简单的。
同样,根据前面的例子,可以猜出,此时的this指向的是window:
var g = 25;
function test2() {
var t3 = 1;
var t4 = t3;
return this.g;
};
虽然我们知道,在js中,函数也是对象,但是像这种方法定义的函数,是无法获得其内部定义的局部变量的。不过有意思的是,我们还是可以写这样的代码:
function test2() {
var t3 = 1;
var t4 = t3;
return test2.t22;
};
test2.t22 = 12;
此时,你可以把test2当作一个方法来使用test2();又或者当作一个对象来使用test2.t22,都是可以访问的。唯一要记住的就是,在test2中this指向的是window而不是test2.
那么如果我在这个方法里面定义对象或者方法,它的this是不是就是指test2这个function呢?
var g = 25;
function test2() {
var t3 = 1;
var t4 = {
t5: t3
};
var t6 = function() {
return t3;
}
return t4.t5;
};
结果显示,我们是可以访问的t3的,而t4和t6中的this,指向的居然也是window。
var g = 25;
function test2() {
var t3 = 1;
var t4 = {
t5: this.g
};
var t6 = function() {
return this.g;
}
return t4.t5;
};
alert(test2())
我们可以这样理解,当我们把test2当作函数来使用的时候,js解释器就一句一句执行该函数,而test2中的this显然是指向window的,所以test2里面的一切,也都指向window
了。而对于该环境中的任何变量,也都是可以直接访问到的了。
如果我们的猜想正确的话,如果我们把test2当作对象来使用,得到的应该就是另一种结果:
var g = 25;
function test2() {
var t3 = 1;
};
test2.t4 = {t5: this.g};
test2.t6 = function() {
return this.g;
};
alert(test2.t4.t5);
alert(test2.t6());
我们发现,t4.t5是可以访问的,而t6()的返回值则是undefine。这又是为什么呢?
其实也很简单,我们只要记住,变量是定义时环境,而函数则是执行是环境。这样我们就不会困惑了,因为对于t5而言,他就是定义是环境,那么this其实是指向当前大括号这个容器的父亲,也就是window。
而函数则要到运行时才会解释,而在运行时,它的this应该是指向test2.
说到这里,我只能说js实在是太绕了,写js也是一件非常痛苦的事情。
但是我们仍然需要继续下去,我们来看最后一个case:
var test3 = function() {
var t7 = 1;
return t7;
}
alert(test3());
基本而言这个函数和上一个仅仅是在形式上有一些区别,我并没有发现他们在使用上的差别。根据js几本比较重要的书籍上的介绍,都是推荐使用这种函数定义方法,因为这样可以“提醒用户函数也是对象”。
最后想提一点是,在函数中定义一个变量,一定要记得加var,否则就会出现下面这种情况:
var test3 = function() {
var t7 = 1;
t8 = 13;
return t8;
}
alert(test3());
alert(t8);
t8已经变成了一个全局的变量。