第一部分
第1章(富有表现力的JavaScript)
揭示了JavaScript语言富有表现力的特点。从中你可以体会到,这种语言允许你用各种各样的编程风格来完成同样的任务,还允许你在面向对象编程的过程中借用函数式编程的概念来丰富其实现方式。
这一章解释了究竟为什么应该使用设计模式,以及它们在JavaScript程序设计中的运用是如何使代码更高效、更易于处理的。
1.1 JavaScript的灵活性
JavaScript最强大的特性是其灵活性。作为JavaScript程序员,只要你愿意,可以把程序写得很简单,也可以写得很复尔。
这种语言也支持多种不同的编程风格。你既可以采用函数式编程风格,也可以采用更复杂一点的面问对象编程风格。
即使你根本不懂函数式编程或面向对象编程,也能写出较为复杂的程序。使用这种语言,哪怕只采用编写一个个简单的函数的方式,你也能高效地完成任务。
这可能是某些人把JavaScript视同玩具的原因之一,但我们却认为这是一个优点。
程序员只要使用这种语音的一个很小的、易于学习的子集就能完成一些有用的任务。这也意味着当你成长为一个更高级的程序员时,JavaScript在你手中的威力也在增长。
JavaScript允许你模仿其他语言的编程模式和惯用法。它也形成了自己的一些编程模式和惯用法。那些较为传统的服务器端编程语言具有的面向对象特性,JavaScript都有。
先来看一个用不同方法完成同样的任务的例子:启动和停止一个动画。如果你看不懂这些例子,别担心。我们在此使用的所有模式和技术都会在本书后面进行讲解。
目前你可以把这一部分看作一个演示在JavaScript中用不同方法完成同一任务的实际例子。如果你习惯于过程式的程序设计,那么可以这样做:
/*Start and stop animations using functions.*/
function startAnimation(){
}
function stopAnimation(){
}
这种做法很简单,但你无法创建可以保存状态并且具有一些仅对其内部状态进行操作的方法的动画对象。下面的代码定义了一个类,你可以用它创建这种对象:
/*Anim class.*/
var Anim=function(){
...
}:
Anim.prototype.start=function(){
};
Anim.prototype.stopfunction(){
};
/*Usage.*/
var myAnim=new Anim();
myAnim.start();
myAnim.stop();
上述代码定义了一个名为Anim的类,并把两个方法赋给该类的prototype属性。第3章将详细讲述这种技术。如果你更喜欢把类的定义封装在一条声明中,则可改用下面的代码:
/*Anim class, with a slightly different syntax for declaring methods. */
var Anim=function(){
}
Anim.prototype={
start: function(){
},
stop:function(){
}
};
这在传统的面向对象程序员看来可能更眼熟一点,他们习惯于看到类的方法声明内嵌在类的
明之中。要是你以前用过这样的编程风格,可能想尝试一下下面的示例。同样,如果代码中有些地方你看不懂,不必为此而焦虑:
/* Add a method to the Function object that can be used to declare methods. */
Function.prototype.method=function(name,fn){
this.prototype[name]=fn;
};
/* Anim class, with methods created using a convenience method. */
var Anim=function(){
};
Anim.method('start',function(){
});
Anim.method('stop',function(){
});
Function.prototype.method用于为类添加新方法。它有两个参数。第一个是字符串,表示新方法的名称;第二个是用作新方法的函数。
你可以进一步修改Function.prototype.method,使其可被链式调用。这只需要让它返回this值即可(方法的链式调用技术将在第6章中讲述):
/*This version allows the calls to be chained.*/
Function.prototype.method=function(name,fn){
this.prototype[name]=fn;
return this;
}
/* Anim class, with methods created using a convenience method and chaining.*/
var Anim=function(){
};
Anim.method('start',function(){
}).method('stop',function(){
});
你已经见识了完成同一项任务的5种不同方法,它们的风格略有差异。基于自己的编程背景,你可能觉得其中的某种方法比别的方法更为合意。
这是件好事:JavaScript允许你用最适合于手头项目的编程风格进行工作。不同的风格在代码篇幅、编码效率和执行性能方面各有其特点。
1.2 弱类型语言
在JavaScript中,定义变量时不必声明其类型。但这并不意味着变量没有类型。一个变量可以属于几种类型之一,这取决于其包含的数据。
JavaScript中有3种原始类型:布尔型、数值型和字符串类型(不区分整数和浮点数是JavaScript与大多数其他主流语言的一个不同之处)。
此外,还有对象类型和包含可执行代码的函数类型,前者是一种复合数据类型(数组是一种特殊的对象,它包含着一批值的有序集合)。
最后,还有空类型(nu11)和未定义类型(undefined)这两种数据类型。原始数据类型按值传送,而其他数据类型则按引用传送。
如果不了解这一点的话,你很可能会碰到一些意想不到的问题。
与其他弱类型语言一样,JavaScript中的变量可以根据所赋的值改变类型。原始类型之间也可以进行类型转换。
toString方法可以把数值或布尔值转变为字符串。parsefloat和parseInt函数可以把字符串转变为数值。
双重“非”操作可以把字符串或数值转变为布尔值:
var bool=!!num;
弱类型的变量带来了极大的灵活性。因为JavaScript会根据需要进行类型转换,所以一般说来,你不用为类型错误操心。
1.3 函数是一等对象
在JavaScript中,函数是一等对象。它们可以存储在变量中,可以作为参数传给其他函数,可以作为返回值从其他函数传出,还可以在运行时进行构造。
在与函数打交道时,这些特性带来了极大的灵活性和极强的表达能力。在阅读本书时你会体会到,这正是用以构建传统的面向对象框
架的基础。
可以用function(){...}这样的语法创建匿名函数。它们没有函数名,但可以被赋给变量。下面是一个匿名函数的示例:
/*An anonymous function, executed immediately.*/
(function(){
varfoo =10;
var bar=2;
alert(foo* bar);
})();
这个函数在定义之后便立即执行,甚至不用赋给一个变量。出现在函数声明之后的一对括号立即对函数进行了调用。括号中空无一物,但也并不是非得如此:
/*An anonymous function with arguments.*/
(function(foo,bar){
alert(foo * bar);
})(10,2);
匿名函数最有趣的用途是用来创建闭包。闭包(closure)是一个受到保护的变量空间,由内嵌函数生成。JavaScript具有函数级的作用域。这意味着定义在函数内部的变量在函数外部不能被访问。
JavaScript的作用域又是词法性质的(lexicallyscoped)。这意味着函数运行在定义它的作用域中,而不是在调用它的作用域中。
把这两个因素结合起来,就能通过把变量包裹在匿名函数中而对其加以保护。你可以这样创建类的私用(private)变量:
/*An anonymous function used as a closure.*/
var baz;
(function(){
var foo=10;
var bar=2;
baz=function(){
return foo * bar;:
})();
baz();// baz can access foo and bar, even though it is executed outside of the
// anonymous function.
变量foo和bar定义在匿名函数中。因为函数baz定义在这个闭包中,所以它能访问这两个变量,即使是在该闭包执行结束后。这是一个复杂的话题,本书中会多次涉及。
在第3章讨论封装的时候将对这种技术详加讲解。
1.4 对象的易变性
在JavaScript中,一切都是对象(除了那三种原始数据类型。即便是这些类型,在必要的时候也会被自动包装为对象),而且所有对象都是易变的(mutable)。
这意味着你能使用一些在大多数别的语言中不允许的技术,例如为函数添加属性:
function displayError(message)
displayError.numTimesExecuted++;
alert(message);
};
displayError.numTimesExecuted =0;
这也意味着你可以对先前定义的类和实例化的对象进行修改:
/*Class Person.*/
function Person(name, age){
this.name = name;
this age age;
}
Person.prototype{
getName:function(){
return this.name;
},
getAge: function(){
return this.age;
},
/*Instantiate the class.*/
var alice =new Person('Alice', 93);
var bill=new Person('Bill', 30);
/*Modify the class.*/
Person.prototype.getGreeting=function(){
return 'Hi'+ this.getName()+ "!';
};
/*Modify aspecific instance.*/
alice.displayGreeting=function(){
alert(this.getGreeting());
}
在这个例子中,类的getGreeting方法是在已经创建了类的两个实例之后才添加的,但这两个实例仍然能获得这个方法,其原因在于prototype对象的工作机制。
对象alice还得到了 disp1ayGreeting方法,而别的实例却没有。
与对象的易变性相关的还有内省(introspection)的概念。
你可以在运行时检查对象所具有的属性和方法,还可以使用这种信息动态实例化类和执行其方法(这种技术称为反射(reection)),甚至不需要在开发时知道它们的名称。
这些技术在动态脚本编程中发挥着重要作用,而静态语言(例如C++)则缺乏这样的特性。
本书中大多数用来模仿传统的面向对象特性的技术都依赖于对象的易变性和反射。
如果你习惯使用C++或Java这类语言,可能会觉得这很奇怪,因为在那些语言中,不能对已经实例化的对象进行扩展,也不能对已经定义好的类进行修改。
而在JavaScript中,任何东西都可以在运行时修改。这是一个强有力的工具,许多在别的语言中无法办到的事都能借助于它而办到。当然,这也有其不利之处。
你可以定义一个具有一套方法的类,却不能肯定这些方法在以后总是完好如初。这是JavaScript中很少进行类型检查的原因之一。
这个问题将在第2章讲述鸭式辨型(duck typing)和接口检查时进行探讨。
1.5 继承
继承在JavaScript中不像在别的面向对象语言中那样简单。JavaScript使用的是基于对象的(原型式(prototypal))继承,它可以用来模仿基于类的(类式(classical))继承。
这两种范型本书都会讲述,编写代码时用哪一种都行,根据手头任务的实际情况,有时其中的某种会更适合一些。
它们在性能上也有不同的表现,这也是在进行选择时需要考虑的重要因素。这个复杂的话题将在第4章中进行探讨。
1.6 JavaScript 中的设计模式
1995年,GoF合作出版了一本名为《设计模式》的书。这本书整理记录了对象间相互作用的各种方式,并针对不同类型的对象创造了一套通用术语。用以创建这些不同类型的对象的套路被称为设计模式(designpattern)。
出于通用性的考虑,书中使用了一种在一定程度上独立于语言的方式来描述这些模式。本书就是专门讨论这些模式在JavaScript中的应用的。
JavaScript强大的表现力赋予了程序员在运用设计模式编写代码时极大的创造性。在JavaScript中使用设计模式主要有如下3原因。
- (1)可维护性。设计模式有助于降低模块间的耦合程度。这使对代码进行重构和换用不同的模块变得更容易,也使程序员在大型团队中的工作以及与其他程序员的合作变得更容易。
- (2)沟通。设计模式为处理不同类型的对象提供了一套通用的术语。程序员因此可以更简明地描述自己的系统的工作方式。你不用进行冗长的说明,往往这样一句话就足够了:“它使用了工厂模式”。每个模式都有自己的名称,这意味着你可以在较高层面上对其进行讨论,而不必涉
- 足过多的细节。
- (3)性能。本书讲述的某些模式是起优化作用的模式。它们可以大幅提高程序的运行速度,并减少需要传送到客户端的代码量。
这方面最重要的例子是享元模式(第13章)和代理模式(第14章)。
你也可能出于如下两个理由而不使用设计模式。
- (1)复杂性。获得可维护性往往要付出代价,那就是代码可能会变得更加复杂、更难被程序设计新手理解。
- (2)性能。尽管某些模式能提升性能,但多数模式对代码的性能都有所拖累。这种拖累可能微不足道,也可能完全不能接受,这取决于项目的具体需求。
- 实现设计模式比较容易,而懂得应该在什么时候使用什么模式则较为困难。
- 未搞懂设计模式的用途就盲目套用,是一种不安全的做法。你应该尽量保证所选用的模式就是最恰当的那种,并且不要过度牺牲性能。
1.7 小结
JavaScript的丰富表现力是其力量之源。即使这种语言缺少一些有用的内置特性,拜其灵活性所赐,你也能自己加入这些特性。
完成一项任务可以有多种方式,你可以根据自己的技术背景和喜好选择编写代码的方式。
JavaScript是弱类型语言。程序员在定义变量时并不指定其类型。函数是一等对象,并且可以动态创建,因此你可以创建闭包。
所有对象和类都是易变的,可以在运行时修改。可供使用的继承范型有两种,即原型式继承和类式继承,它们各有其优缺点。
JavaScript中的设计模式颇有助益,但其不当应用也会产生负面效果。
在JavaScript这类轻灵的语言中,过度复杂的架构会很快把应用程序拖入泥沼。你使用的编程风格和选择的设计模式应该与所要完成的具体工作相称。