JavaScript是一种不同于任何强类型程序设计语言的脚本语言,这决定了它对于许多强类型语言的程序员来说有很多莫名其妙、难以理解的地方,本文是本人对JavaScript的一些实践总结出来的简单易懂的结论(至少对我来说是这样)。我一直笃信任何语言的设计者都不至于脑子短路设计出连他自己都记不清楚的规则,所以很显然,JavaScript很多特性,例如this,看起来无比复杂,事实上不过是没有抽象理解的原因。本文就试图从一个抽象的角度来还原这些复杂规则背后的抽象之美。
一、自定义对象
尽管JavaScript的确是存在一些基元类型(例如数值、字符串、数组等),但我更倾向于在我们的“自定义类型”对象中,是没有类型这个说法的(事实上JavsScript的typeof运算符得到的结果只有固定的几个)。或者说传统的强类型程序设计语言中的类型,在JavaScript中是找不到的。因为JavaScript中的所有自定义对象都是橡皮泥,你可以任意增删改成员。
自定义对象一般用new关键字创建,但实际上函数本身也是一个对象,也是一个自定义对象,所以我先随便创建一个对象:
var a = function() { }; //你可以将这一行改为var a = new Object();,或者new任何东西,都不影响结果。
那么接下来我们就可以为这个对象增加成员:
a.test = "hello world!"; window.alert(a.test); 同样的,也可以修改这个成员,例如我们把它变成一个函数: a.test = function() { window.alert("hello world!"); }; a.test();
所以我们知道,构造一个对象,事实上完全没有必要去用new 函数名()这样的高级语法,如果你只是需要一个很简单的对象,下面的语法就可以了:
function constructor() { var person = new Object(); person.name = "Ivony"; person.sexual = "男"; //... return person; } var manager = constructor(); window.alert(manager.name); window.alert(manager.sexual);
二、全局scope(作用域)。
我们刚刚在定义a这个变量的时候,使用了一个JavaScript的关键字:var。在解释这个关键字之前,我们先考虑下面的代码:
function test() { a = 100; window.alert(window.a); } test();
猜猜结果是什么?
这是许多传统程序员无法理解的地方,明明是对变量a赋值,怎么window.a也赋值了。这就是JavaScript有意思的全局scope现象。
简单的说JavaScript中没有传统程序设计语言中分配于堆栈上的变量的概念,取而代之的是scope对象(很多时候这不是一个真正的对象)。你直接用a = 100这样的代码赋值的时候,你并不是给一个叫做a的变量赋值,而是给scope对象的一个名为a的成员赋值。
如果不使用var关键字,默认的scope对象就是全局scope也就是window,所以a = 100就等同于window.a = 100。
当然,你也可以想到,alert等同于window.alert,那么window.alert也应等同于window.window.alert。事实的确如此,window对象真的有个成员叫做window,而且就是它自身,所以你window后面点多少个window都是一样的。
那么,var关键字的用途就是,将变量附着于函数的作用域(或者说当前的作用域)对象上,而非全局作用域对象。考虑下面的代码:
function a(arg) { window.alert(arg); } function test() { var a = function(arg) { window.alert("alert: " + arg); }; a("hello world"); } test(); a("hello world");
这个执行结果应该就不难猜了。
值得注意的是,使用var关键字声明变量,与声明的位置毫无关系(只要是在函数的花括弧内)!这意味着你将脚本改成这个样子,结果不会有任何变化:
function test() { a = function(arg) { window.alert("alert: " + arg); }; a("hello world"); if (false) { var a = 0; } }
JavaScript只关心,你是否在这个作用域中,使用var修饰了这个变量名。
最后需要说明的问题是,函数的作用域对象并不等同于函数对象!换言之var a = 100并不能使得函数对象多出一个成员a其值为100。函数的作用域对象是函数被调用时存在的对象,这与函数对象(用于调用函数的对象)不是一回事。函数的作用域对象是一个运行时自动创建自动回收的对象,你甚至无法捕获到这个对象。
三、this对象
this对象恐怕是JavaScript里面最难于理解的对象了,有人还总结出了N种情况下this分别代表什么含义。但事实上如果我们了解JavaScript的实际执行原理,就不难搞清楚this到底是什么。this一般等同于调用函数的上下文中,函数的宿主对象。
先看一个最常见的情况:
function test() { this.alert("abc"); } test();
从执行结果来看,test函数中的this应该是window对象,因为调用了window.alert方法。
那为什么在这段脚本中this是window呢?原因就是在调用上下文中test其实是window的一个成员。因为上面的脚本从某种意义上来说等同于:
window.test = function() { this.alert("abc"); } window.test();
很显然调用test的时候,window就是test的宿主对象。
理解了这一点,我们来看下面这段脚本:
function alarm() { this.a = window.alert; test(); } function test() { this.a("abc"); } alarm(); a("hello world!");
怎么理解这一段代码?首先调用alarm函数,这个函数里面的this是什么?没错,是window。为什么?因为alarm调用的时候其实等同于window.alarm,所以函数内的this就是window。然后this.a = window.alert,这等同于window.a = window.alert。然后执行test函数,同理,test函数里面的this也是window。整理后,其实整个代码可以解释成这样:
function alarm() { window.a = window.alert; test(); } function test() { window.a("abc"); } window.alarm(); window.a("hello world!");
来看一个this不是window的情况:
var o = new Object(); //与var o = function() { };一样,都可以创建一个自定义对象,我们只需要一个对象,不必关心它是函数还是什么。 o.text = "hello object!"; //设置o的text成员 o.alarm = function()//定义一个函数,并设置为o的成员 { window.alert(this.text); //注意这个this } alarm = o.alarm; text = "hello world!"; alarm(); o.alarm();
值得注意的是,其实两个alarm是同一个函数,不同的是在调用上下文中,一个的宿主是window,一个的宿主是o,所以调用的结果不同。this所代表的对象,与函数本身没有任何关系,它取决于调用函数的上下文!
最后一个值得特别说明的东西是,函数的scope对象是不能用this捕获到的,考虑如下脚本:
text = "hello world!"; function test() { var text = "hello function!"; var a = function() { window.alert(this.text); } a(); } test();
结果将是hello world!而不是hello function!,当然你可能会想,a的宿主应该是执行函数时的函数的scope对象,然后这个对象应该有个成员text。但事实并非如此,在这种情况下,a的宿主仍然是window。这一点可以理解为:函数的scope对象并不是一个真正的对象,它只是JavaScript运行时用来保存变量的东西。
这个现象,现在并没有特别好的办法去解释,当然,大家只需要记住,如果调用函数的时候,前面没有".",那么this就总会是window了。