前言
本书涵盖了 JavaScript 语言以及 Web 浏览器和 Node 实现的 JavaScript API。我为一些具有先前编程经验的读者编写了这本书,他们想要学习 JavaScript,也为已经使用 JavaScript 的程序员编写了这本书,但希望将他们的理解提升到一个新的水平,并真正掌握这门语言。我写这本书的目标是全面和权威地记录 JavaScript 语言,并深入介绍 JavaScript 程序可用的最重要的客户端和服务器端 API。因此,这是一本长篇详细的书。然而,我希望它会奖励仔细学习,并且您花在阅读上的时间将很容易以更高的编程生产力形式收回。
本书的早期版本包括了一个全面的参考部分。我不再认为在印刷形式中包含这些材料是有意义的,因为在网上很容易找到最新的参考材料。如果您需要查找与核心或客户端 JavaScript 相关的任何内容,我建议您访问MDN 网站。对于服务器端 Node API,我建议您直接访问源并查阅Node.js 参考文档。
本书中使用的约定
我在本书中使用以下排版约定:
斜体
用于强调和指示术语的首次使用。斜体也用于电子邮件地址,URL 和文件名。
固定宽度
用于所有 JavaScript 代码和 CSS 和 HTML 列表,通常用于编程时需要字面输入的任何内容。
固定宽度斜体
有时用于解释 JavaScript 语法。
固定宽度粗体
显示用户应该按照字面意义输入的命令或其他文本
注意
此元素表示一般说明。
重要
此元素表示警告或注意事项。
示例代码
本书的补充材料(代码示例,练习等)可在以下网址下载:
本书旨在帮助您完成工作。一般情况下,如果本书提供示例代码,您可以在您的程序和文档中使用它。除非您复制了代码的大部分内容,否则无需联系我们请求许可。例如,编写一个使用本书多个代码块的程序不需要许可。销售或分发 O’Reilly 图书中的示例需要许可。引用本书并引用示例代码回答问题不需要许可。将本书中大量示例代码合并到产品文档中需要许可。
我们感谢,但通常不要求署名。署名通常包括标题,作者,出版商和 ISBN。例如:“JavaScript: The Definitive Guide,第七版,作者 David Flanagan(O’Reilly)。版权所有 2020 年 David Flanagan,978-1-491-95202-3。”
如果您认为您使用的代码示例超出了合理使用范围或上述许可,请随时通过permissions@oreilly.com与我们联系。
致谢
许多人在创作本书时提供了帮助。我要感谢我的编辑 Angela Rufino,她让我保持在正确的轨道上,对我错过的截止日期的耐心。也感谢我的技术审阅者:Brian Sletten,Elisabeth Robson,Ethan Flanagan,Maximiliano Firtman,Sarah Wachs 和 Schalk Neethling。他们的评论和建议使这本书变得更好。
O’Reilly 的制作团队一如既往地出色:Kristen Brown 管理了制作过程,Deborah Baker 担任制作编辑,Rebecca Demarest 绘制了图表,Judy McConville 创建了索引。
本书的编辑、审阅者和贡献者包括:Andrew Schulman,Angelo Sirigos,Aristotle Pagaltzis,Brendan Eich,Christian Heilmann,Dan Shafer,Dave C. Mitchell,Deb Cameron,Douglas Crockford,Dr. Tankred Hirschmann,Dylan Schiemann,Frank Willison,Geoff Stearns,Herman Venter,Jay Hodges,Jeff Yates,Joseph Kesselman,Ken Cooper,Larry Sullivan,Lynn Rollins,Neil Berkman,Mike Loukides,Nick Thompson,Norris Boyd,Paula Ferguson,Peter-Paul Koch,Philippe Le Hegaret,Raffaele Cecco,Richard Yaker,Sanders Kleinfeld,Scott Furman,Scott Isaacs,Shon Katzenberger,Terry Allen,Todd Ditchendorf,Vidur Apparao,Waldemar Horwat 和 Zachary Kessin。
撰写第七版使我在许多深夜远离了家人。我爱他们,感谢他们忍受我的缺席。
David Flanagan,2020 年 3 月
第一章:介绍 JavaScript
JavaScript 是 Web 的编程语言。绝大多数网站使用 JavaScript,并且所有现代 Web 浏览器——无论是桌面、平板还是手机——都包含 JavaScript 解释器,使 JavaScript 成为历史上部署最广泛的编程语言。在过去的十年中,Node.js 使 JavaScript 编程超越了 Web 浏览器,Node 的巨大成功意味着 JavaScript 现在也是软件开发人员中使用最广泛的编程语言。无论您是从零开始还是已经专业使用 JavaScript,本书都将帮助您掌握这门语言。
如果您已经熟悉其他编程语言,了解 JavaScript 是一种高级、动态、解释性编程语言,非常适合面向对象和函数式编程风格,可能会对您有所帮助。JavaScript 的变量是无类型的。其语法在很大程度上基于 Java,但这两种语言在其他方面没有关联。JavaScript 从 Scheme 语言中继承了头等函数,从鲜为人知的 Self 语言中继承了基于原型的继承。但您不需要了解这些语言,或熟悉这些术语,就可以使用本书学习 JavaScript。
名称“JavaScript”非常具有误导性。除了表面上的语法相似性外,JavaScript 与 Java 编程语言完全不同。JavaScript 早已超越了其脚本语言的起源,成为一种强大而高效的通用语言,适用于严肃的软件工程和具有庞大代码库的项目。
要有用,每种语言都必须有一个平台或标准库,用于执行诸如基本输入和输出之类的操作。核心 JavaScript 语言定义了一个最小的 API,用于处理数字、文本、数组、集合、映射等,但不包括任何输入或输出功能。输入和输出(以及更复杂的功能,如网络、存储和图形)是嵌入 JavaScript 的“主机环境”的责任。
JavaScript 的原始主机环境是 Web 浏览器,这仍然是 JavaScript 代码最常见的执行环境。Web 浏览器环境允许 JavaScript 代码通过用户的鼠标和键盘输入以及通过进行 HTTP 请求来获取输入。它还允许 JavaScript 代码使用 HTML 和 CSS 向用户显示输出。
自 2010 年以来,JavaScript 代码还有另一个主机环境可供选择。与将 JavaScript 限制在与 Web 浏览器提供的 API 一起使用不同,Node 使 JavaScript 可以访问整个操作系统,允许 JavaScript 程序读写文件,通过网络发送和接收数据,并进行和提供 HTTP 请求。Node 是实现 Web 服务器的热门选择,也是编写简单实用程序脚本的便捷工具,可作为 shell 脚本的替代品。
本书大部分内容都集中在 JavaScript 语言本身上。第十一章记录了 JavaScript 标准库,第十五章介绍了 Web 浏览器主机环境,第十六章介绍了 Node 主机环境。
本书首先涵盖低级基础知识,然后构建在此基础上,向更高级和更高级的抽象发展。这些章节应该按照更多或更少的顺序阅读。但是学习新的编程语言从来不是一个线性过程,描述一种语言也不是线性的:每个语言特性都与其他特性相关联,本书充满了交叉引用——有时是向后,有时是向前——到相关材料。本介绍性章节快速地介绍了语言的关键特性,这将使您更容易理解后续章节中的深入讨论。如果您已经是一名实践的 JavaScript 程序员,您可能可以跳过本章节。(尽管在继续之前,您可能会喜欢阅读示例 1-1)
1.1 探索 JavaScript
学习新的编程语言时,重要的是尝试书中的示例,然后修改它们并再次尝试以测试您对语言的理解。为此,您需要一个 JavaScript 解释器。
尝试几行 JavaScript 代码的最简单方法是在您的网络浏览器中打开 Web 开发者工具(使用 F12、Ctrl-Shift-I 或 Command-Option-I),然后选择控制台选项卡。然后,您可以在提示符处输入代码并在输入时查看结果。浏览器开发者工具通常显示为浏览器窗口底部或右侧的窗格,但通常可以将它们分离为单独的窗口(如图 1-1 所示),这通常非常方便。
图 1-1. Firefox 开发者工具中的 JavaScript 控制台
尝试 JavaScript 代码的另一种方法是从https://nodejs.org下载并安装 Node。安装 Node 后,您只需打开一个终端窗口并输入**node
**即可开始像这样进行交互式 JavaScript 会话:
$ node
Welcome to Node.js v12.13.0.
Type ".help" for more information.
> .help
.break Sometimes you get stuck, this gets you out
.clear Alias for .break
.editor Enter editor mode
.exit Exit the repl
.help Print this help message
.load Load JS from a file into the REPL session
.save Save all evaluated commands in this REPL session to a file
Press ^C to abort current expression, ^D to exit the repl
> let x = 2, y = 3;
undefined
> x + y
5
> (x === 2) && (y === 3)
true
> (x > 3) || (y < 3)
false
1.2 你好,世界
当您准备开始尝试更长的代码块时,这些逐行交互式环境可能不再适用,您可能更喜欢在文本编辑器中编写代码。从那里,您可以将代码复制粘贴到 JavaScript 控制台或 Node 会话中。或者您可以将代码保存到文件中(JavaScript 代码的传统文件扩展名为*.js*),然后使用 Node 运行该 JavaScript 代码文件:
$ node snippet.js
如果您像这样以非交互方式使用 Node,它不会自动打印出您运行的所有代码的值,因此您需要自己执行。您可以使用函数console.log()
在终端窗口或浏览器的开发者工具控制台中显示文本和其他 JavaScript 值。例如,如果您创建一个包含以下代码行的hello.js文件:
console.log("Hello World!");
并使用node hello.js
执行文件,您将看到打印出“Hello World!”的消息。
如果您想在网络浏览器的 JavaScript 控制台中看到相同的消息打印出来,请创建一个名为hello.html的新文件,并将以下文本放入其中:
<script src="hello.js"></script>
然后使用file://
URL 将hello.html加载到您的网络浏览器中,就像这样:
file:///Users/username/javascript/hello.html
打开开发者工具窗口以在控制台中查看问候语。
1.3 JavaScript 之旅
本节通过代码示例快速介绍了 JavaScript 语言。在这个介绍性章节之后,我们将从最低级别深入 JavaScript:第二章解释了 JavaScript 注释、分号和 Unicode 字符集等内容。第三章开始变得更有趣:它解释了 JavaScript 变量以及您可以分配给这些变量的值。
这里有一些示例代码来说明这两章的亮点:
// Anything following double slashes is an English-language comment.
// Read the comments carefully: they explain the JavaScript code.
// A variable is a symbolic name for a value.
// Variables are declared with the let keyword:
let x; // Declare a variable named x.
// Values can be assigned to variables with an = sign
x = 0; // Now the variable x has the value 0
x // => 0: A variable evaluates to its value.
// JavaScript supports several types of values
x = 1; // Numbers.
x = 0.01; // Numbers can be integers or reals.
x = "hello world"; // Strings of text in quotation marks.
x = 'JavaScript'; // Single quote marks also delimit strings.
x = true; // A Boolean value.
x = false; // The other Boolean value.
x = null; // Null is a special value that means "no value."
x = undefined; // Undefined is another special value like null.
JavaScript 程序可以操作的另外两个非常重要的类型 是对象和数组。这是第六章和第七章的主题,但它们非常重要,以至于在到达这些章节之前你会看到它们很多次:
// JavaScript's most important datatype is the object.
// An object is a collection of name/value pairs, or a string to value map.
let book = { // Objects are enclosed in curly braces.
topic: "JavaScript", // The property "topic" has value "JavaScript."
edition: 7 // The property "edition" has value 7
}; // The curly brace marks the end of the object.
// Access the properties of an object with . or []:
book.topic // => "JavaScript"
book["edition"] // => 7: another way to access property values.
book.author = "Flanagan"; // Create new properties by assignment.
book.contents = {}; // {} is an empty object with no properties.
// Conditionally access properties with ?. (ES2020):
book.contents?.ch01?.sect1 // => undefined: book.contents has no ch01 property.
// JavaScript also supports arrays (numerically indexed lists) of values:
let primes = [2, 3, 5, 7]; // An array of 4 values, delimited with [ and ].
primes[0] // => 2: the first element (index 0) of the array.
primes.length // => 4: how many elements in the array.
primes[primes.length-1] // => 7: the last element of the array.
primes[4] = 9; // Add a new element by assignment.
primes[4] = 11; // Or alter an existing element by assignment.
let empty = []; // [] is an empty array with no elements.
empty.length // => 0
// Arrays and objects can hold other arrays and objects:
let points = [ // An array with 2 elements.
{x: 0, y: 0}, // Each element is an object.
{x: 1, y: 1}
];
let data = { // An object with 2 properties
trial1: [[1,2], [3,4]], // The value of each property is an array.
trial2: [[2,3], [4,5]] // The elements of the arrays are arrays.
};
这里展示的用方括号列出数组元素或在花括号内将对象属性名映射到属性值的语法被称为初始化表达式,这只是第四章的一个主题。表达式 是 JavaScript 的短语,可以评估以产生一个值。例如,使用.
和[]
来引用对象属性或数组元素的值就是一个表达式。
在 JavaScript 中形成表达式的最常见方式之一是使用运算符:
// Operators act on values (the operands) to produce a new value.
// Arithmetic operators are some of the simplest:
3 + 2 // => 5: addition
3 - 2 // => 1: subtraction
3 * 2 // => 6: multiplication
3 / 2 // => 1.5: division
points[1].x - points[0].x // => 1: more complicated operands also work
"3" + "2" // => "32": + adds numbers, concatenates strings
// JavaScript defines some shorthand arithmetic operators
let count = 0; // Define a variable
count++; // Increment the variable
count--; // Decrement the variable
count += 2; // Add 2: same as count = count + 2;
count *= 3; // Multiply by 3: same as count = count * 3;
count // => 6: variable names are expressions, too.
// Equality and relational operators test whether two values are equal,
// unequal, less than, greater than, and so on. They evaluate to true or false.
let x = 2, y = 3; // These = signs are assignment, not equality tests
x === y // => false: equality
x !== y // => true: inequality
x < y // => true: less-than
x <= y // => true: less-than or equal
x > y // => false: greater-than
x >= y // => false: greater-than or equal
"two" === "three" // => false: the two strings are different
"two" > "three" // => true: "tw" is alphabetically greater than "th"
false === (x > y) // => true: false is equal to false
// Logical operators combine or invert boolean values
(x === 2) && (y === 3) // => true: both comparisons are true. && is AND
(x > 3) || (y < 3) // => false: neither comparison is true. || is OR
!(x === y) // => true: ! inverts a boolean value
如果 JavaScript 表达式就像短语,那么 JavaScript 语句 就像完整的句子。语句是第五章的主题。粗略地说,表达式是计算值但不执行任何操作的东西:它不以任何方式改变程序状态。另一方面,语句没有值,但它们会改变状态。你已经在上面看到了变量声明和赋值语句。另一个广泛的语句类别是控制结构,如条件语句和循环。在我们讨论完函数之后,你将在下面看到示例。
函数 是一段命名和带参数的 JavaScript 代码块,你定义一次,然后可以反复调用。函数直到第八章才会正式介绍,但与对象和数组一样,你在到达该章之前会看到它们很多次。这里有一些简单的示例:
// Functions are parameterized blocks of JavaScript code that we can invoke.
function plus1(x) { // Define a function named "plus1" with parameter "x"
return x + 1; // Return a value one larger than the value passed in
} // Functions are enclosed in curly braces
plus1(y) // => 4: y is 3, so this invocation returns 3+1
let square = function(x) { // Functions are values and can be assigned to vars
return x * x; // Compute the function's value
}; // Semicolon marks the end of the assignment.
square(plus1(y)) // => 16: invoke two functions in one expression
在 ES6 及更高版本中,有一种用于定义函数的简洁语法。这种简洁语法使用=>
将参数列表与函数体分开,因此用这种方式定义的函数被称为箭头函数。箭头函数在想要将匿名函数作为另一个函数的参数传递时最常用。前面的代码重写为使用箭头函数时如下所示:
const plus1 = x => x + 1; // The input x maps to the output x + 1
const square = x => x * x; // The input x maps to the output x * x
plus1(y) // => 4: function invocation is the same
square(plus1(y)) // => 16
当我们将函数与对象一起使用时,我们得到方法:
// When functions are assigned to the properties of an object, we call
// them "methods." All JavaScript objects (including arrays) have methods:
let a = []; // Create an empty array
a.push(1,2,3); // The push() method adds elements to an array
a.reverse(); // Another method: reverse the order of elements
// We can define our own methods, too. The "this" keyword refers to the object
// on which the method is defined: in this case, the points array from earlier.
points.dist = function() { // Define a method to compute distance between points
let p1 = this[0]; // First element of array we're invoked on
let p2 = this[1]; // Second element of the "this" object
let a = p2.x-p1.x; // Difference in x coordinates
let b = p2.y-p1.y; // Difference in y coordinates
return Math.sqrt(a*a + // The Pythagorean theorem
b*b); // Math.sqrt() computes the square root
};
points.dist() // => Math.sqrt(2): distance between our 2 points
现在,正如承诺的那样,这里有一些函数,它们的主体演示了常见的 JavaScript 控制结构语句:
// JavaScript statements include conditionals and loops using the syntax
// of C, C++, Java, and other languages.
function abs(x) { // A function to compute the absolute value.
if (x >= 0) { // The if statement...
return x; // executes this code if the comparison is true.
} // This is the end of the if clause.
else { // The optional else clause executes its code if
return -x; // the comparison is false.
} // Curly braces optional when 1 statement per clause.
} // Note return statements nested inside if/else.
abs(-10) === abs(10) // => true
function sum(array) { // Compute the sum of the elements of an array
let sum = 0; // Start with an initial sum of 0.
for(let x of array) { // Loop over array, assigning each element to x.
sum += x; // Add the element value to the sum.
} // This is the end of the loop.
return sum; // Return the sum.
}
sum(primes) // => 28: sum of the first 5 primes 2+3+5+7+11
function factorial(n) { // A function to compute factorials
let product = 1; // Start with a product of 1
while(n > 1) { // Repeat statements in {} while expr in () is true
product *= n; // Shortcut for product = product * n;
n--; // Shortcut for n = n - 1
} // End of loop
return product; // Return the product
}
factorial(4) // => 24: 1*4*3*2
function factorial2(n) { // Another version using a different loop
let i, product = 1; // Start with 1
for(i=2; i <= n; i++) // Automatically increment i from 2 up to n
product *= i; // Do this each time. {} not needed for 1-line loops
return product; // Return the factorial
}
factorial2(5) // => 120: 1*2*3*4*5
JavaScript 支持面向对象的编程风格,但与“经典”面向对象编程语言有很大不同。第九章详细介绍了 JavaScript 中的面向对象编程,提供了大量示例。下面是一个非常简单的示例,演示了如何定义一个 JavaScript 类来表示 2D 几何点。这个类的实例对象具有一个名为distance()
的方法,用于计算点到原点的距离:
class Point { // By convention, class names are capitalized.
constructor(x, y) { // Constructor function to initialize new instances.
this.x = x; // This keyword is the new object being initialized.
this.y = y; // Store function arguments as object properties.
} // No return is necessary in constructor functions.
distance() { // Method to compute distance from origin to point.
return Math.sqrt( // Return the square root of x² + y².
this.x * this.x + // this refers to the Point object on which
this.y * this.y // the distance method is invoked.
);
}
}
// Use the Point() constructor function with "new" to create Point objects
let p = new Point(1, 1); // The geometric point (1,1).
// Now use a method of the Point object p
p.distance() // => Math.SQRT2
这里介绍了 JavaScript 基本语法和功能的入门之旅到此结束,但本书将继续涵盖语言的其他特性的独立章节:
第十章,模块
展示了一个文件或脚本中的 JavaScript 代码如何使用其他文件或脚本中定义的 JavaScript 函数和类。
第十一章,JavaScript 标准库
涵盖了所有 JavaScript 程序都可以使用的内置函数和类。这包括重要的数据结构如映射和集合,用于文本模式匹配的正则表达式类,用于序列化 JavaScript 数据结构的函数等等。
第十二章,迭代器和生成器
解释了for/of
循环的工作原理以及如何使自己的类可迭代使用for/of
。还涵盖了生成器函数和yield
语句。
第十三章,异步 JavaScript
本章深入探讨了 JavaScript 中的异步编程,涵盖了回调和事件、基于 Promise 的 API,以及async
和await
关键字。尽管核心 JavaScript 语言不是异步的,但异步 API 在 Web 浏览器和 Node 中是默认的,本章解释了处理这些 API 的技术。
第十四章,元编程
介绍了一些对编写供其他 JavaScript 程序员使用的代码库感兴趣的 JavaScript 的高级特性。
第十五章,Web 浏览器中的 JavaScript
介绍了 Web 浏览器主机环境,解释了 Web 浏览器如何执行 JavaScript 代码,并涵盖了 Web 浏览器定义的许多重要 API 中最重要的部分。这是本书中迄今为止最长的一章。
第十六章,使用 Node 进行服务器端 JavaScript
介绍了 Node 主机环境,涵盖了最重要的编程模型、数据结构和 API,这些内容是最重要的理解。
第十七章,JavaScript 工具和扩展
涵盖了一些值得了解的工具和语言扩展,因为它们被广泛使用,可能会使您成为更高效的程序员。
1.4 示例:字符频率直方图
这一章以一个简短但非平凡的 JavaScript 程序结尾。示例 1-1 是一个 Node 程序,从标准输入读取文本,计算该文本的字符频率直方图,然后打印出直方图。您可以像这样调用程序来分析其自身源代码的字符频率:
$ node charfreq.js < charfreq.js
T: ########### 11.22%
E: ########## 10.15%
R: ####### 6.68%
S: ###### 6.44%
A: ###### 6.16%
N: ###### 5.81%
O: ##### 5.45%
I: ##### 4.54%
H: #### 4.07%
C: ### 3.36%
L: ### 3.20%
U: ### 3.08%
/: ### 2.88%
本示例使用了许多高级 JavaScript 特性,旨在演示真实世界的 JavaScript 程序可能是什么样子。您不应该期望立即理解所有代码,但请放心,所有内容将在接下来的章节中解释。
示例 1-1. 使用 JavaScript 计算字符频率直方图
/**
* This Node program reads text from standard input, computes the frequency
* of each letter in that text, and displays a histogram of the most
* frequently used characters. It requires Node 12 or higher to run.
*
* In a Unix-type environment you can invoke the program like this:
* node charfreq.js < corpus.txt
*/
// This class extends Map so that the get() method returns the specified
// value instead of null when the key is not in the map
class DefaultMap extends Map {
constructor(defaultValue) {
super(); // Invoke superclass constructor
this.defaultValue = defaultValue; // Remember the default value
}
get(key) {
if (this.has(key)) { // If the key is already in the map
return super.get(key); // return its value from superclass.
}
else {
return this.defaultValue; // Otherwise return the default value
}
}
}
// This class computes and displays letter frequency histograms
class Histogram {
constructor() {
this.letterCounts = new DefaultMap(0); // Map from letters to counts
this.totalLetters = 0; // How many letters in all
}
// This function updates the histogram with the letters of text.
add(text) {
// Remove whitespace from the text, and convert to upper case
text = text.replace(/\s/g, "").toUpperCase();
// Now loop through the characters of the text
for(let character of text) {
let count = this.letterCounts.get(character); // Get old count
this.letterCounts.set(character, count+1); // Increment it
this.totalLetters++;
}
}
// Convert the histogram to a string that displays an ASCII graphic
toString() {
// Convert the Map to an array of [key,value] arrays
let entries = [...this.letterCounts];
// Sort the array by count, then alphabetically
entries.sort((a,b) => { // A function to define sort order.
if (a[1] === b[1]) { // If the counts are the same
return a[0] < b[0] ? -1 : 1; // sort alphabetically.
} else { // If the counts differ
return b[1] - a[1]; // sort by largest count.
}
});
// Convert the counts to percentages
for(let entry of entries) {
entry[1] = entry[1] / this.totalLetters*100;
}
// Drop any entries less than 1%
entries = entries.filter(entry => entry[1] >= 1);
// Now convert each entry to a line of text
let lines = entries.map(
([l,n]) => `${l}: ${"#".repeat(Math.round(n))} ${n.toFixed(2)}%`
);
// And return the concatenated lines, separated by newline characters.
return lines.join("\n");
}
}
// This async (Promise-returning) function creates a Histogram object,
// asynchronously reads chunks of text from standard input, and adds those chunks to
// the histogram. When it reaches the end of the stream, it returns this histogram
async function histogramFromStdin() {
process.stdin.setEncoding("utf-8"); // Read Unicode strings, not bytes
let histogram = new Histogram();
for await (let chunk of process.stdin) {
histogram.add(chunk);
}
return histogram;
}
// This one final line of code is the main body of the program.
// It makes a Histogram object from standard input, then prints the histogram.
histogramFromStdin().then(histogram => { console.log(histogram.toString()); });
1.5 总结
本书从底层向上解释 JavaScript。这意味着我们从注释、标识符、变量和类型等低级细节开始;然后构建表达式、语句、对象和函数;然后涵盖类和模块等高级语言抽象。我认真对待本书标题中的“权威”一词,接下来的章节将以可能一开始感觉令人望而却步的细节水平解释语言。然而,真正掌握 JavaScript 需要理解这些细节,我希望您能抽出时间从头到尾阅读本书。但请不要觉得您需要在第一次阅读时就这样做。如果发现自己在某一部分感到困惑,请直接跳到下一部分。一旦对整个语言有了工作知识,您可以回来掌握细节。
第二章:词法结构
编程语言的词法结构是指定如何在该语言中编写程序的基本规则集。它是语言的最低级语法:它指定变量名的外观,注释的分隔符字符,以及如何将一个程序语句与下一个分隔开,例如。本短章记录了 JavaScript 的词法结构。它涵盖了:
-
区分大小写、空格和换行
-
注释
-
文字
-
标识符和保留字
-
Unicode
-
可选分号
2.1 JavaScript 程序的文本
JavaScript 是区分大小写的语言。这意味着语言关键字、变量、函数名和其他标识符必须始终以一致的字母大小写输入。例如,while
关键字必须输入为while
,而不是“While”或“WHILE”。同样,online
、Online
、OnLine
和ONLINE
是四个不同的变量名。
JavaScript 会忽略程序中标记之间出现的空格。在大多数情况下,JavaScript 也会忽略换行(但请参见§2.6 中的一个例外)。由于您可以在程序中自由使用空格和换行,因此可以以整洁一致的方式格式化和缩进程序,使代码易于阅读和理解。
除了常规空格字符(\u0020
)外,JavaScript 还识别制表符、各种 ASCII 控制字符和各种 Unicode 空格字符作为空白。JavaScript 将换行符、回车符和回车符/换行符序列识别为行终止符。
2.2 注释
JavaScript 支持两种注释风格。任何位于//
和行尾之间的文本都被视为注释,JavaScript 会忽略它。位于/*
和*/
之间的文本也被视为注释;这些注释可以跨越多行,但不能嵌套。以下代码行都是合法的 JavaScript 注释:
// This is a single-line comment.
/* This is also a comment */ // and here is another comment.
/*
* This is a multi-line comment. The extra * characters at the start of
* each line are not a required part of the syntax; they just look cool!
*/
2.3 文字
文字 是直接出现在程序中的数据值。以下都是文字:
12 // The number twelve
1.2 // The number one point two
"hello world" // A string of text
'Hi' // Another string
true // A Boolean value
false // The other Boolean value
null // Absence of an object
数字和字符串文字的完整详细信息请参见第三章。
2.4 标识符和保留字
标识符 就是一个名字。在 JavaScript 中,标识符用于命名常量、变量、属性、函数和类,并为 JavaScript 代码中某些循环提供标签。JavaScript 标识符必须以字母、下划线(_
)或美元符号($
)开头。后续字符可以是字母、数字、下划线或美元符号。(不允许数字作为第一个字符,以便 JavaScript 可以轻松区分标识符和数字。)以下都是合法的标识符:
i
my_variable_name
v13
_dummy
$str
与任何语言一样,JavaScript 为语言本身保留了某些标识符。这些“保留字”不能用作常规标识符。它们在下一节中列出。
2.4.1 保留字
以下单词是 JavaScript 语言的一部分。其中许多(如if
、while
和for
)是必须避免用作常量、变量、函数或类名称的保留关键字(尽管它们都可以用作对象内的属性名称)。其他一些单词(如from
、of
、get
和set
)在有限的上下文中使用时没有语法歧义,作为标识符是完全合法的。还有其他关键字(如let
)为了保持与旧程序的向后兼容性而不能完全保留,因此有复杂的规则规定何时可以将其用作标识符,何时不行。(例如,如果在类外部用var
声明,let
可以用作变量名,但如果在类内部或用const
声明,则不行。)最简单的方法是避免将这些单词用作标识符,除了from
、set
和target
,它们是安全的并且已经被广泛使用。
as const export get null target void
async continue extends if of this while
await debugger false import return throw with
break default finally in set true yield
case delete for instanceof static try
catch do from let super typeof
class else function new switch var
JavaScript 还保留或限制了某些关键字的使用,这些关键字目前尚未被语言使用,但可能在未来版本中使用:
enum implements interface package private protected public
由于历史原因,在某些情况下不允许将arguments
和eval
用作标识符,并且最好完全避免使用它们。
2.5 Unicode
JavaScript 程序使用 Unicode 字符集编写,您可以在字符串和注释中使用任何 Unicode 字符。为了便于移植和编辑,通常在标识符中仅使用 ASCII 字母和数字。但这只是一种编程约定,语言允许在标识符中使用 Unicode 字母、数字和表意文字(但不允许使用表情符号)。这意味着程序员可以使用数学符号和非英语语言中的单词作为常量和变量:
const π = 3.14;
const sí = true;
2.5.1 Unicode 转义序列
一些计算机硬件和软件无法显示、输入或正确处理完整的 Unicode 字符集。为了支持使用较旧技术的程序员和系统,JavaScript 定义了转义序列,允许我们仅使用 ASCII 字符编写 Unicode 字符。这些 Unicode 转义以字符\u
开头,后面要么跟着恰好四个十六进制数字(使用大写或小写字母 A-F),要么是由一个到六个十六进制数字括在花括号内。这些 Unicode 转义可能出现在 JavaScript 字符串文字、正则表达式文字和标识符中(但不出现在语言关键字中)。例如,字符“é”的 Unicode 转义是\u00E9
;以下是三种包含此字符的变量名的不同写法:
let café = 1; // Define a variable using a Unicode character
caf\u00e9 // => 1; access the variable using an escape sequence
caf\u{E9} // => 1; another form of the same escape sequence
早期版本的 JavaScript 仅支持四位数转义序列。带有花括号的版本是在 ES6 中引入的,以更好地支持需要超过 16 位的 Unicode 代码点,例如表情符号:
console.log("\u{1F600}"); // Prints a smiley face emoji
Unicode 转义也可能出现在注释中,但由于注释被忽略,因此在该上下文中它们仅被视为 ASCII 字符,而不被解释为 Unicode。
2.5.2 Unicode 规范化
如果您在 JavaScript 程序中使用非 ASCII 字符,您必须意识到 Unicode 允许以多种方式对相同字符进行编码。例如,字符串“é”可以编码为单个 Unicode 字符\u00E9
,也可以编码为常规 ASCII 的“e”后跟重音符组合标记\u0301
。这两种编码在文本编辑器中显示时通常看起来完全相同,但它们具有不同的二进制编码,这意味着 JavaScript 认为它们是不同的,这可能导致非常令人困惑的程序:
const café = 1; // This constant is named "caf\u{e9}"
const café = 2; // This constant is different: "cafe\u{301}"
café // => 1: this constant has one value
café // => 2: this indistinguishable constant has a different value
Unicode 标准定义了所有字符的首选编码,并指定了一种规范化过程,将文本转换为适合比较的规范形式。JavaScript 假定它正在解释的源代码已经被规范化,并且不会自行进行任何规范化。如果您计划在 JavaScript 程序中使用 Unicode 字符,您应确保您的编辑器或其他工具对源代码执行 Unicode 规范化,以防止您最终得到不同但在视觉上无法区分的标识符。
2.6 可选分号
像许多编程语言一样,JavaScript 使用分号(;
)来分隔语句(参见第五章)。这对于使代码的含义清晰很重要:没有分隔符,一个语句的结尾可能看起来是下一个语句的开头,反之亦然。在 JavaScript 中,如果两个语句写在不同行上,通常可以省略这两个语句之间的分号。(如果程序的下一个标记是闭合大括号}
,也可以省略分号。)许多 JavaScript 程序员(以及本书中的代码)使用分号明确标记语句的结尾,即使不需要也是如此。另一种风格是尽可能省略分号,只在需要时使用。无论你选择哪种风格,都应该了解 JavaScript 中可选分号的一些细节。
考虑以下代码。由于两个语句出现在不同行上,第一个分号可以省略:
a = 3;
b = 4;
然而,按照以下方式书写,第一个分号是必需的:
a = 3; b = 4;
请注意,JavaScript 并不会将每个换行符都视为分号:通常只有在无法解析代码而需要添加隐式分号时,才会将换行符视为分号。更正式地说(稍后描述的三个例外情况),如果下一个非空格字符无法被解释为当前语句的延续,JavaScript 将换行符视为分号。考虑以下代码:
let a
a
=
3
console.log(a)
JavaScript 解释这段代码如下:
let a; a = 3; console.log(a);
JavaScript 将第一个换行符视为分号,因为它无法解析不带分号的代码let a a
。第二个a
可以作为语句a;
独立存在,但 JavaScript 不会将第二个换行符视为分号,因为它可以继续解析较长的语句a = 3;
。
这些语句终止规则会导致一些令人惊讶的情况。这段代码看起来像是两个用换行符分隔的独立语句:
let y = x + f
(a+b).toString()
但是代码的第二行括号可以被解释为从第一行调用f
的函数调用,JavaScript 会这样解释代码:
let y = x + f(a+b).toString();
很可能这并不是代码作者打算的解释。为了作为两个独立语句工作,这种情况下需要一个显式分号。
一般来说,如果语句以(
、[
、/
、+
或-
开头,那么它可能被解释为前一个语句的延续。以/
、+
和-
开头的语句在实践中相当罕见,但以(
和[
开头的语句在某些 JavaScript 编程风格中并不罕见。一些程序员喜欢在这类语句的开头放置一个防御性分号,以便即使修改了其前面的语句并删除了先前的分号,它仍将正确工作:
let x = 0 // Semicolon omitted here
;[x,x+1,x+2].forEach(console.log) // Defensive ; keeps this statement separate
有三个例外情况不符合 JavaScript 将换行符解释为分号的一般规则,即当它无法将第二行解析为第一行语句的延续时。第一个例外涉及return
、throw
、yield
、break
和continue
语句(参见第五章)。这些语句通常是独立的,但有时会跟随标识符或表达式。如果这些单词之后(在任何其他标记之前)出现换行符,JavaScript 将始终将该换行符解释为分号。例如,如果你写:
return
true;
JavaScript 假设你的意思是:
return; true;
然而,你可能的意思是:
return true;
这意味着你不能在return
、break
或continue
与后面的表达式之间插入换行符。如果插入换行符,你的代码很可能会以难以调试的非明显方式失败。
第二个例外涉及++
和−−
运算符(§4.8)。这些运算符可以是前缀运算符,出现在表达式之前,也可以是后缀运算符,出现在表达式之后。如果要将这些运算符之一用作后缀运算符,它们必须出现在应用于的表达式的同一行上。第三个例外涉及使用简洁的“箭头”语法定义的函数:=>
箭头本身必须出现在参数列表的同一行上。
2.7 总结
本章展示了 JavaScript 程序是如何在最低级别编写的。下一章将带我们迈向更高一级,并介绍作为 JavaScript 程序计算的基本单位的原始类型和值(数字、字符串等)。
第三章:类型、值和变量
计算机程序通过操作值来工作,例如数字 3.14 或文本“Hello World”。在编程语言中可以表示和操作的值的种类称为类型,编程语言的最基本特征之一是它支持的类型集合。当程序需要保留一个值以供将来使用时,它将该值分配给(或“存储”在)一个变量中。变量有名称,并且允许在我们的程序中使用这些名称来引用值。变量的工作方式是任何编程语言的另一个基本特征。本章解释了 JavaScript 中的类型、值和变量。它从概述和一些定义开始。
3.1 概述和定义
JavaScript 类型可以分为两类:原始类型 和 对象类型。JavaScript 的原始类型包括数字、文本字符串(称为字符串)和布尔真值(称为布尔值)。本章的重要部分详细解释了 JavaScript 中的数字(§3.2)和字符串(§3.3)类型。布尔值在§3.4 中介绍。
特殊的 JavaScript 值 null
和 undefined
是原始值,但它们不是数字、字符串或布尔值。每个值通常被认为是其自己特殊类型的唯一成员。关于 null
和 undefined
的更多内容请参见§3.5。ES6 添加了一种新的特殊类型,称为 Symbol,它可以在不影响向后兼容性的情况下定义语言扩展。Symbols 在§3.6 中简要介绍。
任何不是数字、字符串、布尔值、符号、null
或 undefined
的 JavaScript 值都是对象。对象(即类型 object 的成员)是一个属性集合,其中每个属性都有一个名称和一个值(可以是原始值或另一个对象)。一个非常特殊的对象,全局对象,在§3.7 中介绍,但是一般和更详细的对象覆盖在第六章中。
一个普通的 JavaScript 对象是一个无序的命名值集合。该语言还定义了一种特殊类型的对象,称为数组,表示一个有序的编号值集合。JavaScript 语言包括特殊的语法用于处理数组,并且数组具有一些特殊的行为,使它们与普通对象有所区别。数组是第七章的主题。
除了基本对象和数组之外,JavaScript 还定义了许多其他有用的对象类型。Set 对象表示一组值。Map 对象表示从键到值的映射。各种“类型化数组”类型便于对字节数组和其他二进制数据进行操作。RegExp 类型表示文本模式,并支持对字符串进行复杂的匹配、搜索和替换操作。Date 类型表示日期和时间,并支持基本的日期算术。Error 及其子类型表示执行 JavaScript 代码时可能出现的错误。所有这些类型在第十一章中介绍。
JavaScript 与更静态的语言不同之处在于函数和类不仅仅是语言语法的一部分:它们本身是 JavaScript 程序可以操作的值。与任何不是原始值的 JavaScript 值一样,函数和类是一种特殊类型的对象。它们在第八章和第九章中详细介绍。
JavaScript 解释器执行自动垃圾回收以进行内存管理。这意味着 JavaScript 程序员通常不需要担心对象或其他值的销毁或释放。当一个值不再可达时——当程序不再有任何方式引用它时——解释器知道它永远不会再被使用,并自动回收它占用的内存。(JavaScript 程序员有时需要小心确保值不会意外地保持可达——因此不可回收——时间比必要长。)
JavaScript 支持面向对象的编程风格。宽松地说,这意味着与其在全局定义函数来操作各种类型的值,类型本身定义了用于处理值的方法。例如,要对数组a
的元素进行排序,我们不会将a
传递给sort()
函数。相反,我们调用a
的sort()
方法:
a.sort(); // The object-oriented version of sort(a).
方法定义在第九章中介绍。技术上,只有 JavaScript 对象有方法。但是数字、字符串、布尔值和符号值的行为就好像它们有方法一样。在 JavaScript 中,只有null
和undefined
是不能调用方法的值。
JavaScript 的对象类型是可变的,而其原始类型是不可变的。可变类型的值可以改变:JavaScript 程序可以更改对象属性和数组元素的值。数字、布尔值、符号、null
和undefined
是不可变的——例如,谈论更改数字的值甚至没有意义。字符串可以被视为字符数组,你可能期望它们是可变的。然而,在 JavaScript 中,字符串是不可变的:你可以访问字符串的任何索引处的文本,但 JavaScript 没有提供一种方法来更改现有字符串的文本。可变和不可变值之间的差异在§3.8 中进一步探讨。
JavaScript 自由地将一个类型的值转换为另一个类型。例如,如果一个程序期望一个字符串,而你给了它一个数字,它会自动为你将数字转换为字符串。如果你在期望布尔值的地方使用了非布尔值,JavaScript 会相应地进行转换。值转换的规则在§3.9 中解释。JavaScript 自由的值转换规则影响了它对相等性的定义,==
相等运算符执行如§3.9.1 中描述的类型转换。(然而,在实践中,==
相等运算符已被弃用,而是使用严格相等运算符===
,它不进行类型转换。有关这两个运算符的更多信息,请参见§4.9.1。)
常量和变量允许您在程序中使用名称引用值。常量使用const
声明,变量使用let
声明(或在旧的 JavaScript 代码中使用var
)。JavaScript 的常量和变量是无类型的:声明不指定将分配什么类型的值。变量声明和赋值在§3.10 中介绍。
从这个长篇介绍中可以看出,这是一个涵盖广泛的章节,解释了 JavaScript 中数据如何表示和操作的许多基本细节。我们将从直接深入讨论 JavaScript 数字和文本的细节开始。
3.2 数字
JavaScript 的主要数值类型 Number 用于表示整数和近似实数。JavaScript 使用 IEEE 754 标准定义的 64 位浮点格式表示数字,¹这意味着它可以表示大约±1.7976931348623157 × 10³⁰⁸和小约±5 × 10^(−324)的数字。
JavaScript 数字格式允许您精确表示介于−9,007,199,254,740,992(−2⁵³)和 9,007,199,254,740,992(2⁵³)之间的所有整数,包括这两个数。如果使用大于此值的整数值,可能会失去尾数的精度。但请注意,JavaScript 中的某些操作(如数组索引和第四章中描述的位运算符)是使用 32 位整数执行的。如果需要精确表示更大的整数,请参阅§3.2.5。
当一个数字直接出现在 JavaScript 程序中时,它被称为数字文字。JavaScript 支持几种格式的数字文字,如下面的部分所述。请注意,任何数字文字都可以在前面加上减号(-)以使数字为负数。
3.2.1 整数文字
在 JavaScript 程序中,十进制整数被写为数字序列。例如:
0
3
10000000
除了十进制整数文字,JavaScript 还识别十六进制(基数 16)值。十六进制文字以0x
或0X
开头,后跟一串十六进制数字。十六进制数字是数字 0 到 9 或字母 a(或 A)到 f(或 F)中的一个,表示值 10 到 15。以下是十六进制整数文字的示例:
0xff // => 255: (15*16 + 15)
0xBADCAFE // => 195939070
在 ES6 及更高版本中,你还可以使用前缀0b
和0o
(或0B
和0O
)来表示二进制(基数 2)或八进制(基数 8)中的整数,而不是0x
:
0b10101 // => 21: (1*16 + 0*8 + 1*4 + 0*2 + 1*1)
0o377 // => 255: (3*64 + 7*8 + 7*1)
3.2.2 浮点数文字
浮点文字可以有小数点;它们使用实数的传统语法。一个实数由数字的整数部分表示,后跟一个小数点和数字的小数部分。
浮点文字也可以使用指数表示法表示:一个实数后跟字母 e(或 E),后跟一个可选的加号或减号,后跟一个整数指数。这种表示法表示实数乘以 10 的指数次幂。
更简洁地说,语法是:
[*`digits`*][.*`digits`*][(E|e)[(+|-)]*`digits`*]
例如:
3.14
2345.6789
.333333333333333333
6.02e23 // 6.02 × 10²³
1.4738223E-32 // 1.4738223 × 10⁻³²
3.2.3 JavaScript 中的算术
JavaScript 程序使用语言提供的算术运算符与数字一起工作。这些包括+
用于加法,-
用于减法,*
用于乘法,/
用于除法,%
用于取模(除法后的余数)。ES2016 添加了**
用于指数运算。关于这些和其他运算符的详细信息可以在第四章中找到。
除了这些基本算术运算符外,JavaScript 通过一组函数和常量定义为Math
对象的属性支持更复杂的数学运算:
Math.pow(2,53) // => 9007199254740992: 2 to the power 53
Math.round(.6) // => 1.0: round to the nearest integer
Math.ceil(.6) // => 1.0: round up to an integer
Math.floor(.6) // => 0.0: round down to an integer
Math.abs(-5) // => 5: absolute value
Math.max(x,y,z) // Return the largest argument
Math.min(x,y,z) // Return the smallest argument
Math.random() // Pseudo-random number x where 0 <= x < 1.0
Math.PI // π: circumference of a circle / diameter
Math.E // e: The base of the natural logarithm
Math.sqrt(3) // => 3**0.5: the square root of 3
Math.pow(3, 1/3) // => 3**(1/3): the cube root of 3
Math.sin(0) // Trigonometry: also Math.cos, Math.atan, etc.
Math.log(10) // Natural logarithm of 10
Math.log(100)/Math.LN10 // Base 10 logarithm of 100
Math.log(512)/Math.LN2 // Base 2 logarithm of 512
Math.exp(3) // Math.E cubed
ES6 在Math
对象上定义了更多函数:
Math.cbrt(27) // => 3: cube root
Math.hypot(3, 4) // => 5: square root of sum of squares of all arguments
Math.log10(100) // => 2: Base-10 logarithm
Math.log2(1024) // => 10: Base-2 logarithm
Math.log1p(x) // Natural log of (1+x); accurate for very small x
Math.expm1(x) // Math.exp(x)-1; the inverse of Math.log1p()
Math.sign(x) // -1, 0, or 1 for arguments <, ==, or > 0
Math.imul(2,3) // => 6: optimized multiplication of 32-bit integers
Math.clz32(0xf) // => 28: number of leading zero bits in a 32-bit integer
Math.trunc(3.9) // => 3: convert to an integer by truncating fractional part
Math.fround(x) // Round to nearest 32-bit float number
Math.sinh(x) // Hyperbolic sine. Also Math.cosh(), Math.tanh()
Math.asinh(x) // Hyperbolic arcsine. Also Math.acosh(), Math.atanh()
JavaScript 中的算术运算不会在溢出、下溢或除以零的情况下引发错误。当数值运算的结果大于最大可表示的数(溢出)时,结果是一个特殊的无穷大值,Infinity
。同样,当负值的绝对值变得大于最大可表示的负数的绝对值时,结果是负无穷大,-Infinity
。无穷大值的行为如你所期望的那样:将它们相加、相减、相乘或相除的结果是一个无穷大值(可能带有相反的符号)。
下溢发生在数值运算的结果接近零而不是最小可表示数时。在这种情况下,JavaScript 返回 0。如果下溢发生在负数中,JavaScript 返回一个称为“负零”的特殊值。这个值几乎与普通零完全无法区分,JavaScript 程序员很少需要检测它。
在 JavaScript 中,除以零不会导致错误:它只是返回正无穷大或负无穷大。然而,有一个例外:零除以零没有明确定义的值,这个操作的结果是特殊的非数字值 NaN
。如果尝试将无穷大除以无穷大、对负数取平方根或使用无法转换为数字的非数字操作数进行算术运算,也会产生 NaN
。
JavaScript 预定义全局常量 Infinity
和 NaN
分别表示正无穷大和非数字值,并且这些值也作为 Number
对象的属性可用:
Infinity // A positive number too big to represent
Number.POSITIVE_INFINITY // Same value
1/0 // => Infinity
Number.MAX_VALUE * 2 // => Infinity; overflow
-Infinity // A negative number too big to represent
Number.NEGATIVE_INFINITY // The same value
-1/0 // => -Infinity
-Number.MAX_VALUE * 2 // => -Infinity
NaN // The not-a-number value
Number.NaN // The same value, written another way
0/0 // => NaN
Infinity/Infinity // => NaN
Number.MIN_VALUE/2 // => 0: underflow
-Number.MIN_VALUE/2 // => -0: negative zero
-1/Infinity // -> -0: also negative 0
-0
// The following Number properties are defined in ES6
Number.parseInt() // Same as the global parseInt() function
Number.parseFloat() // Same as the global parseFloat() function
Number.isNaN(x) // Is x the NaN value?
Number.isFinite(x) // Is x a number and finite?
Number.isInteger(x) // Is x an integer?
Number.isSafeInteger(x) // Is x an integer -(2**53) < x < 2**53?
Number.MIN_SAFE_INTEGER // => -(2**53 - 1)
Number.MAX_SAFE_INTEGER // => 2**53 - 1
Number.EPSILON // => 2**-52: smallest difference between numbers
在 JavaScript 中,非数字值具有一个不寻常的特征:它与任何其他值(包括自身)都不相等。这意味着您不能写 x === NaN
来确定变量 x
的值是否为 NaN
。相反,您必须写 x != x
或 Number.isNaN(x)
。只有当 x
的值与全局常量 NaN
相同时,这些表达式才为真。
全局函数 isNaN()
类似于 Number.isNaN()
。如果其参数是 NaN
,或者该参数是无法转换为数字的非数字值,则返回 true
。相关函数 Number.isFinite()
如果其参数是除 NaN
、Infinity
或 -Infinity
之外的数字,则返回 true
。全局函数 isFinite()
如果其参数是有限数字或可以转换为有限数字,则返回 true
。
负零值也有些不寻常。它与正零相等(即使使用 JavaScript 的严格相等测试),这意味着这两个值几乎无法区分,除非用作除数:
let zero = 0; // Regular zero
let negz = -0; // Negative zero
zero === negz // => true: zero and negative zero are equal
1/zero === 1/negz // => false: Infinity and -Infinity are not equal
3.2.4 二进制浮点数和舍入误差
实数有无限多个,但只有有限数量的实数(准确地说是 18,437,736,874,454,810,627)可以被 JavaScript 浮点格式精确表示。这意味着当您在 JavaScript 中使用实数时,该数字的表示通常是实际数字的近似值。
JavaScript 使用的 IEEE-754 浮点表示法(几乎所有现代编程语言都使用)是二进制表示法,可以精确表示分数如 1/2
、1/8
和 1/1024
。不幸的是,我们最常使用的分数(尤其是在进行财务计算时)是十进制分数:1/10
、1/100
等。二进制浮点表示法无法精确表示像 0.1
这样简单的数字。
JavaScript 数字具有足够的精度,可以非常接近地近似 0.1
。但是,这个数字无法精确表示可能会导致问题。考虑以下代码:
let x = .3 - .2; // thirty cents minus 20 cents
let y = .2 - .1; // twenty cents minus 10 cents
x === y // => false: the two values are not the same!
x === .1 // => false: .3-.2 is not equal to .1
y === .1 // => true: .2-.1 is equal to .1
由于四舍五入误差,.3 和 .2 的近似值之间的差异并不完全等同于 .2 和 .1 的近似值之间的差异。重要的是要理解这个问题并不特定于 JavaScript:它影响任何使用二进制浮点数的编程语言。此外,请注意代码中的值 x
和 y
非常接近彼此和正确值。计算出的值对于几乎任何目的都是足够的;问题只在我们尝试比较相等值时才会出现。
如果这些浮点数近似值对您的程序有问题,请考虑使用缩放整数。例如,您可以将货币值作为整数分而不是小数美元进行操作。
3.2.5 使用 BigInt 进行任意精度整数运算
JavaScript 的最新特性之一,定义在 ES2020 中,是一种称为 BigInt 的新数值类型。截至 2020 年初,它已经在 Chrome、Firefox、Edge 和 Node 中实现,并且 Safari 中正在进行实现。顾名思义,BigInt 是一个数值类型,其值为整数。JavaScript 主要添加了这种类型,以允许表示 64 位整数,这对于与许多其他编程语言和 API 兼容是必需的。但是 BigInt 值可以有数千甚至数百万位数字,如果你需要处理如此大的数字的话。(但是请注意,BigInt 实现不适用于加密,因为它们不会尝试防止时间攻击。)
BigInt 字面量写为一个由数字组成的字符串,后面跟着一个小写字母 n
。默认情况下,它们是以 10 进制表示的,但你可以使用 0b
、0o
和 0x
前缀来表示二进制、八进制和十六进制的 BigInt:
1234n // A not-so-big BigInt literal
0b111111n // A binary BigInt
0o7777n // An octal BigInt
0x8000000000000000n // => 2n**63n: A 64-bit integer
你可以将 BigInt()
作为一个函数,用于将常规的 JavaScript 数字或字符串转换为 BigInt 值:
BigInt(Number.MAX_SAFE_INTEGER) // => 9007199254740991n
let string = "1" + "0".repeat(100); // 1 followed by 100 zeros.
BigInt(string) // => 10n**100n: one googol
与 BigInt 值进行算术运算的方式与常规 JavaScript 数字的算术运算类似,只是除法会舍弃任何余数并向下取整(朝着零的方向):
1000n + 2000n // => 3000n
3000n - 2000n // => 1000n
2000n * 3000n // => 6000000n
3000n / 997n // => 3n: the quotient is 3
3000n % 997n // => 9n: and the remainder is 9
(2n ** 131071n) - 1n // A Mersenne prime with 39457 decimal digits
尽管标准的 +
、-
、*
、/
、%
和 **
运算符可以与 BigInt 一起使用,但重要的是要理解,你不能将 BigInt 类型的操作数与常规数字操作数混合使用。这一开始可能看起来令人困惑,但这是有充分理由的。如果一个数值类型比另一个更通用,那么可以很容易地定义混合操作数的算术运算,只需返回更通用类型的值。但是没有一个类型比另一个更通用:BigInt 可以表示非常大的值,使其比常规数字更通用。但 BigInt 只能表示整数,使得常规的 JavaScript 数字类型更通用。这个问题没有解决的方法,所以 JavaScript 通过简单地不允许混合操作数来绕过它。
相比之下,比较运算符可以处理混合数值类型(但请参阅 §3.9.1 了解有关 ==
和 ===
之间差异的更多信息):
1 < 2n // => true
2 > 1n // => true
0 == 0n // => true
0 === 0n // => false: the === checks for type equality as well
位运算符(在 §4.8.3 中描述)通常与 BigInt 操作数一起使用。然而,Math
对象的函数都不接受 BigInt 操作数。
3.2.6 日期和时间
JavaScript 定义了一个简单的 Date 类来表示和操作表示日期和时间的数字。JavaScript 的日期是对象,但它们也有一个数值表示作为 时间戳,指定自 1970 年 1 月 1 日以来经过的毫秒数:
let timestamp = Date.now(); // The current time as a timestamp (a number).
let now = new Date(); // The current time as a Date object.
let ms = now.getTime(); // Convert to a millisecond timestamp.
let iso = now.toISOString(); // Convert to a string in standard format.
Date 类及其方法在 §11.4 中有详细介绍。但是我们将在 §3.9.3 中再次看到 Date 对象,当我们检查 JavaScript 类型转换的细节时。
3.3 文本
用于表示文本的 JavaScript 类型是 字符串。字符串是一个不可变的有序 16 位值序列,其中每个值通常表示一个 Unicode 字符。字符串的 长度 是它包含的 16 位值的数量。JavaScript 的字符串(以及其数组)使用从零开始的索引:第一个 16 位值位于位置 0,第二个位于位置 1,依此类推。空字符串 是长度为 0 的字符串。JavaScript 没有一个特殊的类型来表示字符串的单个元素。要表示一个单个的 16 位值,只需使用长度为 1 的字符串。
3.3.1 字符串字面量
要在 JavaScript 程序中包含一个字符串,只需将字符串的字符置于匹配的一对单引号、双引号或反引号中('
或 "
或 `
)。双引号字符和反斜线可能包含在由单引号字符分隔的字符串中,由双引号和反斜线分隔的字符串也是如此。以下是字符串文字的示例:
"" // 空字符串:它没有任何字符
'testing'
"3.14"
'name="myform"'
"Wouldn't you prefer O'Reilly's book?"
"τ is the ratio of a circle's circumference to its radius"
`"She said ''hi''", he said.`
使用反引号界定的字符串是 ES6 的一个特性,允许将 JavaScript 表达式嵌入到字符串字面量中(或 插入 到其中)。这种表达式插值语法在 §3.3.4 中有介绍。
JavaScript 的原始版本要求字符串字面量写在单行上,通常会看到 JavaScript 代码通过使用 +
运算符连接单行字符串来创建长字符串。然而,从 ES5 开始,你可以通过在每行的末尾(除了最后一行)加上反斜杠(\
)来跨多行书写字符串字面量。反斜杠和其后的换行符不属于字符串字面量的一部分。如果需要在单引号或双引号字符串字面量中包含换行符,可以使用字符序列 \n
(在下一节中有介绍)。ES6 的反引号语法允许字符串跨多行书写,此时换行符属于字符串字面量的一部分:
// 一个表示在一行上写的 2 行的字符串:
'two\nlines'
"one\
long\
line"
// 两行字符串分别写在两行上:
`the newline character at the end of this line
is included literally in this string`
请注意,当使用单引号界定字符串时,必须小心处理英语缩写和所有格,例如 can’t 和 O’Reilly’s。由于撇号与单引号字符相同,必须使用反斜杠字符(\
)来“转义”出现在单引号字符串中的任何撇号(转义在下一节中有解释)。
在客户端 JavaScript 编程中,JavaScript 代码可能包含 HTML 代码的字符串,而 HTML 代码可能包含 JavaScript 代码的字符串。与 JavaScript 一样,HTML 使用单引号或双引号来界定其字符串。因此,在结合 JavaScript 和 HTML 时,最好使用一种引号风格用于 JavaScript,另一种引号风格用于 HTML。在下面的示例中,“Thank you” 字符串在 JavaScript 表达式中使用单引号引起,然后在 HTML 事件处理程序属性中使用双引号引起:
<button onclick="alert('Thank you')">Click Me</button>
3.3.2 字符串字面量中的转义序列
反斜杠字符(\
)在 JavaScript 字符串中有特殊用途。与其后的字符结合,它表示字符串中无法用其他方式表示的字符。例如,\n
是表示换行字符的 转义序列。
另一个之前提到的例子是 \'
转义,表示单引号(或撇号)字符。当需要在包含在单引号中的字符串字面量中包含撇号时,这个转义序列很有用。你可以看到为什么这些被称为转义序列:反斜杠允许你从单引号字符的通常解释中逃脱。你不再使用它来标记字符串的结束,而是将其用作撇号:
'You\'re right, it can\'t be a quote'
表 3-1 列出了 JavaScript 转义序列及其表示的字符。三个转义序列是通用的,可以通过指定其 Unicode 字符代码作为十六进制数来表示任何字符。例如,序列 \xA9
表示版权符号,其 Unicode 编码由十六进制数 A9 给出。类似地,\u
转义表示由四个十六进制数字或在大括号中括起的一到五个数字指定的任意 Unicode 字符:例如,\u03c0
表示字符 π,而 \u{1f600}
表示“笑脸”表情符号。
表 3-1. JavaScript 转义序列
如果 \
字符位于除表 3-1 中显示的字符之外的任何字符之前,则反斜杠将被简单地忽略(尽管语言的未来版本当然可以定义新的转义序列)。例如,\#
与 #
相同。最后,正如前面提到的,ES5 允许在换行符之前放置反斜杠,以便跨多行断开字符串文字。
3.3.3 处理字符串
JavaScript 的内置功能之一是能够连接字符串。如果您使用 +
运算符与数字一起使用,它们会相加。但是如果您在字符串上使用此运算符,则会通过将第二个字符串附加到第一个字符串来连接它们。例如:
let msg = "Hello, " + "world"; // 生成字符串 "Hello, world"
let greeting = "Welcome to my blog," + " " + name;
字符串可以使用标准的 ===
相等和 !==
不等运算符进行比较:只有当它们由完全相同的 16 位值序列组成时,两个字符串才相等。字符串也可以使用 <
、<=
、>
和 >=
运算符进行比较。字符串比较只是简单地比较 16 位值。(有关更健壮的区域感知字符串比较和排序,请参见 §11.7.3。)
要确定字符串的长度——它包含的 16 位值的数量——请使用字符串的 length
属性:
s.length
除了 length
属性之外,JavaScript 还提供了丰富的 API 用于处理字符串:
let s = "Hello, world"; // 以一些文本开头。
// 获取字符串的部分
s.substring(1,4) // => "ell": 第 2、3、4 个字符。
s.slice(1,4) // => "ell": 同上
s.slice(-3) // => "rld": 最后 3 个字符
s.split(", ") // => ["Hello", "world"]: 在分隔符字符串处分割
// 搜索字符串
s.indexOf("l") // => 2: 第一个字母 l 的位置
s.indexOf("l", 3) // => 3: 第一个 "l" 在或之后 3 的位置
s.indexOf("zz") // => -1: s 不包含子字符串 "zz"
s.lastIndexOf("l") // => 10: 最后一个字母 l 的位置
// ES6 及更高版本中的布尔搜索函数
s.startsWith("Hell") // => true: 字符串以这些开头
s.endsWith("!") // => false: s 不以此结尾
s.includes("or") // => true: s 包含子字符串 "or"
// 创建字符串的修改版本
s.replace("llo", "ya") // => "Heya, world"
s.toLowerCase() // => "hello, world"
s.toUpperCase() // => "HELLO, WORLD"
s.normalize() // Unicode NFC 标准化:ES6
s.normalize("NFD") // NFD 标准化。也可用 "NFKC", "NFKD"
// 检查字符串的各个(16 位)字符
s.charAt(0) // => "H": 第一个字符
s.charAt(s.length-1) // => "d": 最后一个字符
s.charCodeAt(0) // => 72: 指定位置的 16 位数字
s.codePointAt(0) // => 72: ES6,适用于大于 16 位的码点
// ES2017 中的字符串填充函数
"x".padStart(3) // => " x": 在左侧添加空格,使长度为 3
"x".padEnd(3) // => "x ": 在右侧添加空格,使长度为 3
"x".padStart(3, "*") // => "**x": 在左侧添加星号,使长度为 3
"x".padEnd(3, "-") // => "x--": 在右侧添加破折号,使长度为 3
// 修剪空格函数。trim() 是 ES5;其他是 ES2019
" test ".trim() // => "test": 删除开头和结尾的空格
" test ".trimStart() // => "test ": 删除左侧的空格。也可用 trimLeft
" test ".trimEnd() // => " test": 删除右侧的空格。也可用 trimRight
// 其他字符串方法
s.concat("!") // => "Hello, world!": 只需使用 + 运算符
"<>".repeat(5) // => "<><><><><>": 连接 n 个副本。ES6
请记住,在 JavaScript 中字符串是不可变的。像 replace()
和 toUpperCase()
这样的方法会返回新的字符串:它们不会修改调用它们的字符串。
字符串也可以像只读数组一样处理,您可以使用方括号而不是 charAt()
方法从字符串中访问单个字符(16 位值):
let s = "hello, world";
s[0] // => "h"
s[s.length-1] // => "d"
3.3.4 模板字面量
在 ES6 及更高版本中,字符串字面量可以用反引号括起来:
let s = `hello world`;
然而,这不仅仅是另一种字符串字面量语法,因为这些模板字面量可以包含任意的 JavaScript 表达式。反引号中的字符串字面量的最终值是通过评估包含的任何表达式,将这些表达式的值转换为字符串,并将这些计算出的字符串与反引号中的文字字符组合而成的:
let name = "Bill";
let greeting = `Hello ${ name }.`; // greeting == "Hello Bill."
${
和匹配的 }
之间的所有内容都被解释为 JavaScript 表达式。花括号外的所有内容都是普通的字符串文字。花括号内的表达式被评估,然后转换为字符串并插入到模板中,替换美元符号、花括号和它们之间的所有内容。
模板字面量可以包含任意数量的表达式。它可以使用任何普通字符串可以使用的转义字符,并且可以跨越任意数量的行,不需要特殊的转义。以下模板字面量包括四个 JavaScript 表达式,一个 Unicode 转义序列,以及至少四个换行符(表达式的值也可能包含换行符):
let errorMessage = `\
# \u2718 Test failure at ${filename}:${linenumber}:
${exception.message}
Stack trace:
${exception.stack}
`;
这里第一行末尾的反斜杠转义了初始换行符,使得生成的字符串以 Unicode ✘ 字符 (# \u2718
) 开头,而不是一个换行符。
标记模板字面量
模板字面量的一个强大但不常用的特性是,如果一个函数名(或“标签”)紧跟在反引号之前,那么模板字面量中的文本和表达式的值将传递给该函数。标记模板字面量的值是函数的返回值。例如,这可以用来在将值替换到文本之前应用 HTML 或 SQL 转义。
ES6 中有一个内置的标签函数:String.raw()
。它返回反引号内的文本,不处理反斜杠转义:
`\n`.length // => 1: 字符串有一个换行符
String.raw`\n`.length // => 2: 一个反斜杠字符和字母 n
请注意,即使标记模板字面量的标签部分是一个函数,也不需要在其调用中使用括号。在这种非常特殊的情况下,反引号字符替换了开放和关闭括号。
定义自己的模板标签函数的能力是 JavaScript 的一个强大特性。这些函数不需要返回字符串,并且可以像构造函数一样使用,就好像为语言定义了一种新的文字语法。我们将在§14.5 中看到一个例子。
3.3.5 模式匹配
JavaScript 定义了一种称为正则表达式(或 RegExp)的数据类型,用于描述和匹配文本字符串中的模式。RegExps 不是 JavaScript 中的基本数据类型之一,但它们具有类似数字和字符串的文字语法,因此有时似乎是基本的。正则表达式文字的语法复杂,它们定义的 API 也不简单。它们在§11.3 中有详细说明。然而,由于 RegExps 功能强大且常用于文本处理,因此本节提供了简要概述。
一对斜杠之间的文本构成正则表达式文字。在一对斜杠中的第二个斜杠后面也可以跟随一个或多个字母,这些字母修改模式的含义。例如:
/^HTML/; // 匹配字符串开头的字母 H T M L
/[1-9][0-9]*/; // 匹配非零数字,后跟任意数量的数字
/\bjavascript\b/i; // 匹配 "javascript" 作为一个单词,不区分大小写
RegExp 对象定义了许多有用的方法,字符串也有接受 RegExp 参数的方法。例如:
let text = "testing: 1, 2, 3"; // 示例文本
let pattern = /\d+/g; // 匹配所有一个或多个数字的实例
pattern.test(text) // => true: 存在匹配项
text.search(pattern) // => 9: 第一个匹配项的位置
text.match(pattern) // => ["1", "2", "3"]: 所有匹配项的数组
text.replace(pattern, "#") // => "testing: #, #, #"
text.split(/\D+/) // => ["","1","2","3"]: 以非数字为分隔符进行分割
3.4 布尔值
布尔值表示真或假,开或关,是或否。此类型仅有两个可能的值。保留字true
和false
评估为这两个值。
布尔值通常是您在 JavaScript 程序中进行比较的结果。例如:
a === 4
此代码测试变量a
的值是否等于数字4
。如果是,则此比较的结果是布尔值true
。如果a
不等于4
,则比较的结果是false
。
布尔值通常在 JavaScript 控制结构中使用。例如,JavaScript 中的if/else
语句在布尔值为true
时执行一个操作,在值为false
时执行另一个操作。通常将直接创建布尔值的比较与使用它的语句结合在一起。结果如下:
if (a === 4) {
b = b + 1;
} else {
a = a + 1;
}
此代码检查a
是否等于4
。如果是,则将1
添加到b
;否则,将1
添加到a
。
正如我们将在§3.9 中讨论的那样,任何 JavaScript 值都可以转换为布尔值。以下值转换为,并因此像false
一样工作:
undefined
null
0
-0
NaN
"" // 空字符串
所有其他值,包括所有对象(和数组)转换为,并像true
一样工作。false
和转换为它的六个值有时被称为假值,所有其他值被称为真值。每当 JavaScript 期望布尔值时,假值像false
一样工作,真值像true
一样工作。
例如,假设变量o
可以保存对象或值null
。您可以使用如下if
语句明确测试o
是否非空:
if (o !== null) ...
不等运算符!==
比较o
和null
,并评估为true
或false
。但您可以省略比较,而是依赖于null
为假值,对象为真值的事实:
if (o) ...
在第一种情况下,只有当o
不是null
时,if
的主体才会被执行。第二种情况不那么严格:只有当o
不是false
或任何假值(如null
或undefined
)时,if
的主体才会被执行。哪种if
语句适合你的程序实际上取决于你期望为o
分配什么值。如果你需要区分null
和0
以及""
,那么你应该使用显式比较。
布尔值有一个toString()
方法,你可以用它将它们转换为字符串“true”或“false”,但它们没有其他有用的方法。尽管 API 很简单,但有三个重要的布尔运算符。
&&
运算符执行布尔 AND 操作。只有当它的两个操作数都为真时,它才会评估为真;否则它会评估为假。||
运算符是布尔 OR 操作:如果它的一个(或两个)操作数为真,则它评估为真,如果两个操作数都为假,则它评估为假。最后,一元!
运算符执行布尔 NOT 操作:如果它的操作数为假,则评估为true
,如果它的操作数为真,则评估为false
。例如:
if ((x === 0 && y === 0) || !(z === 0)) {
// x 和 y 都为零或 z 非零
}
这些运算符的详细信息在§4.10 中。
3.5 null 和 undefined
null
是一个语言关键字,其值通常用于指示值的缺失。对null
使用typeof
运算符会返回字符串“object”,表明null
可以被视为指示“没有对象”的特殊对象值。然而,在实践中,null
通常被视为其自身类型的唯一成员,并且它可以用于表示数字、字符串以及对象的“无值”。大多数编程语言都有类似 JavaScript 的null
的等价物:你可能熟悉它作为NULL
、nil
或None
。
JavaScript 还有第二个表示值缺失的值。undefined
值代表一种更深层次的缺失。它是未初始化变量的值,以及查询不存在的对象属性或数组元素的值时得到的值。undefined
值也是那些没有显式返回值的函数的返回值,以及没有传递参数的函数参数的值。undefined
是一个预定义的全局常量(不像null
那样是一个语言关键字,尽管在实践中这并不是一个重要的区别),它被初始化为undefined
值。如果你对undefined
值应用typeof
运算符,它会返回undefined
,表明这个值是一个特殊类型的唯一成员。
尽管存在这些差异,null
和undefined
都表示值的缺失,并且通常可以互换使用。相等运算符==
认为它们相等。(使用严格相等运算符===
来区分它们。)它们都是假值:当需要布尔值时,它们的行为类似于false
。null
和undefined
都没有任何属性或方法。实际上,使用.
或[]
来访问这些值的属性或方法会导致 TypeError。
我认为undefined
表示系统级别的、意外的或类似错误的值缺失,而null
表示程序级别的、正常的或预期的值缺失。我尽量避免使用null
和undefined
,但如果需要将这些值分配给变量或属性,或者将这些值传递给函数或从函数中返回这些值,我通常使用null
。一些程序员努力避免使用null
,并在可能的情况下使用undefined
代替。
3.6 符号
在 ES6 中引入了符号作为非字符串属性名称。要理解符号,您需要知道 JavaScript 的基本 Object 类型是一个无序的属性集合,其中每个属性都有一个名称和一个值。属性名称通常(直到 ES6 之前一直)是字符串。但在 ES6 及以后的版本中,符号也可以用于此目的:
let strname = "string name"; // 用作属性名称的字符串
let symname = Symbol("propname"); // 用作属性名称的符号
typeof strname // => "string": strname 是一个字符串
typeof symname // => "symbol": symname 是一个符号
let o = {}; // 创建一个新对象
o[strname] = 1; // 使用字符串名称定义属性
o[symname] = 2; // 使用符号名称定义属性
o[strname] // => 1: 访问以字符串命名的属性
o[symname] // => 2: 访问以符号命名的属性
符号类型没有文字语法。要获得符号值,您需要调用Symbol()
函数。这个函数永远不会两次返回相同的值,即使使用相同的参数调用。这意味着如果您调用Symbol()
来获取一个符号值,您可以安全地将该值用作属性名称,以向对象添加新属性,而不必担心可能会覆盖同名的现有属性。同样,如果使用符号属性名称并且不共享这些符号,您可以确信程序中的其他代码模块不会意外地覆盖您的属性。
在实践中,符号作为一种语言扩展机制。当 ES6 引入了for/of
循环(§5.4.4)和可迭代对象(第十二章)时,需要定义标准方法,使类能够实现自身的可迭代性。但是,标准化任何特定的字符串名称作为此迭代器方法会破坏现有代码,因此使用了一个符号名称。正如我们将在第十二章中看到的,Symbol.iterator
是一个符号值,可以用作方法名称,使对象可迭代。
Symbol()
函数接受一个可选的字符串参数,并返回一个唯一的符号值。如果提供一个字符串参数,那么该字符串将包含在符号的toString()
方法的输出中。但请注意,使用相同的字符串两次调用Symbol()
会产生两个完全不同的符号值。
let s = Symbol("sym_x");
s.toString() // => "Symbol(sym_x)"
toString()
是 Symbol 实例唯一有趣的方法。但是,还有另外两个与 Symbol 相关的函数您应该了解。有时在使用 Symbols 时,您希望将它们私有化,以确保您的属性永远不会与其他代码使用的属性发生冲突。但是,有时您可能希望定义一个 Symbol 值并与其他代码广泛共享。例如,如果您正在定义某种扩展,希望其他代码能够参与其中,那么就会出现这种情况,就像之前描述的Symbol.iterator
机制一样。
为了满足后一种用例,JavaScript 定义了一个全局 Symbol 注册表。Symbol.for()
函数接受一个字符串参数,并返回与您传递的字符串关联的 Symbol 值。如果该字符串尚未关联任何 Symbol,则会创建并返回一个新的 Symbol;否则,将返回已存在的 Symbol。也就是说,Symbol.for()
函数与Symbol()
函数完全不同:Symbol()
永远不会两次返回相同的值,但Symbol.for()
在使用相同字符串调用时总是返回相同的值。传递给Symbol.for()
的字符串将出现在返回的 Symbol 的toString()
输出中,并且还可以通过在返回的 Symbol 上调用Symbol.keyFor()
来检索。
let s = Symbol.for("shared");
let t = Symbol.for("shared");
s === t // => true
s.toString() // => "Symbol(shared)"
Symbol.keyFor(t) // => "shared"
3.7 全局对象
前面的章节已经解释了 JavaScript 的原始类型和值。对象类型——对象、数组和函数——将在本书的后面章节中单独讨论。但是现在我们必须介绍一个非常重要的对象值。全局对象是一个常规的 JavaScript 对象,具有非常重要的作用:该对象的属性是 JavaScript 程序可用的全局定义标识符。当 JavaScript 解释器启动(或者每当 Web 浏览器加载新页面时),它会创建一个新的全局对象,并赋予它一组初始属性,用于定义:
-
像
undefined
、Infinity
和NaN
这样的全局常量 -
像
isNaN()
、parseInt()
(§3.9.2)和eval()
(§4.12)这样的全局函数 -
像
Date()
、RegExp()
、String()
、Object()
和Array()
(§3.9.2)这样的构造函数 -
像 Math 和 JSON(§6.8)这样的全局对象
全局对象的初始属性不是保留字,但应当视为保留字。本章已经描述了一些这些全局属性。其他大部分属性将在本书的其他地方介绍。
在 Node 中,全局对象有一个名为global
的属性,其值是全局对象本身,因此在 Node 程序中始终可以通过名称global
引用全局对象。
在 Web 浏览器中,Window 对象作为代表浏览器窗口中包含的所有 JavaScript 代码的全局对象。这个全局 Window 对象有一个自引用的window
属性,可以用来引用全局对象。Window 对象定义了核心全局属性,但它还定义了许多其他特定于 Web 浏览器和客户端 JavaScript 的全局对象。Web worker 线程(§15.13)具有与其关联的不同全局对象。工作线程中的代码可以将其全局对象称为self
。
ES2020 最终将globalThis
定义为在任何上下文中引用全局对象的标准方式。截至 2020 年初,这个功能已被所有现代浏览器和 Node 实现。
3.8 不可变的原始值和可变的对象引用
JavaScript 中原始值(undefined
、null
、布尔值、数字和字符串)和对象(包括数组和函数)之间有一个根本的区别。原始值是不可变的:没有办法改变(或“突变”)原始值。对于数字和布尔值来说,这是显而易见的——改变一个数字的值甚至没有意义。然而,对于字符串来说,情况并不那么明显。由于字符串类似于字符数组,您可能希望能够更改任何指定索引处的字符。实际上,JavaScript 不允许这样做,所有看起来返回修改后字符串的字符串方法实际上都是返回一个新的字符串值。例如:
let s = "hello"; // 从一些小写文本开始
s.toUpperCase(); // 返回"HELLO",但不改变 s
s // => "hello": 原始字符串没有改变
原始值也是按值比较的:只有当它们的值相同时,两个值才相同。对于数字、布尔值、null
和undefined
来说,这听起来很循环:它们没有其他比较方式。然而,对于字符串来说,情况并不那么明显。如果比较两个不同的字符串值,JavaScript 会将它们视为相等,当且仅当它们的长度相同,并且每个索引处的字符相同。
对象与原始值不同。首先,它们是可变的——它们的值可以改变:
let o = { x: 1 }; // 从一个对象开始
o.x = 2; // 通过更改属性的值来改变它
o.y = 3; // 通过添加新属性再次改变它
let a = [1,2,3]; // 数组也是可变的
a[0] = 0; // 改变数组元素的值
a[3] = 4; // 添加一个新的数组元素
对象不是按值比较的:即使它们具有相同的属性和值,两个不同的对象也不相等。即使它们具有相同顺序的相同元素,两个不同的数组也不相等:
let o = {x: 1}, p = {x: 1}; // 具有相同属性的两个对象
o === p // => false: 不同的对象永远不相等
let a = [], b = []; // 两个不同的空数组
a === b // => false: 不同的数组永远不相等
对象有时被称为引用类型,以区别于 JavaScript 的原始类型。使用这个术语,对象值是引用,我们说对象是按引用比较的:只有当两个对象值引用同一个基础对象时,它们才相同。
let a = []; // 变量 a 指向一个空数组。
let b = a; // 现在 b 指向同一个数组。
b[0] = 1; // 改变变量 b 引用的数组。
a[0] // => 1: 更改也通过变量 a 可见。
a === b // => true: a 和 b 指向同一个对象,所以它们相等。
从这段代码中可以看出,将对象(或数组)赋给变量只是赋予了引用:它并不创建对象的新副本。如果要创建对象或数组的新副本,必须显式复制对象的属性或数组的元素。这个示例演示了使用for
循环(§5.4.3):
let a = ["a","b","c"]; // 我们想要复制的数组
let b = []; // 我们将复制到的不同数组
for(let i = 0; i < a.length; i++) { // 对于 a[]的每个索引
b[i] = a[i]; // 将 a 的一个元素复制到 b
}
let c = Array.from(b); // 在 ES6 中,使用 Array.from()复制数组
同样,如果我们想比较两个不同的对象或数组,我们必须比较它们的属性或元素。以下代码定义了一个比较两个数组的函数:
function equalArrays(a, b) {
if (a === b) return true; // 相同的数组是相等的
if (a.length !== b.length) return false; // 不同大小的数组不相等
for(let i = 0; i < a.length; i++) { // 遍历所有元素
if (a[i] !== b[i]) return false; // 如果有任何不同,数组不相等
}
return true; // 否则它们是相等的
}
3.9 类型转换
JavaScript 对所需值的类型非常灵活。我们已经看到了布尔值的情况:当 JavaScript 需要一个布尔值时,您可以提供任何类型的值,JavaScript 将根据需要进行转换。一些值(“真值”)转换为 true
,而其他值(“假值”)转换为 false
。其他类型也是如此:如果 JavaScript 需要一个字符串,它将把您提供的任何值转换为字符串。如果 JavaScript 需要一个数字,它将尝试将您提供的值转换为数字(或者如果无法执行有意义的转换,则转换为 NaN
)。
一些例子:
10 + " objects" // => "10 objects": 数字 10 转换为字符串
"7" * "4" // => 28: 两个字符串都转换为数字
let n = 1 - "x"; // n == NaN; 字符串"x"无法转换为数字
n + " objects" // => "NaN objects": NaN 转换为字符串"NaN"
表 3-2 总结了 JavaScript 中值从一种类型转换为另一种类型的方式。表中的粗体条目突出显示了您可能会感到惊讶的转换。空单元格表示不需要转换,也不执行任何转换。
表 3-2. JavaScript 类型转换
表中显示的原始到原始的转换相对简单。布尔值转换已在第 3.4 节中讨论过。对于所有原始值,字符串转换是明确定义的。转换为数字稍微棘手一点。可以解析为数字的字符串将转换为这些数字。允许前导和尾随空格,但任何不是数字文字的前导或尾随非空格字符会导致字符串到数字的转换产生 NaN
。一些数字转换可能看起来令人惊讶:true
转换为 1,false
和空字符串转换为 0。
对象到原始值的转换有点复杂,这是第 3.9.3 节的主题。
3.9.1 转换和相等性
JavaScript 有两个操作符用于测试两个值是否相等。“严格相等操作符”===
在不同类型的操作数时不认为它们相等,这几乎总是编码时应该使用的正确操作符。但是因为 JavaScript 在类型转换方面非常灵活,它还定义了==
操作符,具有灵活的相等定义。例如,以下所有比较都是真的:
null == undefined // => true: 这两个值被视为相等。
"0" == 0 // => true: 在比较之前,字符串转换为数字。
0 == false // => true: 在比较之前,布尔值转换为数字。
"0" == false // => true: 在比较之前,两个操作数都转换为 0!
§4.9.1 解释了==
操作符执行的转换,以确定两个值是否应被视为相等。
请记住,一个值转换为另一个值并不意味着这两个值相等。例如,如果在期望布尔值的地方使用undefined
,它会转换为false
。但这并不意味着undefined == false
。JavaScript 操作符和语句期望各种类型的值,并对这些类型进行转换。if
语句将undefined
转换为false
,但==
操作符从不尝试将其操作数转换为布尔值。
3.9.2 显式转换
尽管 JavaScript 会自动执行许多类型转换,但有时你可能需要执行显式转换,或者你可能更喜欢使转换明确以保持代码更清晰。
执行显式类型转换的最简单方法是使用Boolean()
、Number()
和String()
函数:
Number("3") // => 3
String(false) // => "false": 或者使用 false.toString()
Boolean([]) // => true
除了null
或undefined
之外的任何值都有一个toString()
方法,而这个方法的结果通常与String()
函数返回的结果相同。
顺便提一下,注意Boolean()
、Number()
和String()
函数也可以被调用——带有new
——作为构造函数。如果以这种方式使用它们,你将得到一个行为就像原始布尔值、数字或字符串值的“包装”对象。这些包装对象是 JavaScript 最早期的历史遗留物,实际上从来没有任何好理由使用它们。
某些 JavaScript 操作符执行隐式类型转换,有时会明确用于类型转换的目的。如果+
操作符的一个操作数是字符串,则它会将另一个操作数转换为字符串。一元+
操作符将其操作数转换为数字。一元!
操作符将其操作数转换为布尔值并对其取反。这些事实导致以下类型转换习语,你可能在一些代码中看到:
x + "" // => String(x)
+x // => Number(x)
x-0 // => Number(x)
!!x // => Boolean(x): 注意双重!
在计算机程序中,格式化和解析数字是常见的任务,JavaScript 有专门的函数和方法,可以更精确地控制数字到字符串和字符串到数字的转换。
Number 类定义的toString()
方法接受一个可选参数,指定转换的基数或进制。如果不指定参数,转换将以十进制进行。但是,你也可以将数字转换为其他进制(介于 2 和 36 之间)。例如:
let n = 17;
let binary = "0b" + n.toString(2); // 二进制 == "0b10001"
let octal = "0o" + n.toString(8); // 八进制 == "0o21"
let hex = "0x" + n.toString(16); // hex == "0x11"
在处理财务或科学数据时,您可能希望以控制输出中小数位数或有效数字位数的方式将数字转换为字符串,或者您可能希望控制是否使用指数表示法。Number 类定义了三种用于这种数字到字符串转换的方法。toFixed()
将数字转换为一个字符串,小数点后有指定数量的数字。它永远不使用指数表示法。toExponential()
将数字转换为一个使用指数表示法的字符串,小数点前有一个数字,小数点后有指定数量的数字(这意味着有效数字的数量比您指定的值大一个)。toPrecision()
将数字转换为一个具有您指定的有效数字数量的字符串。如果有效数字的数量不足以显示整数部分的全部内容,则使用指数表示法。请注意,这三种方法都会四舍五入尾随数字或根据需要填充零。考虑以下示例:
let n = 123456.789;
n.toFixed(0) // => "123457"
n.toFixed(2) // => "123456.79"
n.toFixed(5) // => "123456.78900"
n.toExponential(1) // => "1.2e+5"
n.toExponential(3) // => "1.235e+5"
n.toPrecision(4) // => "1.235e+5"
n.toPrecision(7) // => "123456.8"
n.toPrecision(10) // => "123456.7890"
除了这里展示的数字格式化方法外,Intl.NumberFormat 类定义了一种更通用的、国际化的数字格式化方法。详细信息请参见§11.7.1。
如果将字符串传递给Number()
转换函数,它会尝试将该字符串解析为整数或浮点文字。该函数仅适用于十进制整数,并且不允许包含在文字中的尾随字符。parseInt()
和parseFloat()
函数(这些是全局函数,不是任何类的方法)更加灵活。parseInt()
仅解析整数,而parseFloat()
解析整数和浮点数。如果字符串以“0x”或“0X”开头,parseInt()
会将其解释为十六进制数。parseInt()
和parseFloat()
都会跳过前导空格,解析尽可能多的数字字符,并忽略其后的任何内容。如果第一个非空格字符不是有效的数字文字的一部分,它们会返回NaN
:
parseInt("3 blind mice") // => 3
parseFloat(" 3.14 meters") // => 3.14
parseInt("-12.34") // => -12
parseInt("0xFF") // => 255
parseInt("0xff") // => 255
parseInt("-0XFF") // => -255
parseFloat(".1") // => 0.1
parseInt("0.1") // => 0
parseInt(".1") // => NaN:整数不能以 "." 开头
parseFloat("$72.47") // => NaN:数字不能以 "$" 开头
parseInt()
接受一个可选的第二个参数,指定要解析的数字的基数(进制)。合法值介于 2 和 36 之间。例如:
parseInt("11", 2) // => 3:(1*2 + 1)
parseInt("ff", 16) // => 255:(15*16 + 15)
parseInt("zz", 36) // => 1295:(35*36 + 35)
parseInt("077", 8) // => 63:(7*8 + 7)
parseInt("077", 10) // => 77:(7*10 + 7)
3.9.3 对象到原始值的转换
前面的部分已经解释了如何显式将一种类型的值转换为另一种类型,并解释了 JavaScript 将值从一种原始类型转换为另一种原始类型的隐式转换。本节涵盖了 JavaScript 用于将对象转换为原始值的复杂规则。这部分内容很长,很晦涩,如果这是您第一次阅读本章,可以放心地跳到§3.10。
JavaScript 对象到原始值的转换复杂的一个原因是,某些类型的对象有多个原始表示。例如,日期对象可以被表示为字符串或数值时间戳。JavaScript 规范定义了三种基本算法来将对象转换为原始值:
优先选择字符串
这个算法返回一个原始值,如果可能的话,优先选择一个字符串值。
优先选择数字
这个算法返回一个原始值,如果可能的话,优先选择一个数字。
无偏好
这个算法不表达对所需原始值类型的偏好,类可以定义自己的转换。在内置的 JavaScript 类型中,除了日期类以优先选择字符串算法实现外,其他所有类都以优先选择数字算法实现。
这些对象到原始值的转换算法的实现在本节末尾有解释。然而,首先我们解释一下这些算法在 JavaScript 中是如何使用的。
对象到布尔值的转换
对象到布尔值的转换是微不足道的:所有对象都转换为true
。请注意,这种转换不需要使用前述的对象到原始值的算法,并且它确实适用于所有对象,包括空数组甚至包装对象new Boolean(false)
。
对象到字符串的转换
当一个对象需要被转换为字符串时,JavaScript 首先使用优先选择字符串算法将其转换为一个原始值,然后根据表 3-2 中的规则将得到的原始值转换为字符串,如果需要的话。
这种转换会发生在例如,如果你将一个对象传递给一个内置函数,该函数期望一个字符串参数,如果你调用String()
作为一个转换函数,以及当你将对象插入到模板字面量中时。
对象到数字的转换
当一个对象需要被转换为数字时,JavaScript 首先使用优先选择数字算法将其转换为一个原始值,然后根据表 3-2 中的规则将得到的原始值转换为数字,如果需要的话。
内置的 JavaScript 函数和方法期望数字参数时,将对象参数转换为数字的方式,大多数(参见下面的例外情况)期望数字操作数的 JavaScript 操作符也以这种方式将对象转换为数字。
特殊情况的操作符转换
操作符在第四章中有详细介绍。在这里,我们解释一下那些不使用前述基本对象到字符串和对象到数字转换的特殊情况操作符。
JavaScript 中的+
运算符执行数字加法和字符串连接。如果其操作数中有一个是对象,则 JavaScript 会使用no-preference算法将它们转换为原始值。一旦有了两个原始值,它会检查它们的类型。如果任一参数是字符串,则将另一个转换为字符串并连接字符串。否则,将两个参数转换为数字并相加。
==
和!=
运算符以一种允许类型转换的宽松方式执行相等性和不相等性测试。如果一个操作数是对象,另一个是原始值,这些运算符会使用no-preference算法将对象转换为原始值,然后比较两个原始值。
最后,关系运算符<
、<=
、>
和>=
比较它们的操作数的顺序,可用于比较数字和字符串。如果任一操作数是对象,则会使用prefer-number算法将其转换为原始值。但请注意,与对象到数字的转换不同,prefer-number转换返回的原始值不会再转换为数字。
请注意,Date 对象的数字表示可以有意义地使用<
和>
进行比较,但字符串表示则不行。对于 Date 对象,no-preference算法会转换为字符串,因此 JavaScript 对这些运算符使用prefer-number算法意味着我们可以使用它们来比较两个 Date 对象的顺序。
toString()和 valueOf()方法
所有对象都继承了两个用于对象到原始值转换的转换方法,在我们解释prefer-string、prefer-number和no-preference转换算法之前,我们必须解释这两个方法。
第一个方法是toString()
,它的作用是返回对象的字符串表示。默认的toString()
方法并不返回一个非常有趣的值(尽管我们会在§14.4.3 中发现它很有用):
({x: 1, y: 2}).toString() // => "[object Object]"
许多类定义了更具体版本的toString()
方法。例如,Array 类的toString()
方法将每个数组元素转换为字符串,并用逗号将结果字符串连接在一起。Function 类的toString()
方法将用户定义的函数转换为 JavaScript 源代码的字符串。Date 类定义了一个toString()
方法,返回一个可读的(且可被 JavaScript 解析)日期和时间字符串。RegExp 类定义了一个toString()
方法,将 RegExp 对象转换为类似 RegExp 字面量的字符串:
[1,2,3].toString() // => "1,2,3"
(function(x) { f(x); }).toString() // => "function(x) { f(x); }"
/\d+/g.toString() // => "/\\d+/g"
let d = new Date(2020,0,1);
d.toString() // => "Wed Jan 01 2020 00:00:00 GMT-0800 (Pacific Standard Time)"
另一个对象转换函数称为valueOf()
。这个方法的作用定义较少:它应该将对象转换为表示该对象的原始值,如果存在这样的原始值。对象是复合值,大多数对象实际上不能用单个原始值表示,因此默认的valueOf()
方法只返回对象本身,而不是返回原始值。包装类如 String、Number 和 Boolean 定义了简单返回包装的原始值的valueOf()
方法。数组、函数和正则表达式只是继承了默认方法。对于这些类型的实例调用valueOf()
只会返回对象本身。Date 类定义了一个valueOf()
方法,返回其内部表示的日期:自 1970 年 1 月 1 日以来的毫秒数:
let d = new Date(2010, 0, 1); // 2010 年 1 月 1 日(太平洋时间)
d.valueOf() // => 1262332800000
对象到原始值转换算法
通过解释toString()
和valueOf()
方法,我们现在可以大致解释三种对象到原始值的算法是如何工作的(完整细节将延迟到§14.4.7):
-
prefer-string算法首先尝试
toString()
方法。如果该方法被定义并返回一个原始值,那么 JavaScript 使用该原始值(即使它不是字符串!)。如果toString()
不存在或者返回一个对象,那么 JavaScript 尝试valueOf()
方法。如果该方法存在并返回一个原始值,那么 JavaScript 使用该值。否则,转换将失败并抛出 TypeError。 -
prefer-number算法类似于prefer-string算法,只是它首先尝试
valueOf()
,然后尝试toString()
。 -
no-preference算法取决于要转换的对象的类。如果对象是一个 Date 对象,那么 JavaScript 使用prefer-string算法。对于任何其他对象,JavaScript 使用prefer-number算法。
这里描述的规则适用于所有内置的 JavaScript 类型,并且是您自己定义的任何类的默认规则。§14.4.7 解释了如何为您定义的类定义自己的对象到原始值转换算法。
在我们离开这个主题之前,值得注意的是prefer-number转换的细节解释了为什么空数组转换为数字 0,而单元素数组也可以转换为数字:
Number([]) // => 0:这是意外的!
Number([99]) // => 99:真的吗?
对象到数字的转换首先使用prefer-number算法将对象转换为原始值,然后将得到的原始值转换为数字。prefer-number算法首先尝试valueOf()
,然后退而求其次使用toString()
。但是 Array 类继承了默认的valueOf()
方法,它不会返回原始值。因此,当我们尝试将数组转换为数字时,实际上调用了数组的toString()
方法。空数组转换为空字符串。空字符串转换为数字 0。包含单个元素的数组转换为该元素的字符串。如果数组包含单个数字,则该数字被转换为字符串,然后再转换为数字。
3.10 变量声明和赋值
计算机编程中最基本的技术之一是使用名称或标识符来表示值。将名称绑定到值可以让我们引用该值并在我们编写的程序中使用它。当我们这样做时,通常说我们正在为变量赋值。术语“变量”意味着可以分配新值:与变量关联的值可能会随着程序运行而变化。如果我们永久地为一个名称分配一个值,那么我们称该名称为常量而不是变量。
在 JavaScript 程序中使用变量或常量之前,必须声明它。在 ES6 及更高版本中,可以使用let
和const
关键字来声明,我们将在下面解释。在 ES6 之前,变量使用var
声明,这更具特殊性,稍后在本节中解释。
3.10.1 使用 let 和 const 进行声明
在现代 JavaScript(ES6 及更高版本)中,变量使用let
关键字声明,如下所示:
let i;
let sum;
也可以在单个let
语句中声明多个变量:
let i, sum;
在声明变量时给变量赋予初始值是一个良好的编程实践,如果可能的话:
let message = "hello";
let i = 0, j = 0, k = 0;
let x = 2, y = x*x; // 初始化器可以使用先前声明的变量
如果使用let
语句时没有指定变量的初始值,那么变量会被声明,但其值为undefined
,直到你的代码为其赋值。
若要声明常量而不是变量,请使用const
代替let
。const
的工作方式与let
相同,只是在声明时必须初始化常量:
const H0 = 74; // 哈勃常数(km/s/Mpc)
const C = 299792.458; // 真空中的光速(km/s)
const AU = 1.496E8; // 天文单位:到太阳的距离(km)
如其名称所示,常量的值不能被更改,任何尝试这样做都会导致抛出 TypeError。
通常(但不是普遍)约定使用全大写字母的名称来声明常量,例如H0
或HTTP_NOT_FOUND
,以区分它们与变量。
何时使用 const
关于使用const
关键字有两种思路。一种方法是仅将const
用于基本上不变的值,比如所示的物理常数,或程序版本号,或用于识别文件类型的字节序列等。另一种方法认识到我们程序中许多所谓的变量实际上在程序运行时根本不会改变。在这种方法中,我们用const
声明所有内容,然后如果发现我们实际上想要允许值变化,我们将声明切换为let
。这可能有助于通过排除我们不打算的变量的意外更改来防止错误。
在一种方法中,我们仅将const
用于绝对不改变的值。在另一种方法中,我们将const
用于任何偶然不改变的值。在我的代码中,我更喜欢前一种方法。
在第五章,我们将学习 JavaScript 中的for
、for/in
和for/of
循环语句。每个循环都包括一个循环变量,在循环的每次迭代中都会被分配一个新值。JavaScript 允许我们将循环变量声明为循环语法的一部分,这是另一种常见的使用let
的方式:
for(let i = 0, len = data.length; i < len; i++) console.log(data[i]);
for(let datum of data) console.log(datum);
for(let property in object) console.log(property);
也许令人惊讶的是,你也可以使用const
来声明for/in
和for/of
循环的循环“变量”,只要循环体不重新分配新值。在这种情况下,const
声明只是表示该值在一个循环迭代期间是常量:
for(const datum of data) console.log(datum);
for(const property in object) console.log(property);
变量和常量作用域
变量的作用域是定义它的程序源代码区域。使用let
和const
声明的变量和常量是块作用域。这意味着它们仅在let
或const
语句出现的代码块内定义。JavaScript 类和函数定义是块,if/else
语句的主体,while
循环,for
循环等也是块。粗略地说,如果一个变量或常量在一对花括号内声明,那么这些花括号限定了变量或常量定义的代码区域(尽管在声明变量的let
或const
语句之前执行的代码行中引用变量或常量是不合法的)。作为for
、for/in
或for/of
循环的一部分声明的变量和常量具有循环体作为它们的作用域,尽管它们在技术上出现在花括号外部。
当一个声明出现在顶层,不在任何代码块内时,我们称之为全局变量或常量,并具有全局作用域。在 Node 和客户端 JavaScript 模块(见第十章)中,全局变量的作用域是定义它的文件。然而,在传统的客户端 JavaScript 中,全局变量的作用域是定义它的 HTML 文档。也就是说:如果一个 <script>
声明了一个全局变量或常量,那么该变量或常量将在该文档中的所有 <script>
元素中定义(或至少在 let
或 const
语句执行后执行的所有脚本中定义)。
重复声明
在同一作用域内使用多个 let
或 const
声明相同名称是语法错误。在嵌套作用域中声明具有相同名称的新变量是合法的(尽管最好避免这种做法):
const x = 1; // 将 x 声明为全局常量
if (x === 1) {
let x = 2; // 在一个块内,x 可能指向不同的值
console.log(x); // 打印 2
}
console.log(x); // 打印 1:我们现在回到了全局范围
let x = 3; // 错误!尝试重新声明 x 的语法错误
声明和类型
如果你习惯于像 C 或 Java 这样的静态类型语言,你可能会认为变量声明的主要目的是指定可以分配给变量的值的类型。但是,正如你所见,JavaScript 的变量声明没有与之关联的类型。² JavaScript 变量可以保存任何类型的值。例如,在 JavaScript 中将一个数字赋给一个变量,然后稍后将一个字符串赋给该变量是完全合法的(但通常是不良的编程风格):
let i = 10;
i = "ten";
3.10.2 使用 var 声明变量
在 ES6 之前的 JavaScript 版本中,声明变量的唯一方式是使用 var
关键字,没有办法声明常量。var
的语法与 let
的语法完全相同:
var x;
var data = [], count = data.length;
for(var i = 0; i < count; i++) console.log(data[i]);
尽管 var
和 let
具有相同的语法,但它们的工作方式有重要的区别:
-
使用
var
声明的变量没有块级作用域。相反,它们的作用域是包含函数的主体,无论它们在该函数内嵌套多深。 -
如果在函数体外部使用
var
,它会声明一个全局变量。但是用var
声明的全局变量与用let
声明的全局变量有一个重要的区别。用var
声明的全局变量被实现为全局对象的属性(§3.7)。全局对象可以被引用为globalThis
。因此,如果你在函数外部写var x = 2;
,就像你写了globalThis.x = 2;
。但请注意,这个类比并不完美:用全局var
声明创建的属性不能被delete
运算符删除(§4.13.4)。用let
和const
声明的全局变量和常量不是全局对象的属性。 -
与使用
let
声明的变量不同,使用var
可以多次声明同一个变量是合法的。由于var
变量具有函数作用域而不是块作用域,这种重新声明实际上是很常见的。变量i
经常用于整数值,尤其是作为for
循环的索引变量。在具有多个for
循环的函数中,每个循环通常以for(var i = 0; ...
开始。因为var
不将这些变量限定在循环体内,所以每个循环都会(无害地)重新声明和重新初始化相同的变量。 -
var
声明中最不寻常的特性之一被称为提升。当使用var
声明变量时,声明会被提升(或“提升”)到封闭函数的顶部。变量的初始化仍然在你编写的位置,但变量的定义移动到函数的顶部。因此,使用var
声明的变量可以在封闭函数的任何地方使用,而不会出错。如果初始化代码尚未运行,则变量的值可能是undefined
,但在变量初始化之前使用变量不会出错。(这可能是一个错误的来源,也是let
纠正的重要缺陷之一:如果使用let
声明变量但在let
语句运行之前尝试使用它,你将收到一个实际的错误,而不仅仅是看到一个undefined
值。)
使用未声明的变量
在严格模式(§5.6.3)中,如果尝试使用未声明的变量,在运行代码时会收到一个引用错误。然而,在非严格模式下,如果给一个未用let
、const
或var
声明的名称赋值,你将创建一个新的全局变量。无论你的代码嵌套多深,它都将是一个全局变量,这几乎肯定不是你想要的,容易出错,这也是使用严格模式的最好理由之一!
以这种意外方式创建的全局变量类似于用var
声明的全局变量:它们定义了全局对象的属性。但与由正确的var
声明定义的属性不同,这些属性可以使用delete
运算符(§4.13.4)删除。
3.10.3 解构赋值
ES6 实现了一种称为解构赋值的复合声明和赋值语法。在解构赋值中,等号右侧的值是一个数组或对象(一个“结构化”值),而左侧指定一个或多个变量名,使用一种模仿数组和对象字面量语法的语法。当发生解构赋值时,一个或多个值从右侧的值中被提取(“解构”)并存储到左侧命名的变量中。解构赋值可能最常用于作为const
、let
或var
声明语句的一部分初始化变量,但也可以在常规赋值表达式中进行(使用已经声明的变量)。正如我们将在§8.3.5 中看到的,解构也可以在定义函数参数时使用。
这里是使用值数组的简单解构赋值:
let [x,y] = [1,2]; // 同 let x=1, y=2
[x,y] = [x+1,y+1]; // 同 x = x + 1, y = y + 1
[x,y] = [y,x]; // 交换两个变量的值
[x,y] // => [3,2]:递增和交换的值
注意解构赋值如何使处理返回值数组的函数变得简单:
// 将[x,y]坐标转换为[r,theta]极坐标
function toPolar(x, y) {
return [Math.sqrt(x*x+y*y), Math.atan2(y,x)];
}
// 将极坐标转换为直角坐标
function toCartesian(r, theta) {
return [r*Math.cos(theta), r*Math.sin(theta)];
}
let [r,theta] = toPolar(1.0, 1.0); // r == Math.sqrt(2); theta == Math.PI/4
let [x,y] = toCartesian(r,theta); // [x, y] == [1.0, 1,0]
我们看到变量和常量可以作为 JavaScript 的各种for
循环的一部分声明。在这种情况下,也可以在此上下文中使用变量解构。以下是一个代码,循环遍历对象的所有属性的名称/值对,并使用解构赋值将这些对从两个元素数组转换为单独的变量:
let o = { x: 1, y: 2 }; // 我们将循环的对象
for(const [name, value] of Object.entries(o)) {
console.log(name, value); // 打印 "x 1" 和 "y 2"
}
解构赋值的左侧变量数量不必与右侧数组元素数量匹配。左侧的额外变量将被设置为undefined
,右侧的额外值将被忽略。左侧变量列表可以包含额外的逗号以跳过右侧的某些值:
let [x,y] = [1]; // x == 1; y == undefined
[x,y] = [1,2,3]; // x == 1; y == 2
[,x,,y] = [1,2,3,4]; // x == 2; y == 4
如果要在解构数组时将所有未使用或剩余的值收集到一个变量中,请在左侧最后一个变量名之前使用三个点(...
):
let [x, ...y] = [1,2,3,4]; // y == [2,3,4]
我们将在§8.3.2 中再次看到这种方式使用三个点,用于指示所有剩余的函数参数应该被收集到一个单独的数组中。
解构赋值可以与嵌套数组一起使用。在这种情况下,赋值的左侧应该看起来像一个嵌套数组字面量:
let [a, [b, c]] = [1, [2,2.5], 3]; // a == 1; b == 2; c == 2.5
数组解构的一个强大特性是它实际上并不需要一个数组!您可以在赋值的右侧使用任何可迭代对象(第十二章);任何可以与for/of
循环(§5.4.4)一起使用的对象也可以被解构:
let [first, ...rest] = "Hello"; // first == "H"; rest == ["e","l","l","o"]
当右侧是对象值时,也可以执行解构赋值。在这种情况下,赋值的左侧看起来像一个对象字面量:在花括号内用逗号分隔的变量名列表:
let transparent = {r: 0.0, g: 0.0, b: 0.0, a: 1.0}; // 一个 RGBA 颜色
let {r, g, b} = transparent; // r == 0.0; g == 0.0; b == 0.0
下一个示例将全局函数Math
对象的函数复制到变量中,这可能简化了大量三角函数的代码:
// 同 const sin=Math.sin, cos=Math.cos, tan=Math.tan
const {sin, cos, tan} = Math;
在这里的代码中请注意,Math
对象除了被解构为单独变量的三个属性外,还有许多其他属性。那些未命名的属性将被简单地忽略。如果这个赋值的左侧包含一个不是Math
属性的变量,那么该变量将被简单地赋值为undefined
。
在这些对象解构示例中,我们选择了与要解构的对象的属性名匹配的变量名。这保持了语法的简单和易于理解,但并非必须。在对象解构赋值的左侧,每个标识符也可以是一个以冒号分隔的标识符对,第一个是要赋值的属性名,第二个是要赋给它的变量名:
// 同 const cosine = Math.cos, tangent = Math.tan;
const { cos: cosine, tan: tangent } = Math;
我发现当变量名和属性名不同时,对象解构语法变得过于复杂,不太实用,我倾向于在这种情况下避免使用简写。如果你选择使用它,请记住属性名始终位于冒号的左侧,无论是在对象字面量中还是在对象解构赋值的左侧。
当与嵌套对象、对象数组或数组对象一起使用时,解构赋值变得更加复杂,但是是合法的:
let points = [{x: 1, y: 2}, {x: 3, y: 4}]; // 一个包含两个点对象的数组
let [{x: x1, y: y1}, {x: x2, y: y2}] = points; // 解构成 4 个变量
(x1 === 1 && y1 === 2 && x2 === 3 && y2 === 4) // => true
或者,我们可以对一个包含数组的对象进行解构:
let points = { p1: [1,2], p2: [3,4] }; // 一个具有 2 个数组属性的对象
let { p1: [x1, y1], p2: [x2, y2] } = points; // 解构成 4 个变量
(x1 === 1 && y1 === 2 && x2 === 3 && y2 === 4) // => true
像这样复杂的解构语法可能很难编写和阅读,你可能最好还是用传统的代码明确地写出你的赋值,比如let x1 = points.p1[0];
。
3.11 总结
本章需要记住的一些关键点:
-
如何在 JavaScript 中编写和操作数字和文本字符串。
-
如何处理 JavaScript 的其他基本类型:布尔值、符号、
null
和undefined
。 -
不可变的基本类型和可变的引用类型之间的区别。
-
JavaScript 如何隐式地将值从一种类型转换为另一种类型,以及你如何在程序中显式地进行转换。
-
如何声明和初始化常量和变量(包括解构赋值),以及你声明的变量和常量的词法作用域。
¹ 这是 Java、C++和大多数现代编程语言中double
类型的数字的格式。
² 有一些 JavaScript 的扩展,比如 TypeScript 和 Flow (§17.8),允许在变量声明中指定类型,语法类似于let x: number = 0;
。