JavaScript 權威指南第七版(GPT 重譯)(一)

绝不原创的飞龙發表於2024-03-22

前言

本書涵蓋了 JavaScript 語言以及 Web 瀏覽器和 Node 實現的 JavaScript API。我為一些具有先前程式設計經驗的讀者編寫了這本書,他們想要學習 JavaScript,也為已經使用 JavaScript 的程式設計師編寫了這本書,但希望將他們的理解提升到一個新的水平,並真正掌握這門語言。我寫這本書的目標是全面和權威地記錄 JavaScript 語言,並深入介紹 JavaScript 程式可用的最重要的客戶端和伺服器端 API。因此,這是一本長篇詳細的書。然而,我希望它會獎勵仔細學習,並且您花在閱讀上的時間將很容易以更高的程式設計生產力形式收回。

本書的早期版本包括了一個全面的參考部分。我不再認為在印刷形式中包含這些材料是有意義的,因為在網上很容易找到最新的參考材料。如果您需要查詢與核心或客戶端 JavaScript 相關的任何內容,我建議您訪問MDN 網站。對於伺服器端 Node API,我建議您直接訪問源並查閱Node.js 參考文件

本書中使用的約定

我在本書中使用以下排版約定:

斜體

用於強調和指示術語的首次使用。斜體也用於電子郵件地址,URL 和檔名。

固定寬度

用於所有 JavaScript 程式碼和 CSS 和 HTML 列表,通常用於程式設計時需要字面輸入的任何內容。

固定寬度斜體

有時用於解釋 JavaScript 語法。

固定寬度粗體

顯示使用者應該按照字面意義輸入的命令或其他文字

注意

此元素表示一般說明。

重要

此元素表示警告或注意事項。

示例程式碼

本書的補充材料(程式碼示例,練習等)可在以下網址下載:

  • https://oreil.ly/javascript_defgd7

本書旨在幫助您完成工作。一般情況下,如果本書提供示例程式碼,您可以在您的程式和文件中使用它。除非您複製了程式碼的大部分內容,否則無需聯絡我們請求許可。例如,編寫一個使用本書多個程式碼塊的程式不需要許可。銷售或分發 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 所示),這通常非常方便。

js7e 0101

圖 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,以及asyncawait關鍵字。儘管核心 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”。同樣,onlineOnlineOnLineONLINE是四個不同的變數名。

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 語言的一部分。其中許多(如ifwhilefor)是必須避免用作常量、變數、函式或類名稱的保留關鍵字(儘管它們都可以用作物件內的屬性名稱)。其他一些單詞(如fromofgetset)在有限的上下文中使用時沒有語法歧義,作為識別符號是完全合法的。還有其他關鍵字(如let)為了保持與舊程式的向後相容性而不能完全保留,因此有複雜的規則規定何時可以將其用作識別符號,何時不行。(例如,如果在類外部用var宣告,let可以用作變數名,但如果在類內部或用const宣告,則不行。)最簡單的方法是避免將這些單詞用作識別符號,除了fromsettarget,它們是安全的並且已經被廣泛使用。

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

由於歷史原因,在某些情況下不允許將argumentseval用作識別符號,並且最好完全避免使用它們。

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 將換行符解釋為分號的一般規則,即當它無法將第二行解析為第一行語句的延續時。第一個例外涉及returnthrowyieldbreakcontinue語句(參見第五章)。這些語句通常是獨立的,但有時會跟隨識別符號或表示式。如果這些單詞之後(在任何其他標記之前)出現換行符,JavaScript 將始終將該換行符解釋為分號。例如,如果你寫:

return
true;

JavaScript 假設你的意思是:

return; true;

然而,你可能的意思是:

return true;

這意味著你不能在returnbreakcontinue與後面的表示式之間插入換行符。如果插入換行符,你的程式碼很可能會以難以除錯的非明顯方式失敗。

第二個例外涉及++−−運算子(§4.8)。這些運算子可以是字首運算子,出現在表示式之前,也可以是字尾運算子,出現在表示式之後。如果要將這些運算子之一用作字尾運算子,它們必須出現在應用於的表示式的同一行上。第三個例外涉及使用簡潔的“箭頭”語法定義的函式:=>箭頭本身必須出現在引數列表的同一行上。

2.7 總結

本章展示了 JavaScript 程式是如何在最低階別編寫的。下一章將帶我們邁向更高一級,並介紹作為 JavaScript 程式計算的基本單位的原始型別和值(數字、字串等)。

第三章:型別、值和變數

計算機程式透過操作值來工作,例如數字 3.14 或文字“Hello World”。在程式語言中可以表示和操作的值的種類稱為型別,程式語言的最基本特徵之一是它支援的型別集合。當程式需要保留一個值以供將來使用時,它將該值分配給(或“儲存”在)一個變數中。變數有名稱,並且允許在我們的程式中使用這些名稱來引用值。變數的工作方式是任何程式語言的另一個基本特徵。本章解釋了 JavaScript 中的型別、值和變數。它從概述和一些定義開始。

3.1 概述和定義

JavaScript 型別可以分為兩類:原始型別物件型別。JavaScript 的原始型別包括數字、文字字串(稱為字串)和布林真值(稱為布林值)。本章的重要部分詳細解釋了 JavaScript 中的數字(§3.2)和字串(§3.3)型別。布林值在§3.4 中介紹。

特殊的 JavaScript 值 nullundefined 是原始值,但它們不是數字、字串或布林值。每個值通常被認為是其自己特殊型別的唯一成員。關於 nullundefined 的更多內容請參見§3.5。ES6 新增了一種新的特殊型別,稱為 Symbol,它可以在不影響向後相容性的情況下定義語言擴充套件。Symbols 在§3.6 中簡要介紹。

任何不是數字、字串、布林值、符號、nullundefined 的 JavaScript 值都是物件。物件(即型別 object 的成員)是一個屬性集合,其中每個屬性都有一個名稱和一個值(可以是原始值或另一個物件)。一個非常特殊的物件,全域性物件,在§3.7 中介紹,但是一般和更詳細的物件覆蓋在第六章中。

一個普通的 JavaScript 物件是一個無序的命名值集合。該語言還定義了一種特殊型別的物件,稱為陣列,表示一個有序的編號值集合。JavaScript 語言包括特殊的語法用於處理陣列,並且陣列具有一些特殊的行為,使它們與普通物件有所區別。陣列是第七章的主題。

除了基本物件和陣列之外,JavaScript 還定義了許多其他有用的物件型別。Set 物件表示一組值。Map 物件表示從鍵到值的對映。各種“型別化陣列”型別便於對位元組陣列和其他二進位制資料進行操作。RegExp 型別表示文字模式,並支援對字串進行復雜的匹配、搜尋和替換操作。Date 型別表示日期和時間,並支援基本的日期算術。Error 及其子型別表示執行 JavaScript 程式碼時可能出現的錯誤。所有這些型別在第十一章中介紹。

JavaScript 與更靜態的語言不同之處在於函式和類不僅僅是語言語法的一部分:它們本身是 JavaScript 程式可以操作的值。與任何不是原始值的 JavaScript 值一樣,函式和類是一種特殊型別的物件。它們在第八章和第九章中詳細介紹。

JavaScript 直譯器執行自動垃圾回收以進行記憶體管理。這意味著 JavaScript 程式設計師通常不需要擔心物件或其他值的銷燬或釋放。當一個值不再可達時——當程式不再有任何方式引用它時——直譯器知道它永遠不會再被使用,並自動回收它佔用的記憶體。(JavaScript 程式設計師有時需要小心確保值不會意外地保持可達——因此不可回收——時間比必要長。)

JavaScript 支援物件導向的程式設計風格。寬鬆地說,這意味著與其在全域性定義函式來操作各種型別的值,型別本身定義了用於處理值的方法。例如,要對陣列a的元素進行排序,我們不會將a傳遞給sort()函式。相反,我們呼叫asort()方法:

a.sort();       // The object-oriented version of sort(a).

方法定義在第九章中介紹。技術上,只有 JavaScript 物件有方法。但是數字、字串、布林值和符號值的行為就好像它們有方法一樣。在 JavaScript 中,只有nullundefined是不能呼叫方法的值。

JavaScript 的物件型別是可變的,而其原始型別是不可變的。可變型別的值可以改變:JavaScript 程式可以更改物件屬性和陣列元素的值。數字、布林值、符號、nullundefined是不可變的——例如,談論更改數字的值甚至沒有意義。字串可以被視為字元陣列,你可能期望它們是可變的。然而,在 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)值。十六進位制文字以0x0X開頭,後跟一串十六進位制數字。十六進位制數字是數字 0 到 9 或字母 a(或 A)到 f(或 F)中的一個,表示值 10 到 15。以下是十六進位制整數文字的示例:

0xff       // => 255: (15*16 + 15)
0xBADCAFE  // => 195939070

在 ES6 及更高版本中,你還可以使用字首0b0o(或0B0O)來表示二進位制(基數 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 預定義全域性常量 InfinityNaN 分別表示正無窮大和非數字值,並且這些值也作為 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 != xNumber.isNaN(x)。只有當 x 的值與全域性常量 NaN 相同時,這些表示式才為真。

全域性函式 isNaN() 類似於 Number.isNaN()。如果其引數是 NaN,或者該引數是無法轉換為數字的非數字值,則返回 true。相關函式 Number.isFinite() 如果其引數是除 NaNInfinity-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/21/81/1024。不幸的是,我們最常使用的分數(尤其是在進行財務計算時)是十進位制分數:1/101/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:它影響任何使用二進位制浮點數的程式語言。此外,請注意程式碼中的值 xy 非常接近彼此和正確值。計算出的值對於幾乎任何目的都是足夠的;問題只在我們嘗試比較相等值時才會出現。

如果這些浮點數近似值對您的程式有問題,請考慮使用縮放整數。例如,您可以將貨幣值作為整數分而不是小數美元進行操作。

3.2.5 使用 BigInt 進行任意精度整數運算

JavaScript 的最新特性之一,定義在 ES2020 中,是一種稱為 BigInt 的新數值型別。截至 2020 年初,它已經在 Chrome、Firefox、Edge 和 Node 中實現,並且 Safari 中正在進行實現。顧名思義,BigInt 是一個數值型別,其值為整數。JavaScript 主要新增了這種型別,以允許表示 64 位整數,這對於與許多其他程式語言和 API 相容是必需的。但是 BigInt 值可以有數千甚至數百萬位數字,如果你需要處理如此大的數字的話。(但是請注意,BigInt 實現不適用於加密,因為它們不會嘗試防止時間攻擊。)

BigInt 字面量寫為一個由數字組成的字串,後面跟著一個小寫字母 n。預設情況下,它們是以 10 進製表示的,但你可以使用 0b0o0x 字首來表示二進位制、八進位制和十六進位制的 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’tO’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 轉義序列

序列 表示的字元
\0 NUL 字元 (\u0000)
\b 退格符 (\u0008)
\t 水平製表符 (\u0009)
\n 換行符 (\u000A)
\v 垂直製表符 (\u000B)
\f 換頁符 (\u000C)
\r 回車符 (\u000D)
\" 雙引號 (\u0022)
\' 撇號或單引號 (\u0027)
\\ 反斜槓 (\u005C)
\xnn 由兩個十六進位制數字 nn 指定的 Unicode 字元
\unnnn 由四個十六進位制數字 nnnn 指定的 Unicode 字元
\u{n} 由程式碼點 n 指定的 Unicode 字元,其中 n 是 0 到 10FFFF 之間的一到六個十六進位制數字(ES6)

如果 \ 字元位於除表 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 布林值

布林值表示真或假,開或關,是或否。此型別僅有兩個可能的值。保留字truefalse評估為這兩個值。

布林值通常是您在 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) ...

不等運算子!==比較onull,並評估為truefalse。但您可以省略比較,而是依賴於null為假值,物件為真值的事實:


if (o) ...

在第一種情況下,只有當o不是null時,if的主體才會被執行。第二種情況不那麼嚴格:只有當o不是false或任何假值(如nullundefined)時,if的主體才會被執行。哪種if語句適合你的程式實際上取決於你期望為o分配什麼值。如果你需要區分null0以及"",那麼你應該使用顯式比較。

布林值有一個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的等價物:你可能熟悉它作為NULLnilNone

JavaScript 還有第二個表示值缺失的值。undefined值代表一種更深層次的缺失。它是未初始化變數的值,以及查詢不存在的物件屬性或陣列元素的值時得到的值。undefined值也是那些沒有顯式返回值的函式的返回值,以及沒有傳遞引數的函式引數的值。undefined是一個預定義的全域性常量(不像null那樣是一個語言關鍵字,儘管在實踐中這並不是一個重要的區別),它被初始化為undefined值。如果你對undefined值應用typeof運算子,它會返回undefined,表明這個值是一個特殊型別的唯一成員。

儘管存在這些差異,nullundefined都表示值的缺失,並且通常可以互換使用。相等運算子==認為它們相等。(使用嚴格相等運算子===來區分它們。)它們都是假值:當需要布林值時,它們的行為類似於falsenullundefined都沒有任何屬性或方法。實際上,使用.[]來訪問這些值的屬性或方法會導致 TypeError。

我認為undefined表示系統級別的、意外的或類似錯誤的值缺失,而null表示程式級別的、正常的或預期的值缺失。我儘量避免使用nullundefined,但如果需要將這些值分配給變數或屬性,或者將這些值傳遞給函式或從函式中返回這些值,我通常使用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 瀏覽器載入新頁面時),它會建立一個新的全域性物件,並賦予它一組初始屬性,用於定義:

  • undefinedInfinityNaN這樣的全域性常量

  • 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 中原始值(undefinednull、布林值、數字和字串)和物件(包括陣列和函式)之間有一個根本的區別。原始值是不可變的:沒有辦法改變(或“突變”)原始值。對於數字和布林值來說,這是顯而易見的——改變一個數字的值甚至沒有意義。然而,對於字串來說,情況並不那麼明顯。由於字串類似於字元陣列,您可能希望能夠更改任何指定索引處的字元。實際上,JavaScript 不允許這樣做,所有看起來返回修改後字串的字串方法實際上都是返回一個新的字串值。例如:


let s = "hello";   // 從一些小寫文字開始
s.toUpperCase();   // 返回"HELLO",但不改變 s
s                  // => "hello": 原始字串沒有改變

原始值也是按值比較的:只有當它們的值相同時,兩個值才相同。對於數字、布林值、nullundefined來說,這聽起來很迴圈:它們沒有其他比較方式。然而,對於字串來說,情況並不那麼明顯。如果比較兩個不同的字串值,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 型別轉換

轉為字串 轉為數字 轉為布林值
undefined "undefined" NaN false
null "null" 0 false
true "true" 1
false "false" 0
""(空字串) 0 false
"1.2"(非空,數值) 1.2 true
"one"(非空,非數字) NaN true
0 "0" false
-0 "0" false
1(有限的,非零) "1" true
Infinity "Infinity" true
-Infinity "-Infinity" true
NaN "NaN" false
{}(任何物件) 見 §3.9.3 見 §3.9.3 true
[](空陣列) "" 0 true
[9](一個數值元素) "9" 9 true
['a'](任何其他陣列) 使用 join() 方法 NaN true
function(){}(任何函式) 見 §3.9.3 NaN true

表中顯示的原始到原始的轉換相對簡單。布林值轉換已在第 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

除了nullundefined之外的任何值都有一個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-stringprefer-numberno-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 及更高版本中,可以使用letconst關鍵字來宣告,我們將在下面解釋。在 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代替letconst的工作方式與let相同,只是在宣告時必須初始化常量:


const H0 = 74;         // 哈勃常數(km/s/Mpc)
const C = 299792.458;  // 真空中的光速(km/s)
const AU = 1.496E8;    // 天文單位:到太陽的距離(km)

如其名稱所示,常量的值不能被更改,任何嘗試這樣做都會導致丟擲 TypeError。

通常(但不是普遍)約定使用全大寫字母的名稱來宣告常量,例如H0HTTP_NOT_FOUND,以區分它們與變數。

何時使用 const

關於使用const關鍵字有兩種思路。一種方法是僅將const用於基本上不變的值,比如所示的物理常數,或程式版本號,或用於識別檔案型別的位元組序列等。另一種方法認識到我們程式中許多所謂的變數實際上在程式執行時根本不會改變。在這種方法中,我們用const宣告所有內容,然後如果發現我們實際上想要允許值變化,我們將宣告切換為let。這可能有助於透過排除我們不打算的變數的意外更改來防止錯誤。

在一種方法中,我們僅將const用於絕對不改變的值。在另一種方法中,我們將const用於任何偶然不改變的值。在我的程式碼中,我更喜歡前一種方法。

在第五章,我們將學習 JavaScript 中的forfor/infor/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/infor/of迴圈的迴圈“變數”,只要迴圈體不重新分配新值。在這種情況下,const宣告只是表示該值在一個迴圈迭代期間是常量:


for(const datum of data) console.log(datum);
for(const property in object) console.log(property);

變數和常量作用域

變數的作用域是定義它的程式原始碼區域。使用letconst宣告的變數和常量是塊作用域。這意味著它們僅在letconst語句出現的程式碼塊內定義。JavaScript 類和函式定義是塊,if/else語句的主體,while迴圈,for迴圈等也是塊。粗略地說,如果一個變數或常量在一對花括號內宣告,那麼這些花括號限定了變數或常量定義的程式碼區域(儘管在宣告變數的letconst語句之前執行的程式碼行中引用變數或常量是不合法的)。作為forfor/infor/of迴圈的一部分宣告的變數和常量具有迴圈體作為它們的作用域,儘管它們在技術上出現在花括號外部。

當一個宣告出現在頂層,不在任何程式碼塊內時,我們稱之為全域性變數或常量,並具有全域性作用域。在 Node 和客戶端 JavaScript 模組(見第十章)中,全域性變數的作用域是定義它的檔案。然而,在傳統的客戶端 JavaScript 中,全域性變數的作用域是定義它的 HTML 文件。也就是說:如果一個 <script> 宣告瞭一個全域性變數或常量,那麼該變數或常量將在該文件中的所有 <script> 元素中定義(或至少在 letconst 語句執行後執行的所有指令碼中定義)。

重複宣告

在同一作用域內使用多個 letconst 宣告相同名稱是語法錯誤。在巢狀作用域中宣告具有相同名稱的新變數是合法的(儘管最好避免這種做法):


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]);

儘管 varlet 具有相同的語法,但它們的工作方式有重要的區別:

  • 使用 var 宣告的變數沒有塊級作用域。相反,它們的作用域是包含函式的主體,無論它們在該函式內巢狀多深。

  • 如果在函式體外部使用 var,它會宣告一個全域性變數。但是用 var 宣告的全域性變數與用 let 宣告的全域性變數有一個重要的區別。用 var 宣告的全域性變數被實現為全域性物件的屬性(§3.7)。全域性物件可以被引用為 globalThis。因此,如果你在函式外部寫 var x = 2;,就像你寫了 globalThis.x = 2;。但請注意,這個類比並不完美:用全域性 var 宣告建立的屬性不能被 delete 運算子刪除(§4.13.4)。用 letconst 宣告的全域性變數和常量不是全域性物件的屬性。

  • 與使用let宣告的變數不同,使用var可以多次宣告同一個變數是合法的。由於var變數具有函式作用域而不是塊作用域,這種重新宣告實際上是很常見的。變數i經常用於整數值,尤其是作為for迴圈的索引變數。在具有多個for迴圈的函式中,每個迴圈通常以for(var i = 0; ...開始。因為var不將這些變數限定在迴圈體內,所以每個迴圈都會(無害地)重新宣告和重新初始化相同的變數。

  • var宣告中最不尋常的特性之一被稱為提升。當使用var宣告變數時,宣告會被提升(或“提升”)到封閉函式的頂部。變數的初始化仍然在你編寫的位置,但變數的定義移動到函式的頂部。因此,使用var宣告的變數可以在封閉函式的任何地方使用,而不會出錯。如果初始化程式碼尚未執行,則變數的值可能是undefined,但在變數初始化之前使用變數不會出錯。(這可能是一個錯誤的來源,也是let糾正的重要缺陷之一:如果使用let宣告變數但在let語句執行之前嘗試使用它,你將收到一個實際的錯誤,而不僅僅是看到一個undefined值。)

使用未宣告的變數

在嚴格模式(§5.6.3)中,如果嘗試使用未宣告的變數,在執行程式碼時會收到一個引用錯誤。然而,在非嚴格模式下,如果給一個未用letconstvar宣告的名稱賦值,你將建立一個新的全域性變數。無論你的程式碼巢狀多深,它都將是一個全域性變數,這幾乎肯定不是你想要的,容易出錯,這也是使用嚴格模式的最好理由之一!

以這種意外方式建立的全域性變數類似於用var宣告的全域性變數:它們定義了全域性物件的屬性。但與由正確的var宣告定義的屬性不同,這些屬性可以使用delete運算子(§4.13.4)刪除。

3.10.3 解構賦值

ES6 實現了一種稱為解構賦值的複合宣告和賦值語法。在解構賦值中,等號右側的值是一個陣列或物件(一個“結構化”值),而左側指定一個或多個變數名,使用一種模仿陣列和物件字面量語法的語法。當發生解構賦值時,一個或多個值從右側的值中被提取(“解構”)並儲存到左側命名的變數中。解構賦值可能最常用於作為constletvar宣告語句的一部分初始化變數,但也可以在常規賦值表示式中進行(使用已經宣告的變數)。正如我們將在§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 的其他基本型別:布林值、符號、nullundefined

  • 不可變的基本型別和可變的引用型別之間的區別。

  • JavaScript 如何隱式地將值從一種型別轉換為另一種型別,以及你如何在程式中顯式地進行轉換。

  • 如何宣告和初始化常量和變數(包括解構賦值),以及你宣告的變數和常量的詞法作用域。

¹ 這是 Java、C++和大多數現代程式語言中double型別的數字的格式。

² 有一些 JavaScript 的擴充套件,比如 TypeScript 和 Flow (§17.8),允許在變數宣告中指定型別,語法類似於let x: number = 0;

相關文章