大多数开发者会说,动态语言(就像 JS)没有 类型。让我们看看 ES5.1 语言规范(http://www.ecma-international.org/ecma-262/5.1/)在这个问题上是怎么说的:
在本语言规范中的算法所操作的每一个值都有一种关联的类型。可能的值的类型就是那些在本条款中定义的类型。类型还进一步被分为 ECMAScript 语言类型和语言规范类型一个 ECMAScript 语言类型对应于 ECMAScript 程序员使用 ECMAScript 语言直接操作的值。ECMAScript 语言类型有Undefined,Null,Boolean,String,Number,和Object。

注脚

展开查看详情

1. 目 录 致谢 阅前必读 序 第一章:类型 类型的重要意义 内建类型 值作为类型 复习 第二章:值 Arrays Strings Numbers 特殊值 值与引用 复习 第三章:原生类型 内部 [[Class]] 封箱包装器 开箱 原生类型作为构造器 复习 第四章:强制转换 转换值 抽象值操作 明确的强制转换 隐含的强制转换 宽松等价与严格等价 抽象关系比较 复习 第五章:文法 语句与表达式 操作符优先级 自动分号 本文档使用 书栈(BookStack.CN) 构建 - 1 -

2. 错误 函数参数值 try..finally switch 复习 附录A:与环境混合的 JavaScript 附录B: 鸣谢 本文档使用 书栈(BookStack.CN) 构建 - 2 -

3.致谢 致谢 当前文档 《你不懂JS:类型与文法(You Dont Know JS)》 由 进击的皇虫 使用 书栈 (BookStack.CN) 进行构建,生成于 2018-02-10。 书栈(BookStack.CN) 仅提供文档编写、整理、归类等功能,以及对文档内容的生成和导出工 具。 文档内容由网友们编写和整理,书栈(BookStack.CN) 难以确认文档内容知识点是否错漏。如 果您在阅读文档获取知识的时候,发现文档内容有不恰当的地方,请向我们反馈,让我们共同携手, 将知识准确、高效且有效地传递给每一个人。 同时,如果您在日常生活、工作和学习中遇到有价值有营养的知识文档,欢迎分享到 书栈 (BookStack.CN) ,为知识的传承献上您的一份力量! 如果当前文档生成时间太久,请到 书栈(BookStack.CN) 获取最新的文档,以跟上知识更新换 代的步伐。 文档地址:http://www.bookstack.cn/books/You-Dont-Know-JS-types-grammar 书栈官网:http://www.bookstack.cn 书栈开源:https://github.com/TruthHun 分享,让知识传承更久远! 感谢知识的创造者,感谢知识的分享者,也感谢每一位阅读到此处的 读者,因为我们都将成为知识的传承者。 本文档使用 书栈(BookStack.CN) 构建 - 3 -

4.阅前必读 阅前必读 你不懂JS:类型与文法 你不懂JS:类型与文法 从 O’Reilly 购买数字/印刷版 序(David Walsh) 前言 第一章:类型 第二章:值 第三章:原生类型 第四章:强制转换 第五章:文法 附录A:混合环境下的 JavaScript 附录B: 鸣谢 本文档使用 书栈(BookStack.CN) 构建 - 4 -

5.阅前必读 本文档使用 书栈(BookStack.CN) 构建 - 5 -

6.序 序 你不懂JS:类型与文法 序 你不懂JS:类型与文法 序 人们曾说,“JavaScript 是唯一一种开发者在学会之前就使用的语言。” 我每次听到这句话都会笑出来,因为对我来说这是真的,而且我怀疑对于许多其他开发者也是。 JavaScript,甚至可能还有 CSS 和 HTML,在因特网出现的早期都不是大学中教授的核心计算机 语言,所以个人开发很大程度上都是基于开发者的搜索和“看源代码”的能力来将这些基本的 web 语 言拼凑在一起。 我还记得我的第一个高中网站项目。它的任务是创建任意类型的网上商店,而我作为一个 James Bond 的粉丝,决定创建一个黄金眼商店。它有一切东西:黄金眼的迷笛主题音乐在背景中播放,一 个用 JavaScript 制作的瞄准十字在屏幕上跟踪着鼠标,还有在每次点击时播放一次枪响的音效。 Q 本应该会为这个网站中的杰作感到骄傲的。 我说这个故事是因为在那时我确实做了许多开发者今天在做的事情:我在我的项目中拷贝粘贴了大块 儿的 JavaScript 代码,而根本不知道究竟发生了什么。像 jQuery 那样的工具包的广泛使用, 以它们微不足道的方式,延续了这种不学习核心 JavaScript 的模式。 我不是在贬低 JavaScript 工具包的使用;毕竟,我还是 MooToo,ls JavaScript 团队的一 员!但是 JavaScript 工具包如此强大的原因是因为它们的开发者了解基础,和它们的“坑”,并出 色地施用了它们。和这些工具包的有用之处一样,了解这门语言的基础依然是极其重要的,而且有了 Kyle Simpson 的 你不懂JS 系列这样的书,没有借口不学习它们。 类型与文法,这个系列的第三部,是学习核心 JavaScript 基础的杰出教材,这些基础是拷贝粘贴 和 JavaScript 工具包没有和绝不会教你的。强制转换和它的陷阱,原生类型与构造器,和 JavaScript 基本的全部方面都使用专门的代码示例进行了彻底地讲解。和这个系列的其他书籍一 样,Kyle 总是一针见血:没有作秀和文字游戏 —— 这正是我喜爱类型的技术书籍。 享受类型与文法而且不要让它离你的桌子太远! David Walsh http://davidwalsh.name, @davidwalshblog 本文档使用 书栈(BookStack.CN) 构建 - 6 -

7.序 高级 Web 开发者,Mozilla 本文档使用 书栈(BookStack.CN) 构建 - 7 -

8.第一章:类型 第一章:类型 第一章:类型 链接 第一章:类型 大多数开发者会说,动态语言(就像 JS)没有 类型。让我们看看 ES5.1 语言规范 (http://www.ecma-international.org/ecma-262/5.1/)在这个问题上是怎么说的: 在本语言规范中的算法所操作的每一个值都有一种关联的类型。可能的值的类型就是那些在本条款中定义的类型。类型还进一步被分 为 ECMAScript 语言类型和语言规范类型 一个 ECMAScript 语言类型对应于 ECMAScript 程序员使用 ECMAScript 语言直接操作的值。ECMAScript 语言类型有 Undefined,Null,Boolean,String,Number,和 Object。 现在,如果你是一个强类型(静态类型的)语言的爱好者,你可能会反对“类型”一词的用法。在那些 语言中,“类型”的含义要比它在 JS 这里的含义丰富得 多。 有些人说 JS 不应该声称拥有“类型”,它们应被称为“标签”或者“子类型”。 去他的!我们将使用这个粗糙的定义(看起来和语言规范的定义相同,只是改变了措辞):一个 类型 是一组固有的,内建的性质,对于引擎 和开发者 来说,它独一无二地标识了一个特定的值的行为, 并将它与其他值区分开。 换句话说,如果引擎和开发者看待值 42 (数字)与看待值 "42" (字符串)的方式不同,那么 这两个值就拥有不同的 类型 — 分别是 number 和 string 。当你使用 42 时,你就在 试 图 做一些数字的事情,比如计算。但当你使用 "42" 时,你就在 试图 做一些字符串的事情,比 如输出到页面上,等等。这两个值有着不同的类型。 这绝不是一个完美的定义。但是对于这里的讨论足够好了。而且它与 JS 描述它的方式并不矛盾。 链接 类型的重要意义 内建类型 值作为类型 复习 本文档使用 书栈(BookStack.CN) 构建 - 8 -

9.第一章:类型 本文档使用 书栈(BookStack.CN) 构建 - 9 -

10.类型的重要意义 类型的重要意义 抛开学术上关于定义的分歧,为什么 JavaScript 有或者没有 类型 那么重要? 对每一种 类型 和它的固有行为有一个正确的理解,对于理解如何正确和准确地转换两个不同类型的 值来说是绝对必要的(参见第四章,强制转换)。几乎每一个被编写过的 JS 程序都需要以某种形式 处理类型的强制转换,所以,你能负责任、有信心地这么做是很重要的。 如果你有一个 number 值 42 ,但你想像一个 string 那样对待它,比如从位置 1 中将 "2" 作为一个字符抽取出来,那么显然你需要首先将值从 number (强制)转换成一个 string 。 这看起来十分简单。 但是这样的强制转换可能以许多不同的方式发生。其中有些方式是明确的,很容易推理的,和可靠 的。但是如果你不小心,强制转换就可能以非常奇怪的,令人吃惊的方式发生。 对强制转换的困惑可能是 JavaScript 开发者所经历的最深刻的挫败感之一。它曾经总是因为如此 危险 而为人所诟病,被认为是一个语言设计上的缺陷而应当被回避。 带着对 JavaScript 类型的全面理解,我们将要阐明为什么强制转换的 坏名声 是言过其实的,而 且是有些冤枉的 — 以此来反转你的视角,来看清强制转换的力量和用处。但首先,我们必须更好地 把握值与类型。 本文档使用 书栈(BookStack.CN) 构建 - 10 -

11.内建类型 内建类型 内建类型 JavaScript 定义了七种内建类型: null undefined boolean number string object symbol — 在 ES6 中被加入的! 注意: 除了 object 所有这些类型都被称为“基本类型(primitives)”。 typeof 操作符可以检测给定值的类型,而且总是返回七种字符串值中的一种 — 令人吃惊的是,对 于我们刚刚列出的七中内建类型,它没有一个恰好的一对一匹配。 1. typeof undefined === "undefined"; // true 2. typeof true === "boolean"; // true 3. typeof 42 === "number"; // true 4. typeof "42" === "string"; // true 5. typeof { life: 42 } === "object"; // true 6. 7. // 在 ES6 中被加入的! 8. typeof Symbol() === "symbol"; // true 如上所示,这六种列出来的类型拥有相应类型的值,并返回一个与类型名称相同的字符串 值。 Symbol 是 ES6 的新数据类型,我们将在第三章中讨论它。 正如你可能已经注意到的,我在上面的列表中剔除了 null 。它是 特殊的 — 特殊在它与 typeof 操作符组合时是有 bug 的。 1. typeof null === "object"; // true 要是它返回 "null" 就好了(而且是正确的!),但是这个原有的 bug 已经存在了近二十年,而 且好像永远也不会被修复了,因为有太多已经存在的 web 的内容依存着这个 bug 的行为,“修 复”这个 bug 将会 制造 更多的“bug”并毁掉许多 web 软件。 如果你想要使用 null 类型来测试 null 值,你需要一个复合条件: 本文档使用 书栈(BookStack.CN) 构建 - 11 -

12.内建类型 1. var a = null; 2. 3. (!a && typeof a === "object"); // true null 是唯一一个“falsy”(也叫类 false;见第四章),但是在 typeof 检查中返回 "object" 的基本类型。 那么 typeof 可以返回的第七种字符串值是什么? 1. typeof function a(){ /* .. */ } === "function"; // true 很容易认为在 JS 中 function 是一种顶层的内建类型,特别是看到 typeof 操作符的这种行 为时。然而,如果你阅读语言规范,你会看到它实际上是对象(object)的“子类型”。特别地,一 个函数(function)被称为“可调用对象” —— 一个拥有 [[Call]] 内部属性、允许被调用的对 象。 函数实际上是对象这一事实十分有用。最重要的是,它们可以拥有属性。例如: 1. function a(b,c) { 2. /* .. */ 3. } 这个函数对象拥有一个 length 属性,它被设置为函数被声明时的形式参数的数量。 1. a.length; // 2 因为你使用了两个正式命名的参数( b 和 c )声明了函数,所以“函数的长度”是 2 。 那么数组呢?它们是 JS 原生的,所以它们是一个特殊的类型咯? 1. typeof [1,2,3] === "object"; // true 不,它们仅仅是对象。考虑它们的最恰当的方法是,也将它们认为是对象的“子类型”(见第三章), 带有被数字索引的附加性质(与仅仅使用字符串键的普通对象相反),并维护着一个自动更新的 .length 属性。 本文档使用 书栈(BookStack.CN) 构建 - 12 -

13.值作为类型 值作为类型 值作为类型 在 JavaScript 中,变量没有类型 — 值才有类型。变量可以在任何时候,持有任何值。 另一种考虑 JS 类型的方式是,JS 没有“类型强制”,也就是引擎不坚持认为一个 变量 总是持有与 它开始存在时相同的 初始类型 的值。在一个赋值语句中,一个变量可以持有一个 string ,而在 下一个赋值语句中持有一个 nubmer ,如此类推。 值 42 有固有的类型 number ,而且它的 类型 是不能被改变的。另一个值,比如 string 类型的 "42" ,可以通过一个称为 强制转换 的处理从 number 类型的值 42 中创建出来 (见第四章)。 如果你对一个变量使用 typeof ,它不会像表面上看起来那样询问“这个变量的类型是什么?”,因 为 JS 变量是没有类型的。取而代之的是,它会询问“在这个变量里的值的类型是什么?” 1. var a = 42; 2. typeof a; // "number" 3. 4. a = true; 5. typeof a; // "boolean" typeof 操作符总是返回字符串。所以: 1. typeof typeof 42; // "string" 第一个 typeof 42 返回 "number" ,而 typeof "number" 是 "string" 。 undefined vs “undeclared” 当前 还不拥有值的变量,实际上拥有 undefined 值。对这样的变量调用 typeof 将会返回 "undefined" : 1. var a; 2. 3. typeof a; // "undefined" 4. 5. var b = 42; 6. var c; 7. 本文档使用 书栈(BookStack.CN) 构建 - 13 -

14.值作为类型 8. // 稍后 9. b = c; 10. 11. typeof b; // "undefined" 12. typeof c; // "undefined" 大多数开发者考虑“undefined”这个词的方式会诱使他们认为它是“undeclared(未声明)”的同义 词。然而在 JS 中,这两个概念十分不同。 一个“undefined”变量是在可访问的作用域中已经被声明过的,但是在 这个时刻 它里面没有任何 值。相比之下,一个“undeclared”变量是在可访问的作用域中还没有被正式声明的。 考虑这段代码: 1. var a; 2. 3. a; // undefined 4. b; // ReferenceError: b is not defined 一个恼人的困惑是浏览器给这种情形分配的错误消息。正如你所看到的,这个消息是“b is not defined”,这当然很容易而且很合理地使人将它与“b is undefined.”搞混。需要重申的 是,“undefined”和“is not defined”是非常不同的东西。要是浏览器能告诉我们类似于“b is not found”或者“b is not declared”之类的东西就好了,那会减少这种困惑! 还有一种 typeof 与未声明变量关联的特殊行为,进一步增强了这种困惑。考虑这段代码: 1. var a; 2. 3. typeof a; // "undefined" 4. 5. typeof b; // "undefined" typeof 操作符甚至为“undeclared”(或“not defined”)变量返回 "undefined" 。要注意 的是,当我们执行 typeof b 时,即使 b 是一个未声明变量,也不会有错误被抛出。这是 typeof 的一种特殊的安全防卫行为。 和上面类似地,要是 typeof 与未声明变量一起使用时返回“undeclared”就好了,而不是将其结 果值与不同的“undefined”情况混为一谈。 typeof Undeclared 不管怎样,当在浏览器中处理 JavaScript 时这种安全防卫是一种有用的特性,因为浏览器中多个 脚本文件会将变量加载到共享的全局名称空间。 本文档使用 书栈(BookStack.CN) 构建 - 14 -

15.值作为类型 注意: 许多开发者相信,在全局名称空间中绝不应该有任何变量,而且所有东西应当被包含在模块和 私有/隔离的名称空间中。这在理论上很伟大但在实践中几乎是不可能的;但它仍然是一个值得的努力 方向!幸运的是,ES6 为模块加入了头等支持,这终于使这一理论变得可行的多了。 作为一个简单的例子,想象在你的程序中有一个“调试模式”,它是通过一个称为 DEBUG 的全局变 量(标志)来控制的。在实施类似于在控制台上输出一条日志消息这样的调试任务之前,你想要检查 这个变量是否被声明了。一个顶层的全局 var DEBUG = true 声明只包含在一个“debug.js”文件 中,这个文件仅在你开发/测试时才被加载到浏览器中,而在生产环境中则不会。 然而,在你其他的程序代码中,你不得不小心你是如何检查这个全局的 DEBUG 变量的,这样你才 不会抛出一个 ReferenceError 。这种情况下 typeof 上的安全防卫就是我们的朋友。 1. // 噢,这将抛出一个错误! 2. if (DEBUG) { 3. console.log( "Debugging is starting" ); 4. } 5. 6. // 这是一个安全的存在性检查 7. if (typeof DEBUG !== "undefined") { 8. console.log( "Debugging is starting" ); 9. } 即便你不是在对付用户定义的变量(比如 DEBUG ),这种检查也是很有用的。如果你为一个内建的 API 做特性检查,你也会发现不抛出错误的检查很有帮助: 1. if (typeof atob === "undefined") { 2. atob = function() { /*..*/ }; 3. } 注意: 如果你在为一个还不存在的特性定义一个“填补”,你可能想要避免使用 var 来声明 atob 。如果你在 if 语句内部声明 var atob ,即使这个 if 条件没有通过(因为全局 的 atob 已经存在),这个声明也会被提升(参见本系列的 作用域与闭包)到作用域的顶端。在 某些浏览器中,对一些特殊类型的内建全局变量(常被称为“宿主对象”),这种重复声明也许会抛出 错误。忽略 var 可以防止这种提升声明。 另一种不带有 typeof 的安全防卫特性,而对全局变量进行这些检查的方法是,将所有的全局变量 作为全局对象的属性来观察,在浏览器中这个全局对象基本上是 window 对象。所以,上面的检查 可以(十分安全地)这样做: 1. if (window.DEBUG) { 2. // .. 3. } 4. 5. if (!window.atob) { 本文档使用 书栈(BookStack.CN) 构建 - 15 -

16.值作为类型 6. // .. 7. } 和引用未声明变量不同的是,在你试着访问一个不存在的对象属性时(即便是在全局的 window 对 象上),不会有 ReferenceError 被抛出。 另一方面,一些开发者偏好避免手动使用 window 引用全局变量,特别是当你的代码需要运行在多 种 JS 环境中时(例如不仅是在浏览器中,还在服务器端的 node.js 中),全局变量可能不总是 称为 window 。 技术上讲,这种 typeof 上的安全防卫即使在你不使用全局变量时也很有用,虽然这些情况不那么 常见,而且一些开发者也许发现这种设计方式不那么理想。想象一个你想要其他人复制-粘贴到他们程 序中或模块中的工具函数,在它里面你想要检查包含它的程序是否已经定义了一个特定的变量(以便 于你可以使用它): 1. function doSomethingCool() { 2. var helper = 3. (typeof FeatureXYZ !== "undefined") ? 4. FeatureXYZ : 5. function() { /*.. 默认的特性 ..*/ }; 6. 7. var val = helper(); 8. // .. 9. } doSomethingCool() 对称为 FeatureXYZ 变量进行检查,如果找到,就使用它,如果没找到,使 用它自己的。现在,如果某个人在他的模块/程序中引入了这个工具,它会安全地检查我们是否已经定 义了 FeatureXYZ : 1. // 一个 IIFE(参见本系列的 *作用域与闭包* 中的“立即被调用的函数表达式”) 2. (function(){ 3. function FeatureXYZ() { /*.. my XYZ feature ..*/ } 4. 5. // 引入 `doSomethingCool(..)` 6. function doSomethingCool() { 7. var helper = 8. (typeof FeatureXYZ !== "undefined") ? 9. FeatureXYZ : 10. function() { /*.. 默认的特性 ..*/ }; 11. 12. var val = helper(); 13. // .. 14. } 15. 16. doSomethingCool(); 本文档使用 书栈(BookStack.CN) 构建 - 16 -

17.值作为类型 17. })(); 这里, FeatureXYZ 根本不是一个全局变量,但我们仍然使用 typeof 的安全防卫来使检查变得 安全。而且重要的是,我们在这里 没有 可以用于检查的对象(就像我们使用 window.___ 对全局 变量做的那样),所以 typeof 十分有帮助。 另一些开发者偏好一种称为“依赖注入”的设计模式,与 doSomethingCool() 隐含地检查 FeatureXYZ 是否在它外部/周围被定义过不同的是,它需要依赖明确地传递进来,就像这样: 1. function doSomethingCool(FeatureXYZ) { 2. var helper = FeatureXYZ || 3. function() { /*.. 默认的特性 ..*/ }; 4. 5. var val = helper(); 6. // .. 7. } 在设计这样的功能时有许多选择。这些模式里没有“正确”或“错误” — 每种方式都有各种权衡。但总 的来说, typeof 的未声明安全防卫给了我们更多选项,这还是很不错的。 本文档使用 书栈(BookStack.CN) 构建 - 17 -

18.复习 复习 复习 JavaScript 有七种内建 类 型: null 、 undefined 、 boolean 、 number 、 string 、 object 、 symbol 。它们可以 被 typeof 操作符识别。 变量没有类型,但是值有类型。这些类型定义了值的固有行为。 许多开发者会认为“undefined”和“undeclared”大体上是同一个东西,但是在 JavaScript 中,它们是十分不同的。 undefined 是一个可以由被声明的变量持有的值。“未声明”意味着一个变 量从来没有被声明过。 JavaScript 很不幸地将这两个词在某种程度上混为了一谈,不仅体现在它的错误消息上 (“ReferenceError: a is not defined”),也体现在 typeof 的返回值上:对于两者它 都返回 "undefined" 。 然而,当对一个未声明的变量使用 typeof 时, typeof 上的安全防卫机制(防止一个错误)可 以在特定的情况下非常有用。 本文档使用 书栈(BookStack.CN) 构建 - 18 -

19.第二章:值 第二章:值 第二章:值 链接 第二章:值 array 、 string 、和 number 是任何程序的最基础构建块,但是 JavaScript 在这些类型 上有一些或使你惊喜或使你惊讶的独特性质。 让我们来看几种 JS 内建的值类型,并探讨一下我们如何才能更加全面地理解并正确地利用它们的行 为。 链接 Arrays Strings Numbers 特殊值 值与引用 复习 本文档使用 书栈(BookStack.CN) 构建 - 19 -

20.Arrays Arrays Array 和其他强制类型的语言相比,JavaScript 的 array 只是值的容器,而这些值可以是任何类 型: string 或者 number 或者 object ,甚至是另一个 array (这也是你得到多维数组 的方法)。 1. var a = [ 1, "2", [3] ]; 2. 3. a.length; // 3 4. a[0] === 1; // true 5. a[2][0] === 3; // true 你不需要预先指定 array 的大小,你可以仅声明它们并加入你觉得合适的值: 1. var a = [ ]; 2. 3. a.length; // 0 4. 5. a[0] = 1; 6. a[1] = "2"; 7. a[2] = [ 3 ]; 8. 9. a.length; // 3 警告: 在一个 array 值上使用 delete 将会从这个 array 上移除一个值槽,但就算你移 除了最后一个元素,它也 不会 更新 length 属性,所以多加小心!我们会在第五章讨论 delete 操作符的更多细节。 要小心创建“稀散”的 array (留下或创建空的/丢失的值槽): 1. var a = [ ]; 2. 3. a[0] = 1; 4. // 这里没有设置值槽 `a[1]` 5. a[2] = [ 3 ]; 6. 7. a[1]; // undefined 8. 9. a.length; // 3 本文档使用 书栈(BookStack.CN) 构建 - 20 -

21.Arrays 虽然这可以工作,但你留下的“空值槽”可能会导致一些令人困惑的行为。虽然这样的值槽看起来拥有 undefined 值,但是它不会像被明确设置( a[1] = undefined )的值槽那样动作。更多信息可以 参见第三章的“Array”。 array 是被数字索引的(正如你所想的那样),但微妙的是它们也是对象,可以在它们上面添加 string 键/属性(但是这些属性不会计算在 array 的 length 中): 1. var a = [ ]; 2. 3. a[0] = 1; 4. a["foobar"] = 2; 5. 6. a.length; // 1 7. a["foobar"]; // 2 8. a.foobar; // 2 然而,一个需要小心的坑是,如果一个可以被强制转换为10进制 number 的 string 值被用作 键的话,它会认为你想使用 number 索引而不是一个 string 键! 1. var a = [ ]; 2. 3. a["13"] = 42; 4. 5. a.length; // 14 一般来说,向 array 添加 string 键/属性不是一个好主意。最好使用 object 来持有键/ 属性形式的值,而将 array 专用于严格地数字索引的值。 类 Array 偶尔你需要将一个类 array 值(一个数字索引的值的集合)转换为一个真正的 array ,通常你 可以对这些值的集合调用数组的工具函数(比如 indexOf(..) 、 concat(..) 、 forEach(..) 等 等)。 举个例子,各种 DOM 查询操作会返回一个 DOM 元素的列表,对于我们转换的目的来说,这些列表 不是真正的 array 但是也足够类似 array 。另一个常见的例子是,函数为了像列表一样访问它 的参数值,而暴露了 arugumens 对象(类 array ,在 ES6 中被废弃了)。 一个进行这种转换的很常见的方法是对这个值借用 slice(..) 工具: 1. function foo() { 2. var arr = Array.prototype.slice.call( arguments ); 3. arr.push( "bam" ); 4. console.log( arr ); 本文档使用 书栈(BookStack.CN) 构建 - 21 -

22.Arrays 5. } 6. 7. foo( "bar", "baz" ); // ["bar","baz","bam"] 如果 slice() 没有用其他额外的参数调用,就像上面的代码段那样,它的参数的默认值会使它具 有复制这个 array (或者,在这个例子中,是一个类 array )的效果。 在 ES6 中,还有一种称为 Array.from(..) 的内建工具可以执行相同的任务: 1. ... 2. var arr = Array.from( arguments ); 3. ... 注意: Array.from(..) 拥有其他几种强大的能力,我们将在本系列的 ES6 与未来 中涵盖它的细 节。 本文档使用 书栈(BookStack.CN) 构建 - 22 -

23.Strings Strings String 一个很常见的想法是, string 实质上只是字符的 array 。虽然内部的实现可能是也可能不是 array ,但重要的是要理解 JavaScript 的 string 与字符的 array 确实不一样。它们的 相似性几乎只是表面上的。 举个例子,让我们考虑这两个值: 1. var a = "foo"; 2. var b = ["f","o","o"]; String 确实与 array 有很肤浅的相似性 — 也就是上面说的,类 array — 举例来说,它们 都有一个 length 属性,一个 indexOf(..) 方法(在 ES5 中仅有 array 版本),和一个 concat(..) 方法: 1. a.length; // 3 2. b.length; // 3 3. 4. a.indexOf( "o" ); // 1 5. b.indexOf( "o" ); // 1 6. 7. var c = a.concat( "bar" ); // "foobar" 8. var d = b.concat( ["b","a","r"] ); // ["f","o","o","b","a","r"] 9. 10. a === c; // false 11. b === d; // false 12. 13. a; // "foo" 14. b; // ["f","o","o"] 那么,它们基本上都仅仅是“字符的数组”,对吧? 不确切: 1. a[1] = "O"; 2. b[1] = "O"; 3. 4. a; // "foo" 5. b; // ["f","O","o"] JavaScript 的 string 是不可变的,而 array 是相当可变的。另外,在 JavaScript 中 本文档使用 书栈(BookStack.CN) 构建 - 23 -

24.Strings 用位置访问字符的 a[1] 形式不总是广泛合法的。老版本的 IE 就不允许这种语法(但是它们现 在允许了)。相反,正确的 方式是 a.charAt(1) 。 string 不可变性的进一步的后果是, string 上没有一个方法是可以原地修改它的内容的,而 是创建并返回一个新的 string 。与之相对的是,许多改变 array 内容的方法实际上 是 原地 修改的。 1. c = a.toUpperCase(); 2. a === c; // false 3. a; // "foo" 4. c; // "FOO" 5. 6. b.push( "!" ); 7. b; // ["f","O","o","!"] 另外,许多 array 方法在处理 string 时非常有用,虽然这些方法不属于 string ,但我们 可以对我们的 string “借用”非变化的 array 方法: 1. a.join; // undefined 2. a.map; // undefined 3. 4. var c = Array.prototype.join.call( a, "-" ); 5. var d = Array.prototype.map.call( a, function(v){ 6. return v.toUpperCase() + "."; 7. } ).join( "" ); 8. 9. c; // "f-o-o" 10. d; // "F.O.O." 让我们来看另一个例子:翻转一个 string (顺带一提,这是一个 JavaScript 面试中常见的细 节问题!)。 array 拥有一个原地的 reverse() 修改器方法,但是 string 没有: 1. a.reverse; // undefined 2. 3. b.reverse(); // ["!","o","O","f"] 4. b; // ["!","o","O","f"] 不幸的是,这种“借用” array 修改器不起作用,因为 string 是不可变的,因此它不能被原 地修改: 1. Array.prototype.reverse.call( a ); 2. // 仍然返回一个“foo”的 String 对象包装器(见第三章) :( 另一种迂回的做法(也是黑科技)是,将 string 转换为一个 array ,实施我们想做的操作, 本文档使用 书栈(BookStack.CN) 构建 - 24 -

25.Strings 然后将它转回 string 。 1. var c = a 2. // 将 `a` 切分成一个字符的数组 3. .split( "" ) 4. // 翻转字符的数组 5. .reverse() 6. // 将字符的数组连接回一个字符串 7. .join( "" ); 8. 9. c; // "oof" 如果你觉得这很难看,没错。不管怎样,对于简单的 string 它 好用,所以如果你需要某些快速 但是“脏”的东西,像这样的方式经常能满足你。 警告: 小心!这种方法对含有复杂(unicode)字符(星型字符、多字节字符等)的 string 不 起作用。你需要支持 unicode 的更精巧的工具库来准确地处理这种操作。在这个问题上可以咨询 Mathias Bynens 的作品:Esrever(https://github.com/mathiasbynens/esrever)。 另外一种考虑这个问题的方式是:如果你更经常地将你的“string”基本上作为 字符的数组 来执行 一些任务的话,也许就将它们作为 array 而不是作为 string 存储更好。你可能会因此省去很 多每次都将 string 转换为 array 的麻烦。无论何时你确实需要 string 的表现形式的 话,你总是可以调用 字符的 array 的 join("") 方法。 本文档使用 书栈(BookStack.CN) 构建 - 25 -

26.Numbers Numbers Number JavaScript 只有一种数字类型: number 。这种类型包含“整数”值和小数值。我说“整数”时加了 引号,因为 JS 的一个长久以来为人诟病的原因是,和其他语言不同,JS 没有真正的整数。这可能 在未来某个时候会改变,但是目前,我们只有 number 可用。 所以,在 JS 中,一个“整数”只是一个没有小数部分的小数值。也就是说, 42.0 和 42 一样 是“整数”。 像大多数现代计算机语言,以及几乎所有的脚本语言一样,JavaScript 的 number 的实现基 于“IEEE 754”标准,通常被称为“浮点”。JavaScript 明确地使用了这个标准的“双精度”(也就 是“64位二进制”)格式。 在网络上有许多了不起的文章都在介绍二进制浮点数如何在内存中存储的细节,以及选择这些做法的 意义。因为对于理解如何在 JS 中正确使用 number 来说,理解内存中的位模式不是必须的,所 以我们将这个话题作为练习留给那些想要进一步挖掘 IEEE 754 的细节的读者。 数字的语法 在 JavaScript 中字面数字一般用十进制小数表达。例如: 1. var a = 42; 2. var b = 42.3; 小数的整数部分如果是 0 ,是可选的: 1. var a = 0.42; 2. var b = .42; 相似地,一个小数在 . 之后的小数部分如果是 0 ,是可选的: 1. var a = 42.0; 2. var b = 42.; 警告: 42. 是极不常见的,如果你正在努力避免别人阅读你的代码时感到困惑,它可能不是一个 好主意。但不管怎样,它是合法的。 默认情况下,大多数 number 将会以十进制小数的形式输出,并去掉末尾小数部分的 0 。所 本文档使用 书栈(BookStack.CN) 构建 - 26 -

27.Numbers 以: 1. var a = 42.300; 2. var b = 42.0; 3. 4. a; // 42.3 5. b; // 42 非常大或非常小的 number 将默认以指数形式输出,与 toExponential() 方法的输出一样,比 如: 1. var a = 5E10; 2. a; // 50000000000 3. a.toExponential(); // "5e+10" 4. 5. var b = a * a; 6. b; // 2.5e+21 7. 8. var c = 1 / a; 9. c; // 2e-11 因为 number 值可以用 Number 对象包装器封装(见第三章),所以 number 值可以访问内 建在 Number.prototype 上的方法(见第三章)。举个例子, toFixed(..) 方法允许你指定一个 值在被表示时,带有多少位小数: 1. var a = 42.59; 2. 3. a.toFixed( 0 ); // "43" 4. a.toFixed( 1 ); // "42.6" 5. a.toFixed( 2 ); // "42.59" 6. a.toFixed( 3 ); // "42.590" 7. a.toFixed( 4 ); // "42.5900" 要注意的是,它的输出实际上是一个 number 的 string 表现形式,而且如果你指定的位数多 于值持有的小数位数时,会在右侧补 0 。 toPrecision(..) 很相似,但它指定的是有多少 有效数字 用来表示这个值: 1. var a = 42.59; 2. 3. a.toPrecision( 1 ); // "4e+1" 4. a.toPrecision( 2 ); // "43" 5. a.toPrecision( 3 ); // "42.6" 6. a.toPrecision( 4 ); // "42.59" 7. a.toPrecision( 5 ); // "42.590" 本文档使用 书栈(BookStack.CN) 构建 - 27 -

28.Numbers 8. a.toPrecision( 6 ); // "42.5900" 你不必非得使用持有这个值的变量来访问这些方法;你可以直接在 number 的字面上访问这些方 法。但你不得不小心 . 操作符。因为 . 是一个合法数字字符,如果可能的话,它会首先被 翻译为 number 字面的一部分,而不是被翻译为属性访问操作符。 1. // 不合法的语法: 2. 42.toFixed( 3 ); // SyntaxError 3. 4. // 这些都是合法的: 5. (42).toFixed( 3 ); // "42.000" 6. 0.42.toFixed( 3 ); // "0.420" 7. 42..toFixed( 3 ); // "42.000" 42.toFixed(3) 是不合法的语法,因为 . 作为 42. 字面(这是合法的 — 参见上面的讨 论!)的一部分被吞掉了,因此没有 . 属性操作符来表示 .toFixed 访问。 42..toFixed(3) 可以工作,因为第一个 . 是 number 的一部分,而第二个 . 是属性操 作符。但它可能看起来很古怪,而且确实在实际的 JavaScript 代码中很少会看到这样的东西。实 际上,在任何基本类型上直接访问方法是十分不常见的。但是不常见并不意味着 坏 或者 错。 注意: 有一些库扩展了内建的 Number.prototype (见第三章),使用 number 或在 number 上提供了额外的操作,所以在这些情况下,像使用 10..makeItRain() 来设定一个十秒钟的下钱雨 的动画,或者其他诸如此类的傻事是完全合法的。 在技术上讲,这也是合法的(注意那个空格): 1. 42 .toFixed(3); // "42.000" 但是,尤其是对 number 字面量来说,这是特别使人糊涂的代码风格,而且除了使其他开发者(和 未来的你)糊涂以外没有任何用处。避免它。 number 还可以使用科学计数法的形式指定,这在表示很大的 number 时很常见,比如: 1. var onethousand = 1E3; // 代表 1 * 10^3 2. var onemilliononehundredthousand = 1.1E6; // 代表 1.1 * 10^6 number 字面量还可以使用其他进制表达,比如二进制,八进制,和十六进制。 这些格式是可以在当前版本的 JavaScript 中使用的: 1. 0xf3; // 十六进制的: 243 2. 0Xf3; // 同上 3. 本文档使用 书栈(BookStack.CN) 构建 - 28 -

29.Numbers 4. 0363; // 八进制的: 243 注意: 从 ES6 + strict 模式开始,不再允许 0363 这样的的八进制形式(新的形式参见后 面的讨论)。 0363 在非 strict 模式下依然是允许的,但是不管怎样你应当停止使用它,来拥 抱未来(而且因为你现在应当在使用 strict 模式了!)。 至于 ES6,下面的新形式也是合法的: 1. 0o363; // 八进制的: 243 2. 0O363; // 同上 3. 4. 0b11110011; // 二进制的: 243 5. 0B11110011; // 同上 请为你的开发者同胞们做件好事:绝不要使用 0O363 形式。把 0 放在大写的 O 旁边就 是在制造困惑。保持使用小写的谓词 0x 、 0b 、和 0o 。 小数值 使用二进制浮点数的最出名(臭名昭著)的副作用是(记住,这是对 所有 使用 IEEE 754 的语言 都成立的 —— 不是许多人认为/假装 仅 在 JavaScript 中存在的问题): 1. 0.1 + 0.2 === 0.3; // false 从数学的意义上,我们知道这个语句应当为 true 。为什么它是 false ? 简单地说, 0.1 和 0.2 的二进制表示形式是不精确的,所以它们相加时,结果不是精确地 0.3 。而是 非常 接近的值: 0.30000000000000004 ,但是如果你的比较失败了,“接近”是无关紧 要的。 注意: JavaScript 应当切换到可以精确表达所有值的一个不同的 number 实现吗?有些人认为 应该。多年以来有许多选项出现过。但是没有一个被采纳,而且也许永远也不会。它看起来就像挥挥 手然后说“已经改好那个 bug 了!”那么简单,但根本不是那么回事儿。如果真有这么简单,它绝对 在很久以前就被改掉了。 现在的问题是,如果一些 number 不能被 信任 为精确的,这不是意味着我们根本不能使用 number 吗? 当然不是。 在一些应用程序中你需要多加小心,特别是在对付小数的时候。还有许多(也许是大多数?)应用程 序只处理整数,而且,最大只处理到几百万到几万亿。这些应用程序使用 JS 中的数字操作是,而且 将总是,非常安全 的。 要是我们 确实 需要比较两个 number ,就像 0.1 + 0.2 与 0.3 ,而且知道这个简单的相等 本文档使用 书栈(BookStack.CN) 构建 - 29 -