复习 JavaScript

原文:A JavaScript refresh
翻译:dreamana.com

开始之前我们将会涵盖一些 JavaScript 的关键概念。如果你过去几年没有关注 JavaScript 又或者是 JavaScript 新手,我希望这些内容能帮到你。

我们会提及到语言到基础比如变量、函数、作用域以及不同类型,但不会花太多时间在更基础但比如运算符,又或者什么是函数,什么是变量,作为开发者的你应该都知道这些了。我们将通过简单的例子,突出重点,以一个交互开发者的角度,有其他技术比如 Flash(ActionScript3),Java,C#或原生 (C++) 的开发背景,去探讨 JavaScript。

像其他受控语言 (managed language) 一样,JavaScript 运行在一个 JavaScript VM 当中,一个关键的不同点是它不是执行字节码,JavaScript VMs 是基于源代码的,直接翻译源代码为原生代码,通过一个叫 JIT(Just in Time 编译器) 的东西。JIT 在运行时(实时)对代码进行优化工作,取决于不同的架构对平台进行特定对优化。当然,当今大部分浏览器都能运行 JavaScript,下面列出都这些是当前行业界最流行的 JavaScript VMs:

相比起低层语言 (low-level language),JavaScript 提供来一些严谨的便利,比如自动内存分配和垃圾回收机制。虽然这些东西会有速度的开销,但受控语言 提供更多的价值在生产力和平台覆盖方面上。使得现在的开发者们喜欢这些语言多于低层语言,尽管性能有所损失,因为当遇上要发布到多平台上的时候,使用那些低层语言 需花费更高成本。

在我们开始之前,区分出那些浏览器上怎么运作是非常重要的。一方面,我们有核心语言 JavaScript,另一方面,我们有浏览器的 API。历史原因,大多数教程都通常将 JavaScript 和 DOM 合在一块来讲,但他们已经进化成两个强大的独立体。因此我们先谈 JavaScript,之后再深入 DOM 和浏览器 API。这第一篇文章所有提到的只是核心部分,不需要和浏览器 API 打交道,即是纯核心的 JavaScript。 下面列举的是代表着 JavaScript 中通用的核心对象:

要完整的全局对象列表,请查看 Mozilla 的 Global Objects 页。其他对象你可能再 JavaScript 中见过,比如 Window, CanvasRenderingContext2D, XMLHttpRequest 对象,又或者是其他不是 JavaScript 本身去处理的,只是针对特定浏览器的对象,比如网络访问,音频,渲染等等。我们在以后的文章中谈论这些 API。

# 版本

JavaScript 现今是遵照 ECMAScript 规范跨浏览器实现的。直至今日,已经又 5 个版本的 ECMA-262 发布了。“Harmony” 是正在制定当中的最新版:

Edition Date Published
1 June 1997
2 June 1998
3 December 1999
4 Abandoned
5 December 2009
5.1 June 2011
6 (Harmony) In progress

现在大多数浏览器支持 ECMAScript 5.1,以下表格列举了通过了一致性测试的:

Product Version Test Suite Version
Chrome 24.0.1312.57 m ES5.1 (2012-12-17)
Firefox 19 ES5.1 (2013-02-07)
Internet Explorer 10.0 (10.0.9200.16384) ES5.1 (2012-12-17)
Maxthon 3.4.2.3000 ES5.1 (2012-08-26)
Opera 12.14 (build 1738) ES5.1 (2013-02-07)
Safari 6.0.2 (8536.26.17) ES5.1 (2012-12-17)

也有各种 JavaScript 带有在 ECMAScript 中无需实现的附加功能的独立版本。最新的 JavaScript 版本是 1.8。以下表格列举了与 ECMAScript 版本相对应的 JavaScript 版本:

JavaScript version Relation with ECMAScript
JavaScript 1.1 ECMA-262 - Edition 1 is based on JavaScript 1.1.
JavaScript 1.2 ECMA-262 was not complete when JavaScript 1.2 was released. JavaScript 1.2 is not fully compatible with ECMA-262 - Edition 1.
JavaScript 1.3 JavaScript 1.3 is fully compatible with ECMA-262 Edition 1. JavaScript 1.3 resolved the inconsistencies that JavaScript 1.2 had with ECMA-262 while keeping all the additional features of JavaScript 1.2 except == and != which were changed to conform with ECMA-262.
JavaScript 1.4 JavaScript 1.4 is fully compatible with ECMA-262 Edition 1. The third version of the ECMAScript specification was not finalized when JavaScript 1.4 was released.
JavaScript 1.5 JavaScript 1.5 is fully compatible with ECMA-262 Edition 3.

这篇文章中我们会遵照 ECMAScript 5.1 特性集,即大部分浏览器实现的最新版本。我们也将会不时快速过一下 ECMAScript 标准之外的特性的基本信息。当涉及到这些特性,将会明确标出。

# 网页的汇编语言

过去几年,越来越多语言被实现转换出 JavaScript。最近的项目如 Emscripten 已经证实甚至有可能将原生的 C/C++代码编译成 JavaScript。有些人提出 JavaScript 作为网页的汇编语言 的想法。这的确是个有趣的类比。像 TypeScriptDart 语言也已经示范过了,通过交叉编译 (cross-compiling) 成 JavaScript。

TypeScript 是具备可选强类型的 ECMAScript 6 的实现;而 Dart 是更加雄心勃勃的实现方案,作为 Chrome 中能使用的另一门语言运行在理想的 Dart VM 上。最近,像来自 Mozilla 的 asm.js 的努力甚至将这个想法推进到更进一步,提出一个可以被编译器更有效转化的 JavaScript 低层 (low-level) 子集。其他自发创新的,比如 CoffeScript(通常叫做转译器),通过开放语法糖来帮助开发者,而无需处理 JavaScript。

# 无需编译

就像前面提到的,JavaScript 其中一个优美之处就是你无需预编译代码去运行它。你的代码会被直接加载,然后在运行时通过 VM 用 JIT 编译成原生代码。作为一个写 JavaScript 的开发者,可能来自有编译型语言如 C#,Java 或者 ActionScript 开发背景的,你必须一直记住,是没有静态 编译器去预先做优化的,当你点刷新之后所有东西将会在运行时 计算出来。

像 V8 这种 VM 引入了 CrankShaft 引擎,用于在运行时候做关键优化的,通过如 constant folding, inlining, loop-invariant code motion 等优化来帮助提高性能。然而,一定要记住你也可以通过你的代码(优化)来提高性能。在这篇文章中我们将会提到你可以使用的关键优化方法。

Memo

# 工具

你可以自由选择任意文本编辑器去写 JavaScript 代码。这文章中的范例,短小的代码会用 Chrome 的控制台做快速测试。对于更大的项目,会用 WebStorm。你可以照着用这些工具,或者用你自己的。下面是一些写 JavaScript 流行的工具:

现在一起开始写些代码吧。

#REPL

作为一个开发者读到这个,你可能想快速反复地测试一些小东西。传统的,使用基于字节码的语言,你会打代码,点编译,然后字节码会被生成并且运行。每次修改,你要修改源代码,然后好再次点编译并观察发生什么改变。使用 JavaScript,你可以使用 REPL,用更自然灵活的方式去测试东西并且同时编译。那,REPL 代表什么呢?它代表“读取-求值-输出”循环 (read-eval-print loop)。有些基于字节码的语言比如 C#, F#提供了类似的功能,可以很简单地测试一些代码片段,可以在命令行里输入并快速自然地求值。

使用 Chrome 开发者工具或者其他可用的控制台(比如 Firefox 上安装的 Firebug),你可用使用那个控制台直接开始打代码。下图展示 Chrome 的控制台:

当按下回车,你当代码被注入并且执行。我们第一个例子中,当声明一个变量时,控制台只返回了 undefined 因为变量声明不返回值。只要引用已声明当变量将会自动返回它的值:

如果想读取字符串的长度,我们也能直接从控制台中获得自动完成:

下图,取到了字符串的长度:

这提供了一种很好的方式去快速测试一些代码片段。注意:Chrome 开发者工具的控制台上支持多行输入的,只需在打回车 (Enter) 执行代码之前用 Shift+Enter 换行。下图种我们定义已个 foo 函数然后好执行它:

注意:Firefox 中的 Firebug,控制台在一个展开的编辑器界面中支持多行代码输入,回车 (Enter) 换行,Shift+Enter 执行代码。 Firefox 也提供了 ScratchPad 作为开发者工具的一部分。在 ScratchPad 中,可以输入 JavaScript 多行代码,只要选中所需的代码行并按 Shift+F4(MacOS) 或 CTRL+R(Windows) 就能交互地测试。下图展示了 ScratchPad 窗口和控制台显示出结果:

Memo

# 入门指南

如果你来自其他语言比如 C#, C++ 或 ActionScript,你可能已经习惯安装很多工具、编译器、调试器。使用 JavaScript 的一个非常酷的地方就是你实际上只需要一个文本编译器和一个浏览器。当嵌入 HTML 时,JavaScript 代码既可以在 script 标签中内联:

<script>

// some javascript code

</script>

也可以放在一个外部 .js 文件中并引用它,这是更好的习惯:

<script src="js/main.js"></script>

默认地,我们的 JavaScript 代码会在浏览器解析页面的过程种同步执行,自顶向下。因此,不提倡将 JavaScript 代码放在页面的开头;这会导致浏览器等到代码执行练才开始显示页面上的东西。现在,我们仍坚持一个普遍的好习惯,将代码放到 body 标签之前:

...

<script src="js/main.js"></script>

</body>

那样,页面的内容会首先显示出来,一旦那显示列表 (DOM) 载入了,我们的代码就被执行。这也确保了我们的代码获取到 DOM 并且所有对象都可以被编写脚本。我们将会在以后关于 DOM 的文章中花点时间了解 JavaScript 点执行顺序。HTML5 引入了一些新功能如我们提到的时序与 JavaScript 执行顺序。

# 动态类型语言

JavaScript 是一种动态类型语言,意思就是任何变量可以存放任意数据类型。如果那来自静态类型语言,这听起来可能会吓到你。在 JavaScript 种没有静态类型而且也许以后也不会有。可惜,通常认为变量是需要有明确到类型才能进行类型检查。实际上这个不是必须的。类型可以被自动地推断 (type-inference),传递到各处,并提供代码自动完成以及类型检查,即使像 JavaScript 这样的语言。微软的 TypeScript 就是一个好例子。

由于缺少一个类型系统,JavaScript 绝不会强制一个变量的类型,所以你就不能依靠类型进行隐式转换。记住,JavaScript 不依靠静态编译器,VM 直接解析源代码并使用 JIT 在运行时编译成原生代码。在 JavaScript 中创建数组,你可以使用 new 关键字和 Array 函数构造器 (constructor):

var scores = new Array();

或者简单地(使用字面量语法 (literal syntax))

var scores = [];

注意没有类型定义。在运行时 scores 变量会被当作一个数组来计算:

var scores = [];

// outputs: true
console.log ( scores instanceof Array );

因为类型不被强制,变量可以随时持有任意类型:

// store an array
var scores = [];

// store a number
scores = 5;

// store an object
scores = {}; 

// store a string
scores = "Hello";

如果我们尝试调用一个未定义 API,在编译时深不会捕获到错误的,因为没有发生静态的编译:

var scores = []; 

scores.foo();

因为 Array 没 foo 方法,所以将会得到一个运行时异常:

Uncaught TypeError: Object  has no method 'foo'

甚至更简单的对象用同样的东西:

var myObject = {};

myObject.foo();

也会触发以下运行时异常:

Uncaught TypeError: Object  has no method 'foo'

浏览器通过 JavaScript 控制台报告像这样的错误,并且通常还指出触发异常是哪一行代码。在例子中异常没有被捕获,但只要更新一下代码加上 try catch 语句:

var scores = [];

try {
  scores.foo();

} catch (e) {
  console.log ('API not available!');
}

很快我们就会讲回错误处理,但现在先讲下更重要的概念比如变量的定义。

Memo

# 变量与作用域

变量是用 var 关键字声明的:

var score = 12;

但若省略 var 关键字则将变量声明为全局变量:

// declare a global variable
score = 12;

你也猜到,这是不推荐的,声明变量的时候应该都使用 var 关键字。为什么呢?因为 var 关键字影响到作用域。以下代码在 foo 函数中使用一个局部变量,使得外部不能访问它:

function foo() {
    // declare the variable locally
    var score = 12;

    console.log ( score );
}

function foo2() {
    console.log ( score );
}

// triggers: Uncaught ReferenceError: score is not defined
foo2();

注意当上面的代码运行,错误在运行时被捕捉到;记得这是没有静态编译器在此之前捕捉到错误的。省略 var 关键字会让该变量作为全局公开给所有函数:

function foo() {
    // define the variable globally
    score = 12;

    console.log ( score );
}

function foo2() {
    console.log ( score );
}

// outputs: 12
foo();

// outputs: 12
foo2();

另一个使用变量时的重要行为是提升 (hoisting)。这个行为允许你在变量定义之前引用它。尝试引用以个不存在的变量会触发异常:

// triggers: Uncaught ReferenceError: a is not defined
console.log ( a );

但,引用一个变量之后再以 var 声明是可以的,返回它的默认值 undefined:

// outputs: undefined
console.log ( a );

// variable a declared later
var a = 'Hello';

其实背后的情况是所有变量都被移到上下文块顶部,先声明了,但到了我们定义的变量但代码才发生初始化。我们将会看到接下来的章节部分在函数上运用同样的行为。JavaScript 1.5 引入的常量概念在其他语言中也有。常量 (constant) 是非常重要的,在大多数程序中实际上应该要默认使用。

可变性 (mutability) 是一个普通的 bug 之源。一些语言比如函数式编程语言默认基于不变性 (immutability)。使用关键字 const 会确保你的值在初始化之后不被改变。以下代码,我们顶一个叫 LIMT 的常量:

// define a constant
const LIMIT = 512;

// outputs: 512
console.log ( LIMIT );

注意我们到常量是用大写的,这是一个简单区分不变性的好习惯。如果你尝试在允许时改变它的值,原值维持:

// define a constant
const LIMIT = 512;

// outputs: 512
console.log ( LIMIT ); 

// try to overwrite
LIMIT = 45;

// outputs: 512
console.log ( LIMIT );

没有触发异常你可能会觉得奇怪。实际上,有些浏览器会,如 Firefox 13 之后。很可惜现在,const 关键字只有 Firefox 和 Chrome 支持,但 Safari 或者 IE9 和 10 不支持,戏剧性地拖慢了这一特性的实现。结果,如果你照顾大量用户和各种浏览器,就不能使用 const 关键字了。ECMAScript6 定义 const 但用不同的语义类似声明变量那样用 let 语句,用 const 声明常量的地方将会被作用域包围(在函数章节里会提到的概念).

Memo

# 类型转换

正如前面所说的,因为 JavaScript 的动态原本,不能被强制类型。这会使得调试时有所限制,因为变量可以持有任意类型,你可能觉得意外。例如,当一个运行时隐式转换失败,没有异常触发。JavaScript 实际上会在运行时不同场合下进行隐式类型转换。首先,当使用+运算符连接数值和字符串值,字符串类型会优先,并且总能执行串联操作:

// gives: "3hello";
var a = 3 + "hello";

// gives: "hellotrue"
var b = "hello" + true;

如果使用其他算术操作符,数值类型优先:

// outputs: 9
var a = 10 - "1";

// outputs: 20
var b = 10 * "2";

// outputs: 5
var c = 10 / "2";

在对 Number,String,Boolean 类型使用 == 或者 != 运算符时也会发生隐式转换:

// outputs: true
console.log ("1" == 1); // equals to 1 == 1

// outputs: true
console.log ("1" == true); // equals to 1 == 1

// outputs: false
console.log ("1" != 1); // equals to 1 != 1

// outputs: false
console.log ("1" != true); // equals to 1 != 1

// outputs: false
console.log ("true" == true); // equals to NaN == 1

想避免隐式类型转换,并且确保是相同类型与值都相等的话,你可以用严格相等 / 全等 (===)严格不等 (!==) 运算符,这个就永远不会自动执行隐式转换:

// outputs: false
console.log ("1" === 1);

// outputs: false
console.log ("1" === true);

// outputs: true
console.log ("1" !== 1);

// outputs: true
console.log ("1" !== true);

这也是一个好习惯,使用严格运算符取减少含糊性的风险。如果我们需要明确地转换数据类型(显式类型转换),我们可以使用适当的类型转换函数:

// convert a string to a number
var a = Number ("3");

// convert a boolean to a number
var b = Number (true);

// tries to convert a non numeric string to a number
var c = Number ("Hello");

// outputs: 3 1 "NaN"
console.log ( a, b, c );

同样方法,转换一个字符串为数值可以使用 parseInt 和 parseFloat 函数来完成:

// convert a string to a number
var a = parseInt ( "4 chicken" ); 

// convert a boolean to a number
var b = parseFloat ( "1.5 pint" );

// outputs: 4 1.5
console.log ( a, b );

前面我们发现 JavaScript 可以抛出运行时异常,接下来我们就在这部分花点时间。

Memo

# 运行时异常

在所有的项目中我们都需要处理运行时异常。JavaScript 中,像之前看到都,这些异常会被运行时触发或者在代码上显式编写。例如,如果你尝试调用一个对象中没有的方法,这会触发一个运行时错误:

var scores = [];

// triggers: Uncaught TypeError: Object  has no method 'foo'
scores.foo();

任何时候,如果我们需要自己去抛出异常,我们可以使用 throw 关键字和 Error 对象:

throw new Error ('Oops, there is a problem');

正如预期,运行时异常需要处理,不然控制台会输出以下信息:

Uncaught Error: Oops, there is a problem

要处理错误,你可以使用 try catch 表达式。抛出的 Error 中的 message 属性含有错误信息:

try {
    throw new Error ('Oops, there is a problem');

} catch ( e ) {
   // outputs: Oops, there is a problem catched!
   console.log ( e.message + ' catched!');
}

如果我们需要一些逻辑不管在错误异常是否抛出都要执行,可以使用 finally 表达式:

try {
    throw new Error ('Oops, there is a problem');

} catch ( e ) {
    // outputs: Oops, there is a problem catched!
    console.log ( e.message + ' catched!');

} finally {
    // outputs: Code triggered at all times
    console.log ( 'Code triggered at all times' );
}

注意条件性捕获 (conditional catch) 不能像其他语言(如 ActionScript 或 C#)那样的做法 ——将适当类型放在 catch 块中去将异常自动重定向。在 JavaScript 中,我们可以使用单一个 catch 块并在同一块中加上适当的类型检测:

try {
    throw new Error ('Oops, there is a problem');

} catch ( e ) {
    if ( e instanceof BufferError ) {
           // handle buffer error

    } else if ( e instanceof ParseError ) {
           // handle parse error
    }
} finally {
    // outputs: Code triggered at all times
    console.log ( 'Code triggered at all times' );
}

要注意的上 JavaScript 规范中详细说明如果直接在 catch 块中的内联 (inline) 能力。

try {
    throw new Error ('Oops, there is a problem');

} catch ( e if e instanceof BufferError ) {
    // handle buffer error

} catch ( e if e instanceof ParseError ) {
    // handle parsing error

} finally {
    // outputs: Code triggered at all times
    console.log ( 'Code triggered at all times' );
}

很可惜,这个特性不是 ECMAScript 规范的一部分,并且大部分浏览器上不能使用,除了 Firefox,它再一次很好地支持最新的 JavaScript 特性。你可以靠 Firefox 去测试这个特性,但不要用在实际项目中。那性能怎样呢?在 JavaScript 中,异常处理也不怎么影响性能,除了如果在在函数里使用 try catch。所以一定不要这样做:

function test() {
    try {
        var s = 0;
        for (var i = 0; i < 10000; i++) s = i;
        return s;
    } catch ( e ) {};
}

取而代之,将 try catch 移出函数之外:

function test() {
    var s = 0;
    for (var i = 0; i < 10000; i++) s = i;
    return s;
}

try {
    test();
} catch ( e ) {};

接下来,让我们看看 JavaScript 中使不同但数据类型,复合和原始数据类型。

Memo

# 原始与复合数据类型

JavaScript 定义了 6 种数据类型,就像大多数语言,你可以区分成两种类型:

正如预期,原始类型是被作为值来复制的。

var a = "Sebastian";

var b = "Tinic";

var c = a;

a = "Chris"; 

// outputs: Sebastian
console.log ( c );

复合类型 (object) 是其他所有东西,比如像 Window 对象,RegExp, function 等等,是作为引用来传递的。下面例子可以表明这一点:

// create an Array
var a = ['Sebastian', 'Alex', 'Jason'];

// create an Array
var b = ['Sebastian', 'Alex', 'Jason'];

// outputs: false
console.log ( a == b );

即使两个数组含有相同的值,但实际上这里是在比较两个不同的指针,而不是两个相同的值。下面的代码表明不同之处:

// create an Array
var a = ['Sebastian', 'Alex', 'Jason'];

// pass by reference (nothing is copied here)
// b points now to a
var b = a;

// modifying b modifies a
b[1] = 'Scott';

// outputs: ["Sebastian", "Scott", "Jason"]
console.log ( a );

在讲 JavaScript 的特定行为之前,我们先看看布尔型 (boolean)。

Memo

# 布尔型

布尔型的概念也许似所有语言最最简单的部分。然而在 JavaScript 这部分有些值得谈的东西。下面代码突出原始类型作为布尔型检测时会有怎样的行为:

var a = true;

var b = "true";

var c = 1;

var d = false;

// outputs: true
console.log ( a == true );

// outputs: false
console.log ( b == true );

// outputs: true
console.log ( c == true );

// outputs: false
console.log ( d == true );

你注意到了这里的隐式转换了吗?像大多数语言,你可以将任何东西转换成布尔型,通过 Boolean 转换函数或者 Boolean 函数构造器:

var a = new Boolean ( true );

var b = new Boolean ( "true" );

var c = new Boolean ( "false" );

var d = new Boolean ( 1 );

var e = new Boolean ( false );

var f = new Boolean ( undefined );

var g = new Boolean ( null );

// outputs: true
console.log ( a == true );

// outputs: true
console.log ( b == true );

// outputs: true 
console.log ( c == true );

// outputs: true
console.log ( d == true );

// outputs: false
console.log ( e == true );

// outputs: false
console.log ( f == true );

// outputs: false
console.log ( g == true );

逐一我们这里使用 Boolean 构造器来产生 Boolean 对象,而不是用 Boolean 转换函数产生原始类型。最后,在条件表达式中任何类型都会转换成布尔型。下面都代码,条件测试通过并在控制台显示信息:

// converts automatically "false" to new Boolean("false")
if ( "false" ) {
    console.log ("This will be triggered!")
}

Memo

# 数值型

在 JavaScript 中,是没有像 int, uint 或 float 这些概念,因此在小的情况下,一切都是 Number。Number 类型是用于所有小数,并且是用浮点数来代表,在内存中使用 64 位浮点格式。

var a = 4;

var b = 6.9;

var c = -5;

// outputs: number number number
console.log ( typeof a, typeof b, typeof c);

被零除,溢出和下溢都不会触发异常,只会悄悄发生:

// division by zero
var a = 0 / 0;

// number too big
var b = Number.POSITIVE_INFINITY;

// number too big
var c = Number.NEGATIVE_INFINITY;

// outputs: NaN Infinity -Infinity
console.log ( a, b, c );

NaN 不能与 NaN 比较,没有意义:

// outputs: false
console.log (Number.NaN == Number.NaN);

但可以使用 isNaN 函数:

// outputs: true
console.log ( isNaN (Number.NaN) );

现在是时候谈一下 JavaScript 中最重要但核心对象,Object 类型。

Memo

# 对象和属性

创建一个简单对象,我们依赖于 Object 类型。实际上 Object 可以说是 JavaScript 所有东西的核心。等一下我们将回头讲这个。有时候快速定义一个带有少量属性的对象是非常有用的,注意两中语法都是可以的,字面量和非字面量(函数构造器):

// custom object with literal syntax
var person = { name: 'Bob', lastName: 'Groove' };

// custom object with new (object constructor)
var person = new Object();

// create some properties
person.name = 'Bob';
person.lastName = 'Groove';

显然,第一种语法更短而且通常为首选。

注意我们也可以使用 Object.create() API 去创建对象,这允许我们定义这个对象使用哪一个原型 (prototype)。作为一种动态语言,是可以使用多种语法去访问属性的,最常用的一种是点运算符 (.):

// custom object
var person = {};

// create a new property name
person.name = ‘Bob’;

如果你需要动态计算出属性那么也可以使用中括号去访问。请记住这种语法得出的是稍微慢一点的,通常不作为默认方式。而且使得重构更困难。

// custom object
var person = {};

var prop = "name";
person [prop] = 'Bob';

这等价于点运算符语法:

// custom object
var person = {};

person.name = 'Bob';

// outputs: true
console.log ( person.name == person['name'] );

注意总的来说属性访问是往往比较慢,应该尽量少用点。现在所知道的 V8 引擎提供了更快的属性访问,基于另一中方存储存对象属性。其他 VM 使用一个哈希映射 (hashmap) 来查找属性;V8 通过隐藏的类使用一种数组的实现方式,隐藏类被用于有相同类型的对象,存储了访问对象值的偏移值,而不用搜索哈希表。

检查一个属性在一个对象上是否有定义,我们可以使用 in 运算符:

// custom object
var person = { name: 'Bob', lastName: 'Groove' };

// outputs: true
console.log ( "name" in person );

而 hasOwnProperty() API 不会检查原型链,只会直接检查自定义属性:

// custom object
var person = { name: 'Bob', lastName: 'Groove' };

// outputs: false
console.log ( person.hasOwnProperty ("toString") );

// outputs: true
console.log ( person.hasOwnProperty ("name") );

默认地,所有实例属性是动态的,并且可以使用 delete 关键字删除它们:

// custom object
var person = {};

// add a new property
person.age = 40;

// delete it
delete person.age;

// try to retrieve it
// outputs: undefined
console.log(person.age);

由于某些 VM(如 V8(Chrome))的底层机制,所以不推荐删除对象的属性。这样做会导致隐藏类的结构改动和性能损耗。如果你不再需要某个属性,将它设置成 null 而不要删除它。ECMAScript 5 引入了一系列更好控制对象扩展和属性的 API。如果我们想确保不再为某个对象添加属性,我们可以使用 Object.preventExtensions() API:

// custom object
var person = { name: 'Bob', lastName: 'Groove' };

// prevents any extensibility
Object.preventExtensions(person);

// set the age
person.age = 25;

// outputs: undefined
console.log ( person.age );

// delete the name property
delete person.name;

// outputs: undefined
console.log ( person.name );

考虑到,对象上的方法其实就是引用着某些函数的属性,所以我们这个对象也不能再增加任何方法。然而,我们这个对象的属性仍然可以被删除。任何时候,我们可以检测这个对象是是否可以扩展,通过使用 Object.isExtensible() API:

// custom object
var person = { name: 'Bob', lastName: 'Groove' };

// prevents any extensibility
Object.preventExtensions(person);

// outputs: false
console.log ( Object.isExtensible ( person ) );

如果我们想阻止任何扩展和删除,我们还可以通过 Object.seal() API 去封闭这对象。一旦对象被封闭了,原有属性仍然可以被改变和读取,但是不能增加新东西并且删除也是禁止的:

// custom object
var person = { name: 'Bob', lastName: 'Groove' };

// we seal our object
Object.seal ( person );

// outputs: Bob
console.log ( person.name );

// attempt to null the property
person.name = null;

// outputs: null
console.log ( person.name );

// change the name
person.name = "David";

// outputs: David
console.log ( person.name );

// attempt to create a new property
person.age = 30;

// outputs: undefined
console.log ( person.age );

// attempt to delete the property
delete person.name;

// outputs: David
console.log ( person.name );

最后,如果我们真的想要确保所有级别的不变性,可以使用 Object.freeze() API:

// custom object
var person= { name: 'Bob', lastName: 'Groove' };

// we freeze our object
Object.freeze ( person );

// attempt to change the name value
person.name = "David";

// outputs: Bob
console.log ( person.name );

// attempt to delete the property
delete person.name;

// outputs: Bob
console.log ( person.name );

// attempt to null the property
person.name = null;

// outputs: Bob
console.log ( person.name );

检查任意对象是否被封闭或者冻结,我们可以使用 Object.isSealed() 和 Object.isFrozen() APIs:

// outputs: true
console.log ( Object.isSealed (person) );

// outputs: true
console.log ( Object.isFrozen (person) );

注意,对象可以同时被封闭和冻结,一旦被封闭或者冻结,就不能撤销 (undo)。总结如下:

如果我们需要更细化的级别,实际可以通过 Object.getOwnPropertyDescriptor() API 返回的一个属性描述对象去更多了解每个属性。下面的代码,我们读取 person 的属性描述对象:

// custom object
var person = { name: 'Bob', lastName: 'Groove' };

// retrieve the property descriptor for ‘name’
var desc = Object.getOwnPropertyDescriptor(person, 'name');

// outputs: true
console.log(desc.writable);

// outputs: true
console.log(desc.configurable);

// outputs: true
console.log ( desc.enumerable );

// outputs: "Bob"
console.log(desc.value);

一个属性检查对象有以下属性:

默认状态,所有属性都是 configurable, enumerable 和 writeable 的,但如果我们 seal 那个对象,配置就会修改:

// custom object
var person = { name: 'Bob', lastName: 'Groove' };

// we seal the object
Object.seal ( person );

// retrieve the property descriptor for ‘name’
var desc = Object.getOwnPropertyDescriptor(person, 'name');

// outputs: true
console.log(desc.writable);

// outputs: false
console.log(desc.configurable);

// outputs: true
console.log ( desc.enumerable );

// outputs: "Bob"
console.log(desc.value);

如果我们 freeze 它,那么所有都会被锁住,除了 enumerable:

// custom object
var person = { name: 'Bob', lastName: 'Groove' };

// we freeze the object
Object.freeze ( person ); 

var desc = Object.getOwnPropertyDescriptor(person, 'name');

// outputs: false
console.log(desc.writable);

// outputs: false
console.log(desc.configurable);

// outputs: true
console.log ( desc.enumerable );

// outputs: "Bob"
console.log(desc.value);

在程序中需要确保某些不变性的级别的情况下,封闭或冻结一个对象是很有用的。就如刚才看到的,Object.getOwnPropertyDescriptor() API 返回任意属性的性质描述。你可能在想,是否能够定义一个新属性同时定义它的性质?可以,这也行。直到现在,我们用点运算符定义新属性: (译者注:为了区分,将原文的 property 译作“属性”,attribute 译作“性质”)

myObject.foo = myValue;

使用这个语法实际上是一个快捷方式,使属性默认是 enumerable, configurable 和 writeable. ECMAScript 5 定义了一个更细化的 API 叫做 Object.defineProperty(),允许你使用一个属性描述器定义属性性质。这个 API 有以下特征:

Object.defineProperty(obj, prop, descriptor)

下面的代码,我们创建一个 name 属性,使它 writeable, enumerable 和 configurable:

// custom object
var myObject = {};

Object.defineProperty(myObject, "name", {value : 'Bob',
                               writable : true,
                               enumerable : true,
                               configurable : true});

// outputs: Bob
console.log ( myObject.name );

假设我们将所有性质设置成 true,我们可以枚举这个属性,修改,甚至删除它:

// custom object
var myObject = {};

// we create the property name with specific attributes
Object.defineProperty(myObject, "name", {value : 'Bob',
                               writable : true,
                               enumerable : true,
                               configurable : true});

// outputs: Bob
console.log ( myObject.name );

// outputs: name
for ( var p in myObject ) {
    console.log ( p );
}

// we update the name
myObject.name = 'Stevie';

// outputs: Stevie
console.log ( myObject.name );

// we delete the property
delete myObject.name;

// outputs: undefined
console.log ( myObject.name );

如果我们修改性质,我们可以更细分,禁止配置或更新但仍然允许枚举:

// custom object
var myObject = {};

// we create the property name with specific attributes
Object.defineProperty(myObject, "name", {value : 'Bob',
                               writable : false,
                               enumerable : true,
                               configurable : false});

// outputs: Bob
console.log ( myObject.name );

// outputs: name
for ( var p in myObject ) {
    console.log ( p );
}

// write access fails silently
myObject.name = 'Stevie';

// deletion fails silently
delete myObject.name;

// outputs: Bob
console.log ( myObject.name );

甚至更强大,如果我们用 get 和 set 性质作为描述对象的一部分,我们可以用相同的 API 去定义 getter/setters 的实现:

// custom object
var myObject = {};

// we define the getter
function getter() {
    return this.nameValue;
}

// we define the setter
function setter(newValue) {
    this.nameValue = newValue;
}

// we create the property name with specific attributes
Object.defineProperty(myObject, "name", {
                                   get: getter,
                                   set: setter});

// we change the value
myObject.name = 'Stevie';

// outputs: Stevie
console.log ( myObject.name );

我们给 name 属性定义 getter/setter。注意我们使用了 nameValue 别名作去引用属性,但其实可以用其他值,用 foo 也一样可以。

// we define the getter
function getter() {
    return this.foo;
}

// we define the setter
function setter(newValue) {
    this.foo = newValue;
}

我们现在能完全控制这个值的读和写。下面的代码,确保了任何从 name 属性读入的字符串总能正确表现大小写:

// we define the getter
function getter() {
    return this.nameValue.charAt(0).toUpperCase()+this.nameValue.substr(1).toLowerCase();
}

下面的代码,我们使用一个大小写不敏感的字符串,当取回这个值的时候,字符串就正确地被格式化了:

// custom object
var myObject = {};

// we define the getter
function getter() {
    return this.nameValue.charAt(0).toUpperCase()+this.nameValue.substr(1).toLowerCase();
}

// we define the setter
function setter(newValue) {
    this.nameValue = newValue;
}

// Example of an object property added with defineProperty with a data property descriptor
Object.defineProperty(myObject, "name", {
                                   get: getter,
                                   set: setter});

// we change the value
myObject.name = 'stevie';

// outputs: Stevie
console.log ( myObject.name );

很强大对吗?注意 Object 类还有一个 defineProperties() API,允许你一次定义多个属性。最后,要枚举一个对象中的属性,我们可以用 Object.keys() API:

// custom object
var person = { name: 'Bob', lastName: 'Groove' };

// outputs: ["name", "lastName"]
console.log ( Object.keys ( person ) );

你可能会想是否有不变性 (immutability) 让属性读取得更快呢?很不幸,没有。要注意这些 API 对 Object 进行 seal, freeze 或禁止 extension 过去一直很慢,让属性读取更慢。最近一些测试表明性能比过去好了许多,但就性能影响方面而言还是多留个心眼。

Memo

# 几乎所有东西都是对象

在 JavaScript 中,几乎所有东西都是一个 Object。让我们试试看下面的代码:

function foo(){};

// outputs: true
console.log ( foo instanceof Object );

var countries = ['USA', 'FRANCE'];

// outputs: true
console.log ( countries instanceof Object );

var person = { name: "Bob", lastName: "Groove" };

// outputs: true
console.log ( person instanceof Object );

不出所料,复合类型是一种 Object。甚至 function,后面再来谈这个。但原始类型,比如像字符串 , 数字或者布尔值呢?

var result = true;

// outputs: false
console.log ( result instanceof Object );

var name = 'Bob';

// outputs: false
console.log ( name instanceof Object );

var score = 190;

// outputs: false
console.log ( score instanceof Object );

那么你可能觉得奇怪了,怎么这些类型会有方法定义在它们上面?如在字符串调用 length:

var name = 'Bob';

// outputs: 3
console.log ( name.length );

在背后,当原始类型的方法被调用的时候会创建一个包装对象 (wrapping object)。这个术语叫 “boxing”。在运行时中,上面的代码实际上会在内部生成这些:

// outputs: 3
console.log ( (new String (name)).length );

一旦调用 length getter,那个包装对象(这里表现为一个盒子)就会被抛弃并垃圾回收。这就是无法在原始类型中存储数据的原因。我们写进的临时对象会立刻被抛弃。过后取回我们的属性会创建另一个盒子结果会返回 undefined:

var name = 'Bob'; 

// write some data
name.foo = 'Some Data'; // equals to (new String (name)).foo = 'Some Data';

// outputs: undefined
console.log ( name.foo ); // equals to console.log ( (new String (name)).foo );

但用函数构造器创建的原始类型上存储数据是可以的,因为没有发生隐式 boxing/unboxing:

// create the string with new (function constructor)
var name = new String("Bob");

// write some data
name.foo = "Some Data";

// outputs: Some Data
console.log(name.foo);

现在的 JavaScript 虚拟机处理 boxing/unboxing 是够快的了,不用担心太多他的性能影响。 另外,null 和 undefined 都不是 Object 类型。下面代码演示了:

// outputs: false
console.log ( null instanceof Object );

// outputs: false
console.log ( undefined instanceof Object );

Memo

# null 与 undefined

刚才我们看到,null 与 undefined 上一种特殊类型。在 JavaScript 中,任何没有初始化的变量都是 undefined:

var myObject;
var i;

// outputs: undefined undefined
console.log ( myObject, i );

未定义的属性也一样:

var myObject = { name: "Bob" };

// outputs: undefined
console.log ( myObject.firstName );

有些语言有些时候初始化原始类型值或对象为 null。这能让开发者使用 null 作为初始化检测。被运行时自动初始化为 null 是不会发生在 JavaScript 中的,一定要记住这一点。如果你忘记了,你可能被诱导去用许多 null 检测在代码中,像下面这样:

var myArray;

// if the Array is not initialized, then initialize it
if ( myArray == null ) {
  myArray = new Array();
  console.log ('Array initialized');
} else console.log ('already created');

这问题是这里的 null 检测不是真的可靠。记住,我们的 Array 没有被初始化,因此返回 undefined。再加上,null 和 undefined 是两中不同的类型,但当不是用严格等于的时候比较它们会判定为 true:

// outputs: object undefined
console.log ( typeof null, typeof undefined );

// outputs: true
console.log ( undefined == null );

还记得隐式转换吗?我们这里用的是 == 操作符。如果用严格等于操作符 (===),类型和值都检测,那么就失败了:

// outputs: false
console.log ( undefined === null );

记得早前我们用严格运算符去解决含糊。这里再一次证明是有用的。前面的代码现在就进入了 else 区块:

var myArray;

// if the Array is not initialized, then initialize it
if ( myArray === null ) {
  myArray = new Array();
  console.log ('Array initialized');
} else console.log ('already created');

你也可以只依赖于一个布尔运算,简单地做一个 if not:

var myArray;

// if the Array is not initialized, then initialize it
if ( !myArray ) {
  myArray = new Array();
  console.log ('Array initialized');
} else console.log ('already created');

注意这个检测,内部会转化为:

if ( !Boolean(undefined) )

为了保持我们最初尝试的方法的连贯性,只需将我们的变量设为 null,这是种好习惯,明确地强调它没有被初始化,但预计稍后将会:

// initialize to null
var myArray = null;

// if the Array is not initialized, then initialize it
if ( myArray == null ) {
  myArray = new Array();
  console.log ('Array initialized');
} else console.log ('already created');

我们的代码变得足够可靠,甚至使用严格等于:

// initialize to null
var myArray = null;

// if the Array is not initialized, then initialize it
if ( myArray === null ) {
  myArray = new Array();
  console.log ('Array initialized');
} else console.log ('already created');

如果运行代码将输出:

Array initialized

Memo

现在一起来看看循环。

# 循环

循环是编程语言中的一个重要部分。JavaScript 支持所有基本循环类型,比如 for, for in, while 和 do while:

var lng = 200;

// classic for
for ( var i = 0; i < lng; i++ )  { 
}

var myObject = { name: "Bob", age : 30 };

// object enumeration
for ( var p in myObject ) {
    /* outputs:
    name : Bob
    age : 30
    */
    console.log ( p, " : ", myObject[p] );
}

var i = 0; 

// while loop
while ( i < lng ) {
    console.log ( i );
    i++;
}

// do while loop
do { 
    console.log ( i );
    i++;
} while ( i < lng )

有些其他语言还提供支持 for each 循环。在 JavaScript 里,采用了更函数化的途径去实现,就是用 Array.forEach() API,稍后将提及到。 要注意的是,使用 for in 循环去枚举对象的属性,因为 ECMA-262 没有指定枚举顺序,这是与执行程序有关的 (implementation dependent) 而且通常大部分浏览器的做法都是匹配定义的顺序。

// custom object
var myObject = { name: "Bob", age: 20 };

// enumerate
for ( var p in myObject ) {
    /*
    // outputs:
    name
    age
    */
    console.log ( p );
}

如果我们改变定义的顺序,会得到相反的枚举:

// custom object
var myObject = { age: 20, name: "Bob" };

// enumerate
for ( var p in myObject ) {
    /*
    // outputs:
    age
    name
    */
    console.log ( p );
}

然而会有例外的情况,如果你有数字型的属性,这些会列在非数字型的前面。

// custom object
var myObject = { age: 20, name: "Bob", "12":"2343" };

// enumerate
for ( var p in myObject ) {
    /*
    // outputs:
    12
    age
    name
    */
    console.log ( p );
}

就性能而言,for in 循环倾向于慢,当性能是关键需求的时候千万不要严重依赖它。

Memo

# 数组

作为开发者,我们可能在开发当中不断地使用数组。我们不断地使用它们去存储,引用,循环,迭代,几乎到处都是。 下面的代码,我们创建一个数组并且在构造器中初始化它的长度:

// create an Array
var myArray = new Array(5);

// outputs: 5
console.log ( myArray.length );

注意如果多于一个参数,取而代之是数组构造器会将这些值添加到数组中:

// create an array
var myArray = new Array(5, 10, 30, 20);

// outputs: [5, 10, 30, 20]
console.log ( myArray );

如果设定的长度大于数组本身长度,会用 undefined 填充:

// create an Array
var myArray = new Array(5);

// outputs: 5
console.log ( myArray.length );

// increase the array size
myArray.length = 7;

// outputs: undefined
console.log ( myArray[6] );	

同样方法,添加额外的逗号会在数组中增加 undefined 值:

// create an Array
var myArray = ["Bob", "James", , "Tom"];

// outputs: 4
console.log ( myArray.length );

// outputs: undefined
console.log ( myArray[2] );

数组也是一个对象可以像映射一样用。在下面的代码,我们在它上面创建动态属性。命名了的,非数字属性不会影响数组长度:

var myArray = [];

// create a custom dynamic property
myArray['name'] = 'Bob';

// outputs: 0
console.log(myArray.length);

// outputs: Bob
console.log(myArray['name']);

此时,一个数组基本上被当作纯对象。 在 Array 上使用的 API 非常类似于其他语言。你可以像过去一样用赋值器和访问器 API 如 pop(), push(), splice() 等等:

// create an Array
var myArray = new Array(200);

// outputs: 200
console.log ( myArray.length );

// push one element
myArray.push ( 50 );

// outputs: 201
console.log ( myArray.length );

但是不仅仅这些。 ECMAScript 5.1 引入了一系列数组对象的遍历 API,下面用一些例子举例说明如何使用它们:

forEach: 给数组中每一个元素调用一个函数

// some values
var values = [1, 2, 3]

/*
outputs:
1
2
3
*/
values.forEach(function(item) { console.log ( item ) });

every: 如果数组中每一个元素满足所提供的测试函数则返回 true

var data = [ 12, "bobby", "willy", 58, "ritchie" ];

function every ( element, index, source ) {
    return ( element instanceof Number );
}

// is this an Array containing numbers only?
var onlyNumbers = data.every ( every );

// outputs : false
console.log( onlyNumbers );

filter: 创建一个新数组,含有所提供的过滤函数返回 true 的所有元素

var users = [ { prenom : "Bobby", age : 18, sexe : "H" },
            { prenom : "Linda", age : 18, sexe : "F" },
            { prenom : "Ritchie", age : 16, sexe : "H"},
            { prenom : "Stevie", age : 15, sexe : "H" } ]

function some ( element, index, source ) {
    return ( element.sexe == "F" );
}

// is there a female in this Array?
var result = users.some ( some );

// outputs : true

map: 创建一个新数组,含有每个元素调用过所提供函数的结果

var users = [ { name : "Bobby", age : 18 },
                        { name : "Willy", age : 21 },
                        { name : "Ritchie", age : 16 },
                        { name : "Stevie", age : 21 } ];

function filter ( element, index, source ) {
    return ( element.age >= 21 );
}

var legalUsers = users.filter ( filter );

function browse ( element, index, source ) {
    console.log ( element.name, element.age );
}

/* outputs :
Willy 21
Stevie 21
*/
legalUsers.forEach( browse );

reduce: Apply a function simultaneously against two values of the array (from left-to-right) as to reduce it to a single value.

var values = [20, 30, 40, 50];

function reduce ( previousValue, currentValue, index, source ) {
    return previousValue + currentValue;
}

// we create a table that holds the reduction
var reduced = values.reduce ( reduce );

// outputs : 140
console.log ( reduced );

reduceRight: Apply a function simultaneously against two values of the array (from right-to-left) as to reduce it to a single value.

var values = [20, 30, 40, 50];

function reduce ( previousValue, currentValue, index, source ) {
    return previousValue + currentValue;
}

// we create a table that holds the reduction
// reduceRight starts from the right
var reduced = values.reduceRight ( reduce );

// outputs : 140
console.log ( reduced );

虽然这些 API 都是很强大的,但都比较慢。再一次提醒,如果需要注重性能,不能太依赖使用他们。例如,map 函数可以用下面的代码代替:

var names = ["bobby", "willy", "ritchie"];

var lng = names.length; 

for (var i = 0; i < lng; i++) {
  var item = names[i];
  names[i] = item.charAt(0).toUpperCase() + item.substr(1).toLowerCase();
}

这个版本尽管没那么优雅,但会运行得更快。为什么呢?背后有一些原因:

JavaScript 不支持字典 (dictionary)。以下代码,我们用一个简单的对象去映射另一个含有坐标信息的对象

// create an Array
var myArray = [];

// an object key
var key = {}

// map the canvas to a custom literal object
myArray[key] = { x : 400, y : 400};

// outputs: Object {x: 400, y: 400}
console.log(myArray[key]);

这个例子会让你误以为数组可以当成字典用。背后其实是那个 key 对象被转成字符串,等价于创建一个 [object Object] 属性:

// map the canvas to a custom literal object
myArray[key] = { x : 400, y : 400}; // equals to: myArray["[object Object]"] = { x : 400, y : 400};

当处理数组的时候,性能是经常提到的话题。接下来部分,我们将花一些时间讲数组的性能优化。下面的代码我们循环数组的元素:

// some names
var names = ["Daniel", "Divya", "Veronique"];

// store the length
var lng = names.length;

/* outputs:
Daniel
Divya
Veronique
*/
for (var i = 0; i < lng; i++) {
    var name = names[i];
}

注意我们在 lng 变量里缓存了数组的长度尔不是每次迭代的时候重新计算长度。你可以保持这种好习惯。每次重新计算长度会非常影响性能。 此外,因为希望简洁,通常都会习惯于使用字面量语法 (Literal Syntax) 去创建比如数组这样的对象:

// empty array
var data = [];

for (var i = 0; i < 1000000; i++) {
    data[i] = i;
}

注意我们的数组是空的,我们没有定义长度去预设 (pre-allocate) 它。然而,一些 VM 比如 V8 或 SpiderMonkey 默认是不预设数组的,上面的代码确实会比下面的代码快很多:

var lng = 500000;

// slower than using [] or new Array()
var data = new Array(lng);

for (var i = 0; i < lng; i++) {
    data[i] = i;
}

这性能提升有 5% 和 10%,但不止。还有,也不推荐在数组内使用混合类型。即使这不是常用,但要小心这显然会强制 VM 去处理它们,消耗花时间去转换非预期的类型。下面代码我们计算总和:

// some values of the same type
var values = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

var sum = 0;

var lng = values.length;

for ( var i = 0; i< lng; i++ ) {
    sum += values [ i ];
}

现在,如果数组内有多种类型,这代码会运行得更慢:

// some mixed types
var values = [ 1, 2, "3", 4, 5, 6, 7, 8, 9, "10"];

var sum = 0;

var lng = values.length; 

for ( var i = 0; i< lng; i++ ) {
    sum += values [ i ];
}

此外,也要避免数组有空洞,这些洞是由使用过 delete 或者将指定的索引设置为 null:

// some scores
var scores = [200, 100, 456, 231, 800, 453];

// create a hole
scores [2] = null;

// create another hole
delete scores [4];

使用 length 属性置空数组也是一种非常有效的方法。 通常的方案是在循环当中,你可能总想着分配一个新数组去重置它。比起每次都重新创建空白实例那么大时间和内存开销,其实只需将它的 length 设为 0

// create an Array
var myArray = new Array(5);

// outputs: 5
console.log ( myArray.length );

// empties the array
myArray.length = 0;

// outputs: 0
console.log ( myArray.length );

最后,当索引寻找数组中的元素,将那个值存到本地变量值,避免使用数组语法(中括号),那个慢,因此如下代码:

for ( var i = 0; i< lng; i++ ) {
    myArray[i].x = Math.random()*500;
    myArray[i].y = Math.random()*500;
    myArray[i].friction = Math.random();
}

写成这样会更好:

for ( var i = 0; i< lng; i++ ) {
    // store a reference in a local variable
    var element = myArray[i];
    element.x = Math.random()*500;
    element.y = Math.random()*500;
    element.friction = Math.random();
}

在新增的迭代 API 里面,JavaScript1.7 引入了属性迭代器。让我们去看一下。

Memo

# 迭代器

JavaScript 1.7 引入了迭代器,通过 Iterator 对象。迭代器是非常有用的,使得开发者可以减少迭代时引入的状态数量。原来,没有迭代器,在 JavaScript 里面循环遍历一个数组会像这样:

var countries = [ 'France', 'USA', 'Japan' ];

var lng = countries.length; 

for ( var i = 0; i< lng; i++) {
  // outputs:
  France
  USA
  Japan
 console.log ( countries[i] );
}

注意到这里定义了的变量数量,变量 i 递增,与一个最大值长度比较,而且变量 i 在数组还用于指示索引,有很多容易犯错的地方。我们如何去减少到最低并且写出更安全的代码呢?这就让迭代器来解救:

// some data
var countries = [ 'France', 'USA', 'Japan' ];

// create the Iterator through the Iterator function
var it = Iterator(countries);

// outputs: [0, "France"]
console.log ( it.next() );

// outputs: [1, "USA"]
console.log ( it.next() );

// outputs: [2, "Japan"]
console.log ( it.next() );

多亏有迭代器,更少的变量定义,我们从 Interator 对象里面查询下一个 item,这就够了。但要注意,现在迭代器只作为 JavaScript 1.7 的一部分,不是 ECMAScript。因此,Chrome 或者 IE 是不支持这样一个特性的,只有 Firefox。 基本上要避免在重要的项目上使用迭代器。

Memo

# 日期

Date 类返回当前时间的信息,但也常常通过 Date.now() API 去做基准测试。用下面例子来说明:

var start = Date.now();

var iterations = 50000000;

var buffer = new Array(iterations);

for(var i = 0; i < iterations; i++) {
  buffer[i] = i;
}

// outputs: 2703
console.log(Date.now() - start);

We capture the time (ms) before the code we want to benchmark is triggered, once finished we compare the two. Note that iterating over this array takes around 3 seconds today in Chrome 24 and around 2 seconds in Safari 6 and Firefox 18 on MacOS. The use of a Date object to calculate performance is the most popular way to benchmark JavaScript code, but some more advanced techniques are being used today too. Here are some of the limitations of the Date.now() approach:

To get more granular performance metrics, we can rely on the new performance.now() API part defined on the performance property. This API returns a floating point since the page started loading providing microseconds in the fractional, which can be very useful for benchmarking. Let’s have a quick look at the difference:

// outputs: 1359349137666
console.log( Date.now() );

// outputs: 22.16799999587238
console.log ( window.performance.now() );

If we change our previous benchmark to use the performance.now()API, we get a more granular number:

var start = window.performance.now();

var iterations = 50000000;

var buffer = new Array(iterations); 

for(var i = 0; i < iterations; i++) {
  buffer[i] = i;
}

// outputs: 3303.2060000114143
console.log(window.performance.now() - start);

This API is part of the navigation timing feature available now in Chrome, Firefox and IE9 but unfortauntely not Safari or Opera. We will come back to this feature in a future article to study sequencing around loading and initialization.

Memo

# 函数

JavaScript has support for first-class functions, which means they can be passed as parameters to other functions, or be returned, assigned to variables and stored into arrays. Actually, functions can be declared three ways, as named, as an expression or anonymous:

// named function
function sayHello () {
  console.log('Hello');
}

// function expression
var sayHello = function() {
  console.log ('Hello');
}

// anonymous function used as the listener
button.addEventListener ("click", function (e) {
  console.log ( e.currentTarget );
})

One first difference between the two is hoisting. Remember we saw previously that behavior with hoisting of variables. With functions, the named definition is always interpreted first, so if we call sayHello() before defining it, it will just work:

// outputs: First definition
sayHello();

// named function
function sayHello () {
  console.log('First definition');
}

// function expression
var sayHello = function() {
  console.log ('Second definition');
}

// outputs: Second definition
sayHello();

Because named functions are interpreted first, function expressions always have precedence over named functions:

// function expression
var sayHello = function() {
  console.log ('First definition');
}

// named function
function sayHello () {
  console.log('Second definition');
}

// outputs: First definition
sayHello();

Finally, named functions cannot be renamed, whereas with a function expression, the variable pointing to it can be renamed.

Note that we can simply inline the call right after its definition, this technique called an immediately invoked function expression (or IIFE) allows us to define the function and trigger it at the same time:

// named function with call inlined
(function sayHello() {
  console.log('First definition');
}());

Note that this syntax can also be useful to prevent from polluting the global scope (Window). By declaring variables with var inside a function, these definitions will be scoped only inside the function. In the code below, we only expose a global entry point through the getUsers function, other functions will behave as private:

(function () {

  getUsers = function () {
    // entry point exposed to the global namespace
    return 'getting users';
  }

  var checkTime = function () {
    // logic to check time
  }

  var authenticate = function () {
    // logic for authentication
  }
}());

// outputs: getting users
console.log ( getUsers() );

// triggers: Uncaught ReferenceError: checkTime is not defined
console.log ( checkTime() );

Note that the getUsers definition is now global. If we want even better control, we can rely on the reveal module pattern:

var module = (function () {

  var getUsers = function () {
    // entry point exposed to the global namespace
    return 'getting users';
  }

  var checkTime = function () {
    // logic to check time
  }

  var authenticate = function () {
    // logic for authentication
  }

  return {
    getUsers:getUsers
  }

}());

// outputs: getting users
console.log ( module.getUsers() );

// triggers: Uncaught ReferenceError: checkTime is not defined 
checkTime();

Through this pattern, we no longer rely on a global definition. We externalize safely the definitions we want to make public or not, without ever polluting the global scope.

Let’s have a look now at a function itself and what we can do with it:

function calculate(a, b, c){};

// grab the number of parameters
// outputs: 3
console.log (calculate.length);

// store data
calculate.x = 12;

// retrieve it
// outputs: 12
console.log (calculate.x);

As we saw earlier, functions are objects, and like with arrays, we can use the brackets syntax to access or write properties, which can be useful if we need to evaluate the properties dynamically:

function calculate(a, b, c){};

// grab the number of parameters
// outputs: 3
console.log (calculate.length);

// store data
calculate['x'] = 12;

// retrieve it
// outputs: 12
console.log (calculate['x']);

We can also easily access all the parameters passed to a function using the arguments array:

function average() {
    var total = 0;
    var lng = arguments.length;

    for ( var i = 0; i< lng; i++ ) {
        total += arguments [ i ];
    }
    return total / lng;
} 

// outputs: 263.5
console.log ( average ( 10, 29, 893, 122 ) );

All functions create a local variable arguments in the function body when they’re called which holds all the parameters passed. It is also worth noting that variables defined in a parent function are accessible inside a nested function (closure):

function foo() {
    var a = 'From parent function';

    var innerFoo = function() {
        console.log ( a );
    }();
}

// outputs: From parent function
foo();

On the other hand, the parent function, has no way to access local variables from the inner function:

function foo() {
    var a = 'From parent function';

    var innerFoo = function() {
        var b = 'From inner function';
        console.log ( a );
    }();
    console.log ( b );
}

// outputs: Uncaught ReferenceError: b is not defined
foo();

Memo

# 执行的上下文

One of JavaScript difficulties resides in understanding how the this keyword behaves. If you have been developing with ActionScript, the this keyword behaves like with ActionScript 1 and 2. You may think that this will always point to the original context the function has been defined on, it actually points to the context of execution:

function foo() {
  console.log ( this );
}

// outputs: Window {top: Window, window: Window, location: Location, external: Object, chrome: Object…}
foo();

By default, the global scope is the Window object, the core document class of the browser. In this example, the function becomes a method of the Window object:

// outputs: Window {top: Window, window: Window, location: Location, external: Object, chrome: Object…}
window.foo();

In this example, two variables x and y are defined on the global scope, therefore properties of the Window object:

// some position
var x = 200;
var y = 300;

function foo() {
  console.log ( this.x, this.y, x, y );
}

// outputs: 200 300 200 300
foo();

In this case, this points to the global context (Window), where our properties are defined. By omitting this, we implicitly target the global scope and can also access the x and y properties. At runtime, the VM will look for variables defined in the local scope of the foo function, if not available, it will look for them in the parent context. If local variables are defined inside the function, these will be chosen first. Therefore, the this keyword allows us to resolve ambiguity and always target the current context of execution, not the local scope:

// some position
var x = 200;
var y = 300;

function foo() {
    // local variables
    var x = 500;
    var y = 500;
    console.log ( this.x, this.y, x, y );
}

// outputs: 200 300 500 500
foo();

Remember at all times that the context of execution may vary. In the code below, even though the foo function is defined on the Window object, passing its reference exports it to the context of myObject:

// some position
var x = 200;
var y = 300;

function foo() {
    console.log ( this.x, this.y, x, y );
}

var myObject = { x : 400, y : 400 };

// pass a reference to the foo function
myObject.foo = foo;

// outputs: 200 300 200 300
myObject.foo.call ( this );

We can also new a function using its name and treat it like a function constructor:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

var p = new Point(30, 30);

// outputs: Point {x: 30, y: 30}
console.log ( p );

// outputs: 30, 30
console.log ( p.x, p.y );

In this scenario, this allows us to reference the current context of the object that we instantiated and define its properties. If we do not use this, we declare x and y as global variables (or local if preceded with var) and fail to define the object properties:

function Point(x, y) {
  x = x;
  y = y;
}

var p = new Point(30, 30);

// outputs: Point {}
console.log ( p );

// outputs: undefined undefined
console.log ( p.x, p.y );

We can use the this keyword to reference the current scope to define our new distance method through a distance property:

function Point(x, y) {
  this.x = x;
  this.y = y;

  this.distance = function (point) {
    var dx = Math.abs ( this.x - point.x );
    var dy = Math.abs ( this.y - point.y );
    return Math.sqrt (dx*dx+dy*dy);
  }
}

var p1 = new Point(30, 30);
var p2 = new Point(50, 90);

// outputs: 63.245553203367585
console.log ( p1.distance( p2 ) );

To augment the capabilities of a custom or native object, and perform subclassing, we would have to rely on the prototype object.

Memo

# prototype, 我们的老朋友

JavaScript is built on top of the concept of prototype, let’s have a quick look at how this works. If we wanted to define a distance method on our Point object, we would have to write:

function Point(x, y) {
    this.x = x;
    this.y = y;

    this.distance = function (point) {
      var dx = Math.abs ( this.x - point.x );
      var dy = Math.abs ( this.y - point.y );
      return Math.sqrt (dx*dx+dy*dy);
    }
}

var p1 = new Point(30, 30);
var p2 = new Point(50, 90);

// outputs: 63.245553203367585
console.log ( p1.distance( p2 ) );

Now if we wanted the Point object to be extended (subclassed) at some point, we could rely on the prototype object:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.distance = function (point) {
  var dx = Math.abs ( this.x - point.x );
  var dy = Math.abs ( this.y - point.y );
  return Math.sqrt (dx*dx+dy*dy);
}

var p1 = new Point(30, 30);
var p2 = new Point(50, 90);

// outputs: 63.245553203367585
console.log ( p1.distance( p2 ) );

Or use the call function, to execute the Point constructor in the context of our Point3D object:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

function Point3D(x, y, z) {
    Point.call(this, x, y);
    this.z = z;
}

Remember to always use the keyword to access the current class properties, otherwise, you will be targeting the global scope of execution, where our methods are defined (Window), not our class instance. In the code below we illustrate an example:

function Point(x, y) {
  this.x = x;
  this.y = y;
  this.enabled = false;
}

Point.prototype.distance = function (point) {
  var dx = Math.abs ( this.x - point.x );
  var dy = Math.abs ( this.y - point.y );
  console.log ( enabled );
  return Math.sqrt (dx*dx+dy*dy);
}

var p1 = new Point(30, 30);
var p2 = new Point(50, 90);

// throws: Uncaught ReferenceError: enabled is not defined 
console.log ( p1.distance( p2 ) );

To augment native classes, we can use the same technique. In the code below we augment the Array class by adding a new shuffle API:

Array.prototype.shuffle = function() {
    var lng = this.length;

   for( var i = 0; i< lng; i++ ) {
      var tmp = this[i];
      var randomNum = Math.floor(Math.random()*this.length);
      this[i] = this[randomNum];
      this[randomNum] = tmp;
   }
}

// create an array to be shuffled
var myArray = ["a","b","c","d","e"];

// shuffle it
myArray.shuffle();

// outputs: ["b", "a", "d", "c", "e", shuffle: function]
console.log(myArray);

Extending native classes this way is very powerful and efficient. In a few lines, we augmented the capabilities of the core Array class, pretty neat. But there is also a major risk when doing this. Some future versions of JavaScript could implement such functionalities, colliding with your custom implementations and break your code. We just covered in the previous section how the keyword this behaves. In the example below, we are using a closure to check the distance:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.distance = function (point) {
  var dx = Math.abs ( this.x - point.x );
  var dy = Math.abs ( this.y - point.y );

  (function checkDistance () {
    console.log ( this.x, this.y );
  })();

  return Math.sqrt (dx*dx+dy*dy);
}

var p1 = new Point(30, 30);
var p2 = new Point(50, 90);

// outputs: undefined undefined
// outputs: 63.245553203367585
console.log ( p1.distance( p2 ) );

Given that the parent function executes in the context of the Point object, we would expect the same for the inner function. In this scenario, the scope becomes the global object Window, which returns undefined for both properties x and y. Fortunately, we can save a reference to the scope from the parent function and access it from the closure:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.distance = function (point) {
  var dx = Math.abs ( this.x - point.x );
  var dy = Math.abs ( this.y - point.y );

  var ref = this;

  (function checkDistance () {
    console.log ( ref.x, ref.y );
  })();

  return Math.sqrt (dx*dx+dy*dy);
}

var p1 = new Point(30, 30);
var p2 = new Point(50, 90);

// outputs: 30 30
// outputs: 63.245553203367585
console.log ( p1.distance( p2 ) );

Memo

# 闭包

The concept of closure can be a little tricky to understand at first, but real power lives behind closures. A closure is a nested function that captures non-local variables from containing scopes and is exported outside of its original scope. The code below illustrates the idea:

function increment () {
  var x = 0;

  return function () {
    return x++;
  }
}

// grab a reference
var ref = increment();

// outputs: 0
console.log ( ref() );

// outputs: 1
console.log ( ref() );

// outputs: 2
console.log ( ref() );

// outputs: 3
console.log ( ref() );

The parent function increment defined a local variable x, which is accessible from the inner function. Once returned, the inner function is exported to a different scope and has captured the x variable with it. The beauty is that the x variable is protected because out of reach, and cannot be overwritten. In the code below, we define another variable x to see if it collides:

function increment () {
  var x = 0;
  return function () {
    return x++;
  }
}

// grab a reference
var ref = increment();

// outputs: 0
console.log ( ref() );

// outputs: 1
console.log ( ref() );

// outputs: 2
console.log ( ref() );

var x = 0; 

// outputs: 3
console.log ( ref() );

The original x variable is unaffected, captured inside the closure. Also, you have to be mindful of garbage collection when using closures. In the code below, we have a variable MAX_VALUE:

function increment () {
  var x = 0;
  var MAX_VALUE = 100;

  return function () {
    return x++;
  }
}

Because the closure does not capture the MAX_VALUE variable, it is lost and immediately eligible for garbage collection.

Memo

# 垃圾回收

Like any managed language, JavaScript relies on a garbage collector (GC) to reclaim memory. It is very important to understand that garbage collection is trigger by memory allocation, not object disposal, and cannot be controlled by JavaScript developers. If you don’t understand the mechanics behind garbage collection, you may write non memory efficient code or worse create applications with memory leaks and consuming way too much memory.

Writing inefficient code may also pressure the garbage collector which will lead to collection happening synchronously on the UI thread. This could cause the UI to lock and make your application unresponsive. You should always be paying attention to the GC to ensure that our content stays as responsive as possible. Garbage collectors in JavaScript are mark and sweep based and will collect objects which have no remaining references, it is therefore very important to null the references of the objects you want to be collected. In the code below, we null the single reference we have:

// custom object
var myObject = {};

// null the only reference available
myObject = null;

Remember that nulling the reference has no direct impact on the GC. Later on, at some point, when the GC will be requesting more memory, objects without remaining references will be collected and memory will be reclaimed. In the code below, we have another reference to our object in an array:

// custom object
var myObject = {};

// an Array holds one reference
var arrayReferences = [ myObject ];

// we null one of the two references
myObject = null;

In this scenario, one reference still remains. This reference will prevent our object from being collected. As expected, to completely dispose our object, we need to clean all references:

// custom object
var myObject = {};

// an Array holds one reference
var arrayReferences = [ myObject ];

// we null one of the two references
myObject = null;

// we remove the other reference
arrayReferences[0] = null;

// or simpler
arrayReferences.length = 0;

The general following good practices will always help write more GC friendly code:

Let’s spend some time actually on the third item and see how object pooling can be useful to write GC friendly code.

Memo

# 对象池

Even though some GC experts state that newing objects should not be costly with an optimized GC, the reality is that still today in most languages, the GC is probably going to be affected by expensive instantiation of new objects. Developers over the years have developed techniques to minimize the number of allocation performed in their content. The idea is simple, the objects are allocated at the initialization of the application and available from a pool. Once done with an object, it is placed back into the pool for later use.

Note that this is valuable when the size of the objects you are pooling are big enough so that the cost of instantiating them is more expensive than retrieving and storing in the pool. Here is below an example of a pool class. Note that we rely here on the prototype object:

function ObjectPool (cls) {
    this.cls = cls;
    this.MAX_VALUE = 0;
    this.GROWTH_VALUE = 0;
    this.counter = 0;
    this.pool = new Array();
    this.currentSprite = null;
}

ObjectPool.prototype.initialize = function(maxPoolSize, growthValue) {
    this.MAX_VALUE = maxPoolSize;
    this.GROWTH_VALUE = growthValue;
    this.counter = maxPoolSize;

    var i = maxPoolSize;

    this.pool = new Array(this.MAX_VALUE);

    while( --i > -1 )
           this.pool[i] = new this.cls();
}

ObjectPool.prototype.getInstance = function() {
    if ( this.counter > 0 )
        return currentSprite = this.pool[--this.counter];

    var i = this.GROWTH_VALUE;

    while( --i > -1 )
            this.pool.unshift ( new this.cls() );

    this.counter = this.GROWTH_VALUE;

    return this.getInstance();
}

ObjectPool.prototype.disposeSprite = function (disposedSprite) {
    this.pool[this.counter++] = this.disposedSprite; 
}

To use it, we would be using this simple code:

function Enemy (){}

Enemy.prototype.sayHello = function() {
    return 'Hello from an Enemy';
}

// create the pool
var pool = new ObjectPool(Enemy);

// initialize the pool
pool.initialize(200, 10);

// retrieve the instance
var myEnemy = pool.getInstance();

// use the object
console.log ( myEnemy.sayHello() );

// call dispose (which would need to be implemented on the object)
myEnemy.dispose();

// one disposed, put it back to the pool for later use
pool.dispose ( myEnemy );

At initialization time, we allocate the required amount of enemies. Every time a new enemy is needed, we grab it from the pool. Once done with it, we return it where it is deactivated.

Memo

I hope you enjoyed this refresh!