js学习

第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指向的是顶层结构。在下一章中,我们将会看到使用闭包来进行信息隐藏的方式,它是另一个有效减少全局污染的方法。


第4章  函数

JavaScript中最好的特性就是它对函数的实现。它几乎无所不能。但是,想必你也能预料到,函数在JavaScript里也并非万能药。

函数包含一组语句,它们是JavaScript的基础模块单元,用于代码复用、信息隐藏和组合调用。函数用于指定对象的行为。一般来说,所谓编程就是将一组需求分解成一组函数与数据结构的技能。

4.1   函数对象

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

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

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

4.2   函数定义

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

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

var add=function(a,b){

         return a+b;

};

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

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

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

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

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

4.3   调用

调用一个函数将暂停当前函数的执行,传递控制权和参数给新函数。除了声明时定义的形式参数,每个函数接收两个附加的参数: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’

4.4   参数

当函数被调用时,会得到一个“免费”奉送的参数,那就是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属性,但它缺少所有的数组方法。我们将在本章结尾看到这个设计错误导致的后果。

4.5   返回

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

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

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

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

4.6   异常

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属性以确定异常的类型。

4.7   给类型增加方法

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;

       }

};

4.8   递归

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

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

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当前并没有提供尾递归优化。深度递归的函数可能会因为返回堆栈溢出而运行失败。

4.9   作用域
在编程语言中,作用域控制着变量与参数的可见性及生命周期。对程序员来说这是一个重要的帮助,因为它减少了名称冲突,并且提供了自动内存管理。
大多数使用C语言语法的语言都拥有块级作用域。在一个代码块中(括在一对花括号中的语句集)定义的所有变量在代码块的外部是不可见的。定义在代码块中的变量在代码块执行结束后会被释放掉。这是件好事。
糟糕的是,尽管代码块的语法似乎表现出它支持块级作用域,但实际上JavaScript并不支持。这个混淆之处可能成为错误之源。
JavaScript确实有函数作用域。定义在函数中的参数和变量在函数外部是不可见的。但在一个函数中的任何位置定义的变量在该函数中的任何地方都可见(默然说话:我的天呀,真是一个灾难。。。。)。
很多现代语言都推荐尽可能迟地声明变量。而用在JavaScript上却会成为糟糕的建议,因为它缺少块级作用域。所以,最好的做法是在函数体的顶部声明函数中可能用到的所有变量。
4.10   闭包
只有函数作用域的好处是内部函数可以访问定义它们的外部函数的参数和变量(除了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 οnlοad=”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].οnclick=function(e){
                     alert(i);//因为这里是直接引用了变量i,而不是副本,所以当点击节点时,总是显示循环之后i的值
              }
       }
}
<body οnlοad="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].οnclick=function(e){
                     return function(){
                            alert(e);
                     };
              }(i);
       }
};
 
<body οnlοad=" 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,这样就可以避免以上情况(默然说话:中文版的源代码有误,中文翻译也有误,害我花了半个小时的时间才理解这段文字的本意,为了让读者容易理解,我对 函数进行了修改)
4.11   回调
函数可以让不连续事件的处理变得更容易。例如:假定有这么一个序列,由用户交互开始,向服务器发送请求,最终显示服务器的响应。最纯朴的定法可能会是这样的:
request=prepare_the_request();
response=send_request_synchronously(request);
display(response);
这种方式的问题在于网络上的同步请将会导致客户端进入假死状态。如果网络传输或服务器很慢,响应性的降低将是不可接受的。
更好的方式是发起异步的请求,提供一个当服务器的响应到达时将被调用的回调函数。这样客户端不会被阻塞。
request=prepare_the_request();
send_request_asynchronously(request,function(response){
       display(response);
})
(默然说话:不要试图运行这两段代码,因为这两段代码仅仅是用来说明的,属于伪代码)
4.12   模块
模块是一个提供接口却隐藏状态与实现的函数或对象,我们可以使用函数和闭包来构造模块。通过使用函数去产生模块,我们几乎可以完全摒弃全局变量的使用,从而缓解这个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就是一组函数的集合,而且那些函数被授予特权,拥有使用或修改私有状态的 能力。
4.13   级联
有一些方法没有返回值。如果我们让这些方法返回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元素并提供了其他功能的对象。该方法允许我们移动元素,修改它的尺寸和样式,并添加行为。这些方法每一个都返回该对象,所以调用返回的结果可以被下一次调用所用。
级 联可以产生出具备很强表现力的接口。它也能帮助控制那种构造试图一次做很多事情的接口的趋势(默然说话:说实话,我非常不喜欢这样的编码,因为这样编码易 读性太差。级联基本上适用于那些一次编码之后再也不修改的代码,或者适用于那些你不想让包括你自己在内的任何人都看不懂的代码)。
4.14  套用
函数也是值,从而我们可以用有趣的方式去操作函数值。套用允许我们将函数与传递给它的参数相结合去产生出一个新的函数。
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方法的常规数组。
4.15  默记法(memoization)
函数可以用对象去记住先前操作的结果,从而能避免无谓的运算。这种优化被称为默记法(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);
});

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值