第2章 JavaScript和JavaScript工具

第2章 JavaScript和JavaScript工具

“JavaScript是最受轻视的语言,因为它不是其他语言。如果你擅长其他语言,但现在必须在只支持JavaScript的环境里工作,那么你必须使用JavaScript,这真是太烦人了。在这种情况下,大多数人一开始不屑于学习JavaScript,随后却会惊讶地发现JavaScript和他们原本想要使用的其他语言差别如此之大,而且这些差别非常重要。”

——道格拉斯·克罗克福德

道格拉斯·克罗克福德(Douglas Crockford)将JavaScript总结为一门很多人使用却鲜有人学习的语言。他写了一本书,列举了该语言的合理用法和强大功能,同时指出了存在问题、需要回避的部分。如果你需要经常使用JavaScript,应该花时间和精力全面学习。道格拉斯·克罗克福德忽略了该语言中的很多功能,将精力集中在一个强大、简洁的子集,这种方式能帮助程序员更好地学习JavaScript。

除了学习JavaScript语言本身(后来被标准化,称为ECMAScript),还需要花时间学习特定的编程环境。其他语言运行在操作系统、关系型数据库或宿主应用之上,而JavaScript最初被设计成在浏览器上运行。ECMAScript语言规范(http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf)明确指明了这点。

ECMAScript最初被设计成一门Web脚本语言,提供了一种让浏览器里的页面更加生动的机制,并且将基于Web的客户端-服务器端架构中一些服务器端的计算任务交给浏览器。

——ECMAScript语言规范

核心JavaScript语言需要结合两个不同的API来理解:浏览器对象模型(BOM)和文档对象模型(DOM)。浏览器对象模型包含window对象及其子对象:navigatorhistoryscreenlocationdocumentdocument对象是文档对象模型的根节点,是页面内容结构的一个树状表示。一些对JavaScript的抱怨其实是针对浏览器对象模型和文档对象模型的实现问题而言的。不能全面理解这些API,就不能有效地在Web浏览器里进行JavaScript开发。

本章剩余部分介绍了在浏览器里使用JavaScript进行开发需要了解的主要内容。这个介绍不够全面,只是强调了读者想要深入了解这门语言所需掌握的入手点和知识点。

2.1 学习JavaScript

教学中广泛采用了Java作为编程语言,和其有关的认证也已经存在了很多年,因此和Java相关的知识已经被充分理解、标准化,成为通识了。人们经常在学校里就学过Java,工作后又经过自学取得了相关认证。同样的情况并没有发生在JavaScript上,但还是有一些关于JavaScript的好书可以帮助大家学习的。

  • JavaScript: The Good Partshttp://shop.oreilly.com/product/9780596517748.do,O'Reilly出版),道格拉斯·克罗克福德著,该书在前面已经提到过。在圈子里,就某些问题对道格拉斯·克罗克福德提出异议已经成为一种时尚,那是因为他是公认的权威,他帮助很多JavaScript开发者形成了自己的思想。有时候,他提出一些过于严格的“法则”,但是如果你不了解JavaScript语言的子集(他认为属于“好的部分”)和他尽力避免使用的部分,那就是自讨苦吃。

  • Secrets of the JavaScript Ninjahttp://www.amazon.com/Secrets-JavaScript-Ninja-John-Resig/dp/193398869X,Manning Publications出版),John Resig、Bear Bibeault著。John Resig是jQuery之父,他对现实中和浏览器兼容性、操作DOM相关的挑战有着深刻的理解。

  • 还有一些书类似于标准语言的参考手册,包括JavaScript: The Definitive Guidehttp://shop.oreilly.com/product/9780596805531.do,O'Reilly出版)、Professional JavaScript for Web Developmenthttp://www.amazon.com/Professional-JavaScript-Developers-Nicholas-Zakas/dp/1118026691,Wrox Press出版,Nicholas C. Zakas 著)。它们比前面两本书内容更全面(作者的个人观点也少)。它们可能不是那种需要从头读到尾的书,但是在深入某个具体的主题时却是非常有用的。

本节不打算重复你在上述书或其他书中所能学到的全部内容,而是帮助你上手,评估自己的JavaScript知识。本章还会引用其他书籍和资源,如果你碰到想要深入研究的术语或概念,可参考它们。


自觉阶段

学习过程中的关键一环是知道自己知道什么,以及知道自己能够知道什么。达克效应(http://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect)是一种认知上的偏差,它描述了这样一种倾向:低技能的人错误地认为他们的能力高于平均水平。鉴于围绕JavaScript的困惑和其被嘲笑为一种“玩具语言”的频率,本节的目标(和自觉阶段学习模型相关;自觉阶段学习模型参见http://en.wikipedia.org/wiki/Four_stages_of_competence)旨在让读者意识到要学些什么。


2.2 JavaScript的历史

关于JavaScript的历史已有详尽的记录,Brendan Eich在1995年用10天时间写出了JavaScript的初始版本(http://www.w3.org/community/webed/wiki/A_Short_History_of_JavaScript)。但如果将JavaScript放在计算机科学的历史里来考量,尤其是和现存第二古老的高级程序语言Lisp相联系,应该会更有意义。John McCarthy在1958年(比Fortran晚一年)发明了Lisp,这是一种计算机程序的数学表达。Scheme是Lisp两种主要的方言之一。奇怪的是,尽管和其他语言的设计反差强烈,Scheme却在JavaScript的历史里扮演了异常重要的角色。图2-1列出了一些影响了JavaScript设计的主要语言。

图像说明文字

图2-1 JavaScript语法继承关系

Scheme极简主义的设计风格并没有体现在JavaScript中,JavaScript相对冗长的语法来自其他语言,这点在JavaScript 1.1规范(http://hepunx.rl.ac.uk/~adye/jsspec11/intro.htm#1006028)中有所提及:

JavaScript的语法大多来自Java,同时继承了Awk和Perl的一些语法,其基于原型的对象模型间接受到Self的影响。

——JavaScript 1.1规范

这和Scheme截然相反,Scheme的语法没有受多种语言的影响。Perl直接影响了JavaScript的某些部分,比如对正则表达式的支持。然而Perl的箴言:不止一种方法去做一件事(TMTOWTDI,“there's more than one way to do it”,参见http://en.wikipedia.org/wiki/There's_more_than_one_way_to_do_it)可能在更广的范围上影响了JavaScript。至少可以反过来说,“只用一种方式去做一件事”(在Python社区里很流行)并不适用。请看如下创建和初始化数组的不同方式:

var colors1 = [];
colors1[0] = "red";
colors1[1] = "orange";

var colors2 = ["yellow", "green", "blue"];

var colors3 = new Array(2);
colors3[0] = "indigo";
colors3[1] = "violet";

var colors4 = new Array();
colors4[0] = "black";
colors4[1] = "white";

因此,看起来JavaScript(及其受到多种语言的影响和语法上的变种)和Scheme(Lisp的极简方言)之间似乎没有任何关联。但是,JavaScript的确和Scheme关系紧密(http://brendaneich.com/tag/history/),直接受其影响:

就像我常说的,Netscape的其他人也可以作证,我受雇于Netscape时承诺“在浏览器里使用Scheme”。

——Brendan Eich

这一点也反映在ECMAScript语言规范(http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf)里:

ECMAScript中的一些技术和其他编程语言使用的类似,尤其是Java、Self和Scheme。

——ECMAScript语言规范

来自Scheme的影响也被其他人辨认出来了。道格拉斯·克罗克福德根据Daniel Paul Friedman那本经典书The Little Schemerhttp://mitpress.mit.edu/books/little-schemer,MIT Press出版),写了一篇文章“The Little JavaScripter”(http://www.crockford.com/javascript/little.html,列举了Scheme和JavaScript的共同点。Lisp社区(欧洲Lisp研讨会,参见http://www.european-lisp-symposium.org/)也将ECMAScript描述为一种“Lisp方言”。JavaScript和Scheme语言之间的相似性不可否认,这是由创造者的本意决定的。

2.3 一门函数式语言

Java开发者倾向于站在面向对象的角度解决问题。尽管JavaScript也支持面向对象,但是这却不是解决问题最高效的方式。使用JavaScript的函数式编程能力会更高效。理解了什么是函数式编程和它的含义,就弄清楚了这门语言的本质和能力。

JavaScript和Scheme语言相像的主要特征是它是一门函数式编程语言,这既和它的起源相关,也和它的语法相关。这里的函数式编程语言是指既支持函数式编程(http://en.wikipedia.org/wiki/Functional_programming),又支持将函数当作一级对象(http://en.wikipedia.org/wiki/First-class_function)。JavaScript的这一基本概念为语言的其他方面提供了方向。对很多程序员,尤其是那些以类似Java这样还未直接支持函数式编程的语言为基础的程序员来说,使用函数式编程是非常大的范式迁移1

1函数式编程在JVM上已经存在一段时间了,使用过一些基于JVM的脚本语言,包括Rhino JavaScript实现。Java 8计划加入Lambda表达式、闭包和相关语言特性。Java 8还会加入一个新的JavaScript实现:Nashorn。随着不断加入新功能,未来几年,JavaScript开发,尤其是函数式编程将变成Java开发者需要深入学习的内容。

2.3.1 作用域

作用域是指程序中变量可见和可操作的范围,在JavaScript中这个概念让人很难捉摸。像很多其他语言一样,函数可用来包括一组语句。这样就能复用函数,同时将信息可见性限制在一个易理解的模块化单元中。ECMAScript语言规范(http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf)定义了三个执行上下文:全局、eval和函数。和其他类C语言不同,JavaScript没有区块级作用域,但是有函数级作用域。像if语句这样的区块结构不会产生新的作用域。

使用JavaScript的危险之一是方法或变量被提升到作用域顶端,它们是在那里定义的。函数声明在作用域执行时就已经存在了,所以函数被提升到执行上下文的顶端。按照经验,可在作用域顶端使用var声明所有要在作用域里用到的变量来避免这类问题:

//这不是Java中的成员变量……
var x = 'set';

var y = function () {

// 你可能想不到 -> var x; 被提升到这一行!

    if (!x) { // 你可能觉得该变量此时已经被初始化
              // 但是却没有,因此会执行该段代码

        var x = 'hoisted';
    }
    alert(x);
}

//……这条语句会弹出警告,显示"hoisted"
y();

这个例子还包含了一些在Java中见不到的特性。

  • 在JavaScript中,nullundefined和其他一些值被当作false
  • if语句中的条件判断表达式是!x,感叹号代表逻辑非操作,因此,如果xundefined(或者null),则if (!x)true。如果x是一个数字或字符串,则会像使用过其他语言的开发者期望的那样,值为false
  • 使用var关键字定义局部变量,没有使用该关键字定义的变量为全局变量。使用var关键字定义的局部变量的作用域和函数的作用域相关。
  • 创建一个函数后将其赋值给变量y,这对Java程序员来说有些奇怪,因为在他们的世界里,方法只和类或对象的实例关联。该语法展现了JavaScript函数式语言的本质。

2.3.2 一级函数

从任何严格的意义上来说,拥有限定作用域的函数并不能归为函数式语言。函数式语言是指支持一级函数的语言。根据Structure and Interpretation of Computer Programshttp://mitpress.mit.edu/sicp/full-text/book/book-Z-H-12.html#%_idx_1218)这本书的描述,一级函数可以赋给一个变量,作为参数传递给另外一个函数,作为函数的返回值,或者包含在其他数据结构中。下面的例子(为说明问题人为设计的)展示了这些功能,还有一些专家认为函数式编程语言必须具备一个特性:支持匿名函数。

//
// 可在任何一款现代浏览器的JavaScript控制台里执行下面的程序
//

// 将函数赋给变量
var happy = function(){
    return ':)';
}

var sad = function(){
    return ':(';
}

// 该函数接收一个函数(作为参数),又返回一个函数
var mood = function(aFunction){
        return aFunction
}

// 将函数加入一种数据结构,即数组中
list = [happy, sad]

//……加入JavaScript对象
response = {fine: happy, underTheWeather: sad}

// 传入一个函数,调用后返回另一个函数, 并将该函数赋给一个变量
var iAmFeeling = mood(happy);
console.log(iAmFeeling());

// 再来一次
var iAmFeeling = mood(sad);
console.log(iAmFeeling());

// 函数可以被包含在一个数据结构里
// 这里的数据结构是一个JavaScript对象

console.log(response.fine());

// 或者你想使用数组这种数据结构……
console.log(list[0]());

// 最后,直接定义和使用一个匿名函数
console.log(function(){
        return ";)";
}());

从这个例子中可以清楚地看到,函数是JavaScript中的基本单元,是名副其实的一级对象。它可以脱离对象和其他结构单独存在,也可以出现在任何表达式可以出现的地方。和JavaScript中其他对象的区别是:函数可以被调用。由于函数是一级对象,而且是主要的可执行单元,使用函数可以写出短小精悍的代码。和函数相关的作用域产生了一些习惯用法,很多JavaScript新手对此并不熟悉。


JavaScript真的是函数式的吗?

有些人质疑JavaScript是否能称得上是一门函数式编程语言。毕竟,函数式编程是在模仿没有副作用的数学函数。而任何使用过JavaScript的人,都见识过其臭名昭著的全局上下文,而且操作DOM时,几乎百分之百会用到充满副作用的函数。引用透明性就无从谈起了。而且,很多JavaScript程序都知悉周围环境,变量是可变的。纯函数式编程语言使用不可变变量(这带来了很多好处,比如方便实现并发操作)。

JavaScript有对象和基于原型的继承,因此也可以说它是面向对象的,至少它是一种支持多范型的编程语言。

JavaScript包含函数,支持将函数作为一级对象,这是无可争辩的事实。读者可自行选择“函数式编程语言”的定义(因为并不存在一个权威的定义),对JavaScript是否属于函数式编程语言做出自己的判断。本书使用函数式编程是因为其突出了JavaScript中优质的功能。读者如需从这方面更加深入了解JavaScript,可参考Michael Fogus所著的Functional JavaScripthttp://shop.oreilly.com/product/0636920028857.do,O'Reilly出版)一书,该书介绍了很多使用JavaScript的函数式编程技巧,其中很多都使用了underscore.js类库(http://underscorejs.org/)。


2.3.3 函数声明和表达式

JavaScript中的字面函数由以下四部分组成:

  • function操作符;
  • 可省略的函数名;
  • 一对小括号(包含零到多个参数);
  • 一对大括号(包含零到多条语句)。

在JavaScript中,一个合法的最小化函数声明如下所示:

function(){}

函数可以有函数名,这和传统的类C语言的语法风格比较像:

function myFunctionName(){}

没有名字的函数称为匿名函数。匿名函数可以在一个表达式中使用,可以被赋给一个变量。有些人喜欢这样的语法,因为它能让人清楚地意识到变量保存的是一个函数值:

var x = function () {}

具名函数也可以赋给一个变量:

var x = function y() {}

这种使用方式,让函数外部得以用变量x引用函数,函数内部也可以通过函数名y引用该函数(递归调用)。

函数可以关联至一个对象,这时称之为方法。对象被隐式传递给其所调用的方法,方法可以访问和操作对象里的数据,通过this关键字引用对象,如下所示:

var obj = {}; // 创建一个新的JavaScript对象
obj.myVar = 'data associated with an object'
obj.myFunc= function(){return 'I can access ' + this.myVar;} // this: 引用该对象
console.log(obj.myFunc())

可以在函数中定义其他函数,内部的函数可以访问外部函数的变量。一个函数返回一个内部函数时,就形成了闭包。返回的对象既包含了函数本身,也包含创建函数时的环境:

function outer() {
    var val = "I am in outer space";
    function inner() {
        return val;
    }
    return inner;
}

var alien = outer();
console.log(alien());

立即执行函数(immediate function)将代码限定在函数的局部作用域内,避免了污染全局作用域:

(function() {console.log('in an immediate function')}());

2.3.4 函数调用

有四种方式调用函数:

  • 函数;
  • 方法;
  • 构造函数;
  • 使用call()apply()

不同的方式会影响this关键字引用的对象。第一种方式(函数),在非严格模式下,this指全局上下文;在严格模式下,返回undefined或者在执行上下文中得到的值。接下来的两种方式(方法和构造函数)是面向对象编程所特有的,其中方法调用是调用和对象关联的函数,而调用构造函数会创建一个新对象。和以上三种方式不同,call()apply()允许在调用一个函数时,显式指定上下文。

图像说明文字thisthat
JavaScript中有个惯例,乍看之下让人莫名其妙:

var that = this

在理解了JavaScript中的this是如何工作的之后,这种用法就一目了然了。this随上下文(作用域)而变,所以一些开发者将它赋给that,以访问this原来所指的值。

2.3.5 函数参数

前面提到过,函数通过签名中声明的命名参数接收参数。有一个特殊的变量arguments,它保存了所有传入函数的参数,不管是有名的还是没名的。下面的例子展示了如何分别使用标准的函数调用、call()apply()将三个数字相加:

function add(){
    var sum=0;
    for (i=0; i< arguments.length; i++){
        sum+=arguments[i];
    }
    return sum;
}

console.log(add(1,2,3));
console.log(add.apply(null, [2,3,4]));
console.log(add.call(null,3,4,5));

2.3.6 对象

在Java中,对象是所定义的类的实例。在JavaScript中,对象只是一组属性的集合。JavaScript中的对象也有继承(从它的原型那里),面向对象的设计原则对它也是适用的,但是它和Java中的方式大相径庭。可以在JavaScript中创建类(http://www.phpied.com/3-ways-to-define-a-javascript-class/),但是以Java中的方式去思考它是行不通的(Java中的类是基本的、必需的)。

基于类和基于原型的继承让JavaScript和Java如此不同,这也导致了很多疑惑。JavaScript中的其他特性也可以和Java对照着来看。

2.4 面向Java开发者的JavaScript

如果仅从表面上看,大多数开发者认为JavaScript只不过组装了Java或其他类C语言的语法(for循环、条件语句等),但是如果看一个完整的Java程序,差别马上就出来了。下面是一个经典的Java程序,展现了和JavaScript代码以及开发方式上的不同。

2.4.1 HelloWord.java

/**
 * HelloWorld
 */
class HelloWorld{
    public static void main (String args[]){
            System.out.println("Hello World!");
    }
}

想要在命令行里看到这个久负盛名的Hello World程序的输出,你必须:

(1) 创建一个名为HelloWorld.java的源文件;

(2) 将Java代码编译为类文件(在命令行里使用javac编译器);

(3) 执行类文件(在命令行里使用java解释器)。

如果你使用像Eclipse或IntelliJ这样的集成开发环境,这些步骤都有对应的菜单项。很简单,执行该程序,输出“Hello World!”。但是这个简单的例子却说明了Java和JavaScript的诸多不同之处。

下面是对应的JavaScript程序,输出结果是一样的:

console.log('Hello World')

1. 程序执行

首先,JavaScript是一门解释性语言,不需要编译。该在什么环境里执行JavaScript代码呢?如果你安装了node(http://nodejs.org/),可在node的命令行里执行:

> console.log("Hello World")

Hello World

也可以在浏览器控制台里执行。在Chrome中,选择视图→开发者→JavaScript控制台(如图2-2所示)。 图2-2 Chrome JavaScript控制台

图2-2 Chrome JavaScript控制台

在Firefox中,选择工具→Web开发者→Web控制台(如图2-3所示)。

图2-3 Firefox JavaScript控制台
图2-3 Firefox JavaScript控制台

其他一些现代浏览器也提供了类似的功能。


宿主对象

从技术的角度讲,JavaScript本身没有内置输入/输出功能(由运行时环境提供,这里是由浏览器提供)。根据ECMA标准(http://www.ecma-international.org/ecma-262/5.1/#sec-4):

ECMAScript并未被定义成一门计算完备的语言。事实上,本标准未定义如何输入外部数据,也未定义如何将计算结果输出。相反,ECMAScript程序的运行环境不仅负责提供本标准描述的对象和结构,也负责提供和环境相关的宿主对象。对它们的描述超出了本标准的范围,本标准只略微提及它们的一些属性和方法,这些属性和方法可以在ECMAScript程序中访问或调用。

标准中未定义这些不仅是因为好奇,微软的IE浏览器有好几个版本都没有console.log,常常会引发一些不可预料的错误。很多和JavaScript相关的挑战和问题,其责任都在运行环境(通常是浏览器)。DOM是一个跨平台、和语言无关的引用及操作HTML元素的方法,它本身并不是JavaScript语言的一部分。


让我们再回到那个Hello World程序。你可能已经注意到,我们使用了单引号,而不是双引号,也没有以分号结尾。JavaScript的语法更宽容(或者说更含混)。有很多发明来减少因此造成的困惑。语言本身添加了严格模式(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/Strict_mode)。由道格拉斯·克罗克福德开发的JSLint(http://www.jslint.com/)等工具强迫程序员使用JavaScript语言中“好的部分”,你可参考他写的书(http://www.amazon.com/exec/obidos/ASIN/0596517742/wrrrldwideweb)深入了解这方面内容。可以这样说,JavaScript有很多陷阱,不守纪律的开发者或团队使用它会产生一些难于调试的问题。花些时间,好好学习该门语言以养成一些习惯和实践来避免这些问题,是很值得做的一件事。

2. 文件系统组织

Java项目的文件和目录结构与代码的结构是直接相关的。一个源文件通常包含一个(公有)类,类名和文件名相同。文件的目录结构反映了类的包名(对于内部类、访问修饰符和其他一些结构会有例外,但是在Java项目里,文件和目录结构通常都遵循类似的结构)。有时候,这些限制会带来不便(特别是对于小型项目)。随着项目的成长,这种方式清晰地表明了代码的组织结构。不用打开文件,只需瞥一眼目录结构就能立即知晓一个项目是否组织混乱。

JavaScript则没有这种限制,文件或目录结构没有必然的联系。因此,随着项目的成长,你要特别注意代码的组织方式,如果是一个大型开发团队,甚至要花时间来重构代码的组织结构。Alex MacCaw在JavaScript Web Applicationshttp://shop.oreilly.com/product/0636920018421.do,O'Reilly出版)一书中很好地解释了这些:

开发大型JavaScript应用的秘诀是不要开发大型JavaScript应用。你应该将应用分解成一些彼此独立的模块。开发者在开发应用时常犯的错误之一是引入了太多的互相依赖,大量的JavaScript代码生成大量的HTML标签。这样的应用难以维护和扩展,必须想方设法避免。

——Alex MacCaw

关于代码组织,还有其他一些方面需要考虑。除了命名文件和将代码保存在合适的文件里,文件之间的依赖需要依次加载。当JavaScript代码被放到Web服务器上,按需加载可以提升效率(等待所有文件都下载完成会让浏览器僵死)。可使用由RequireJS(http://requirejs.org/docs/whyamd.html)等类库支持的AMD(Asynchronous Module Definition,异步模块定义)API(https://github.com/amdjs/amdjs-api/wiki/AMD)来提升性能。该API按模块定义JavaScript代码,让模块和其依赖能异步加载。

2.4.2 带变量的HelloWord.java

演示Hello World程序的下一步通常是用它跟一个变量代表的名字“打声招呼”:

/**
 * HelloWorld2
 */
class HelloWorld2 {

    public static void main (String args[]) {
        String name;
        System.out.println("Hello " + name);
    }
}

上述代码不能编译:

HelloWorld2.java:5: variable name might not have been initialized

var name;
console.log('Hello ' + name);

上述JavaScript程序可以运行,但它却是让人产生困惑的源头之一:太多的值被当成false。求值的过程充满困惑,难于记忆:

// 结果都是false
console.log(false     ? 'true' : 'false');
console.log(0         ? 'true' : 'false');
console.log(NaN       ? 'true' : 'false');
console.log(''        ? 'true' : 'false');
console.log(null      ? 'true' : 'false');
console.log(undefined ? 'true' : 'false');

下面这些结果是true

// 结果都是true
console.log('0'       ? 'true' : 'false');
console.log('false'   ? 'true' : 'false');
console.log([]        ? 'true' : 'false');
console.log({}        ? 'true' : 'false');

在Java中初始化变量后,程序就可以编译并执行了:

/**
 * HelloWorld2
 */
class HelloWorld2{
    public static void main (String args[]){
        String name = "Java";
        System.out.println("Hello " + name);
    }
}

同样,如果给JavaScript中的变量一个初始值,输出会如我们期待的那样:

var name='JavaScript';
console.log('Hello ' + name)

如果在全局作用域,关键字var并不是必需的,去掉它程序行为没有任何不同。如果是在一个函数里调用,var会创建一个局部变量。一般来说,应该在函数里使用关键字var声明变量,防止污染全局命名空间。

在Java的例子中,声明变量需要指定类型。JavaScript是一种弱类型的语言,不需要这样做。typeof操作符可以显示大多数常用类型信息,如表2-1所示。

表2-1 JavaScript中typeof操作符的例子

类型

结果

例子

Undefined

“undefined”

typeof undefined

Null

“object”

typeof null

Boolean

“boolean”

typeof true

Number

“number”

typeof 123

String

“string”

typeof "hello"

Function object

“function”

typeof function(){}

2.5 最佳开发实践

JavaScript有其自身的挑战和特性,需要特别的开发流程。尽管我们可以死搬硬套熟悉的Java开发流程,但使用适合JavaScript的工具和流程会更好。

2.5.1 编码规范和约定

本书的大部分内容都关乎如何做到客户端和服务器端的松耦合。不可见JavaScript是使客户端UI层实现松耦合的一组最佳实践:

  • 使用HTML定义页面数据结构;
  • 使用CSS为数据结构增加样式;
  • 使用JavaScript为页面增加交互功能。

也可以这样说:

  • 避免在CSS中使用JavaScript;
  • 避免在JavaScript中使用CSS;
  • 避免在HTML中使用JavaScript;
  • 避免在JavaScript中使用HTML。

2.5.2 浏览器

浏览器无处不在,以致于很多Web用户都不清楚它和底层操作系统的区别。浏览器不仅是终端用户浏览网页的环境,它还是个IDE。浏览器集成了调试器、代码格式化工具、分析工具和许多其他工具(有些以插件和扩展的形式存在),在开发过程中会用到它们。

很长一段时间内,Firefox和Firebug(https://getfirebug.com/)等开发插件和扩展是开发中流行的浏览器,Chrome也自带了一些开发者工具,大有后来者居上之势。


Chrome小贴士

花点时间研究浏览器提供的开发者工具和影响浏览器行为的命令行选项是值得的。为了开发者访问方便和提高生产效率,经常会越过一些安全限制或牺牲一点性能,比如在Chrome中:

  • 浏览器有清除缓存(https://groups.google.com/forum/#%21msg/angular/wMRtJZ7R480/5bFH_ZhgdRwJ)等选项,可以防止更改代码时引起的困惑;
  • 根据操作系统和浏览器版本的不同,命令行语法也略有差异,比如通过以下命令可以在OS X上运行Chrome:

    /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome

在命令行里加入一些标签可改变浏览器行为。如果你的文件使用Ajax(比如使用AngularJS),--allow-fileaccess-from-files标签可以让你脱离Web服务器开发。如果你需要使用JSONP引用本机(localhost),则必须使用--disable-web-security选项。

在Chrome中,还有一些隐藏功能。在地址栏里键入chrome://chrome-urls/,会列出所有可用的通往其他配置或管理界面的URL。为了小试牛刀,键入chrome://flags/来列出当前版本的浏览器所提供的实验性功能吧。


浏览器作为JavaScript的开发环境的另一个例子是许多JavaScript开发在线合作网站的出现,比如JSFiddle(http://jsfiddle.net/)和jsbin(http://jsbin.com/)。可以在这些网站上创建示例应用以重现缺陷、和其他人以精确的方式交流语言中的某些特定问题。没有理由使用脱离上下文的JavaScript代码片段。在线上提问或演示某项技术时,提供一个具体的例子或者实现一个小型的演示程序是很正常的。

2.5.3 集成开发环境

WebStorm(http://www.bit.ly/jb-webstorm)是一款非常优秀的IDE,它又轻又快,包括了调试器、一个很棒的项目视图、强大的快捷键、通过使用插件的扩展方式和一些其他功能。虽然不是必需的,但WebStorm通过使用向导、快捷键和代码补全功能,将很多最佳实践付诸其中。

2.5.4 单元测试

有很多为JavaScript开发的单元测试框架(http://www.bit.ly/1gvnROC)。书中用到的Jasmine(http://pivotal.github.io/jasmine/)是一个行为驱动开发(BDD,http://www.bit.ly/1dkUNKU)的框架,它语法简单,而且没有外部依赖。

Java的单元测试能在每次构建项目时通过构建工具或脚本运行。JavaScript的单元测试可使用一个Node.js的模块,Karma(https://github.com/karma-runner/karma,原来叫作Testacular)在多浏览器里执行,并且每当修改并保存源文件后,就会自动执行。这一点很重要。如果你能自律编写单元测试,每当文件保存时就执行单元测试这一能力将能够在早期发现缺陷。这点对JavaScript来说尤其有用,因为JavaScript没有编译器,不能在早期验证代码的合法性。有效的单元测试常常扮演一个伪编译器的角色,它们能够立刻反馈代码的质量,并且一有缺陷,就能检测到。

2.5.5 文档

JavaScript中有很多自动化文档生成工具。JSDoc(https://github.com/jsdoc3/jsdoc)有着和Javadoc类似的标记和输出,Dox(https://github.com/visionmedia/dox)是一个生成文档的Node.js模块。

文学编程(http://en.wikipedia.org/wiki/Literate_programming,由高德纳在20世纪70年代提出)让程序员用他们自己思考的逻辑和流程顺序开发程序。Docco(http://jashkenas.github.io/docco/)是一个Node.js模块,它将代码和注释组织成一种类似文章的格式。虽然Docco不直接验证和执行代码规范,使用它却能鼓励大家使用良好的代码结构和注释,而不是不假思索地复制粘贴。

2.6 项目

这是一个小型的展示对象继承关系的JavaScript项目,包含了单元测试和文档。所有文件均能通过访问GitHub(https://github.com/java-javascript/client-server-web-apps/tree/master/Chapter-2-JavaScript-And-Tools/Animals)获得。

Animal.js是所有对象的根对象:

// Animal处于对象继承关系的顶部
function Animal() {}

// 定义speak方法,子类中有该方法的不同实现
Animal.prototype.speak = function() {
    return "Animal is speaking.";
};

它有两个子类,一个是Cat.js

// 定义Cat类
function Cat() {
    Animal.call(this);
}

// 设置对象的原型
Cat.prototype = new Animal();

// 使用类名为构造函数命名
Cat.prototype.constructor = Cat;

// 实现具体的函数
Cat.prototype.speak = function(){
    return "meow";
}

一个是Dog.js

// 定义Dog类
function Dog() {
    Animal.call(this); // 调用父对象的构造函数
}

// Dog继承自Animal
Dog.prototype = new Animal();

// 更新构造函数以和新类匹配
Dog.prototype.constructor = Dog;

// 替换speak方法
Dog.prototype.speak = function(){
    return "woof";
}

最新版本的Jasmine可在GitHub(https://github.com/pivotal/jasmine)上获得:

curl -L \
https://github.com/downloads/pivotal/jasmine/jasmine-standalone-1.3.1.zip \
-o jasmine.zip

下面是一个测试了上述定义的每个类的单元测试,可以在Jasmine上运行:

// 使用beforeEach测试Animal
describe("Animal", function() {

    beforeEach(function() {
         animal = new Animal();
    });

    it("should be able to speak", function() {
        expect(animal.speak()).toEqual("Animal is speaking.");
    });
});

// Dog继承自Animal,重写了speak方法
// 测试中使用了一个局部变量
describe("Dog", function() {

    it("should be able to speak", function() {
        var dog = new Dog();
        expect(dog.speak()).toEqual("woof");
    });
});

// 还能更简洁一点:Cat继承自Animal
// 在一行内同时调用构造函数和speak方法
describe("Cat", function() {

    it("should be able to speak", function() {
        expect((new Cat()).speak()).toEqual("meow");
    });
});

最简单的测试方式是在浏览器里打开SpecRunner.html,结果如图2-4所示。

图2-4 使用Jasmine的例子

如果需要在文件每次改动后自动运行测试,需要安装Node.js(http://nodejs.org/)。可以通过如下查看Node.js的命令确认是否成功安装了Node.js:

node --version
v0.8.15

还需要安装Node.js的包管理工具:

npm --version
1.1.66

安装了这些后,就可安装Karma:

npm install karma

安装成功后,可通过如下命令查看帮助:

karma --help

项目的配置文件通过init命令生成,该命令会生成一个karma.conf.js文件,后续可以修改此文件,使其指向项目中的JavaScript文件,在浏览器里运行。一经配置,使用start选项,就可以在每次更改文件后自动运行单元测试。

使用npm安装docco模块,用以生成文档:

npm install docco

运行docco命令会在docs文件夹生成HTML文档。图2-5展示了生成的文档。注释在左,高亮语法显示的代码在右。可以通过右上角的下拉列表选择要显示的其他文件:

docco app/*.js

图像说明文字安装Pygments
如果你看见如下信息:“Could not use Pygments to highlight the source”,这是指一个叫Pygments(http://pygments.org/)的使用Python编写的语法高亮显示工具。

在Mac或Linux系统下,使用如下命令安装:

sudo easy_install pygments

我们快速浏览了JavaScript对开发的支持。使用其中的一些工具,能有效提高代码的质量和可维护性。

图2-5 Docco截图

图2-5 Docco截图

本章只是概述了这门复杂且普遍使用的语言。对于这里所讲述的内容,其他图书提供了更为深入的讲解。比如由Alex McCaw所著的Java Script Web Applicationshttp://www.bit.ly/js-web-applications?cc=73c675463d90439868abb117faf4f9a2,O'Reilly出版)一书讲解了构建大型JavaScript应用所使用的模式(模型-视图-控制器)和相关技术。由Nicholas C. Zakas所著的Maintainable JavaScripthttp://shop.oreilly.com/product/0636920025245.do,O'Reilly出版)一书讲解了编写可维护、可扩展的JavaScript代码的实践和标准。当然,还有很多在线资源,比如Mozilla Developer Network(https://developer.mozilla.org/en-US/docs/Web/JavaScript)和StackOverflow(http://stackoverflow.com/questions/tagged/javascript?sort=frequent&pagesize=15)。

JavaScript不是Java,它有自己迅速发展的生态系统,其开发工作有很多项目支持。了解这一语言和相关开发工具,这会为你探索后续将要介绍的很多框架和类库打下基础。

目录