简介
为什么需要这个重新介绍呢?因为 JavaScript 已经完全可以被称为世界上被误解最严重的编程语言了。虽然它被当做玩具来用,但是藏在让人迷惑的简单表象下面的,是强大的语言特性。从2005年,一大批“高级”的JavaScript应用程序开始涌现,这表明对于任何网页开发人员来说,对 javascript 的深入认知已经成为一项相当重要的技能。
那么让我们先了解这个语言的历史。JavaScript是在1995年由Netscape的一位名为Brendan Eich的工程师创立,1996年初,首先使用于Netscape2。它最早被称作LiveScript,但后因一个失策的市场推广方案,将其用当时很流行的Sun公司的Java语言冠名-尽管这两者之间的共性微乎其微。这就成为了混淆的开始。
三个月之后,微软发布了这个语言与其IE浏览器最兼容的版本,名为JScript。Netscape将这个语言提交给Ecma国际组织,一个欧洲的标准化组织,1997年该组织颁布了ECMAScript标准第一版。1999年这个标准在ECMAScript第三版中有了重大改变,并被稳定沿用,尽管目前正在制定版本4的标准。
这个稳定性对于开发者来说是个大好消息,因为它给了各种各样实现大量的时间去修改并兼容这个标准。在这里,我将不去关注那些第三版的术语,而是贯穿始终的使用javascript术语。
与大多数编程语言不相同的,JavaScript 语言并没有输入/输出的概念。它被设计成在一个宿主环境下运行的脚本语言,它帮助给宿主环境提供与外界交流的机制。那么,最普遍的宿主环境就是浏览器,但是在Adode Acrobat, photoshop, Yahoo的 widget 中,也同样可以找到 JavaScript 解释器。
概览
让我们从任何一种语言都不可缺少的一个组成部分“类型”开始。JavaScript 程序操作值(value),这些值都有各自的类型,JavaScript 的类型包括:
- 数字
- 字符串
- 布尔
- 函数
- 对象
好像还少东西哦,还有未定义(Undefined )和空(Null)(这俩略微有些怪)。还有作为一类特殊对象的数组。以及预定义好的对象:日期 和 正则表达式。另外,从技术上来严格定义的话,函数(function)也只是一种特殊类型的对象。那么,现在这个“类型”图,就应该变成下面这样了:
- 数字
- 字符串
- 布尔
- 对象
- 函数
- 数组
- 日期
- 正则
- 空(Null)
- 未定义(Undefined)
数字
根据定义,JavaScript 中的数值类型是指“IEEE 754中的双精度64位格式”。这里有一些有趣的推理。在 JavaScript 中不存在整数值(integer),所以,当习惯了C或java中的数学算法时,你不得不在 Javascript中更加小心一点。看看下面的例子:
0.1 + 0.2 = 0.30000000000000004
标准的数字操作符是被支持的,包括加、减、取模(或取余)等等。还有一个我之前忘记提到的内置对象 Math,它可以处理更多的高级数学函数和常量值:
Math.sin(3.5); d = Math.PI * r * r;
你已可以通过内置函数parseInt()
将字符串转换为整型。这个函数中的第二个变量表示数值的进制,可选填,通常这样写:
parseInt("123", 10) 123 parseInt("010", 10) 10
如果你没有提供进制信息,你会得到一个意外的结果:
parseInt("010") IE9+,Chrome:10 IE8:8
这是因为,函数parseInt
根据开头的0来决定将那个字符串转换为八进制数字。
如果你想使用2进制来转换,则更改进制信息为2:
parseInt("11", 2) 3
如果待转换的字符串不存在数值形式,函数就会返回一个特殊的值NaN
(是not a number的缩写):
parseInt("hello", 10) NaN
NaN
是不好的:如果你把它作为输入数值传给任何一个数学操作,结果都会返回一个NaN
:
NaN + 5 NaN
你还可以使用内置函数isNaN()
来检测一个变量是否为NaN:
isNaN(NaN) true
JavaScript还有两个特殊值:正无穷
and 负无穷
:
1 / 0 Infinity -1 / 0 -Infinity
字符串
JavaScript中的字符串是一些字符序列。更准确的说,他们是Unicode字符序列,每一个字符由一个16位数字表示。这个信息应该会让那些需要国际化的网页的开发者感到高兴。
如果你想表示一个单独的字符,你只需使用长度为1的字符串即可。
通过读取它的长度
属性,可以得到一个字符串的长度。
"hello".length 5
这就是我们第一次使用到javascript对象了,前面我已经提到过字符串类型变量实际上也是一个对象了吧,他们也拥有方法:
"hello".charAt(0) h "hello, world".replace("hello", "goodbye") goodbye, world "hello".toUpperCase() HELLO
其他类型
JavaScript区分null
和undefined
,前者是“对象”类型的对象,表示一个空值(non-value),后者是“未定义(undefined)”类型的对象,表示一个还没有被分配的值。我们将要在后面讨论变量,但在javascript中,声明一个变量却不赋予值是可以的。如果你这样做了,那么这个变量的类型就是undefined
。
JavaScript有一个布尔类型,可能的值是true
和false
(两个都是关键字)。通过如下的规则,任何值都可以被转换成布尔型:
false
,0
, 空字串(""
),NaN
,null
, 和undefined
都被转换为false
- 其他值会被转换为
true
你可以使用函数Boolean()
来进行明确的转换:
Boolean("") false Boolean(234) true
不过通常并不需要这么做,因为javaScript会在希望一个布尔型值的时候,隐含地执行这个转换操作,比如在if
的条件语句。由于这个原因,我们通常所说的"true values"和"false values"分别代表那些转换为布尔值后为true
和false
的,或者,将这些值分别称为"truthy"(真值)和"falsy"(伪值)。
javascript中支持布尔操作符。比如:&&
(逻辑与),||
(逻辑或),和!
(逻辑非)。
变量
在javascript中声明一个新变量的方法是使用关键字var
:
var a; var name = "simon";
如果你声明了一个变量却没有给他赋任何值,那么它的类型就是undefined
操作符
javascript的数字操作符包括+
, -
,*
, /
和%
(模除)。赋值操作符=
,以及一些复合操作符,如+=
和-=
,它们可以被展开为x = x 操作符 y
的形式。
x += 5 x = x + 5
你可以使用++
和--
来分别实现自增和自减。这两个可以作为前缀或后缀操作符使用。
+
操作符还可以用来连接字符串:
"hello" + " world" hello world
如果你用一个字符串加上一个数字(或其他值),那么每一个操作数都会被首先转换为字符串。如下所示:
"3" + 4 + 5 345 3 + 4 + "5" 75
也就是说,可以通过给一些值前面加上一个空字符串的方法来转换它的类型。
JavaScript中的比较操作使用<
, >
, <=
和>=
。他们对于字符串或数字都可用。相等(的处理)就不那么直观了。两个等号的操作符,有类型自适应的功能,如下:
"dog" == "dog" true 1 == true true
三个等号对于操作符两边的数值的类型有严格判定,如下:
1 === true false true === true true
相似的,还有!=
和!==
操作符
javascript还提供按位操作符(bitwise)
控制结构
JavaScript的控制结构与C语言系统中的其他编程语言类似。可以使用if
和else
来定义条件语句,并且可以拼接这些判断。
var name = "kittens"; if (name == "puppies") { name += "!"; } else if (name == "kittens") { name += "!!"; } else { name = "!" + name; } name == "kittens!!"
JavaScript有while
循环和do-while
循环。前者可以很好的执行基本的循环操作;后者可以在你确认循环体至少被执行一次时使用:
while (true) { // 一个无限循环! } do { var input = get_input(); } while (inputIsNotValid(input))
JavaScript中的for
循环与C和Java中的相同。你可以使用它在一行代码中提供控制信息。
for (var i = 0; i < 5; i++) { // 将会执行五次 }
&&
和||
操作符使用短路逻辑,也就是说他们是否会执行第二个操作数是取决于第一个操作数的值是否满足条件。这个逻辑在把属性传给一个对象之前先检查该对象是否为空很有用:
var name = o && o.getName();
或用来设置默认值:
var name = otherName || "default";
JavaScript 有一个三元操作符:
var allowed = (age > 18) ? "yes" : "no";
switch语句可以用于判断一个字符串或数字值的多重分支:
switch(action) { case 'draw': drawit(); break; case 'eat': eatit(); break; default: donothing(); }
如果你不添加break
语句,执行会被默认的继续到下一个判断中去。在绝大多数情况下你并不期望这样-除非你只是为了便于调试,在某个标签上特意写了继续向下执行的注释:
switch(a) { case 1: // 继续向下 case 2: eatit(); break; default: donothing(); }
default语句是可选的。如果你愿意,你可以在switch部分或case部分使用这个表达式;在switch的表达式和case的表达式之间是使用 ===
操作符进行比较的:
switch(1 + 3): case 2 + 2: yay(); break; default: neverhappens(); }
对象
JavaScript中的对象是简单的“名字-数值”对儿,也就是说,他们与下面这些相似:
- Python中的字典
- perl 和 ruby 里的散列(哈希)
- C和C++里的散列表
- Java里的HashMap
- php里面的关联数组
这种数据结构被使用得如此广泛,以至于这本身就是它多才多艺的明证。由于javascript中的核心类型都是一个对象,任何的javascript程序就自然的在大量的散列表中进行查找,这样使得它速度很快。
“名称”部分是一个JavaScript字符串,“值”部分可以使任何javascript的数据类型--包括对象。这使得你可以为任意的复杂数据创建数据结构。
有两种基本的方法创建一个空对象:
var obj = new Object();
和:
var obj = {};
这两种方法在语义上是相等的;第二种方法被称作“类实例文本化定义语法”(object literal syntax),它使定义来的更方便。这种方法在很早以前版本的语言中并不存在,这就是为什么你发现很多代码并未使用这种新的定义方法。
一旦创建之后,一个对象的属性可以通过如下的两种方式赋值:
obj.name = "Simon" var name = obj.name;
和:
obj["name"] = "Simon"; var name = obj["name"];
这两种方式也是语义上等效的。第二种方法的优点是:属性的名称被看作一个字符串,这就意味着它可以在运行时被计算。它也可以被用来设置和获得某些以预留关键字作为名字的属性的值:
obj.for = "Simon"; // 语法错误,因为for是一个预留关键字 obj["for"] = "Simon"; // 工作正常
“类实例文本化定义语法”也可以用来在对象中定义一个对象:
var obj = { name: "Carrot", "for": "Max", details: { color: "orange", size: 12 } }
属性存取可以通过链式表示方法:
obj.details.color orange obj["details"]["size"] 12
数组
javascript中的数组实际上是一个特殊类型的对象。它的工作原理与普通的对象十分相似(数字的属性可以自然的通过使用[]语法来存取值),但数组还含有一个神奇的属性--长度属性length
。它通常是比数组最大索引大一的数值。
创建数组的老方式是:
var a = new Array(); a[0] = "dog"; a[1] = "cat"; a[2] = "hen"; a.length 3
使用数组示例化文本(array literal)方式可以更加方便:
var a = ["dog", "cat", "hen"]; a.length 3
在数组赋值时给在最后添加逗号,可能在不同的浏览器中产生冲突,所以不要那么做。
注意,array.length
并不是指数组中准确的元素的个数,如下所示:
var a = ["dog", "cat", "hen"]; a[100] = "fox"; a.length 101
记住:数组的长度是一个比最高索引值大1的数值:
如果你查询了一个不存在的数组索引,那么你会得到undefined
:
typeof(a[90]) undefined
可以通过如下方式遍历一个数组:
for (var i = 0; i < a.length; i++) { // Do something with a[i] }
这样做效率稍微有些低,因为每循环一遍都要计算一次长度。改进的方法是:
for (var i = 0, len = a.length; i < len; i++) { // Do something with a[i] }
一种更好的写法是:
for (var i = 0, item; item = a[i]; i++) { // Do something with item }
在这里我们使用了两个变量。for循环中间部分的表达式仍旧是用来监测是否为真--如果成功,那么循环继续。因为i
每次递增1,这个数组的每个元素即被按顺序地传递给item变量。当一个“falsy”元素(如undefined
)被发现时,循环就结束了。
注意,这个技巧只能在你确认数组中不包含“falsy”值时才可以使用。如果你想要遍历可能包含0或空字符串的数组,你应该使用i, j
的写法替代。
遍历数组的另外一种方式是使用for...in
循环。注意,如果有人向Array.prototype
添加新的属性,通过这样的循环它们也同样会被遍历:
for (var i in a) { // Do something with a[i] }
如果你希望在一个数组后面添加元素,最安全的方式是:
a[a.length] = item; // 与 a.push(item)等效;
因为a.length
是比数组的最大索引值大一的数组,这样就可以保证你在数组的最后分配了一个空的位置给新的元素。
数组类包含了许多方法:
a.toString(), a.toLocaleString(), a.concat(item, ..), a.join(sep), a.pop(), a.push(item, ..), a.reverse(), a.shift(), a.slice(start, end), a.sort(cmpfn), a.splice(start, delcount, [item]..), a.unshift([item]..)
concat
返回一个新数组,将元素添加在结尾。pop
移除并返回最后一个元素。push
在数组的结尾添加一个或多个元素(类似于ar[ar.length]
)slice
返回一个子数组sort
对数组排序splice
把数组中的一部分去掉并用其它值取代unshift
将元素拼接到数组的开头
函数
要理解javaScript,最核心的就是要理解对象和函数两个部分。最基本的函数就如下所示那么简单:
function add(x, y) { var total = x + y; return total; }
这个例子包括了你需要了解的关于基本函数的所有部分。一个javascript函数可以包含0个或多个已命名的变量。函数体中的表达式数量亦没有限制。你可以声明函数自己的局部变量。return
语句在任何情况下返回一个值并结束函数。如果没有使用return语句(或者一个没有值的空(an empty)被返回),javascript会返回undefined
。
已命名的参数更像是一个指示,而没有其它什么作用。你不用给一个函数传递它预期的参数也可以调用之;在这种情况下,这些参数会被设置为undefined
。
add() NaN // 不能在未定义对象上进行加操作
你还可以传入多于函数本身期望的变量个数的值。
add(2, 3, 4) 5 // 将前两个值相加,4被忽略了
这看起来有些愚蠢,但是函数可以在函数体中存取一个名为arguments的内部对象,这个对象就如同一个类似于数组的对象一样,它包括了所有被传入函数的值。让我们重写一下上面的函数,使他可以接收我们所期望的任意数量的值
function add() { var sum = 0; for (var i = 0, j = arguments.length; i < j; i++) { sum += arguments[i]; } return sum; } add(2, 3, 4, 5) 14
这实际上并不比直接写成2 + 3 + 4 + 5
更加有用。那么让我们创建一个求平均数的函数:
function avg() { var sum = 0; for (var i = 0, j = arguments.length; i < j; i++) { sum += arguments[i]; } return sum / arguments.length; } avg(2, 3, 4, 5) 3.5
这个很有用,但是却带来了新的问题。avg()
函数处理一个由逗号连接的变量串,但如果你想得到一个数组的平均值怎么做呢?可以将函数修改如下:
function avgArray(arr) { var sum = 0; for (var i = 0, j = arr.length; i < j; i++) { sum += arr[i]; } return sum / arr.length; } avgArray([2, 3, 4, 5]) 3.5
但是,如果能重用我们已经创建的那个函数会更好。很幸运的,JavaScript允许你使用apply()
来调用一个函数,并传递给它一个包含了参数的数组。
avg.apply(null, [2, 3, 4, 5]) 3.5
传给apply()
的第二个参数是一个用于数组,它将被当作avg()
的参数使用;第一个参数null
我们将在后面讨论。这也正说明了事实上函数也是一种对象。
JavaScript允许你创建匿名函数:
var avg = function() { var sum = 0; for (var i = 0, j = arguments.length; i < j; i++) { sum += arguments[i]; } return sum / arguments.length; }
这个函数在语义上与function avg()
相等。他的功能十分强大你可以在任何地方放置你的完整函数,就像写普通的表达式一样。这个特性打开了各种各样的,聪明的技巧。下面是一个“隐藏”一些局部变量的方法,就好像在C中的块范围一样:
var a = 1; var b = 2; (function() { var b = 3; a += b; })(); a 4 b 2
JavaScript中允许你递归的调用函数。当处理树形结构(比如浏览器DOM)时,这非常有用。
function countChars(elm) { if (elm.nodeType == 3) { // TEXT_NODE return elm.nodeValue.length; } var count = 0; for (var i = 0, child; child = elm.childNodes[i]; i++) { count += countChars(child); } return count; }
这里重点说明一个潜在的问题,就是如何处理匿名函数的递归调用(因为他们并没有实际的名称)?答案是通过arguments
对象,那个用来指代一系列参数的对象,它还提供了一个名为arguments.callee
的属性。这个东西通常是指向当前的(调用)函数,因此它可以用来进行递归调用:
var charsInBody = (function(elm) { if (elm.nodeType == 3) { // TEXT_NODE return elm.nodeValue.length; } var count = 0; for (var i = 0, child; child = elm.childNodes[i]; i++) { count += arguments.callee(child); } return count; })(document.body);
由于arguments.callee
是指当前的函数,而且所有的函数都是对象,那么你可以使用arguments.callee
来保存对这同一个函数的多重调用的一些信息。下面的例子讲的是一个可以记住它被调用了多少次的函数:
function counter() { if (!arguments.callee.count) { arguments.callee.count = 0; } return arguments.callee.count++; } counter() 0 counter() 1 counter() 2
自定义对象
在经典面向对象语言中,对象是指数据和在这些数据上进行的操作的集合。那么让我们来定义一个人名对象,他包括人的姓和名两个域(field)。名字的表示有两种方法:“姓 名”或“名,姓”。使用我们前面讨论过的函数和对象,可以像下面这样操作:
function makePerson(first, last) { return { first: first, last: last } } function personFullName(person) { return person.first + ' ' + person.last; } function personFullNameReversed(person) { return person.last + ', ' + person.first } s = makePerson("Simon", "Willison"); personFullName(s) Simon Willison personFullNameReversed(s) Willison, Simon
上面的写法可以满足需要,但是看起来很笨拙。你将会在全局名字空间中写很多函数。我们真正需要的其实是如何使一个函数隶属于一个对象。而函数本身就是对象,那么很容易得到如下示例:
function makePerson(first, last) { return { first: first, last: last, fullName: function() { return this.first + ' ' + this.last; }, fullNameReversed: function() { return this.last + ', ' + this.first; } } } s = makePerson("Simon", "Willison") s.fullName() Simon Willison s.fullNameReversed() Willison, Simon
这里出现了一些我们之前没有见过的东西:关键字'this
',它被使用在函数体内,'this
'指代当前的对象,也就是指是在哪个对象里调用了那个函数。如果你在一个对象上使用点或者花括弧,那么这个对象就成为了'this
'。如果并没有使用“点”操作符来调用,那么'this
'将指向全局对象(global object)。这是一个很常出错的地方。例如:
s = makePerson("Simon", "Willison") var fullName = s.fullName; fullName() undefined undefined
当我们调用fullName()
时,'this
'实际上是指向全局对象的。而并没有名为first
或last
的全局变量,那么它们两个的返回值都会是undefined
。
下面我们将使用关键字'this
'的优势来改进我们的makePerson
函数:
function Person(first, last) { this.first = first; this.last = last; this.fullName = function() { return this.first + ' ' + this.last; } this.fullNameReversed = function() { return this.last + ', ' + this.first; } } var s = new Person("Simon", "Willison");
我们引入了另外一个关键字:'new
',它和'this
'密切相关。它的作用是创建一个崭新的空对象,然后使用指向那个对象的'this
'调用制定的函数。被'new
'调用的函数成为构造函数。习惯的做法是将这些函数的首字母大写,这样用new
调用他们的时候就容易识别了
那么我们的人名对象可以进一步完善了,但还是它们有一些不太好的角落。每次我们创建一个人名对象的时候,我们都在其中创建了两个新的函数对象--那么如果这个代码可以共享不是会更好么?
function personFullName() { return this.first + ' ' + this.last; } function personFullNameReversed() { return this.last + ', ' + this.first; } function Person(first, last) { this.first = first; this.last = last; this.fullName = personFullName; this.fullNameReversed = personFullNameReversed; }
这种写法好在:我们只创建一次方法函数,在构造器中调用它们。那是否还有更好的方法呢?答案是肯定的。
function Person(first, last) { this.first = first; this.last = last; } Person.prototype.fullName = function() { return this.first + ' ' + this.last; } Person.prototype.fullNameReversed = function() { return this.last + ', ' + this.first; }
Person.prototype
是一个可以被Person
的所有实例共享的对象。它是一个名叫"prototype chain"的查询链的一部分:当你企图取得一个Person
并没有定义的原型时,JavaScript会先检查这个Person.prototype
来判断是否存在。所以,任何赋与 Person.prototype
的东西都会为所有通过 this
对象构造的实例可用。
这个工具功能十分强大,JavaScript允许你在程序中的任何时候修改原型(prototype)中的一些东西。也就是说你可以在运行时给已存在的对象添加格外的方法:
s = new Person("Simon", "Willison"); s.firstNameCaps(); TypeError on line 1: s.firstNameCaps is not a function Person.prototype.firstNameCaps = function() { return this.first.toUpperCase() } s.firstNameCaps() SIMON
有趣地是,你还可以给JavaScript的内置函数的原型(prototype)添加东西。让我们给String
添加一个方法用来返回逆序的字符串:
var s = "Simon"; s.reversed() TypeError on line 1: s.reversed is not a function String.prototype.reversed = function() { var r = ""; for (var i = this.length - 1; i >= 0; i--) { r += this[i]; } return r; } s.reversed() nomiS
我们的新方法也可以在字符串文本化定义语法(string literals)
"This can now be reversed".reversed() desrever eb won nac sihT
正如我前面提到的,原型是组成链的一部分。那个链的根节点是Object.prototype
,它包括了一个toString()
方法--这个方法是在你试图将一个对象展现为字符串时调用的。这对于调试我们的Person
对象很有用:
var s = new Person("Simon", "Willison"); s [object Object] Person.prototype.toString = function() { return '<Person: ' + this.fullName() + '>'; } s <Person: Simon Willison>
你是否还记得之前我们说的avg.apply()
中的那个第一个参数null?那我们可以回过头来看一看。apply()
的第一个参数应该是一个被当作'this
'来看待的对象。比如,下面是一个 'new
'方法的简单实现:
function trivialNew(constructor) { var o = {}; // Create an object constructor.apply(o, arguments); return o; }
这并不是一个new
的精确副本,因为它并没有创建原型(prototype)链。想举例说明apply()
有些困难,因为它并不是你会经常使用的函数,但是了解它还是很有用的。
apply()
有一个姐妹函数,名叫call
,它也可以允许你设置'this
',但是带有一个扩展的参数列表,而不是一个数组。
function lastNameCaps() { return this.last.toUpperCase(); } var s = new Person("Simon", "Willison"); lastNameCaps.call(s); // Is the same as: s.lastNameCaps = lastNameCaps; s.lastNameCaps();
内嵌函数
JavaScript中允许在一个函数内部定义函数。这个我们在之前的makePerson()
例子中也见到过。关于javascript嵌套函数的一个很重要的细节是:它们可以存取父函数作用域中的变量:
function betterExampleNeeded() { var a = 1; function oneMoreThanA() { return a + 1; } return oneMoreThanA(); }
这就给写更多的可重复使用的代码提供了方便。如果一个函数依赖于其他的一个或两个函数,而它们对你其余的代码却没有用处,那么你可以将它们嵌套在会被别人调用的那个函数内部。这样可以减少在全局环境(global scope)下的函数的数量。这通常都是一件很好的事情。
这也是一个抵制全局变量“诱惑”的好方法。当写一些复杂代码时,程序员通常都企图使用全局变量来共享给多个函数--这样做会使代码很难维护。内嵌函数可以共享父函数的变量,所以你可以使用这个机制把一些函数内嵌在一起,这样可以有效地防止“污染”你的全局空间--你可以称它为“局部全局local globals”。虽然这种方法应该谨慎使用,但它确实很有用,应该掌握。
闭包
下面我们将看到的是javaScript中不能不提到的功能最强大的结构之一:闭包。但它同时也有很多潜在的困扰。那么它究竟是做什么的呢?
function makeAdder(a) { return function(b) { return a + b; } } x = makeAdder(5); y = makeAdder(20); x(6) ? y(7) ?
makeAdder
函数的名字自身应该就揭示了很多秘密:它创建了一个新的'adder'函数,这个函数自身带有一个参数,它被调用的时候这个参数会被加在外层函数传进来的参数上。
这里发生的事情和前面介绍过的内嵌函数十分相似:一个函数被定义在了另外一个函数的内部,内部函数可以存取外部函数的变量。这里唯一的不同就是:外部函数被返回了,那么常识告诉我们局部变量“应该”不再存在。但是它们却仍然存在--否则adder函数将不能工作。也就是说,这里存在了makeAdder
的局部变量的两个不同的“副本”--一个是a
等于5,另一个是a
等于20。所以那些函数的结果就如下所示了:
x(6) // 返回 11 y(7) // 返回 27
这里就是实际发生了的事情。每当javascript执行一个函数的时候,会创建一个'范围对象',并用来保存在这个函数中创建的局部变量。它和被传入函数的变量一同被初始化。这与那些保存的所有全局变量和函数的全局对象(global object)相似,但有一些很重要的不同点是:第一,每次函数被执行的时候,就会创建一个新的,特定的范围对象;第二,与全局对象(在浏览器里面是当做 window 对象来访问的)不同的是,你可以从javascript代码中直接访问范围对象。目前并没有机制可以遍历当前的范围对象里面的属性。
所以当调用makeAdder
时,创建了一个范围对象,它带有一个属性:a
,被当做参数传入makeAdder
函数。makeAdder
然后返回一个新创建的函数。通常JavaScript的垃圾回收器会在这个点上清理掉makeAdder
创建的范围对象,但是返回的函数却保留着一个指向那个范围对象的引用。结果,这个范围对象将不会被垃圾回收器回收,直到指向makeAdder
返回的那个函数对象的引用计数为零。
范围对象组成了一个名为范围链的链。它类似于原形(prototype)链一样,被javascript的对象系统使用。
一个闭包就是一个函数和被创建的函数中的范围对象的组合。
闭包允许你保存状态--所以,它们通常可以代替对象来使用。
内存泄露
使用闭包的一个不幸的负面影响是,在IE浏览器中它会很容易导致泄露内存。JavaScript是一种垃圾回收的语言--对象在被创建的时候分配内存,然后当指向这个对象的引用计数为零时,浏览器会回收内存。宿主环境提供的对象是通过这个环境来处理的。
浏览器主机需要处理大量的对象来描绘一个正在被展现的HTML页面--DOM对象。浏览器负责管理它们的内存分配和回收。
对于这些,IE浏览器使用它自己的垃圾回收机制,而不是JavaScript自身使用的方法。在两者之间的交互可能会导致内存的泄露。
在IE中,每当在一个javascript对象和一个本地对象之间形成循环引用时,就发生了内存泄露。如下所示:
function leakMemory() { var el = document.getElementById('el'); var o = { 'el': el }; el.o = o; }
上面的循环引用导致了内存泄露;IE不会释放被el
和o
使用的内存,直到浏览器被完全重起。
上面的例子很容易被忽视;内存泄露只会在长时间运行的应用程序中,或泄露了大量内存(由于巨大的数据结构或在循环中泄露)的时候发生不可忽略的影响。
很少发生那么明显的泄露现象--通常泄露的数据结构有很多层的引用(references),使循环引用并不明显。
闭包很容易发生无意识的内存泄露。如下:
function addHandler() { var el = document.getElementById('el'); el.onclick = function() { this.style.backgroundColor = 'red'; } }
上面的代码创建了一个元素,当它被点击的时候变红。同时它也发生了内存泄露。为什么?因为对el
的引用被不小心地是在一个匿名的内嵌函数中。这就在javascript对象(这个内嵌函数)和本地对象之间(el
)创建了一个循环引用。
那么对于这个问题,有很多种解决方法,最简单的一种如下:
function addHandler() { var el = document.getElementById('el'); el.onclick = function() { this.style.backgroundColor = 'red'; } el = null; }
它通过打断循环引用来工作。
令人惊奇的,一种打断被闭包引入的循环引用的窍门是添加另外一个闭包:
function addHandler() { var clickHandler = function() { this.style.backgroundColor = 'red'; } (function() { var el = document.getElementById('el'); el.onclick = clickHandler; })(); }
内置函数被直接执行,并在clickHandler
创建的闭包中隐藏了它的内容。
另外一种避免闭包的好方法是在window.onunload
事件期间打断循环引用。很多事件库会为你做这件事情。注意这样做将使Firefox 1.5中的 bfcache不能工作。所以除非有其他必需的原因,否则你不应该在Firefox中注册一个unload
的监听器。
原始文档信息 作者: Simon Willison 最后更新时间: March 7, 2006 版权: © 2006 Simon Willison, contributed under the Creative Commons: Attribute-Sharealike 2.0 license. 更多信息: For more information about this tutorial (and for links to the original talk's slides), see Simon's Etech weblog post.