《javascript 语言精粹》精华部分

第1章 精华

JavaScript的特性中有一部 分特性带来的麻烦远远超出它们的价值。其中,一些特性是因为规范很不完善,从而可能导致可移植性的问题;一些特性会导致生成难以理解和修改的代码;一些特 性促使我的代码风格过于复杂且易于出错;还有一些特性就是设计错误。有时候语言的设计者也会犯错。

大多数编程语言都有精华部分和鸡 肋部分。我发现如果只使用精华部分而避免使用鸡肋的部分,我可以成为一个更好的程序员。毕竟,用糟糕的部件怎么可能构建出好东西呢?

标准委员会想要移除一门语言中的 缺陷部分,这几乎是不可能的,因为这样做会损害所有依赖于那些鸡肋部分的糟糕程序。除了在已存在的一大堆缺陷上堆积更多的特性,他们通常无能为力。并且新 旧特性并不总是能和谐共处,可能从而产生出更多的鸡肋部分。

但是,你有权力定义你自己的子 集。你完全可以基于精华部分去编写更好的程序。JavaScript中鸡肋部分的比重超出了预料。在短到令人吃惊的时间里,它从不存在发展到全球采用。它从来没有在实验 室里被试用和打磨。当它还非常粗糙时,它就被直接集成到网景的Navigator 2浏览器中。随着JavaTM的小应用程序(Java applets)的失败,JavaScript变成了默认的”网页语言”。作为一门编程语言,JavaScript的流行几乎完全不受它的质量的影响。

好在JavaScript有一些非常精华的部分。JavaScript最本质的部分被深深地隐藏着,以至于多年来对它的主流观点是:JavaScript就是一个丑陋的、没用的玩具。本书的目的就是要揭示JavaScript中的精华,让大家知道它是一门杰出的动态编程语言。

或许只学习精华部分的最大好处就 是你可以不用考虑鸡肋的部分。忘掉不好的模式是非常困难的。这是一个非常痛苦的工作,我们中的大多数人都会很不愿意面对。有时候,制定语言的子集是为了让 学生更好的学习。但在这里,我制定的JavaScript子集是为了主专 业人员更好的工作。

1.1 为什么要使用JavaScript

JavaScript是一门重要的语 言,因为它是web浏览器的语言。它与浏览器的结合使它成为世界上最流行的编程语言之一。同时,它也是世界上最被轻视 的编程语言之一。浏览器的API和文档对象模型(DOM)相当糟糕,导致JavaScript遭到不公平的指责。

JavaScript是最被轻视的语 言,因为它不是所谓的主流语言。如果你擅长某些主流语言,但却在一个只支持JavaScript的环境中编程,那么被迫使用JavaScript确是相当令人厌烦的。大多数人觉得没必要去先学好JavaScript,但结果他们会惊讶地发现,JavaScript跟他们宁愿使用的主流语言有很大不同,而且这些不同至为关键。

JavaScript令人惊异的事情 是,在对这门语言没有太多了解,甚至对编程都没有太多了解的情况下,你也能用它来完成工作。它是一门拥有极强表达能力的语言。当你知道要做什么时,它甚至 能表现得更好。编程是很困难的事情。绝不应该在对此一无所知时便开始你的工作。

1.2 分析JavaScript

JavaScript建立在一些非常 好的想法和少数非常坏的想法之上。

那些非常好的想法包括函数、弱类 型、动态对象和一个富有表现力的字面量表示法。那些坏的想法包括基于全局变量的编程模型。

JavaScript的函数是(主 要)基于词法作用域(lexical scoping)的顶级对象。JavaScript是第一个成为主流的lambda语言。实际上,相对Java而言,JavaScript与Lisp和Scheme有更多的共同点。它是披着C外衣的Lisp。这使得JavaScript成为一个非常强大的语言。

现今大部分编程语言中都流行要求 强类型。其原理在于强类型允许编译器在编译时检测错误。我们能越早检测和修复错误,付出的代价就越小。JavaScript是一门弱类型的语言,所以JavaScript编译器不能检测出类型错误。另一方面,弱类型其实是自由的。我们无须建立复杂的次,我永远不用做强制 类型转换,也不用疲于应付类型系统以得到想要的行为。

JavaScript有非常强大的对 象字面量表示法。通过列出对象的组成部分,它们就能简单地被创建出来。这种表示法产生了流行的数据交换格式——JSON。

原型继承是JavaScript中一个有争议的特性。JavaScript有一个无类别(class-free)的对象系统,在这个系统中,对象直接从其他对象继承属性。这真的很强大,但是对那些被训练使用类去 创建对象的程序员们来说,原型继承是一个陌生的概念。如果你尝试对JavaScript直接应用基于类的设计模式,你将会遭受挫折。但是,如果你学习使用JavaScript的原型本质,那么你的努力将会有所回报。

JavaScript在关键思想的选 择上饱受非议。虽然在大多数情况下,这些选择是合适的。但是有一个选择相当糟糕:JavaScript依赖于全局变量来进行连接。所有编译单元的所有顶级变量被撮合到一个被称为全局对象的公共命名空间 中。这是一件糟糕的事情,因为全局变量是魔鬼,并且在JavaScript中它们是基础性的。

在少数情况下,我们不能忽略鸡肋 的部分。另外还有一些不可避免的糟粕,当涉及这些部分时,我们会将它们指出来。如果你想学习那些鸡肋的部分及如何拙劣地使用它们,请参阅任何其他的JavaScript书籍。

JavaScript是一门有许多差 异的语言。它包含很多错误和尖锐的边角(sharp edges),所以你可 能会疑惑:”为什么我要使用JavaScript?”有两个答 案。第一个是你没有选择。Web已变成一个重要的应用开发平 台,而JavaScript是唯一一门所有浏览器都可以识别的语言。很不幸,Java在浏览器环境中失败了。JavaScript的蓬勃发展,恰恰说明了JavaScript确有其过人之处。

另一个答案是,尽管JavaScript有缺陷,但是它真的很优秀。它既轻量又富有表现力。并且一旦你熟练掌握了它,就会发现函数式编程是 一件很有趣的事。

但是为了更好地使用这门语言,你 必须知道它的局限。我将会无情地揭示它们。不要因此而气馁。这门语言的精华部分足以弥补它鸡肋的不足。

1.3 一个简单的试验场

如果你有一个Web浏览器和任意一个文本编辑器,那么你就有了运行JavaScript程序所需要的一切。首先,请创建一个HTML文件,可以命名为program.html:

 <html> <body> <pre> <script src="program.js"></script> </pre> </body> </html> 

接下来,在同一个文件夹内,创建 一个脚本文件,可以命名为program.js:

 document.writeln('Hello, world!'); 

下一步,用你的浏览器找开你的HTML文件去查看结果。本书贯彻始终都会用到一个method方法去定义新方法。下面是它的定义:

 Function.prototype.method = function(name, func){ this.prototype[name] = func; return this; }; 

我会在第4章解释它。

第2章 语法

本章介绍JavaScript的精华部分的语法,并简要地概述其语言结构。

2.1 空白

空白可能表现为格式化字符或注释 的形式。空白通常没有意义,但是偶尔须要用它来分隔字符序列,否则它们就会被合并成一个单一的符号。例如,对如下代码来说:

 var that = this; 

var和that之间的空格是不能被移除的,但是其他的空格都可以被移除。

JavaScript提供两种注释形 式,一种是用/* */包围的块注释,另一种是以//为开头的行注释。注释应该被充分地用来提高程序的可读性。必须注意的是,注释一定要精确地描述代码。 没有用的注释比没有注释更糟糕。

用/* */包围的块注释形式来自于一门叫PL/I(注释:Programming Language One的简写。当中的”I” 其实是罗马数字的”一”,它是一种IBM公司在19世 纪50年代发明的第三代高级编程语言)的语言。在JavaScript中,那些字符也可能出现在正则表达式字面上,所以块注释对于被注释的代码块来说是不安全的。例如:

 /* var rm_a = /a*/.match(s); */ 

导致了一个语法错误。所以,我建 议避免使用/* */注释,而用//注释代替它。

2.2 标识符

标识符由一个字母开头,其后可选 择性地加上一个或多个字母数字或下划线。标识符不能使用下面这些保留字:

abstract boolean break byte case catch char class const continue debugger default delete do double else enum export extends false final finally float for function goto if implements import in instanceof int interface long native new null package private protected public return short static super switch synchronized this throw throws transient true try typeof var volatile void while with

在这个列表中的大部分保留字尚未 用在这门语言中。这个列表不包括一些本应该被保留而没有保留的字,诸如undefined、NaN和Infinity。JavaScript不允许使用保留字来命名变量或参数。更糟糕的是,JavaScript不允许在对象字面量中,或者在一个属性存取表达式的点号之后,使用保留字作为对象的属性名。

标识符被用于语句、变量、参数、 属性名、运算符和标记。

2.3 数字

JavaScript只有一个单一的 数字类型。它在内部被表示为64位的浮点数,和Java的double一样。在JavaScript中,1和1.0是相同的值。

如果一个数字字面量有指数部分, 那么这个字面量的值是由e之前的部分乘以10的e之后部分的次方计算出来的。所以100和1e2是相同的数字。

负数可以用前缀运算符-来构成。

值NaN是一个数值,它表示一个不能产生正常结果的运算结果。NaN不等于任何值,包括它自己。你可以用函数isNaN(number)检测NaN。

值Infinity表示所有大于1.79769313486231570e+308的值。

数字拥有方法(参见第8章)。JavaScript有一个对象Math,它包含一套作用于数字的方法。例如,可以用Math.floor(number)方法将一个数字转换成一个整数。

2.4 字符串

字符串字面量可以被包围在单引号 或双引号中,它可能包含0个或多个字符。\是转义字符。JavaScript在被创建的时候,Unicode是一个16位的字符集,所以JavaScript中的所有字符都 是16位的。

JavaScript没有字符类型。 要表示一个字符,只须创建仅包含一个字符的字符串即可。

转义字符允许把那些正常情况下不 被允许的字符插入到字符串中,比如反斜线、引号和控制字符。\u约定允许指定用数字表示的字符码位。

“A”===”\u0041″

字符串有一个ength属性。例如,”seven”.length是5。

字符串是不可变的。一旦字符串被 创建,就永远无法改变它。但通过+运算符去连接其他的字符串从而得到 一个新字符串是很容易的。两个包含着完全相同的字符且字符顺序也相同的字符串被认为是相同的字符串。所以:

'c' + 'a' + 't' === 'cat'

是true。

字符串有一些方法(参见第8章)。

2.5 语句

一个编译单元包含一组可执行的语 句。在web浏览器中,每个<script>标签都提供一个被编译且立即执行的编译单元。因为缺少链接器,JavaScript把它们一起抛入一个公共的全局命名空间中。附录A有更多关于全局变量的内容。

当var语句被用在函数的内部时,它定义了这个函数的私有变量。

switch、while、for和do语句允许有一个可选的前置标签(label),它配合break语句来使用。

语句往往按照从上到下的顺序被执 行。JavaScript可以通过条件语句(if和switch)、循环语句(while、for和do)、强制跳转语句(break、return和throw)和函数调用来改变这个执行序列。

代码块是包在一对花括号中的一组 语句。不像许多其他的语言,JavaScript中的代码块不会 创建一个新的作用域,因此变量应该被定义在函数的顶端,而不是在代码块中。

if语句根据表达式的值改变程序的控 制流程。如果表达式的值为真,那么执行then代码块,否则,执行可选的else分支。

下面列出的值被当作假:

fase

null

undefined

数字0

数字NaN

其他所有的值都被当作真,包括true,字符串”false”,以及所有的对象。

switch语句执行一个多路分支。 它把其表达式的值和所有指定的case条件进行匹配。其表达式可能 产生一个数字或字符串。当找到一个精确的匹配时,执行匹配的case从句中的语句。如果没有找到任何匹配,则执行可选的default语句。

一个case从句包含一个或多个case表达式。case表达式不一定必须是常量。为了防止继续执行下一个case,case语句后应该跟随一上强制跳转语句。你可以用break语句去退出一个switch语句。

while语句执行一个简单的循环。 如果表达式值为假,那么循环将终止。而当表达式值为真时,代码块将被执行。

for语句是一个结构更复杂的循环语 句。它有两种形式。

常见的形式由三个可选从句控制: 初始化从句(initialization)、条件从句(condition)和增量从句(increment)。首先,;初始化从句被执行,它的作用通常是初始化循环变量。接着计算条件从句的值。典型的情况是它根据一个完成条件检测循环变量。如果条件从句被省略 掉,则假定返回的条件是真。如果条件从句的值为假,那么循环将终止。否则,执行代码块,然后执行增量从句,接着循环会重复执行条件从句。

另一种形式(被称为for in语句)会枚举一个对象的所有属性名(或键名)。在每次循环中,对象的另一个属性名字符串被赋值给for和in之间的变量。

通常你须通过检测object.hasOwnProperty(variable)来确定这个属性名就是该对象的成员,还是从其原型链里找到的。

 for(myvar in obj){ if(obj.hasOwnProperty(myvar)){ … } } 

do语句就像while语句,唯一的区别是它在代码块执行之后而不是之前检测表达式的值。这就意味着代码块将总是要执行至 少一次。

try语句执行一个代码块,并捕获该 代码块抛出的任何异常。catch从句定义了一个新的变量,它将接收该异常对象。

throw语句抛出一个异常。如果throw语句在一个try代码块中,那么控制权会跳到catch从句中。如果throw语句在函数中,则该函数调用被放弃,且控制权会跳到调用该函数的try语句的catch从句中。

throw语句中的表达式通常是一个 对象字面量,它包含一个name属性和一个message属性。异常捕获器可以使用这些信息去决定该做什么。

return语句会使一人函数提前返 回。它也可以指定要被返回的值。如果没有指定返回表达式,那么其返回值是undefined。

JavaScript不允许在return关键字和表达式之间换行。

break语句会使程序退出一个循环 语句或switch语句。它可以指定一个可选的标签,那将会使程序退出带该标签的语句。

JavaScript不允许在break关键字和标签之间换行。

一个expression语句可以给一个或多个变量或成员赋值,或者调用一个方法,或者从对象中删除一个属性。运算符=被用于赋值。不要把它和恒等运算符===混淆。运算符+=可以用于加法运算或连接字符串。

2.6 表达式

三元运算符?有三个运算数。如果 第一个运算数值为真,它产生第二个运算数的值。但是,如果第一个运算数为假,它会产生第三个运算数的值。

表2-1:运算符优先级

.[]()属性存取及函数调用
delete new typeof +-!一元运算符
*/%乘法、除法、取模
+-加法/连接、减法
>= <= > <不等式运算符
=== !==等式运算符
&&逻辑与
||逻辑或
?:三元

typeo运算符产生的值有’number’、’string’、’boolean’、’undefined’、’function’、’object’。如果运算数是一个数组或null,那么结果是’object’,这是不对的。第6章和附录A将会有更多关于typeof的内容。

/运算符可能会产生一个非整数结果, 即使两个运算数都是整数。

函数调用引发函数的执行。函数 调用运算符是跟随在函数名后面的一对圆括号。圆括号中可能包含将会传递给这个函数的参数。第4章将会有更多关于函数的内容。

一个属性存取表达式用于指定一 个对象或数组的属性或元素。下一章我将详细描述它。

2.7 字面量

对象字面量是一种方便指定新对象 的表示法。属性名可以是标识符或字符串。这些名字被当作字面量名而不是变量名来对待,所以对象的属性名在编译时才能知道。属性的值就是表达式。下一章将会 有更多关于对象字面量的信息。

数组字面量是一个方便指定新数组 的表示法。第6章将会有更多关于数组字面量的内容。

第7章将会有更多关于正则表达式的内容。

函数字面量定义了函数值。它可以 有一个可选的名字,用于递归地调用自己。它可以指定一个参数列表,这些参数将作为变量由调用时传递的实际参数(arguments)初始化。函数的主体包括变量定义和语句。第4章将会有更多关于函数的内容。

第3章 对象

JavaScript的简单类型包括 数字、字符串、布尔值(true和false)、null值和undefined值。其他所有的值都是对象。数字、字符串和布尔值”貌似”对象,因为它们拥有方法,但它们是不可变 的。JavaScript中的对象是可变的键-值集合(keyed collections)。在JavaScript中,数组是对象,函数是对象,正则表达式是对象,当然,对象自然也是对象。

对象是属性的容器,其中每个属性 都拥有名字和值。属性的名字可以是包括空字符串在内的任意字符串。属性值可以是除undefined值之外的任何值。

JavaScript中的对象是无类 型(默然说话:或者说JavaScript只有一种类型,就是对象)(class-free)的。它对新属性的名字和值没有约束。对象适合用于收集和管理数据。对象可以包含其他对象,所以它们 可以容易地表示成树形或图形结构。

JavaScript包括一个原型链 特性,允许对象继承另一对象的属性。正确地使用它能减少对象初始化的时间和内存消耗。

3.1 对象定义

对象定义提供了一种非常方便地创 建新对象值的表示法。一个对象定义就是包围在一对花括号中的零或多个”名:值”对。对象定义可以出现在任何允许表达式出现的地方。

 var empty_object={}; var stooge={ "first-name":"Jerome", "last-name":"Howard" }; 

属性名可以是换手空字符串在内的 任何字符串。在对象定义中,如果属性名是一个合法的JavaScript标识符且不是保留字,并不强制要求用引号括住属性名。所以用引号括住”first-name”是必须的,但是否括住first_name则是可选的。逗号用来分隔多个”名:值”对。

属性的值可以从包括另一个对象定 义在内的任意表达式中获得。对象是可嵌套的:

 var flight={ airline:"Oceanic", number:815, departure:{ IATA:"SYD", time:"2004-09-22 14:55", city:"Sydney" }, arrival:{ IATA:"LAX", time:"2004-09-23 10:42", city:"Los Angeles" } }; 

3.2 检索

要检索对象中包含的值,可以采用 在[]后缀中括住一个字符串表达式的方式。如果字符串表达式是一个常数,而且它是一个合法的JavaScript标识符而非保留字,那么也可以用点(.)表示法代替。优先考虑使用点(.)表示法,因为它更紧凑且可读性更 好。

 stooge["first-name"]                        //"Joe" flight.departure.IATA                       //"SYD" 

如果你尝试检索一个并不存在的成 员元素的值,将返回一个undefined值。

 stooge["middle-name"]                          //undefined stooge["FIRST-NAME"]                          //undefined flight.status                                      //undefined 

||运算符可以用来填充默认值:

 var middle=stooge["middle-name"] || "(none)"; var status=flight.status || "unknown"; 

尝试检索一个undefined值将会导致TypeError异常。这可以通过&&运算符来避免错误。

 flight.equipment                                                                 //undefined flight.equipment.model                                                             //throw "TypeError" flight.equipment && flight.equipment.model                            //undefined 

3.3 更新

对象中的值可以通过赋值语句来更 新。如果属性名已经存在于对象中,那么这个属性的值被替换。

 stooge['first-name']='Jerome'; 

如果对象之前并没有这个属性,那 么该属性就被扩充到该对象中。

 stooge["middle-name"] = "Lester"; flight.equipment={ model:"Boeing 777" }; 

3.4 引用

对象通过引用来传递。它们永远不 会被拷贝:

 var x=stooge; x.nickname='Curly'; var nick=stooge.nickname;//因为x和stooge是指向同一个对象的引用,所以nick为'Curly' var a={},b={},c={};//a,b和c每个都引用一个不同的空对象 a=b=c={}//a,b和c都引用同一个空对象。 

3.5 原型

每个对象都连接到一个原型对象, 它可以继承属性。所有通过对象定义创建的对象都连接到Object.prototype这个JavaScript中标准的对象。

当你创建一个新对象时,你可以选 择某个对象作为它的原型。JavaScript提供的实现机制 杂乱而复杂,但其实它可以被明显地简化。我们将给Object增加一个beget方法。这个beget方法创建一个使用原对象作为其原型的新对象。下章将会有更多关于函数的内容。

 if(typeof(Object.beget!=='function')){ Object.beget=function(o){ var F=function(){}; F.prototype=o; return new F(); }; } var another_stooge=Object.beget(stooge); 

原型链在更新时是不起作用的。当 我们对某个对象做出改变时,不会触及到该对象的原型:

 another_stooge['first-name']='Harry'; another_stooge['middle-name']='Moses'; another_stooge.nickname='Moe'; stooge['first-name'];                 //不会被更新为'Harry' 

原型连接只有在检索值的时候才被 用到。如果我们尝试去获取对象的某个属性值,且该对象没有此属性名,那么JavaScript会试着从原型对象中去获取属性值,直到该过程最后到达终点Object.prototype。如果找不到该属性,结果就是undefined。这个过程称为委托

如果我们添加一个新的属性到原型 中,该属性会立即对所有基于该原型创建的对象可见。

 stooge.profession='actor'; another_stooge.profession               //'actor' 

我们将会在第6章中看到更多关于原型链的内容。

3.6 反射

JavaScript似乎没有直接提 供相关反射的处理,所以需要自己编写代码实现,检查对象并确定对象有什么属性。

我们可以使用typeof来确定属性的类型。

 typeof(flight.number)                            //'number' typeof(flight.status)                        //'string' typeof(flight.arrival)                       //'object' typeof(flight.manifest)                           //'undefined' 

但原型链中的任何属性也会产生一 个值。我们可以使用hasOwnProperty方法,如果对象拥有独有的属性,它将返回true。hasOwnProperty方法不会检查原型链。

 flight. hasOwnProperty('number')                //true flight. hasOwnProperty('constructor')           //false 

3.7 枚举

for in语句可用来遍历一个对象 中的所有属性名。该枚举过程将会列出所有的属性——包括函数和你可能不关心的原型中的属性——所以有必要过滤掉那些你不想要的值。最为常用的过滤器是hasOwnProperty方法,以及使用typeof来排除函数:

 for(var name in another_stooge){ if(typeof(another_stooge[name])!=='function'){ document.writeln(name+":"+another_stooge[name]); } } 

for in无法保证属性名出现的顺 序,因此要对任何可能出现的顺序有所准备。如果你想要确保属性以特定的顺序出现,最好的办法就是完全避免使用for in语句,而是创建一个数组,在其中以正确的顺序包含属性名。再通过使用for而不是for in,可以得到我们想要的属性,而不用担心可能发掘出原型链中的属性,并且我们按正确的顺序取得了它们的 值。

 var i; var properties = [ 'first-name', 'middle-name', 'last-name', 'profession' ]; for (i = 0; i < properties.length; i += 1) { document.writeln(properties[i] + ': ' + another_stooge[properties[i]]); } } 

3.8 删除

delete运算符可以用来删除对象 的属性。它将会移除对象中确定包含的属性。它不会触及原型链中的任何对象。

删除对象的属性可能会让来自原型 链中的属性浮现出来:

 another_stooge.nickname                             //'Moe' //删除another_stooge的nickname属性,从而暴露出原型的nickname属性 delete another_stooge.nickname; another_stooge.nickname                       //'Curly' 

3.9 减少全局变量污染

JavaScript可以很随意地定 义那些可保存所有应用资源的全局变量。不幸的是,全局变量削弱了程序的灵活性,所以应该避免。

最小化使用全局变量的一个方法是 在你的应用中创建唯一一个全局变量:

 var MYAPP={}; 

该变量此时变成了你的应用的容 器:

 MYAPP.stooge={ "first-name":"Joe", "last-name":"Howard" }; MYAPP.flight={ airline:"Oceanic", number:815, departure:{ IATA:"SYD", time:"2004-09-22 14:55", city:"Sydney" }, arrival:{ IATA:"LAX", time:"2004-09-23 10:42", city:"Los Angeles" } }; 

只要把多个全局变量都整理在一个 名称空间下,你将显著降低与其他应用程序、组件或类库之间产生糟糕的相互影响的可能性。你的程序也会变得更容易阅读,因为很明显MYAPP.stooge指向的是顶层结构。在下一章中,我们将会看到使用闭包来进行信息隐藏的方式,它是另一个有效减少全 局污染的方法。

JavaScript 中最好的特性就是它对函数的实现。它几乎无所不能。但是,想必你也能预料到,函数在 JavaScript 里也并非万能药。函数包含一组语句,它们是 JavaScript 的基础模块单元,用于代码复用、信息隐藏和组合调用。函数用于指定对象的行为。一般来说,所谓编程就 是将一组需求分解成一组函数与数据结构的技能。

在JavaScript中函数就是对象。对象是”名:值”对的集合并拥有一个连到原型对象的隐藏连接。对象定义产生的对象连接到 Object.prototype。函数对象连接到 Function.prototype (该原型对象本身连接到 Object.prototype)。每个函数在创建时附有两个附加的隐藏属性:函数的上下文和实现函数行为的代码(JavaScript 创 建一个函数对象时,会给该对象设置一个”调用”属性。当 JavaScript 调用一个函数时,可理解为执行了此函数的”调用”属性。具体参阅 ECMAScript 规范的 13.2 Creating Function Object)。

每个函数对象在创建时也随带有一个prototype属性。它的值是一个拥有constructor属 性且值即为该函数的对象。这和隐藏连接到Function.prototype完全不同。这个令人费解的构造过程的意义将会在下个章节中揭示。

因为函数是对象,所以它们可以像任何其他的值一样被使用。函数可以存 放在变量,对象和数组中,函数可以被当作参数传递给其他函数,函数也可以再返回函数。而且,因为函数是对象,所以函数可以拥有方法。

函数对象可以通过函数定义来创建:

//创建一个名为add的变量,并用来把两 个数字相加的函数赋值给它。

var add=function(a,b){

return a+b;

};

函数定义包括四个部分。第一个部分是关键字function

第二部分是函数名,它可以被省略。函数可以用它的名字来递归地调用自 己。此名字也能被调试器和开发工具来识别函数。如果没有给函数命名,比如上面这个例子,它会被认为是匿名函数。

函数的第三部分是包围在圆括号中的一组参数。其中每个参数用逗号分 隔。这些名称将被定义为函数中的变量。它们不像普通的变量那样将被初始化为undefined,而是 在该函数被调用时初始化为实际提供的参数的值。

第四部分是包围在花括号中的一组语句。这些语句是函数的主体。它们在 函数被调用时执行。

函数定义可以出现在任何允许表达式出现的地方。函数也可以被定义在其 他函数中。一个内部函数自然可以访问自己的参数和变量,同时它也能方便地访问它被嵌套在其中的那个函数的参数与变量。通过函数定义创建的函数对象包含一个 连到外部上下文的连接。这被称为闭包。它是JavaScript强大表现力的根基。

调用一个函数将暂停当前函数的执行,传递控制权和参数给新函数。除了 声明时定义的形式参数,每个函数接收两个附加的参数:this和arguments。参数this在面向对象编程中非常重要,它的值取决于调用的模式。在JavaScript中一共有四种调用模式:方法调用模式、函数调用模式、构造器调用模式和apply调用模式。这些模式在如何初始化关键参数this上存在差异。

调用运算符是一对圆括号。圆括号内可包含零个或多个用逗号隔开的表达 式。每个表达式产生一个参数值。每个参数值被赋予函数声明时定义的形式参数名。当实际参数个数与形式参数的个数不匹配时不会导致运行时错误。如果实际参数 过多,超出的参数值将被忽略。如果实际参数值过少,缺失的值将会被替换为undefined。对参 数值不会进行类型检查:任何类型的值都可以被传递给参数。

方法调用模式

当一个函数被保存为对象的一个属性时,我们称它为一个方法。当一个方法被调用时,this被绑定到该对象。如果一个调用表达式包含一个属性存取表达式(即一个点(.)表达式或下标([])表达式),那么它被 当作一个方法来调用。

 //创建myObject。它有一个value属性和一个increment方法。 //increment方法接受一个可选的参数。如果参数不是数字,那么默认使用数字1. var myObject={ value:0; increment:function(inc){ this.value+=typeof inc==='number'?inc:1; } }; myObject.increment(); document.writeln(myObject.value);                     //1 myObject.increment(2); document.writeln(myObject.value);                     //3 

方法可以使用this去访问对象,所 以它能从对象中取值或修改该对象。this到对象的绑定发生在调用的时候。这个”超级”迟绑定使得函数可以对this高度复用。通过this可取得它们所属对 象的上下文的方法称为公共方法

函数调用模式

当一个函数并非一个对象的属性时,那么它被当作一个函数来调用:

 var sum=add(3,4);                    //sum的值为7 

当函数以此模式调用时,this被绑定到全局对象。这是语言设计上的一个错误。倘若语言设计正确,当内部函数被调用时,this应该仍然绑定到外部函数的this变量。这个设计错 误的后果是方法不能利用内部函数来帮助它工作,因为内部函数的this被绑定了错误的 值,所以不能共享该方法对对象的访问权。幸运的是,有一个很容易的解决方案:如果该方法定义一个变量并给它赋值为this,那么内部函数就可以通过那个变量访问到this。按照约定,我 给那么变量命名为that

 //给myObject增加一个double方法 myObject.double=function(){ var that=this;                            //解决方法 var helper=function(){ that.value=add(that.value,that.value); }; helper();                                   //以函数的形式调用helper }; //以方法的形式调用double myObject.double(); document.writeln(myObject.getValue());                   //6 

构造器调用模式

JavaScript是一门基于原型继承的语言。这意味着对象可以直接从其他对象继承属性。该语言是无类别的。

这偏离了当今编程语言的主流。当今大多数语言都是基于类的语言。尽管 原型继承有着强大的表现力,但它并不被广泛理解。JavaScript本身对其原型的本质也缺乏信心,所以它提供了一套和基于类的语言类似的对象构建语法。有类型化语言 编程经验的程序员们很少有愿意接受原型继承的,并且认为借鉴类型化语言的语法模糊了这门语言真实的原型本质。真是两边都不讨好。

如果在一个函数前面带上new来调用,那么将创建一个隐藏连接到该函数的prototype成员 的新对象,同时this将会被绑定到那个新对象上。

new前缀也会改变return语句的行为。 我们将会在后面看到更多相关的内容。

 //创建一个名为Quo的构造器函数。它构造一个带有status属性的对象。 var Quo=function(string){ this.status=string; }; //给Quo的所有实例提供一个名 为get_status的公共方法。 Quo.prototype.get_status=function(){ return this.status; }; //构造一个Quo实例 var myQuo=new Quo("confused"); document.writeln(myQuo.get_status());                            //confused 

按照约定,需要结合new前缀调用的函数被称为构造器函数,它们保存在以首字母大写的变量里。如果调用构造器函数时没有在前面 加上new,可能会发生非常糟糕的事情,既没有编译时警告,也没有运行时警告。

个人不推荐使用这种形式的构造器函数。下一章会看到更好的替代方式。

apply调用模式

因为JavaScript是 一门函数式的面向对象编程语言,所以函数可以拥有方法。

apply方法让我们构建一个参数数组并用其去调用函数。它也允许我们选择this的值,apply方法接收两个参 数,第一个是将被绑定给this的值,第二个就是一个参数数组。

 var array=[3,4]; var sum=add.apply(null,array);                          //sum的值为7 //构造一个包含status成员的对象。 var statusObject={ status:'A-OK' }; //statusObject并没有继承自Quo.prototype, 但我们可以在statusObject上调用get_status方 法,尽管statusObject并没有一个名为get_status的方 法。 var status=Quo.prototype.get_status.apply(statusObject); //status值为'A-OK' 

当函数被调用时,会得到一个”免费”奉送的参数,那就是arguments数组。通过它函数可以访问所有它被调用时传递给它的参数列表,包括那些没有被分配给函数声明时定义的 形式参数的多余参数。这使得编写一个无须指定参数个数的函数成为可能:

 //构造一个将很多个值相加的函数 //注意该函数内部定义的变量sum不会与函数外部定义的sum产生冲突。 //该函数只会看到内部的那个变量。 var sum=function(){ var i,sum=0; for(i=0;i<arguments.length;i++){ sum+=arguments[i]; } return sum; }; document.writeln(sum(4,8,15,16,23,42));                     //108 

这不是一个特别有用的模式。在第6章中,我们将会看到如何给数组添加一个相似的方法来达到同样的效果。

因为语言的一个设计错误,arguments并不是一个真正的数组。它只是一个”类似数组”的对象。arguments拥有一个length属性,但它缺 少所有的数组方法。我们将在本章结尾看到这个设计错误导致的后果。

当一个函数被调用时,它从第一个语句开始执行,并在遇到关闭函数体的}时结束。函数把控制权交还给调用该函数的程序部分。

return语句可用来使函数提前返回。当return被执行时,函 数立即返回而不再执行余下的语句。

一个函数总是会返回一个值。如果没有指定返回值,则返回undefined。

如果函数以new前缀的形式来调 用,且返回值不是一个对象,则返回this(该新对象)。

JavaScript提供了一套异常处理机制。异常是干扰程序正常流程的非正常(但并非完全是出乎意料)的事故。当查出 这样的事故时,你的程序应该抛出一个异常:

 var add=function(a,b){ if(typeof(a)!=='number' || typeof(b)!=='number'){ throw{ name:'TypeError', message:'加法需要数字' }; } return a+b; } 

throw语句中断函数的执行。它应该抛出一个exception对 象,该对象包含可识别异常类型的name属性和一个描述性的message属性。你也 可以添加其他的属性。

该exception对象 将被传递到一个try语句的catch从句:

//构造一个try_if函数,用不正 确的方式调用之前的add函数

var try_it=function(){

try{

add(“seven”);

}catch(e){

document.writeln(e.name+”:”+e.message);

}

}

Try_It();

如果在try代码块内抛出了一 个异常,控制权就会跳转到它的catch从句。

一个try语句只会有一个将 捕获所有异常的catch代码块。如果你的处理手段取决于异常的类型,那么异常处理器必须检查异常对象的name属性以确定异常的类型。

JavaScript允许给语言的基本类型增加方法。在第3章中,我们已经看到, 通过给Object.prototype添加方法来使得该方法对所有对象可用。这样的方式对函数、数组、字符串、数字、正则表达式和布尔值 同样适用。

举例来说,我们可以通过给Function.prototype增加方法来使得该方法对所有函数可用:

Function.prototype.method=function(name,func){

this.prototype[name]=func;

return this;

}

通过给Function.prototype增加一个emthod方法,我们就 不必键入prototype这个属性名。这个缺点也就被掩盖了。

JavaScript并没有单独的整数类型,因此有时候只提取数字中的整数部分是必要的。JavaScript本身提供的取整方法有些丑陋。我们可以通过给Number.prototype添加一个integer方法来改善它。它会根据数字的正负来判断是使用Math.ceiling还是Math.floor。

Number.method(‘integer’,function(){

return Math[this<0?'ceiling':'floor'](this);

});

document.writeln((-10/3).integer()); //-3

JavaScript缺少一个移除字符串末端空白的方法。那是一个很容易修复的疏忽:

String.method(‘trim’,function(){

return this.replace(/^\S+|\s$/g,”);

});

通过给基本类型增加方法,我们可以大大提高语言的表现力。因为JavaScript原型继承的动态本质,新的方法立刻被赋予到所有的值(对象实例)上,哪怕值(对象实例)是在方法被创 建之前就创建好了。

基本类型的原型是公共的结构,所以在类库混用时务必小心。一个保险的 做法就是只在确定没有该方法时才添加它。

//有条件地增加一个方法

Function.prototype.method=function(name,func){

if(!this.prototype[name]){

this.prototype[name]=func;

}

};

递归是一种强大的编程技术,它将一个问题分解为一组相似的子问题,每 一个都用一个寻常解去解决。

“汉诺塔”是一个著名的难题。塔的设备包括三根柱子和一套直径各不相 同的空心圆盘。开始时源柱子上的所有圆盘都按照较小的圆盘放在较大的圆盘之上的顺序堆叠。目标是通过每次移动一个圆盘到另一根柱子上,最终将一堆圆盘移动 到目标柱子上,过程中不可以将大的圆盘放置在较小的圆盘之上。这个难题有一个寻常解:

var hanoi=function(disc,src,sux,dst){

if(disc>0){

hanoi(disc-1,src,dst,aux);

document.writenln(“移 动盘子”+disc+”:从”+src+”到”+dst);

hanoi(disc-1,aux,src,dst);

}

}

hanoi(3,’源’,'中间’,'目的’);

圆盘数量为3时它返回这样的解法:

移动盘子1:从源到目的

移动盘子2:从源到中间

移动盘子1:从目的到中间

移动盘子3:从源到目的

移动盘子1:从中间到源

移动盘子2:从中间到目的

移动盘子1:从源到目的

hanoi函数把一堆圆盘从一根柱子移到另一根柱子,必要时使用辅助的柱子。它把该问题分解成三个子问题。首 先,它移动一对圆盘中较小的圆盘到辅助柱子上,从而露出底下较大的圆盘。然后它就移动底下的圆盘到目标柱子上。最后,它将刚才较小的圆盘从辅助柱子上再移 动到目标柱子上。通过递归地调用自身去处理一对圆盘的移动,从而解决那些子问题。

传递给hanoi函数的参数包 括当前移动的圆盘编号和它将要用到的三根柱子。当它调用自身的时候,它去处理当前正在处理的圆盘之上的圆盘。最终,它会以一个不存在的圆盘编号去调用。在 那样的情况下,它不执行任何操作。由于该函数对非法值不予理会,我们也就不必担心它会导致死循环。

递归函数可以非常高效地操作树形结构,比如浏览器端的文档对象模型(DOM)。每次递归调用时处理给定树的一小段。

var walk_the_DOM=function walk(node,func){

func(node);

node=node.firstChild;

while(node){

walk(node,func);

node=node.nextSibling;

}

};

//定义getElementsByAttribute函数。它取得一个属性名称字符串

//和一个可选的匹配值。

//它调用walk_the_DOM,传递一个用来查找节点属性点的函数

//匹配的节点会累积到一个结果数组中

var getElementsByAttribute=function(att,value){

var results=[];

walk_the_DOM(document.body,function(node){

var actual=node.nodeType===1&&node.getAttribute(att);

if(typeof actual===’string’ && (actual===value||typeof value!==’string’)){

results.push(node);

}

});

return results;

};

一些语言提供了尾递归优化。即如果函数返 回自身递归调用的结果,那么调用的过程会被替换为一个循环,它可以显著提高速度。遗憾的是,JavaScript当前并没有提供尾递归优化。深度递归的函数可能会因为返回堆栈溢出而运行失败。

在编程语言中,作用域控制着变量与参数的可见性及生命周期。对程序员来说这是一个重要的帮助,因为它减少了名称冲突,并且提供了自动内存管理。

大多数使用C语言语法的语言都拥有块级作用域。在一个代码块中(括在一对花括号中的语句集)定义的所有变量在代码块的外部是不可见的。定义在代 码块中的变量在代码块执行结束后会被释放掉。这是件好事。

糟糕的是,尽管代码块的语法似乎表现出它支持块级作用域,但实际上JavaScript并不支持。这个混淆之处可能成为错误之源。

JavaScript确实有函数作用域。定义在函数中的参数和变量在函数外部是不可见的。但在一个函数中的任何位置定义的变量在该函数中的任何 地方都可见(默然说话:我的天呀,真是一个灾难。。。。)。

很多现代语言都推荐尽可能迟地声明变量。而用在JavaScript上却会成为糟糕的建议,因为它缺少块级作用域。所以,最好的做法是在函数体 的顶部声明函数中可能用到的所有变量。

只有函数作用域的好处是内部函数可以访问定义它们的外部函数的参数和变量(除了this和arguments)。这是一件非常好的事情。

我们的getElementsByAttribute函数可以工作是因为它声明了一个results变量,且传递给walk_the_DOM的 内部函数也可以访问results变量。

一个更有趣的情形是内部函数拥有比它的外部函数更长的生命周期。

之前,我们构造了一个myObject对象,它拥有一个value属性和一个increment方法。假定我们希望保护该值不会被非法更改。

与前面直接定义一个对象不同,我们通过调用一个函数的形式去初始化myObject,该函数将返回一个对象。此函数定义了一个value变量。 该变量对increment和getValue方法总是可见的,但函数的作用域使得它对其他的程序来说是不可见的。

var myObject=function(){

var value=0;

return {

increment:function(inc){

value+=typeof inc===’number’?inc:1;

},

getValue:function(){

return value;

}

}

}();

我们并没有把一个函数赋值给myObject,我们是把调用该函数后返回的结果赋值给它(默 然说话:注意最后一行的())。该函数返回一个包含两个方法的对象,并且这些方法继续享有访问value变量 的特权。

本章之前的Quo构造器产生出带有status属性和get_status方法的一个对象。但那看起来并不是十分有趣。为什么要用一个 getter方法去访问本可以直接访问到的属性呢?如果status是私有属性时,它才是更有意义的。所以,让我们定义另一种形式的quo函数来做此事:

//创建一个名为quo的构造函数。

//它构造出带有get_status方法和status私有属性的一个对象。

var quo=function(status){

return {

get_status:function(){

return status;

},

set_status:function(st){

status=st;

}

};

};

//构造一个quo实例

var myQuo=quo(“amazed”);

document.writeln(myQuo.get_status());

这个quo函数被设计成无须在前面加上new来使用,所以名字也没有首字母大写(默 然说话:当然,你也可以加new, 效果是一样的)。当我们调用quo时,它返回包含get_status方法的一个新对象。该对象的一个引用保 存在myQuo中。即使quo函数已经运行结束,但get_status方法仍然享有访问status的特权。get_status方法并不是访问该参数 的一个拷贝,它访问的就是该参数本身。因为该函数可以访问它被创建时所处的上下文环境。这就被称为闭包。

//定义一个函数,它设置一个DOM节点为黄色,然后把它渐变为白色

var fade=function(node){

var level=1;

var step=function(){

var hex=level.toString(16);

node.style.backgroundColor=’#FFFF’ +hex+hex;

if(level<15){

level+=1;

setTimeout(step,100);

}

};

step();

};

<body onload=”fade(document.body)”></body>

我们调用fade,把document.body作为参数传递给它(HTML<body>标签所创建的节点).fade函数设置 level为1。它定义了一个step函数;接着调用step函数,fade函数结束。

step函数把fade函数的level变量转化为16进制字符。接着,它修改fade函数得到的节点的背景色。然后查看fade函数的 level变量。如果背景还没变成白色,那就增大level变量再使用setTimeout让自己再次运行。

step很快被再次调用,这时fade函数早已运行结束,但只要fade的内部函数需要,它的变量就会保留(默 然说话:耶!伟大的闭包!!!)。

理解内部函数能访问外部函数的实际变量本身而不是一个副本非常重要,看下面的例子。

//糟糕的例子

//构造一个函数,用错误的方式给一个数组中的节点设置事件处理程序。

//当点击一个节点时,按照预想应该弹出一个对话框显示节点的序号

//但其实所有的事件总是会显示节点的数目。

var add_the_handlers=function(nodes){

var i;

for(i=0;i<nodes.length;i++){

nodes[i].onclick=function(e){

alert(i);//因为这里是直接引用了变量 i,而不是副本,所以当点击节点时,总是显示循环之后i的值

}

}

}

<body onload=”add_the_handlers(document.getElementsByTagName(‘div’))”>

<div style=”width:300px;height:300px;border:1px solid black;”></div>

<div style=”width:300px;height:300px;border:1px solid black;”></div>

<div style=”width:300px;height:300px;border:1px solid black;”></div>

<div style=”width:300px;height:300px;border:1px solid black;”></div>

</body>

add_the_handlers函数目的是给每个事件处理函数一个唯一值(默然说话:即每一次循环时i的 值,它需要很多个i的副本,每个i值都不一样),但它 直接引用了i,所以每个事件处理函数都得到了循环后i最终的值。

//好例子

//构造一个函数,用正确的方式给一个数组中的节点设置事件处理程序。

//你点击一个节点,将会弹出不同的序号

var add_the_handlers=function(nodes){

var i;

for(i=0;i<nodes.length;i++){

nodes[i].onclick=function(e){

return function(){

alert(e);

};

}(i);

}

};

<body onload=” add_the_handlers(document.getElementsByTagName(‘div’))”>

<div style=”width:300px;height:300px;border:1px solid black;”></div>

<div style=”width:300px;height:300px;border:1px solid black;”></div>

<div style=”width:300px;height:300px;border:1px solid black;”></div>

<div style=”width:300px;height:300px;border:1px solid black;”></div>

</body>

</html>

现在,我们定义了一个函数并立即传递i进去执行,而不是把一个函数赋值给onclick。那个函数将返回一个事件处理函数。这个事件处理函数打 印的是e而不是i,这样就可以避免以上情况(默然说话:中文版的源代码有误,中文翻译也有误,害我花了半个小时的时间才理解这段文 字的本意,为了让读者容易理解,我对函数进行了修改)

函数可以让不连续事件的处理变得更容易。例如:假定有这么一个序列,由用户交互开始,向服务器发送请求,最终显示服务器的响应。最纯朴的定法可 能会是这样的:

request=prepare_the_request();

response=send_request_synchronously(request);

display(response);

这种方式的问题在于网络上的同步请将会导致客户端进入假死状态。如果网络传输或服务器很慢,响应性的降低将是不可接受的。

更好的方式是发起异步的请求,提供一个当服务器的响应到达时将被调用的回调函数。这样客户端不会被阻塞。

request=prepare_the_request();

send_request_asynchronously(request,function(response){

display(response);

})

(默然说话:不要试图运行这两段代码,因为这两段代码仅仅是用来说明的,属于伪代码)

模块是一个提供接口却隐藏状态与实现的函数或对象,我们可以使用函数和闭包来构造模块。通过使用函数去产生模块,我们几乎可以完全摒弃全局变量 的使用,从而缓解这个JavaScript的最为糟糕的特性之一所带来的影响。

举例来说,假定我们想要给String增加一个deentityify方法。它的任务是寻找字符串中的HTML字符实体并替换为它们对应的字 符。在一个对象中保存字符实体的名字和它们对应的字符是有意义的。但我们该在哪里保存该对象呢?我们可以把它放到一个全局变量中,但全局变量是魔鬼。我们 可以把它定义在该函数中,但是那有运行时的损耗,因为该函数在每次被执行的时候该定义都会被初始化一次。理想的方式是将其放入一个闭包,

String.method(‘deentityify’,function(){

//字符映射表,它映射字符的名字到对应的字符

var entity={

quot:’”‘,

lt:’<’,

gt:’>’

};

//返回deentityify方法

return function(){

//这才是deentityify方法。它调用字符串的replace方法,

//查找’&’开头和’;'结束的子字符串。如果这些字符可以在字符 映射表中找到,

//那么就将该字符替换为映射表中的值,它用到了一个正则表达式(参见第七 章)

return this.replace(/&([^&;]+);/g,

function(a,b){

var r=entity[b];

return typeof r===’string’?r:a;

}

);

};

}());

请注意最后一行,我们用()运算法立刻调用我们刚刚构造出来的函数。这个调用所创建并返回的函数才是deentityify方法。

document.writeln(“&lt; &quot;&gt; “.deentityify()); //输出<”>

模块模式利用了函数作用域和闭包来创建绑定对象与私有成员的关联,在这个例子中,只有deentityify方法有权访问字符映射表这个数据对 象。

模块模式的一般形式是:一个定义了私有变量和函数的函数,利用闭包创建可以访问私有变量和函数的特权函数;最后返回这个特权函数,或者把它们保 存到一个可访问到的地方。

使用模块模式就可以摒弃全局变量的使用。它促进了信息隐藏和其他优秀的设计实践。对于应用程序的封装,或者构造其他单例对象(译注:JavaScript的单例就是用定义对象的 方法创建对象。它通常作为工具为程序其他部分提供功能支持。),模块模式非常有效。

模块模式也可以用来产生安全的对象。假定我们想要构造一个用来产生序列号的对象:

var serialMaker=function(){

//返回一个用来产生唯一字符串的对象。

//唯一字符串由两部分组成:前缀+序列号

//该对象包含一个设置前缀的方法,一个设置序列号的方法

//和一个产生唯一字符串的gensym方法

var prefix=”;

var seq=0;

return {

setPrefix:function(p){

prefix=String(p);

},

setSeq:function(s){

seq=s;

},

gensym:function(){

var result=prefix+seq;

seq+=1;

return result;

}

};

};

var seqer=serialMaker();

seqer.setPrefix(‘Q’);

seqer.setSeq(1000);

var unique=seqer.gensym();//unique的值是”Q1000″

alert(unique);

seqer包含的方法都没有用this或that。因此没有办法损害seqer。除非调用对应的方法,否则没法改变prefix或seq的值。 seqer对象是可变的,所以它的方法可能会被替换,但替换后的方法依然不能访问私有成员。seqer就是一组函数的集合,而且那些函数被授予特权,拥有 使用或修改私有状态的能力。

有一些方法没有返回值。如果我们让这些方法返回this而不是undefined,就可以启动级联。在一个级联中,我们可以在单独一条的语句中 依次调用同一个对象的很多方法。一个启用级联的Ajax类库可能允许我们以这样的形式去编码:

//默然说话:这段代码仅为了说明级联的概念,无法运行,其实级联就是Java中的连续打点调用方法的形式

getElement(‘myBoxDiv’).

move(350,150).

width(100).

height(100).

color(‘red’).

border(’10px outset’).

padding(’4px’).

appendText(‘Please stand by’).

on(‘mousedown’,function(m){

this.startDrag(m,this.getNinth(m));

}).

on(‘mousemove’,'drag’).

on(‘mouseup’,'stopDrag’).

later(2000,function(){

this.color(‘yellow’).

setHTML(“What hath God wraught?”).

slide(400,40,200,200);

}).

tip(‘This box is resizeable’);

在这个例子中,getElement函数产生一个对应于id=”myBoxDiv”的DOM元素并提供了其他功能的对象。该方法允许我们移动元 素,修改它的尺寸和样式,并添加行为。这些方法每一个都返回该对象,所以调用返回的结果可以被下一次调用所用。

级联可以产生出具备很强表现力的接口。它也能帮助控制那种构造试图一次做很多事情的接口的趋势(默 然说话:说实话,我非常不喜欢这样的编码,因为这样编码易读性太差。级联基本上适用于那些一次编码之后再也不修改的代码,或者适用于那些你不想让包括你自 己在内的任何人都看不懂的代码)。

函数也是值,从而我们可以用有趣的方式去操作函数值。套用允许我们将函数与传递给它的参数相结合去产生出一个新的函数。

var add1=add.curry(1);

document.writeln(add1(6)); //书上写结 果是7,可我实际调试的结果是undefined

add1是把1传递给add函数的curry方法后创建的一个函数。add1函数把1添加到它的参数中。JavaScript并没有curry 方法,但我们可能通过给Function.prototype添加功能来实现:

Function.method(‘curry’, function ( ) {

var slice = Array.prototype.slice,

args = slice.apply(arguments),

that = this;

return function ( ) {

return that.apply(null, args.concat(slice.apply(arguments)));

};

});

curry方法通过创建一个闭包,它包括了原始函数和被套用的参数。curry方法返回另一个函数,该函数被调用时,会返回一个结果,这个结果 包括了curry方法传入的参数和自己的参数。它使用了Array的concet方法把它们连接在了一起。

由于arguments数组并非一个真正的数组,所以它并没有concat方法。要避开这个问题,我们必须在两个arguments数组上都应 用数组的slice方法。这样产生出拥有concat方法的常规数组。

函数可以用对象去记住先前操作的结果,从而能避免无谓的运算。这种优化被称为默记法(memoization:用于加快程序运算速度的一种优化技术,原书中文版翻译为记忆,我在这里翻译为默记法)。 JavaScript的对象和数组要实现这种优化是非常方便的。

比如说,我们想要一个递归函数来计算Fibonacci。一个Fibonacci数字是之前两个Fibonacci数字之和。最前面的两个数字 是0和1.

var fibonacci=function(n){

return n<2?n:fibonacci(n-1)+fibonacci(n-2);

}

for(var i=0;i<=10;i++){

document.writeln(‘//’+i+’:'+fibonacci(i)+’<br />’);

}

运行结果:

//0:0

//1:1

//2:1

//3:2

//4:3

//5:5

//6:8

//7:13

//8:21

//9:34

//10:55

程序可以工作,但fibonacci函数被调用了453次。我们调用了11次,而它自身调用了442次。如果我们让该函数应用默记法,就可以显 著地减少它的运算量。

我们在一个名为memo的数组里保存我们的存储结果,存储结果可以隐藏在闭包中。当我们的函数被调用时,这个函数首先看是否已经知道存储结果, 如果已经知道,就立即返回这个存储结果。

var fibonacci=function(){

var memo=[0,1];

var fib=function(n){

var result=memo[n];

if(typeof result!==’number’){

result=fib(n-1)+fib(n-2);

memo[n]=result;

}

return result;

};

return fib;

}();

这个函数返回同样的结果,但它只被调用了29次。我们调用了它11次。它自身调用了18次。

我们可以把这种形式一般化,编写一个函数来帮助我们构造带默记法功能的函数。memoizer函数将取得一个初始的memo数组和 fundamental函数。它返回一个管理memo存储和在需要时调用fundamental函数的shell函数。我们传递这个shell函数和该函 数的参数给fundamental函数:

var memoizer=function(memo,fundamental){

var shell=function(n){

var result=memo[n];

if(typeof result!==’number’){

result=fundamental(shell,n);

memo[n]=result;

}

return result;

};

return shell;

};

现在,我们可以使用memoizer来定义fibonacci函数,提供其初始的memo数组和fundamental函数:

var fibonacci=memoizer([0,1],function(shell,n){

return shell(n-1)+shell(n-2);

});

通过设计能产生出其他函数的函数,可以极大减少我们必须要做的工作。例如:要产生一个默记法的阶乘函数,我们只须提供基本的阶乘公式即可:

var factorial=memoizer([1,1],function(shell,n){

return n*shell(n-1);

});

在那些基于类的语言(比如Java)中,继承(inheritance或extends)提供了两个有用的服务。首先,它是代码重用的一种形式。如果一个新的类与一个已存在的类大部分相 似,那么你只须具体说明其不同点即可。类继承的另一个好处是它包括了一套类型系统的规范。由于程序员无须编写显式类型转换的代码,他们的工作量将大大减 轻,这是一件很好的事情,因为类型转换时会丢失类型系统在安全上的好处。

JavaScript是一门弱类型语 言,从不需要类型转换。对象的起源是无关紧要的。对于一个对象来说重要的是它能做什么,而不是它从哪里来。

JavaScript提供了一套更为 丰富的代码重用模式。它可以模拟那些基于类的模式,同时它也可以支持其他更具表现力的模式。在JavaScript中可能的继承模式有很多。在本章中,我们将研究几种最为直接的模式。当然还有更多更为复杂的结构,但 保持它的简单通常是最好的。

在基于类的语言中,对象 是类 实例,并且类可以从另一个类继承。JavaScript是一门基于原型 的语言,这意味着对象直接从其他对象继承。

当一个函数对象被创建时,Function构造器产生的函数对象会运行类似这样的一些代码:

this.prototype={constructor:this};

新函数对象被赋予一个prototype属性,其值是包含一个constructor属性且属性值为该新函数对象。该prototype对象是存放继承特征的地方。因为JavaScript语言没有提供一种方法去确定哪个函数是打算用来作构造器的,所以每个函数都会得到一个prototype对象。constructor属性没什么用。重要的是prototype对象。

当采用构造器调用模式,即使用new前缀去调用一个函数时,这将修改函数执行的方式。如果new运算符是一个方法而不是一个运算符,它可能会像这样执行:

Function.method(‘new’,function(){

//创建一个新对象,它继承自构造器函数的原型对象。

var that=Object.beget(this.prototype);

//调用构造器函数,绑定this到新对象上。

var other = this.apply(that,arguments);

//如果它的返回值不是一个对象,就返回该新对象

return (typeof other === ‘object’ && other)||that;

});

我们可以定义一个构造器并扩充它 的原型:

var Mammal=function(name){

this.name=name;

};

Mammal.prototype.get_name=function(){

return this.name;

};

Mammal.prototype.says=function(){

return this.saying || ”;

};

现在,我们可以构造一个实例:

var myMammal=new Mammal(‘Herb the Mammal’);

var name=myMammal.get_name();//Herb the Mammal

我们可以构造另一个伪类来继承Mammal,这是通过定义它的constructor函数并替换它的prototype为一个Mammal的实例来实现的:

var Cat=function(name){

this.name=name;

this.saying=’meow’;

};

//替换Cat.prototype为一个新的Mammal实例

Cat.prototype=new Mammal();

//扩充新原型对象,增加purr和get_name方法。

//扩充新原型对象,增加purr和get_name方法。

Cat.prototype.purr=function(n){

var i,s=”;

for(i=0;i<n;i+=1){

if(s){

s+=’-';

}

s+=’r';

}

return s;

};

Cat.prototype.get_name=function(){

return this.says()+’ ’+this.name+’ ’+this.says();

};

var myCat=new Cat(‘Henrietta’);

var says=myCat.says();//’meow’

var purr=myCat.purr(5);//’r-r-r-r-r’

var name=myCat.get_name();

alert(says);

alert(purr);

alert(name);

//’meow Henrietta meow’

伪类模式本意是想向面向对象靠 拢,但它看起来格格不入。我们可以隐藏一些丑陋的细节,这是通过使用method方法定义一个inherits方法来实现的:

Function.method(‘inherits’,function(Parent){

this.prototype=new Parent();

return this;

});

我们的inherits和method方法都返回this,这将允许我们可以以级联的样式编程。现在可以只用一行语句构造我们的Cat。

var Cat=function(name){

this.name=name;

this.saying=’meow’;

}.inherits(Mammal).method(‘purr’,function(n){

var i,s=”;

for(i=0;i<n;i+=1){

if(s){

s+=’-';

}

s+=’r';

}

return s;

}).method(‘get_name’,function(){

return this.says()+’ ‘+this.name+’ ‘+this.says();

});

现在看起来没那么怪异了。但我们 是否真的有所改善呢?我们现在有了行为像”类”的构造器函数,但仔细去看,它们可能有着令人惊讶的行为:没有私有环境;所有的属性都是公开的。无法访问super(父类)的方法。

更糟糕的是,使用构造器函数存在 一个严重的危害。如果你在调用构造器函数时忘记了在前面加上new前缀,那么this将不会绑定到一个新对象上。可悲的是,this将被绑定到全局对象上,所以你不但没有扩充新对象,反而将破坏全局变量。既没有编译时警告,也没有运 行时警告。

这是一个严重的语言设计错误。为 了降低这个问题带来的风险,所有的构造器函数都约定命名成首字母大写的形式,并有不以首字母大写的形式拼写任何其他的东西。这样我们至少可以通过目视检查 去发现是否缺少了new前缀。一个更好的方案就是不要使用伪类。

有时候,构造器要接受一大串参 数。这可能是令人烦恼的,因为要记住参数的顺序可能非常困难。在这种情况下,如果我们在编写构造器时使其接受一个简单的对象说明符可能会更加友好。所以, 与其这样写:

var myObject=maker(f,l,m,c,s);

不如这么写:

var myObject=maker({

first:f,

last:l,

state:s,

city:c

})

当与JSON(参与附录E)一起工作时,这还可以有一个间接的好处。JSON文本只能描述数据,但有时数据表示的是一个对象,将该数据与它的方法关联起来是有用的。如果构造器取 得一个对象说明符,可以容易做到,因为我们可以简单地传递该JSON对象给构造器,而它将返回一个构造完全的对象。

在一个纯粹的原型模式中,我们会 摒弃类,转而专注于对象。基于原型的继承相比基于类的继承在概念上更为简单:一个新对象可以继承一个旧对象的属性。也许你对此感到陌生,但它真的很容易理 解。你通过构造一个有用的对象开始,接着可以构造更多和那个对象类似的对象。可以完全避免把一个应用拆解成一系列嵌套抽象类的分类过程。

让我们先定义一个有用的对象:

var myMammal={

name:’Herb the Mammal’,

get_name:function(){

return this.name;

},

says:function(){

return this.saying||’ ‘;

}

};

一旦有了一个想要的对象,我们就 可以利用第3章中介绍过的Object.beget方法构造出更多的实例来。

var myCat=Object.beget(myMammal);

myCat.name=’Henrietta’;

myCat.saying=’meow’;

myCat.purr=function(n){

var i,s=”;

for(i=0;i<n;i+=1){

if(s){

s+=’-';

}

s+=’r';

}

return s;

};

myCat.get_name=function(){

return this.says+’ ‘+this.name+’ ‘+this.says;

};

这是一种”差异化继承”。通过定 制一个新的对象,我们指明了它与所基于的基本对象的区别。

迄今为止,所看到的继承模式的一 个弱点就是我们没法保护隐私。对象的所有属性都是可见的。我们没法得到私有变量和私有函数。有时候那不要紧,但有时候却是大麻烦。遇到这些麻烦的时候,一 些不知情的程序员接受了一种伪装私有的模式。如果想构造一个私有的属性,我们就给它起一个奇怪的名字,并且希望其他使用代码的用户假装看不到这些奇怪的成 员元素。幸运的是,我们有更好的选择,那就是模块模式的应用。

我们从构造一个将产生对象的函数 开始,给它起的名字将以一个小写字母开头,因为它并不需要使用new前缀。该函数包括四个步骤。

1.它创建一个新对象。

2.它选择性地定义私有实例变量和方 法。这些就是函数中通过var语句定义的普通变量。

3.它给这个新对象扩充方法。那些方 法将拥有特权去访问参数,以及在第二步中通过var语句定义的变量。

4.它返回那个新对象。

这里是一个函数化构造器的伪代码 模板:

var constructor=function(spec,my){

var that,其他的私有实例变量;

my=my||{};

把共享的变量和函数添加到my中

that=一个新对象

添加给that的特权方法

return that;

}

spec对象包含构造器需要构造一个 新实例的所有信息。spec的内容可能会被复制到私有变量中,或者被其他函数改变,或者方法可以在需要的时候访问spec的信息。

my对象是一个为继承链中的构造器提 供秘密共享的容器。my对象可以选择性地使用。如果没有传入一个my对象,那么会创建一个my对象。

接下来,声明该对象私有的实例变 量和方法。通过简单的声明变量就可以做到。构造器的变量和内部函数变成了该实例的私有成员。内部函数可以访问spec,my,that,以及其他私有变量。

接下来,给my对象添加共享的秘密成员。这是通过赋值语句来做的:

my.memeber=value;

现在,我们构造了一个新对象并将 其赋值给that。有很多方式可以构造一个新对象。

之后,我们扩充that,加入组成该对象接口的特权方法。我们可以分配一个新函数成为that的成员方法。或者,更安全地,我们可以先将函数定义为私有方法,然后再将它们分配给that:

var methodical=function(){

……

};

that.methodical=methodical;

分两步去定义methodical的好处是,如果其他方法想要调用methodical,它们可以直接调用methodical()而不是that.methodical()。如果该实例被三十或篡改,甚至that.methodical被替换掉了,调用methodical的方法将同样会继续工作,因为它们私有的methodical不受该实例修改的影响。

最后,我们返回that。

现在我们将这个模式应用到mammal例子里。此处不需要my,所以我们先抛开它,但将使用一个spec对象。

name和saying属性现在是完全私有的。它们只有通过get_name和says两个特权方法才可以访问。

var mammal=function(spec){

var that={};

that.get_name=function(){

return spec.name;

};

that.says=function(){

return spec.saying||”;

};

return that;

};

var myMammal=mammal({name:’Herb’});

在伪类模式中,构造器函数Cat不得不重复构造器Mammal已经完成的工作。在函数化模式中那不再需要了,因为构造器Cat将会调用构造器Mammal,让Mammal去做对象创建中的大部分工作,所以Cat只须关注自身的差异即可。

var cat=function(spec){

spec.saying=spec.saying||’meow’;

var that=mammal(spec);

that.purr=function(n){

var I,s=”;

for(i=0;i<n;i+=1){

if(s){

s+=’-';

}

s+=’r';

}

return s;

};

that.get_name=function(){

return that.says()+’ ‘+spec.name+’ ‘+that.says();

};

return that;//默然说话:这里原书源代码又是错误的。

};

var myCat=cat({name:’Henrietta’});

函数化模式还给我们提供了一个处 理父类方法的方法。我们将构造一个superior方法,它取得一个方 法名并返回调用那个方法的函数。该函数将调用原来的方法,尽管属性已经变化了。

Object.method(‘superior’,function(name){

var that=this,

method=that[name];

return function(){

return method.apply(that,arguments);

};

});

让我们在coolcat上试验一下,coolcat就像cat一样,除了它有一个更酷的调用父类方法的get_name方法。它只需要一点点的准备工作。我们将声明一个super_get_name变量,并且把调用superior方法所返回的结果赋值给它。

var coolcat=function(spec){

var that=cat(spec),

super_get_name=that.superior(‘get_name’);

that.get_name=function(n){

return ‘like ‘+super_get_name()+’ baby’;

};

return that;

};

var myCoolCat=coolcat({name:’Bix’});

var name=myCoolCat.get_name();//’like meow Bix meow baby’

函数化模式有很大的灵活性。它不 仅不像伪类模式那样需要很多功夫,还让我们得到更好的封装和信息隐藏,以及访问父类方法的能力。

如果对象的所有状态都是私有的, 那么该对象就成为一个”受限(tamper-proof)”对象。 该对象的属性可以被替换或删除,但该对象的完整性不会受到损害。如果我们用函数化的样式创建一个对象,并且该对象的所有方法都不使用this或that,那么该对象就是持久性的。一个持久性对象就是一个简单功能函数的集合。

一个持久性的对象不会被损害。访 问一个持久性的对象时,除非被方法授权,否则攻击者不能访问对象的内部状态。

5.5 部件

我们可以从一套部件中组合出对象 来。例如,我们可以构造一个能添加简单事件处理特性到任何对象上的函数。它会给对象添加一个on方法,一个fire方法和一个私有的事件注册表对象。

var eventuality=function(that){

var registry={};

that.fire=function (event){

//在一个对象上触发一个事件。该事件可以是一个包含事件名称的字符串,

//或者是一个拥有包含事件名称的type属 性的对象。

//通过’on’方法注册的事件处理程序中匹 配事件名称的函数将被调用。

var array,func,handler,i,

type=typeof event===’string’?event:event.type;

//如果这个事件存在一组事件处理程序,那么就遍历它们并按顺序依次执行

if(registry.hasOwnProperty(type)){

array=registry[type];

for(i=0;i<array.length;i++){

handler=array[i];

//每个处理程序包含一个方法和一组可选的参数。

//如果该方法是一个字符串形式的名字,那么寻找该函数

func=handler.method;

if(typeof func===’string’){

func=this[func];

}

//调用一个处理程序。如果该条目包含参数,那么传递它们过去。否则,传递该事件对象

func.apply(this,handler.parameters||[event]);

}

}

return this;

};

that.on=function(type,method,parameters){

//注册一个事件。构造一条处理程序条目。将它插入到处理程序数组中,

//如果这种类型的事件还不存在,就构造一个

var handler={

method:method,

parameters:parameters

};

if(registry.hasOwnProperty(type)){

registry[type].push(handler);

}else{

registry[type]=[handler];

}

return this;

};

return that;

};

我们可以在任何单独的对象上调用eventuality,授予它事件处理方法。我们也可以赶在that被返回前在一个构造器函数中调用它。用这种方式,一个构造器函数可以从一套部件中组装出对象来。

数组是一段线性分配的内存,它通 过整数去计算偏移并访问其中的元素。数组可以是很快的数据结构。不幸的是,JavaScript的数组不是这样的。它提供了一种拥有类似数组特性的对象。它把数组的下标转变成字符串,用其作为属 性。它明显地比一个真正的数组慢,但它可以更方便地使用。属性的检索和更新的方式与对象一模一样,除了有一个可以用整数作为属性名的特性外。数组有它们自 己的定义格式。数组也有一套非常有用的内置方法,我将在第8章描述它们。

数组定义提供了一种非常方便地创 建新数组的表示法。一个数组定义是在一对方括号中包围零个或多个用逗号分隔的值的表达式。数组定义可以出现在任何表达式可以出现的地方。数组的第一个值将 获得属性名’0′,第二个值将获得属性名’1′,依次类推:

var empty=[];

var numbers=['zero','one','two','three','four','five','six','seven','eight','nine'];

empty[1] //undefined

numbers[1] //’one’

empty.length //0

numbers.length //10

而以下的对象定义将产生一个相似 的结果:

var numbers_object={

’0′:’zero’,’1′:’one’,’2′:’two’,’3′:’three’,

’4′:’four’,’5′:’five’,’6′:’six’,’7′:’seven’,

’8′:’eight’,’9′:’nine’

};

numbers和numbers_object都是包含有10个属性的对象,并且那些属性刚好有相同的名字和值。但是它们也有一些显著的不同。numbers继承自Array.prototype,而numbers_object继承自Object.prototype,所以numbers继承了大量有用的方法。同时,numbers也有一个诡异的length属性,而numbers_object则没有。

在大多数语言中,一个数组的所有 元素都要求是相同的类型。JavaScript允许数组包含任 意混合类型的值:

var misc=['string',98.6,true,false,null,undefined,['nested','array'],{object:true},NaN,Infinity];

misc.length //10

每个数组都有一个length属性。和大多数其他语言不同,JavaScript数组的length是没有上界的。如果你用大于或等于当前length的数字作为下标来保存一个元素,那么length将增大,而不会发生数组边界越界错误。

length属性的值是这个数组的最 大整数属性名加上1.它不一定等于数组里的属性的个数:(默然说话:我的天!那在一些特殊情况下,想要通过一个i的自增来遍历一个数组将是不可靠的了!)

var myArray=[];

myArray.length; //0

myArray[10000]=true;

alert(myArray.length); //10001

//myArray只包含一个属性

[]后缀下标运算符将它的表达式转换 成一个字符串,如果该表达式有toString方法,就使用该方法 的值。这个字符串将被用作属性名。如果这个字符串看起来像一个大于等于这个数组当前的length且小于4 294 967 295的正整数,那么这个数组的length就会被重新设置为新的下标加1。

你可以直接设置length的值。设置更大的length对数组无影响。而把length设小将导致所有下标大于等于新length的属性被删除(默然说话:经验证,对length的设置只影响整数下标,对于字符串下标的元素无影响):

numbers.length=3; //['zero','one','two']

通过把下标指定为一个数组的当前length,可以附加一个新元素到该数组的尾部。

numbers[numbers.length]=’shi’; //['zero','one','two','shi']

有时用push方法可以更方便地完成同样的事情:

numbers.push(‘go’); //['zero','one','two','shi','go']

6.3 删除

由于JavaScript的数组其实就是对象,所以delete运算符可以用来从数组中移除元素:

delete numbers[2]; //['zero','one',undefined,'shi','go']

不幸的是,那样会在数组中遗留一 个空洞。这是因为排在被删除元素之后的元素保留了它们最初的名字(下标)。而你通常想要的是递减后面每个元素的名字(下标)。

幸运的是,JavaScript数组有一个splice方法。它可以对数组做个手术,删除一些元素并将它们替换为其他的元素。第一个参数是数组中的一个序 号。第二个参数是要删除的元素个数。任何额外的参数会在序号那个点的位置被插入到数组中:

numbers.splice(2,1); //['zero','one','shi','go']

6.4 枚举

因为JavaScript的数组其实就是对象,所以for in语句可以用来遍历一个数组的所有属性。但for in无法保证属性的顺序,而大多数的数组应用都期望按照阿拉伯数字顺序来产生元素。此外,可能从原型链中 得到意外属性的问题依旧存在。常规for语句可以避免这些问题。(默然说话:但如果数组的情况比较特殊,那在遍历时就需要小心了)

6.5 混淆的地方

在JavaScript编程中,一个常见的错误是在需要使用数组时使用对象,而在需要使用对象时使用了数组。其实规则很简 单:当属性名是小而连续的整数时,你应该使用数组。否则,使用对象。

JavaScript使用typeof运算符报告数组的类型是’object’,这没有什么意义,也说明JavaScript本身对于数组和对象的区别是混乱的。

JavaScript在区别数组和对 象上没有一个好的机制。可以通过定义我们自己的is_array函数来避开这个缺陷:

var is_array=function(value){

return value && typeof value===’object’ && value.constructor===Array;

}

不幸的是,它在识别从不同的窗口 或帧里构造的数组时会失败。如果想要准确地检测那些外部的数组,我们不得不做更多的工作:

var is_array=function(value){

return value && typeof value===’object’ &&

typeof value.length===’number’ &&

tyeof value.splice===’function’ &&

!(value.propertyIsEnumerable(‘length’));

}

因为有了这样的一个测试,就有可 能写一个函数,当传递的是单一值时只做一件事,而在传递一组值时要做很多的事情。

6.6 方法

JavaScript提供了一套作用 于数组的方法。这些方法是被储存在Array.prototype中的 函数。在第3章中,我们看到Object.rototype是可以被扩充的。同样,Array.prototype也可以被扩充。

举例来说,假设我们想要给array增加一个方法,它将允许我们对数组进行计算:

Array.method(‘reduce’,function(f,value){

var i;

for(i=0;i<tis.length;i+=1){

value=f(this[i],value);

}

return value;

});

通过给Array.prototype扩充一个函数,每个数组都继承了这人方法。在这个例子中,我们定义了一个reduce方法,它接受一个函数和一个初始值作为参数。它遍历这个数组,以当有元素和该初始传来参数调用这人 函数,并且计算出一个新值。当完成时,它返回这个值。如果我们传入一个将两个数字要加的函数,它会计算相加的和。如果我们传入将两个数字相乘的函数,它会 计算其乘积:

Array.method(‘reduce’,function(f,value){

var i;

for(i=0;i<this.length;i+=1){

value=f(this[i],value);

}

return value;

});

//创建一人数字数组

var data=[4,8,15,16,23,42];

//定义两个简单的函数。一个是将两个数字相加,另一个是将两个数字相乘

var add=function(a,b){

return a+b;

}

var mult=function(a,b){

return a*b;

}

//调用data的reduce方法,传入add函数

var sum=data.reduce(add,0)//和是108

//再次调用reduce方法,这次传入mult函数

var product=data.reduce(mult,1);//乘积是7418880

因为数组其实就是对象,所以我们 可以直接给一个单独的数组添加方法:

//给data数组添加一个total方法

data.total=function(){

return this.reduce(add,0);

}

因为字符串’total’不是整数,所以给数组增加一个total不会改变它的长度。

6.7 维度

JavaScript的数组通常不会 初始化。如果你用[]得到一个新数组,它将是空的。如果你访问一个不存在的元素,则将得到undefined。如果你了解这人问题,或者你在尝试获取每个元素之前都很有预见性地设置了它,那么万事大吉。但如 果你实现的算法是假设每个元素都从一个已知的值开始(例如0),那么你必须自己准备好这人数组。JavaScript应该提供一个类似Array.dim这样的方法来做这个事情,但我们可以很容易纠正这个疏忽:

Array.dim=function(dimension,initial){

var a=[],i;

for(i=0;i<dimension;i+=1){

a[i]=initial;

}

return a;

}

//创建一个包含10个0的数组

var myArray=Array.dim(10,0);

JavaScipt没有多维数组,但 就像大多数类C语言一样,它支持元素为数组的数组。

var matrix={

[0,1,2],

[3,4,5],

[6,7,8]

};

matrix[2][1]//7

为了创建一个二维数组或一个元素 为数组的数组,你必须自己去创建那个第二维的数组:

for(i=0;i<n;i+=1){

my_array[i]=[];

}

//注意:Array.dim(n,[])在这里不能工作。

//如果使用它,每个元素都指向同一个数组的引用,那是非常糟糕的。

一个空矩阵的每个单元将拥有一个 初始值undefined。如果你希望它们有不同的初始值,你必须明确地设置它们。同样,JavaScript应该对矩阵提供更好的支持。好在我们也可以修正它:

Array.matrix=function(m,n,initial){

var a,i,j,mat=[];

for(i=0;i<m;i+=1){

a=[];

for(j=0;j<n;j+=1){

a[j]=0;

}

mat[i]=a;

}

return mat;

}

//构造一个用0填充的4*4矩阵

var myMatrix=Array.matrix(4,4,0);

document.writerln(myMatrix[3][3]);//0

//用来构造一个 同值矩阵的方法

Array.identity=function(n){

var i, mat=Array.matrix(n,n,0);

for(i=0;i<n;i+=1){

mat[i][i]=1;

}

return mat;

}

myMatrix=Array.identity(4);

document.writeln(myMatrix[3][3]); //1

JavaScript的许多特性都借 鉴自其他语言。语法借鉴自Java,函数借鉴自Scheme,原型继承借鉴自Self。而JavaScript的正则表达式特性则借鉴自Perl。

正则表达式起源于对形式语言的数 学研究。

在JavaScript中,正则表达式的语法是对Perl版的改进和发展,它非常接近源自贝尔实验室的原始形式。正则表达式的书写规则出奇的复杂,因为它们把 某些位置上的字符串解析为运算符,而把仅在位置上稍微不同的相同字符串又当作字符串本身。比不易书写更糟糕的是,这使得正则表达式不仅难以阅读,而且修改 时充满危险。要想正确地阅读它们,就必须对正则表达式的整个复杂性有相当彻底的理解。为了缓解这个问题,我对其规则进行了些许简化。这里所展示的正则表达 式可能稍微有些不够简洁,但也会让正确地使用它们变得稍微容易一点。

有点让人感到费解的是,JavaScript的正则表达式难以分段阅读,因为它们不支持注释和空白。正则表达式的所有部分都被紧密排列在一起, 使得它们几乎无法被辨认。当它们在安全应用中进行扫描和验证时,这点就须要特别地留意。如果你不能阅读和理解一个正则表达式,你如何能确保它对所有的输入 都能正确地工作呢?然而,尽管有这些明显的缺点,但正则表达式还是被广泛地使用着。

这里有一个例子。它是一个用来匹 配URL的正则表达式。在JavaScript程序中,正则表达式必须写在一行中。正则表达式中的空白是至关重要的:

var parse_url=/^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/;

var url=”http://www.ora.com:80/goodparts?q#fragment”;

让我们来调用parse_url的exec方法。如果能成功地匹配我们传给它的字符串,它将会返回一个数组,该数组包含了从这个url中提取出来的片段:

var result=parse_url.exec(url);

var names=['url','scheme','slash','host','port','path','query','hash'];

var blanks=’ ‘;

var i;

for(i=0;i<names.length;i+=1){

document.writeln(‘<br />’+names[i]+’:'+blanks.substring(names[i].length),result[i]);

}

这段代码产生的结果如下:

url: http://www.ora.com:80/goodparts?q#fragment

scheme:http

slash: //

host: www.ora.com

port: 80

path: goodparts

query: q

hash: fragment

让我们来看看parse_url的每部分的因子是如何工作的:

^

^字符表示这个字符串的开始。它是一 个标记,用来防止exec跳过不像URL(non-URL-like)的前缀:

(?:([A-Za-z]+):)?

这个因子匹配一个协议名,但仅当 它之后跟随一个:(冒号)的时候才匹配。(?:…)表示一个非捕获型分组(noncapturing group)。后缀?表示这个分组是可选的。

它表示重复0或1次。(…)表示一个捕获型分组(capturing group)。一个捕获型分组将复制它所匹配的文本,并将其放入result数组中。每个捕获型分组都将被指定一个编号。第一个捕获型分组的编号是1,所以该分组所匹配的文本拷贝将出现在result[1]中。[…]表示一个字符类。这个字符类A-Za-z包含26个大写字母和26个小写字母。连字符(-)表示范围从A到Z。后缀+表示这个字符类将被匹配1次或多次。这个组后面跟着字符:,它将按字面进行匹配:

(\/{0,3})

下一个因子是捕获型分组2.\/表示一个应该被匹配的/(斜杠)。它用\(反斜杠)来进行转义,这样它就不会被错误地解释为这个正则表达式的结束符。后缀{0,3}表示/将被匹配0次,或者1到3次之间:

([0-9.\-A-Za-z]+)

下一个因子是捕获型分组3.它将匹配一个主机名,由1个或多个数字、字母或.或-组成。-将被转义为\-以防止与表示范围的连字符相混淆:

(?::(\d+))?

下一个可选的因子将匹配端口号, 它是由一个前置:加上1个或多个数字而组成的序列。\d表示一个数字字符。1个或多个数字组成的数字串将被捕获 型分组4捕获:

(?:\/([^?#]*))?

我们有另一个可选的分组。该分组 以一个/开始。之后的字符类[^?#]以一个^开始,它表示这个类包含除?和#之外的所有字符。*表示这个字符类将被匹配0次或多次。

注意我在此是不严谨的。这个类匹 配除?和#之外的所有字符,其中包括了行结束符、控制字符,以及其他大量不应在此被匹配的字符。大多数情况下,它会照我们希望的去做,但某些不好的文本可 能会有漏进来的风险。不严谨的正则表达式是一个常见的安全漏洞发源地。写不严谨的正则表达式比写严谨的正则表达式要容易很多:

(?:\?([^#]*))?

接下来,我们还有一个以一个?开 始的可选分组。它包含捕获型分组6,这个分组包含0个或多个非#字符:

(?:#(.*))?

我们的最后一个可选分组是以#开始的。.将匹配除行结束符以外的所有字符:

$

$表示这个字符串的结束。它让我们确 信在这个URL的尾部没有其他更多内容。

以上便是正则表达式parse_url的所有因子。

parse_url的正则表达式还可 以编写得更复杂,但我不建议这样做。短小并简单的正则表达式是最好的。惟有如此,我们才有信心让它们正确地工作并在需要时能成功地修改它们。

JavaScript的语言处理程序 之间兼容性非常高。这门语言中最没有移植性的部分就是对正则表达式的实现。结构复杂或令人费解的正则表达式很有可能导致移植性问题。在执行某些匹配时,嵌 套的正则表达式也能导致极恶劣的性能问题。因此简单是最好的策略。

让我们来看另一个例子:一个匹配 数字的正则表达式。数字可能由一个整数部分加上一个可选的减号、一个可选的小数部分和一个可选的指数部分组成:

var parse_number=/^-?\d+(?:\.\d*)?(?:e[+\-]?\d+)?$/i;

var test=function(num){

document.writeln(“测试”+num+”:”)

document.writeln(parse_number.test(num));

document.writeln(“<br />”)

};

test(’1′); //true

test(‘number’); //false

test(’98.6′); //true

test(’123.21.86.100′); //false

test(’123.45E-67′); //true

test(’123.45d-67′); //false

parse_number成功地从这 些字符串中检验出哪些符合我们的规范,而哪些不符合,但对那些不符合的字符串,它并没有告诉我们这些数字测试失败的缘由和位置。

让 们来分 解parse_number:

/^ $/i

我们又用^和$来框定这个正则表达式。它将导致文本中的所有字符都要针对这个正则表达式进行匹配。如果我们省略了这些标识,那么只要一个字符串包含一个数字, 这个正则表达式就会告诉我们。但有了这些标识,只有当一个字符串的内容仅为一个数字时,它才会告诉我们。如果我们仅包含^,它将匹配以一个数字开头的字符串。如果我们仅包含$,则匹配以一个数字结尾的字符串。

i标识规定当匹配字母时忽略大小写。 在我们的模式中唯一可能出现的字母是e。我们希望e也能匹配E。我们可以将e因子写成[Ee]或(?:E|e),但不必这么麻烦,因为我们使用了标识符i:

-?

减号后面的?后缀表示这个减号是 可选的:

\d+

\d的含义和[0-9]一样。它匹配一个数字。后缀+规定它可以匹配1个或多个数字:

(?:\.\d*)?

(?: . . .)?表示一个可选 的非捕获型分组。通常用非捕获型分组来替代少量不优美的捕获型分组是很好的方法,因为捕获会有性能上的损失。这个分组将匹配后面跟随0个或多个数字的小数点:

(?:e[+\-]?\d+)?

这是另外一个可选的非捕获型分 组。它将匹配一个e(或E)、一个可选的正负号及一个或多个数字。

有两个方法来创建一个RegExp对象。优先的方法是直接声明正则表达式。

正则表达式被包围在一对斜杠中。 这有点令人难以捉摸,因为斜杠也被用作除法运算符和注释符。

有3个标志能在RegExp中设置。它们分别由字母g、i和m来标示(见表7-1)。这些标志被直接添加在正则表达式的末尾:

//构造一个匹配JavaScript字符串的正则表达式对象

var my_regexp=/”(?:\\.|[^\\\"])”/g;

表7-1正则表达式标志

标志含义
g全局的(匹配多次;准确含义随方法而变)
i大小写不敏感(忽略字符大小写)
m多行(^和$能匹配行结束符)

(默然说话:原书中以上标志均写为大写字母,这在JavaScript中是错误的,三个标志均只能写成小写字母。)

创建一个正则表达式的另一个方法 是使用RegExp构造器。这个构造器接收一个字符串,并把它编译为一个RegExp对象。创建这个字符串时请多加小心,因为反斜杠在正则表达式和在字符串中有一些不同的含义。通常需 要双写反斜杠及对引号进行转义:

//使用RegExp构造一个正则表达式

var my_regexp=new RegExp(“\”(?:\\.|[^\\\\\\\"])*\”",’g');

第二个参数是一个指定标志的字符 串。RegExp构造器适用于正则表达式必须在运行时动态生成的情况。

表7-2:RegExp对象的属性

属性用法
global如果标志g被使用,值为true
ignoreCase如果标志i被使用,值true
lastIndex下一次exec匹配开始听索引。初始值为0
multiline如果标志m被使用,值为true
source正则表达式源代码文本

一个选择包含1个或多个正则表达式序列。这些序列被|(竖线)字符分隔。如果这些序列中的任何一项符合匹配条件,那 么这个选择就被匹配。它尝试按顺序依次匹配这些序列项。所以:

“into”.match(/in|int/)

将在into中匹配in。而不是int,因为in已被匹配成功了。

一个序列包含1个或多个正则表达式因子。每个因子能选择是否跟随一个量词,这个量词决定着这个因子被允许出现的次 数。如果没有指定这个量词,那么该因子将被匹配一次。

一个因子可以是一个字符、一个由 圆括号包围的组、一个字符类,或者是一个转义序列。除了控制字符和特殊字符以外,所有的字符都将被按照字面的意思进行处理:

\ / [ ] ( ) { } ? + * | ^ $

如果你希望上面列出的字符都按字 面的本意去匹配,那么必须要用一个\来进行转义。如果你记不清哪些要转 义,那你可以给任何特殊字符都添加一个\前缀来使其字面化。\前缀不能使字母或数字字面化(默然说话:即所以的字母和数字不应该进行转义)。

一个未被转义的.(点)将匹配除行结束符以外的任何字符。

当lastIndex属性值为0时,一个未转义的^将匹配该文本的开始。当指定了m标识时,它也能匹配行结束符。

一个未转义的$将匹配该文本的结束。当指定了m标志时,它也能匹配行结束符。

反斜杠字符在正则表达式因子中与 其在字符串中一样均表示转义,但是在正则表达式因子中,它稍有一点不同。

像在字符串中一样,\f是换页符,\n是换行符,\r是回车符,\t是制表符,并且\u允许使用四位的十六进制常量指定 一个Unicode字符。但要注意:\b不是退格符。

\d等同于[0-9]。它匹配一个数字。而\D则表示非数字:[^0-9]。

\s等同于[\f\n\r\t\u00DB\u0020\u00A0\u2028\u2029]。这是Unicode空白符的一个不完全子集。\S则表示相反的一个子集(即非空白符) [^\f\n\r\t\u00DB\u0020\u00A0\u2028\u2029]。

\w等同于[0-9A-Z_a-z](可用于变量命名的部分子集)。\W则表示与其相反:[^0-9A-Z_a-z]。这似乎 应该是表示出现在变量中的字符。可实际上它对任何语言来说都是无用的。如果你要匹配一个变量的命名规则,你得自己指定规则。

\b被指定为一个字边界标志,这将方 便于对文本的字边界进行匹配。不幸的是,它使用\w去寻找字边界,所以它对多语言应该来说是完全无用的。

\1是指向分组1所捕获到的文本的一个引用,所以它能被再次匹配。例如,你能用下面的正则表达式来搜索文本中的所有单 词:

var doubled_words=/[A-Za-z\u00C0-\u1fff\u2800-\ufffd'\-]+\s+\1/gi;

doubled_words将寻找出 现重复的单词(包含1个或多个字母的字符串),该单词的后面跟着1个或多个空白,然后再跟着与它相同的单词。

\2是指向分组2的引用,\3是指向分组3的引用,依此类推。

分组共有4种。

捕获型

一个捕获型分组是一个被包围在圆 括号中的正则表达式选择。任何匹配这个分组的字符将被捕获。每个捕获型分组都被指定了一个数字。在正则表达式中第一个捕获(的是分组1.第二个(是分组2。

非捕获型

非捕获型分组有一个(?:前缀。非捕获型分组仅做简单的匹配;并不会捕获所匹配文本。这会有微弱的性能优势。非捕获型分组不 会干扰捕获型分组的编号。

向前正向匹配

向前正向匹配组有一个(?=前缀。它类似于非捕获型分组,但在这个组匹配后,文本将倒回到它开始的地方,实际上并不匹配任何东 西。这不是一个好的特性。

向前负向匹配

向前负向匹配分组有一个(?!前缀。它类似于向前正向匹配分组,但只有匹配失败时它才进行匹配。这不是一个好的特性。

正则表达式类是一种指定一组字符 的便利方式。例如,如果想匹配一个元音字母,我们可以写作(?:a|e|i|o|u),但它可以被更方便地写成一个类[aeiou]。

类提供另外两个方便性。第一个是 能够指定字符范围。所以,一组由32个ASCII的特殊字符组成的集合:

! ” # $ % & ‘ ( ) * + , – . / :

; < = > ? @ [ \ ] ^ _ ` { | } ~

可以被写为:

(?:!|”|#|\$|%|&|’|\(|\)|\*|\+|,|-|\.|\/|:|;|<|=|>|@|\[|\\|]|\^|_|` |\{|\||\}|~)

稍微更好看一些的写法是:

[!-\/:-@\[-`{-~]

它包括从!到/、从:到@、从[到`和从{到~r的字符。但它看起来依旧相当难以阅读(默然说话:我觉得写成[!"#$% &'()*+,\-.\ /:;<=>?@\[\\\]\^_`{|}~]也是可以的)。

另一个方便之处是类的求反。如果[后的第一个字符是^,那么 个类将排除这些特殊字符。所以

[^!-\/:-@\[-`{-~]

将匹配任何一个非ASCII特殊字符的字符。

7.3.7 类转义

字符类内部的转义规则和正则表达 式因子的相比稍有不同。[\b]是退格符。下面是在字符类中需要被转义的特殊字符:

- / [ \ ] ^

7.3.8 量词

因子可以用一个正则表达式量词后 缀来决定这个因子应该被匹配的次数。包围在一对花括号中的一个数字表示这个因子应该被匹配的次数。所以,/www/匹配的/w{3}/一样。{3,6}将匹配3、4、5或6次。{3,}将匹配3次或更多次。

?等同于{0,1}。*等同于{0,}+则等同于{1,}。

如果只有一个量词,则趋向于进行 贪婪性匹配,即匹配尽可能多的重复直至达到上限。如果这个量词还有一个额外的后缀?,那么则趋向于进行懒惰性匹配,即试图匹配尽可能少的必要重复。一般情况下最好坚持使用贪婪性匹配。

JavaScript包含了少量可用 在标准类型上的标准方法。

Array

array.concat(item…)

concat方法返回一个新数组,它 包含array的浅复制(shallow copy)并将1个或多个参数item附加在其后。如果参数item是一个数组,那么它的每个元素会被分别添加。此外,请参见本章后面的array.push(item…)方法。

var a=['a','b','c'];

var b=['x','y','z'];

var ca.concat(b,true);

//c是['a','b','c', 'x','y','z',true]

array.join(separator)

join方法把一个array构造成一个字符串。它将array中的每个元素构造成一个字符串,并用一个separator为分隔符把它们连接在一起。默认的separator是’,'。为了实现无间隔的连接,我们可以使用空字符串作为separator。

如果你想把大量的片段组装成一个 字符串,把这些片段放到一个数组中并用join方法连接它们通常比用+运算符连接这些片段要快。

var a=['a','b','c'];

a.push(‘d’);

var c=a.join(”); //c是’abcd’

array.pop()

pop和push方法使数组array像堆栈(stack)一样工作。pop方法移除array中的最后一个元素并返回该元素。如果该array是空的,它会返回undefined。

var a=['a','b','c'];

var c=a.pop(); //a是['a','b'],c是’c’

array.push(item…)

push方法将一个或多个参数item附加到一个数组的尾部。不像concat方法那样,它会修改该数组array,如果参数item是一个数组,它会将参数数组作为单个元素整个添加到数组中。它返回这个数组array的新长度值。

var a=['a','b','c'];

var b=['x','y','z'];

var c=a.push(b,true);

array.reverse()

reverse方法反转array中的元素的顺序。它返回当前的array:

var a=['a','b','c'];

var b=a.reverse();

//a和b都是['c','b','a']

array.shift()

shift方法移除数组array中的第一个元素并返回该元素。如果这个数组array是空的,它会返回undefined。shift通常比pop慢得多。

var a=['a','b','c'];

var c=a.shift(); //a是['b','c']c是’a’

array.slice(start,end)

slice方法对array中的一段做浅复制。第一个被复制的元素是array[start](含array[start])。它将一直复制到array[end](不含array[end])为止。end参数是可选的,并且默认值是该数组的长度array.length。如果两个参数中的任何一个是负数,array.length将和它们相加来试图使它们成为非负数。如果start大于等于array.length,得到的结果将是一个新的空数组。千万别把slice和splice混淆了。此外请参见本章后面的string.slice。

var a=['a','b','c'];

var b=a.slice(0,1); //b是['a']

var c=a.slice(1); //c是['b','c']

var d=a.slice(1,2); //d是['b']

array.sort(comparefn)

sort方法对array中的内容进行适当的排序。它不能正确地给一组数字排序:

var n=[4,8,15,16,23,42];

n.sort();

//n是[15,16,23,4,42,8]

JavaScript的默认比较函数 假定所要被排序的元素都是字符串。它尚未足够智能到在比较这些元素之前先检测它们的类型,所以当它比较这些的时候会将它们转化为字符串,从而导致一个令人 吃惊的错误结果。

不过,你可以对其进行改进。你可 以自己写一个如何完成两数比较的函数来替换掉默认的比较函数。这个比较函数应该接受两个参数,如果这两个参数相等则返回0,如果第一个参数应该排在前面,则返回负数,如果第二个参数应该排在前面,则返回正数。

n.sort(function(a,b){return a-b;});//默 然说话:所传的参数,就是自定义的比较函数

上面这个函数将给数字排序,但它 不能给字符串排序。如果我们想要给任何简单值数组排序,则必须做更多的工作:

var m=['aa','bb','a',4,8,15,16,23,42];

m.sort(function (a,b){

if(a===b){

return 0;

}

if(typeof a ===typeof b){

return a<b?-1:1;

}

return typeof a < typeof b ? -1:1;

});//m是[4,8,15,16,23,42,'a','aa','bb']

如果大小写不重要,你的比较函数 应该在比较运算数之前先将它们转化为小写。此外请参见本章后面的string.localeCompare。

//by函数接受一个表示成员名的字符串作为参数

//并返回一个可以用来对包含该成员的对象数组进行排序的比较函数:

var by=function(name){

return function(o,p){

var a,b;

if(typeof o === ‘object’ && typeof p===’object’ && o && p){

a=o[name];

b=p[name];

if(a===b){

return 0;

}

if(typeof a ===typeof b){

return a<b?-1:1;

}

return typeof a < typeof b ? -1:1;

}else{

throw{

name:’Error’,

message:’Expected an object when sorting by’+name

};

}

};

};

var s=[

{first:'Joe',last:'Besser'},

{first:'Moe',last:'Howard'},

{first:'Joe',last:'Derita'},

{first:'Shemp',last:'Howard'},

{first:'Larry',last:'Fine'},

{first:'Curly',last:'Howard'}

];

s.sort(by(‘first’));

//s是[

//{first:'Curly',last:'Howard'},

//{first:'Joe',last:'Derita'},

//{first:'Joe',last:'Besser'},

//{first:'Larry',last:'Fine'},

//{first:'Moe',last:'Howard'},

//{first:'Shemp',last:'Howard'}

//]

array.splice(start,deleteCount,item…)

splice方法从array中移除1个或多个元素,并用新的item替换它们。参数start是从数组array中移除元素的开始位置。参数deleteCount是要移除的元素个数。如果有额外的参数,那些item都将插入到所移除元素的位置上。它返回一个包含被移除元素的数组。

var a=['a','b','c'];

var r=a.splice(1,1,’ache’,'bug’);

//a是['a','ache','bug','c']

//r是['b']

array.unshift(item…)

unshift方法像push方法一样用于将元素添加到数组中,但它是把item插入到array的开始部分而不是尾部。它返回array的新的长度值:

var a=['a','b','c'];

var r=a.unshift(‘?’,'@’);

//a是['?','@','a','b','c']

//r是5

Function

function.apply(thisArg,argArray)

apply方法调用函数function,传递一个将被绑定到this上的对象和一个可选的参数数组。apply方法被用在apply调用模式中(参见第4章)。

Function.method(‘bind’,function(that){

//返回一个函数,调用这个函数就像它是那个对象的方法一样。

var method=this,

slice=Array.prototype.slice,

args=slice.apply(arguments,[1]);

return function(){

return method.apply(that,args.concat(slice.apply(arguments,[0])));

};

});

var x=function(){

return this.value;

}.bind({value:666});

alert(x());//666

Number

number.toExponential(fractionDigits)

toExponential方法把这 个number转换成一个指数形式的字符串。可选参数fractionDigits控制其小数点后的数字位数。它的值必须在0至20之间。

document.writeln(Math.PI.toExponential(0)+’<br />’);

document.writeln(Math.PI.toExponential(2)+’<br />’);

document.writeln(Math.PI.toExponential(7)+’<br />’);

document.writeln(Math.PI.toExponential(16)+’<br />’);//默然说话:其实Math.PI只精确到小数点后15位

document.writeln(Math.PI.toExponential()+’<br />’);

number.toFixed(fractionDigits)

toFixed方法把这个number转换成为一个十进制数形式的字符串。可选参数fractionDigits控制其小数点后的数字位数。它的值必须在0和20之间。默认为0:

document.writeln(Math.PI.toFixed(0)+’<br />’);

document.writeln(Math.PI.toFixed(2)+’<br />’);

document.writeln(Math.PI.toFixed(7)+’<br />’);

document.writeln(Math.PI.toFixed(16)+’<br />’);

document.writeln(Math.PI.toFixed()+’<br />’);

number.toPrecision(precision)

toPrecision方法把这个number转换成为一个十进制数形式的字符串。可选参数precision控制有效数字的位数。它的值必须在1和21之间:(默然说话:原书有误,经实际测试,应该为1到21之间)

document.writeln(Math.PI.toPrecision(1)+’<br />’);

document.writeln(Math.PI.toPrecision(2)+’<br />’);

document.writeln(Math.PI.toPrecision(7)+’<br />’);

document.writeln(Math.PI.toPrecision(16)+’<br />’);

document.writeln(Math.PI.toPrecision()+’<br />’);

number.toString(radix)

toString方法把这个number转换成为一个字符串。可选参数radix控制基数。它的值必须在2和36之间。默认的radix是以10为基数的。radix参数最常用的是整数,但是它可以用任意的数字。(默然说话:换个说法,radix这个参数就是表示按几进制来转换这个数字,2表示二进制,以此类推。)

Object

object.hasOwnProperty(name)

如果这个object包含了一个名为name的属性,那么hasOwnProperty方法返回true。原型链中的同名属性是不会被检查的。这个方法对name就是hasOwnProperty时不起作用,此时会返回false:

var a={member:true};

var b=Object.beget(a);//beget来自第3章

var t=a.hasOwnProperty(‘member’);

var u=b.hasOwnProperty(‘member’);

var v=b.member;

RegExp

regexp.exec(string)

exec方法是使用正则表达式的最强 大(和最慢)的方法。如果它成功地匹配regexp和字符串string,它会返回一个数组。数组中下标为0的元素将包含正则表达式regexp匹配的子字符串。下标为1的元素是分组1捕获的文本,下标为2的元素是分组2捕获的文本。如果匹配失败,那么它会返回null。

如果regexp带有一个g标志(全局标志),事情变得更加复杂了。查找不是从这个字符串的起始位置开始,而是从regexp.lastIndex(它初始化为0)位置开始。如果匹配成功,那么regexp.lastIndex将 被设置为该匹配后第一个字符的位置。不成功的匹配会重置regexp.lastIndex为0。

这就允许你通过在一个循环中调用exec去查询一个匹配模式在一个字符串中发生几次。有两件事情需要注意。如果你提前退出了这个循环,再次 进入这个循环前必须把regexp.lastIndex重置到0。^因子也仅匹配regexp.lastIndex为0的情况。

//把一个简单的HTML文本分解为标签和文本。

//(entityify方法请参见string.replace)

//对每个标签和文本,都产生一个数组包含如下元素

//[0]整个匹配的标签和文本

//[1]/(斜杠),如果有的话

//[2]标签名

//[3]属性,如果有任何属性的话

//默然说话:这个方法将在后面的内容中详细说明

String.method(‘entityify’,function(){

var character={

‘<’:'&lt;’,

‘>’:'&gt;’,

‘&’:'&amp;’,

‘”‘:’&quot;’

};

//返回string.entityify方法,它返回调用替换方法的结果

//它的replaceValue函数返回在一个对象中查找一个字符的结果

//这种对象的用法通常优于switch语句

return function(){

return this.replace(/[<>&"]/g,function(c){

return character[c];

});

};

}());

var text=’<html><body bgcolor=linen><p>’+

‘This is <b>bold<\/b>!<\/p><\/body><\/html>’;

var tags=/[^<>]+|<(\/?)([A-Za-z]+)([^<>]*)>/g;

var a,i;

while((a=tags.exec(text))){

for(i=0;i<a.length;i+=1){

document.writeln((‘//['+i+'] ‘+a[i]).entityify()+’<br />’);

}

document.writeln(‘<br />’);

}

regexp.test(string)

test方法是使用正则表达式的最简 单(和最快)的方法。如果该regexp匹配string,它返回true,否则,它返回false。不要对这个方法使用g标识:

var b=/&.+;/.test(‘frank &amp; beans’);

//b是true

String

string.charAt(pos)

charAt方法返回在string中pos位置处的字符。如果pos小于0或大于等于字符串的长度string.length,它会返回空字符串。由于JavaScript没有字符这种数据类型。这个方法返回的结果是一个字符串。

var name=’Curly’;

var initial=name.charAt(0); //initial是’C’

string.charCodeAt(pos)

charCodeAt方法同charAt一样,只不过它返回的不是一个字符串,而是以整数形式表示的在string中的pos位置处的字符的ASCII码。

var name=’Curly’;

var initial=name.charCodeAt(0); //initial是67

string.concat(string…)

concat方法通过将其他的字符串 连接在一起来构造一个新的字符串。它很少被使用,因为+运算符更为方便:

var s=’C’.concat(‘a’,'t’); //s是’Cat’

string.indexOf(searchString,position)

indexOf方法在string内查找另一个字符串searchString。如果它被找到,则返回第一个匹配字符的位置,否则返回-1.可选参数position可设置从string的某个指定的位置开始查找:

var text=’Mississippi’;

var p=text.indexOf(‘ss’); //p是2

p=text.indexOf(‘ss’,3); //p是5

p=text.indexOf(‘ss’,6); //p是-1

stirng.lastIndexOf(searchString,position)

lastIndexOf方法和indexOf方法类似,只不过它是从该字符串的末尾开始查找而不是从开头:

var text=’Mississippi’;

var p=text.lastIndexOf(‘ss’); //p是5

p=text. lastIndexOf(‘ss’,3); //p是2

p=text. lastIndexOf(‘ss’,6); //p是5

string.localeCompare(that)

localeCompare方法比较 两个字符串。如何比较字符串的规则没有详细的说明。如果string比字符串that小,那么结果为负数。如果它们是相等的,那么结果为0.这类似于array.sort比较函数的约定:

var m=['AAA','A','aa','a','Aa','aaa'];

m.sort(function(a,b){

return a.localeCompare(b);

});

alert(m);

//默然说话:IE下面是[a,A,aa,Aa,aaa,AAA],而谷歌浏览器是[A,AAA,Aa,a,aa,aaa],火狐的与IE一致,苹果的是[A,a,Aa,aa,AAA,aaa],Opera与谷歌的一致

string.match(regexp)

match方法匹配一个字符串和一个 正则表达式。它依据g标识来决定如何进行匹配。如果没有g标识,那么调用string.match(regexp)的结果与调用regexp.exec(string)的结果相同。然而,如果regexp带有g标识,那么它返回一个包含除捕获分组之外的所有匹配的数组:

var text=’<html><body bgcolor=linen><p>’+

‘This is <b>bold<\/b>!<\/p><\/html>’;

var tags=/[^<>]+|<(\/?)([A-Za-z]+)([^<>]*)>/g;

var a,i;

a=text.match(tags);

for(i=0;i<a.length;i+=1){

document.writeln(‘<br />’+(‘//['+i+']‘+a[i]).entityify());

}

string.replace(searchValue,replaceValue)

replace方法对string进行查找和替换的操作,并返回一个新的字符串。参数searchValue可以是一个字符串或一个正则表达式对象。如果它是一个字符串,那么searchValue只会在第一次出现的地方被替换,所以下面的代码结果是”mother-in_law”:

var result=”mother_in_law”.replace(‘_’,'-’);

这或许令你失望。

如果searchvalue是一个正则表达式并且带有g标志,那么它将替换所有匹配之处。如果它没有带g标志,那么它将仅替换第一个匹配之处。

replaceValue可以是一个 字符串或一个函数。如果replaceValue是一个字符串,字符$拥有特别的含义:

var oldareacode=/\((\d{3})\)/g;

var p=’(555)666-1212′.replace(oldareacode,’$1-’);

美元符号序列替换对象
$$$
$&整个匹配的文本
$number分组捕获的文本
$`匹配之前的文本
$’区配之后的文本

如果replaceValue是一个函数,此方法将对每个匹配依次调用它,并且该函数返回的字符串将被用作替换文本。传递给这个 函数的第一个参数是整个被匹配的文本。第二个参数是分组1捕获的文本,下一个参数是分组2捕获的文本,依此类推:

String.method(‘entityify’,function(){

var character={

‘<’:'&lt;’,

‘>’:'&gt;’,

‘&’:'&amp;’,

‘”‘:’&quot;’

};

//返回string.entityify方法,它返回调用替换方法的结果

//它的replaceValue函数返回在一个对象中查找一个字符的结果

//这种对象的用法通常优于switch语句

return function(){

return this.replace(/[<>&"]/g,function(c){

return character[c];

});

};

}());

alert(“<&>”.entityify());

string.search(regexp)

search方法和indexOf方法类似,只是它接受一个正则表达式对象作为参数而不是一个字符串。如果找到匹配,它返回第一个匹 配的首字符位置,如果没有找到匹配,则返回-1.此方法会忽略g标志,且没有position参数:

var text=’and in it he says “Any damn fool could’;

var pos=text.search(/["']/);//pos是18

string.slice(start,end)

slice方法复制string的一部分来构造一个新的字符串。如果start参数是负数,它将与string.length相加。end参数是可选的,并且它的默认值是string.length。如果end参数是负数,那么它将与string.length相加。end参数是一个比最末一个字符的位置值还大的数。要想得到从位置p开始的n个字符,就用 string.slice(p,p+n)。此外,请参见分别在本章前面和后面介绍的string.substring和array.slice。

string.split(separator,limit)

split方法把这个string分割成片段来创建一个字符串数组。可选参数limit可以限制被分割的片段数量。separator参数可以是一个字符串或一个正则表达式。

如果separator是一个空字符串,将返回一个单字符的数组:

var digits=’0123456789′;

var a=digits.split(”,5);

//a是['0','1','2','3','456789']

//默然说话:经IE,Opera,谷歌,firefox测试,a的值是['1','2','3','4']

否则,此方法会在string中查找所有separetor出现的地方。分隔符两边的每个单元文本都会被复制到该数组中。此方法会忽略g标志:

var ip=’192.168.1.0′;

var b=ip.split(‘.’);

alert(b);

var c=’|a|b|c|’.split(‘|’);

alert(c);

var text=’last, first ,middle’;

var d=text.split(/\s*,\s*/);

alert(d);

有一些特例须特别注意。来自分组 捕获的文本将会被包含在被分割后的数组中:

var e=text.split(/\s*(,)\s*/);

alert(e);//e是['last',',','first',',',middle']

当separator是一个正则表达式时,有一些JavaScript的实现在输出数组中会禁止空字符串:

var c=’|a|b|c|’.split(‘/\|/’);

alert(c);

//默然说话:很奇怪,这个程序在我所有的浏览器都得不到答案,似乎那个正则表达式是错误的,因为它并没有返回一个数组, 而是直接返回了这个字符串。

string.substring(start,end)

substring的用法和slice方法一样,只是它不能处理负数参数。没有任何理由去使用substring方法,请用slice替代它。

string.toLocaleLowerCase()

toLocaleLowerCase方 法返回一个新字符串,它使用本地化的规则把这个string中的所有字母转换为小写格式。这个方法主要是用在土耳其语上,因为在土耳其语中’I'转换为’I',而不是’I'。

string.toLocaleUpperCase()

toLocaleUpperCase方 法返回一个新字符串,它使用本地化的规则把这个string中的所有字母转换为大写格式。这个方法主要是用在土耳其语上,因为在土耳其语中’I'转换为’I',而不是’I'。

string.toLowerCase()

toLowerCase方法返回一个 新的字符串,这个string中的所有字母都被转化为小写格式。

string.toUpperCase()

toUpperCase方法返回一个 新的字符串,这个string中的所有字母都被转化为大写格式。

String.fromCharCode(char…)

String.fromCharCode函数从一串数字中返回一个字符串。

var a=String.fromCharCode(67,97,116);

//a是’Cat’