JavaScript 語言概覽
JavaScript 是一種多正規化、動態語言,它具有型別和運算子、標準內建物件和方法。它的語法基於 Java 和 C 語言——這些語言中的許多結構也適用於 JavaScript。JavaScript 支援使用物件原型和類進行面向物件程式設計。它還支援函數語言程式設計,因為函式是一等物件,可以透過表示式輕鬆建立並像任何其他物件一樣傳遞。
本頁面旨在為具有其他語言(如 C 或 Java)背景的讀者快速概述各種 JavaScript 語言特性。
資料型別
讓我們從任何語言的構建塊開始:型別。JavaScript 程式操作值,這些值都屬於一種型別。JavaScript 提供了七種原始型別
- Number:用於所有數值(整數和浮點數),除了非常大的整數。
- BigInt:用於任意大的整數。
- String:用於儲存文字。
- Boolean:
true和false——通常用於條件邏輯。 - Symbol:用於建立不會衝突的唯一識別符號。
- Undefined:表示變數尚未賦值。
- Null:表示一個刻意的非值。
其他一切都稱為Object。常見的物件型別包括
函式在 JavaScript 中不是特殊的資料結構——它們只是可以呼叫的特殊型別的物件。
數字
JavaScript 有兩種內建的數字型別:Number 和 BigInt。
Number 型別是 IEEE 754 64 位雙精度浮點值,這意味著整數可以在 -(253 − 1) 和 253 − 1 之間安全表示而不會丟失精度,浮點數可以儲存到 1.79 × 10308。在數字內部,JavaScript 不區分浮點數和整數。
console.log(3 / 2); // 1.5, not 1
因此,一個看起來是整數的實際上隱式是浮點數。由於 IEEE 754 編碼,有時浮點算術可能不精確。
console.log(0.1 + 0.2); // 0.30000000000000004
對於期望整數的操作,例如位運算,數字將被轉換為 32 位整數。
數字字面量也可以帶有字首來指示基數(二進位制、八進位制、十進位制或十六進位制),或者帶有指數字尾。
console.log(0b111110111); // 503
console.log(0o767); // 503
console.log(0x1f7); // 503
console.log(5.03e2); // 503
BigInt 型別是任意長度的整數。它的行為類似於 C 的整數型別(例如,除法向零截斷),但它可以無限增長。BigInts 用數字字面量和 n 字尾指定。
console.log(-3n / 2n); // -1n
支援標準算術運算子,包括加法、減法、取餘等。BigInts 和數字不能在算術運算中混合使用。
Math 物件提供了標準的數學函式和常量。
Math.sin(3.5);
const circumference = 2 * Math.PI * r;
有三種方法可以將字串轉換為數字
parseInt(),它解析字串以獲取整數。parseFloat(),它解析字串以獲取浮點數。Number()函式,它將字串解析為數字字面量,並支援多種不同的數字表示。
你也可以使用一元加號 + 作為 Number() 的簡寫。
數值還包括NaN(“Not a Number”的縮寫)和Infinity。許多“無效數學”操作將返回 NaN——例如,如果嘗試解析非數字字串,或對負值使用Math.log()。除以零會產生 Infinity(正或負)。
NaN 具有傳染性:如果將其作為任何數學運算的運算元,結果也將是 NaN。NaN 是 JavaScript 中唯一不等於其自身的值(根據 IEEE 754 規範)。
字串
JavaScript 中的字串是 Unicode 字元序列。對於任何處理過國際化的人來說,這應該是個好訊息。更準確地說,它們是UTF-16 編碼的。
console.log("Hello, world");
console.log("你好,世界!"); // Nearly all Unicode characters can be written literally in string literals
字串可以用單引號或雙引號書寫——JavaScript 不區分字元和字串。如果你想表示單個字元,只需使用由該單個字元組成的字串。
console.log("Hello"[1] === "e"); // true
要查詢字串的長度(以程式碼單元計),請訪問其length 屬性。
字串具有實用方法來操作字串和訪問有關字串的資訊。由於所有原始型別本質上都是不可變的,這些方法會返回新字串。
+ 運算子對字串進行了過載:當其中一個運算元是字串時,它執行字串連線而不是數字加法。一種特殊的模板字面量語法允許你更簡潔地編寫帶有嵌入表示式的字串。與 Python 的 f-strings 或 C# 的插值字串不同,模板字面量使用反引號(而不是單引號或雙引號)。
const age = 25;
console.log("I am " + age + " years old."); // String concatenation
console.log(`I am ${age} years old.`); // Template literal
其他型別
JavaScript 區分null(表示刻意的非值,並且只能透過 null 關鍵字訪問)和undefined(表示值的缺失)。有多種方法可以獲得 undefined
- 沒有值的
return語句(return;)隱式返回undefined。 - 訪問不存在的物件屬性(
obj.iDontExist)返回undefined。 - 沒有初始化的變數宣告(
let x;)將隱式將變數初始化為undefined。
JavaScript 有一個布林型別,可能的值是 true 和 false——兩者都是關鍵字。任何值都可以根據以下規則轉換為布林值
false、0、空字串("")、NaN、null和undefined都變為false。- 所有其他值都變為
true。
你可以使用Boolean() 函式顯式執行此轉換
Boolean(""); // false
Boolean(234); // true
然而,這很少是必要的,因為當 JavaScript 期望布林值時,例如在 if 語句中,它會默默地執行此轉換(參見控制結構)。因此,我們有時會談論“真值”和“假值”,分別表示在布林上下文中變為 true 和 false 的值。
支援布林運算,例如 &&(邏輯與)、||(邏輯或)和 !(邏輯非);參見運算子。
Symbol 型別通常用於建立唯一識別符號。使用Symbol() 函式建立的每個符號都保證是唯一的。此外,還有註冊符號,它們是共享常量,以及眾所周知的符號,它們被語言用作某些操作的“協議”。你可以在符號參考中閱讀更多相關資訊。
變數
JavaScript 中的變數使用三個關鍵字之一宣告:let、const 或 var。
let 允許你宣告塊級變數。宣告的變數在其封閉的塊內可用。
let a;
let name = "Simon";
// myLetVariable is *not* visible out here
for (let myLetVariable = 0; myLetVariable < 5; myLetVariable++) {
// myLetVariable is only visible in here
}
// myLetVariable is *not* visible out here
const 允許你宣告其值永不更改的變數。該變數在其宣告的塊內可用。
const Pi = 3.14; // Declare variable Pi
console.log(Pi); // 3.14
用 const 宣告的變數不能重新賦值。
const Pi = 3.14;
Pi = 1; // will throw an error because you cannot change a constant variable.
const 宣告只阻止重新賦值——如果變數是物件,它們不會阻止變數值的突變。
const obj = {};
obj.a = 1; // no error
console.log(obj); // { a: 1 }
var 宣告可能具有令人驚訝的行為(例如,它們不是塊作用域),在現代 JavaScript 程式碼中不鼓勵使用它們。
如果你宣告一個變數但沒有給它賦值,它的值是 undefined。你不能在沒有初始化器的情況下宣告 const 變數,因為你以後無論如何都無法更改它。
let 和 const 宣告的變數仍然佔據它們定義在的整個作用域,並且在實際宣告行之前處於一個稱為暫時性死區的區域。這與變數遮蔽有一些有趣的互動,這在其他語言中不會發生。
function foo(x, condition) {
if (condition) {
console.log(x);
const x = 2;
console.log(x);
}
}
foo(1, true);
在大多數其他語言中,這會列印“1”和“2”,因為在 const x = 2 行之前,x 仍然應該引用上層作用域中的引數 x。在 JavaScript 中,由於每個宣告都佔據整個作用域,這將在第一個 console.log 上丟擲錯誤:“無法在初始化之前訪問 'x'”。有關更多資訊,請參閱 let 的參考頁面。
JavaScript 是動態型別的。型別(如上一節所述)僅與值關聯,而不與變數關聯。對於 let 宣告的變數,你總是可以透過重新賦值來更改其型別。
let a = 1;
a = "foo";
運算子
JavaScript 的數字運算子包括 +、-、*、/、%(餘數)和 **(冪)。值使用 = 賦值。每個二元運算子也都有一個複合賦值對應物,例如 += 和 -=,它們擴充套件為 x = x operator y。
x += 5;
x = x + 5;
你可以使用 ++ 和 -- 分別進行遞增和遞減。這些可以用作字首或字尾運算子。
+ 運算子還執行字串連線
"hello" + " world"; // "hello world"
如果你將一個字串新增到數字(或其他值),所有內容都會首先轉換為字串。這可能會讓你感到困惑
"3" + 4 + 5; // "345"
3 + 4 + "5"; // "75"
將空字串新增到某些內容是將其本身轉換為字串的一種有用方法。
JavaScript 中的比較可以使用 <、>、<= 和 >= 進行,這些運算子適用於字串和數字。對於相等性,如果你提供不同型別,雙等號運算子會執行型別強制轉換,有時會產生有趣的結果。另一方面,三等號運算子不嘗試型別強制轉換,通常更受青睞。
123 == "123"; // true
1 == true; // true
123 === "123"; // false
1 === true; // false
雙等號和三等號也有它們的不等式對應物:!= 和 !==。
JavaScript 還具有位運算子和邏輯運算子。值得注意的是,邏輯運算子不只適用於布林值——它們透過值的“真值”來工作。
const a = 0 && "Hello"; // 0 because 0 is "falsy"
const b = "Hello" || "world"; // "Hello" because both "Hello" and "world" are "truthy"
&& 和 || 運算子使用短路邏輯,這意味著它們是否執行第二個運算元取決於第一個運算元。這對於在訪問屬性之前檢查空物件很有用
const name = o && o.getName();
或者用於快取值(當假值無效時)
const name = cachedName || (cachedName = getName());
語法
JavaScript 語法與 C 家族非常相似。有幾點值得一提
- 識別符號可以包含 Unicode 字元,但它們不能是保留字之一。
- 註釋通常是
//或/* */,而許多其他指令碼語言如 Perl、Python 和 Bash 使用#。 - JavaScript 中的分號是可選的——語言會在需要時自動插入。然而,有一些需要注意的注意事項,因為與 Python 不同,分號仍然是語法的一部分。
要深入瞭解 JavaScript 語法,請參閱詞法語法參考頁面。
控制結構
JavaScript 具有與 C 家族中其他語言相似的控制結構集。條件語句由if 和 else 支援;你可以將它們連結在一起
let name = "kittens";
if (name === "puppies") {
name += " woof";
} else if (name === "kittens") {
name += " meow";
} else {
name += "!";
}
name === "kittens meow";
JavaScript 沒有 elif,而 else if 實際上只是一個由單個 if 語句組成的 else 分支。
JavaScript 有 while 迴圈和 do...while 迴圈。前者適用於基本迴圈;後者適用於你希望確保迴圈體至少執行一次的迴圈
while (true) {
// an infinite loop!
}
let input;
do {
input = get_input();
} while (inputIsNotValid(input));
JavaScript 的for 迴圈與 C 和 Java 中的相同:它允許你在單行上提供迴圈的控制資訊。
for (let i = 0; i < 5; i++) {
// Will execute 5 times
}
JavaScript 還包含另外兩個著名的 for 迴圈:for...of,它迭代可迭代物件,最明顯的是陣列,以及for...in,它訪問物件的所有可列舉屬性。
for (const value of array) {
// do something with value
}
for (const property in object) {
// do something with object property
}
switch 語句可用於基於相等性檢查的多個分支。
switch (action) {
case "draw":
drawIt();
break;
case "eat":
eatIt();
break;
default:
doNothing();
}
與 C 類似,case 子句在概念上與標籤相同,因此如果你不新增 break 語句,執行將“穿透”到下一級。然而,它們實際上不是跳轉表——任何表示式都可以是 case 子句的一部分,而不僅僅是字串或數字字面量,它們將逐一評估,直到其中一個等於要匹配的值。比較使用 === 運算子在兩者之間進行。
與 Rust 等某些語言不同,控制流結構在 JavaScript 中是語句,這意味著你不能將它們賦值給變數,例如 const a = if (x) { 1 } else { 2 }。
JavaScript 錯誤使用try...catch 語句處理。
try {
buildMySite("./website");
} catch (e) {
console.error("Building site failed:", e);
}
可以使用throw 語句丟擲錯誤。許多內建操作也可能丟擲錯誤。
function buildMySite(siteDirectory) {
if (!pathExists(siteDirectory)) {
throw new Error("Site directory does not exist");
}
}
一般來說,你無法判斷剛剛捕獲的錯誤的型別,因為任何東西都可以從 throw 語句中丟擲。然而,你通常可以假設它是一個 Error 例項,如上例所示。有一些內建的 Error 子類,如 TypeError 和 RangeError,你可以使用它們來提供有關錯誤的額外語義。JavaScript 中沒有條件捕獲——如果你只想處理一種型別的錯誤,你需要捕獲所有內容,使用 instanceof 識別錯誤型別,然後重新丟擲其他情況。
try {
buildMySite("./website");
} catch (e) {
if (e instanceof RangeError) {
console.error("Seems like a parameter is out of range:", e);
console.log("Retrying...");
buildMySite("./website");
} else {
// Don't know how to handle other error types; throw them so
// something else up in the call stack may catch and handle it
throw e;
}
}
如果呼叫堆疊中的任何 try...catch 未捕獲到錯誤,程式將退出。
有關控制流語句的完整列表,請參閱參考部分。
物件
JavaScript 物件可以看作是鍵值對的集合。因此,它們類似於
- Python 中的字典。
- Perl 和 Ruby 中的雜湊。
- C 和 C++ 中的雜湊表。
- Java 中的 HashMap。
- PHP 中的關聯陣列。
JavaScript 物件是雜湊表。與靜態型別語言中的物件不同,JavaScript 中的物件沒有固定的形狀——屬性可以隨時新增、刪除、重新排序、修改或動態查詢。物件鍵總是字串或符號——即使是陣列索引,它們通常是整數,但在底層實際上是字串。
物件通常使用字面量語法建立
const obj = {
name: "Carrot",
for: "Max",
details: {
color: "orange",
size: 12,
},
};
物件屬性可以透過點(.)或方括號([])進行訪問。使用點表示法時,鍵必須是有效的識別符號。另一方面,方括號允許使用動態鍵值索引物件。
// Dot notation
obj.name = "Simon";
const name = obj.name;
// Bracket notation
obj["name"] = "Simon";
const name = obj["name"];
// Can use a variable to define a key
const userName = prompt("what is your key?");
obj[userName] = prompt("what is its value?");
屬性訪問可以鏈式呼叫
obj.details.color; // orange
obj["details"]["size"]; // 12
物件始終是引用,因此除非有明確的物件複製操作,否則對物件的修改將對外部可見。
const obj = {};
function doSomething(o) {
o.x = 1;
}
doSomething(obj);
console.log(obj.x); // 1
這也意味著兩個獨立建立的物件永遠不會相等(!==),因為它們是不同的引用。如果你持有同一物件的兩個引用,修改其中一個將透過另一個可見。
const me = {};
const stillMe = me;
me.x = 1;
console.log(stillMe.x); // 1
有關物件和原型的更多資訊,請參閱Object 參考頁面。有關物件初始化器語法的更多資訊,請參閱其參考頁面。
本頁面省略了關於物件原型和繼承的所有細節,因為你通常可以使用類實現繼承,而無需觸及底層機制(你可能聽說過它很晦澀)。要了解它們,請參閱繼承和原型鏈。
陣列
JavaScript 中的陣列實際上是一種特殊型別的物件。它們的工作方式與常規物件非常相似(數字屬性自然只能使用 [] 語法訪問),但它們有一個神奇的屬性叫做 length。這總是比陣列中的最高索引大一。
陣列通常用陣列字面量建立
const a = ["dog", "cat", "hen"];
a.length; // 3
JavaScript 陣列仍然是物件——你可以為它們分配任何屬性,包括任意數字索引。唯一的“魔法”是當你設定特定索引時,length 會自動更新。
const a = ["dog", "cat", "hen"];
a[100] = "fox";
console.log(a.length); // 101
console.log(a); // ['dog', 'cat', 'hen', empty × 97, 'fox']
我們上面得到的陣列被稱為稀疏陣列,因為中間存在未填充的槽,這將導致引擎將其從陣列降級為雜湊表。確保你的陣列是密集填充的!
越界索引不會丟擲錯誤。如果你查詢一個不存在的陣列索引,你將得到 undefined 值作為返回
const a = ["dog", "cat", "hen"];
console.log(typeof a[90]); // undefined
陣列可以包含任何元素,並且可以任意增長或縮小。
const arr = [1, "foo", true];
arr.push({});
// arr = [1, "foo", true, {}]
陣列可以使用 for 迴圈進行迭代,就像在其他類 C 語言中一樣
for (let i = 0; i < a.length; i++) {
// Do something with a[i]
}
或者,由於陣列是可迭代的,你可以使用for...of 迴圈,它與 C++/Java 的 for (int x : arr) 語法同義
for (const currentValue of a) {
// Do something with currentValue
}
陣列附帶了大量的陣列方法。其中許多方法會迭代陣列——例如,map() 會對每個陣列元素應用回撥,並返回一個新陣列
const babies = ["dog", "cat", "hen"].map((name) => `baby ${name}`);
// babies = ['baby dog', 'baby cat', 'baby hen']
函式
與物件一樣,函式是理解 JavaScript 的核心元件。最基本的函式宣告如下所示
function add(x, y) {
const total = x + y;
return total;
}
JavaScript 函式可以接受 0 個或更多引數。函式體可以包含任意數量的語句,並且可以宣告自己的變數,這些變數是該函式區域性的。可以在任何時候使用return 語句返回值,終止函式。如果未使用 return 語句(或沒有值的空 return),JavaScript 返回 undefined。
函式可以以多於或少於其指定的引數進行呼叫。如果你在不傳遞函式期望的引數的情況下呼叫函式,它們將被設定為 undefined。如果你傳遞的引數多於函式期望的引數,函式將忽略多餘的引數。
add(); // NaN
// Equivalent to add(undefined, undefined)
add(2, 3, 4); // 5
// added the first two; 4 was ignored
還有許多其他引數語法可用。例如,剩餘引數語法允許將呼叫者傳遞的所有額外引數收集到一個數組中,類似於 Python 的 *args。(由於 JS 在語言級別上沒有命名引數,因此沒有 **kwargs。)
function avg(...args) {
let sum = 0;
for (const item of args) {
sum += item;
}
return sum / args.length;
}
avg(2, 3, 4, 5); // 3.5
在上面的程式碼中,變數 args 包含了傳遞給函式的所有值。
rest 引數將儲存宣告位置之後的所有引數,但不會儲存之前的引數。換句話說,function avg(firstValue, ...args) 將把傳遞給函式的第一個值儲存在 firstValue 變數中,其餘引數儲存在 args 中。
如果一個函式接受一個引數列表,而你已經將它們儲存在一個數組中,你可以在函式呼叫中使用展開語法來將陣列展開為元素列表。例如:avg(...numbers)。
我們提到過 JavaScript 沒有命名引數。不過,可以使用物件解構來實現它們,這使得物件可以方便地打包和解包。
// Note the { } braces: this is destructuring an object
function area({ width, height }) {
return width * height;
}
// The { } braces here create a new object
console.log(area({ width: 2, height: 3 }));
還有預設引數語法,它允許被省略的引數(或作為 undefined 傳遞的引數)具有預設值。
function avg(firstValue, secondValue, thirdValue = 0) {
return (firstValue + secondValue + thirdValue) / 3;
}
avg(1, 2); // 1, instead of NaN
匿名函式
JavaScript 允許你建立匿名函式——即沒有名稱的函式。實際上,匿名函式通常用作其他函式的引數,立即賦值給可用於呼叫函式的變數,或者從另一個函式返回。
// Note that there's no function name before the parentheses
const avg = function (...args) {
let sum = 0;
for (const item of args) {
sum += item;
}
return sum / args.length;
};
這使得匿名函式可以透過呼叫 avg() 並傳入一些引數來呼叫——也就是說,它在語義上等同於使用 function avg() {} 宣告語法宣告函式。
還有另一種定義匿名函式的方法——使用箭頭函式表示式。
// Note that there's no function name before the parentheses
const avg = (...args) => {
let sum = 0;
for (const item of args) {
sum += item;
}
return sum / args.length;
};
// You can omit the `return` when simply returning an expression
const sum = (a, b, c) => a + b + c;
箭頭函式在語義上不等同於函式表示式——有關更多資訊,請參閱其參考頁面。
匿名函式還有另一種用處:它可以同時宣告和在一個表示式中呼叫,這被稱為立即呼叫函式表示式 (IIFE)
(function () {
// …
})();
關於 IIFE 的用例,你可以閱讀使用閉包模擬私有方法。
遞迴函式
JavaScript 允許你遞迴呼叫函式。這對於處理樹結構特別有用,例如在瀏覽器 DOM 中找到的那些。
function countChars(elm) {
if (elm.nodeType === 3) {
// TEXT_NODE
return elm.nodeValue.length;
}
let count = 0;
for (let i = 0, child; (child = elm.childNodes[i]); i++) {
count += countChars(child);
}
return count;
}
函式表示式也可以命名,這使得它們可以遞迴。
const charsInBody = (function counter(elm) {
if (elm.nodeType === 3) {
// TEXT_NODE
return elm.nodeValue.length;
}
let count = 0;
for (let i = 0, child; (child = elm.childNodes[i]); i++) {
count += counter(child);
}
return count;
})(document.body);
如上所示,提供給函式表示式的名稱僅在函式自己的作用域內可用。這允許引擎進行更多最佳化,併產生更具可讀性的程式碼。該名稱也顯示在偵錯程式和某些堆疊跟蹤中,這可以在除錯時為你節省時間。
如果你習慣了函數語言程式設計,請注意 JavaScript 中遞迴的效能影響。儘管語言規範指定了尾呼叫最佳化,但由於恢復堆疊跟蹤和可除錯性的困難,只有 JavaScriptCore(Safari 使用)實現了它。對於深度遞迴,請考慮使用迭代而不是遞迴,以避免堆疊溢位。
函式是一等物件
JavaScript 函式是一等物件。這意味著它們可以被賦值給變數,作為引數傳遞給其他函式,並從其他函式返回。此外,JavaScript 開箱即用地支援閉包,無需顯式捕獲,讓你方便地應用函數語言程式設計風格。
// Function returning function
const add = (x) => (y) => x + y;
// Function accepting function
const babies = ["dog", "cat", "hen"].map((name) => `baby ${name}`);
請注意,JavaScript 函式本身就是物件——就像 JavaScript 中的其他所有事物一樣——你可以在它們上新增或更改屬性,就像我們之前在“物件”部分看到的那樣。
內部函式
JavaScript 函式宣告允許在其他函式內部。巢狀函式在 JavaScript 中的一個重要細節是它們可以訪問其父函式作用域中的變數
function parentFunc() {
const a = 1;
function nestedFunc() {
const b = 4; // parentFunc can't use this
return a + b;
}
return nestedFunc(); // 5
}
這在編寫更易於維護的程式碼方面提供了極大的便利。如果一個被呼叫的函式依賴於一個或兩個對程式碼其他部分無用的其他函式,你可以將這些實用函式巢狀在其中。這可以減少全域性作用域中的函式數量。
這對於抵制全域性變數的誘惑也是一個很好的方法。在編寫複雜的程式碼時,經常會誘惑使用全域性變數在多個函式之間共享值,這會導致難以維護的程式碼。巢狀函式可以共享其父級中的變數,因此你可以使用該機制將函式耦合在一起,而不會汙染全域性名稱空間。
類
JavaScript 提供了與 Java 等語言非常相似的類語法。
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
return `Hello, I'm ${this.name}!`;
}
}
const p = new Person("Maria");
console.log(p.sayHello());
JavaScript 類只是必須使用new 運算子例項化的函式。每次例項化一個類時,它都會返回一個包含該類指定的方法和屬性的物件。類不強制任何程式碼組織——例如,你可以有函式返回類,或者每個檔案中有多個類。這是一個說明類建立如何隨意的示例:它只是一個從箭頭函式返回的表示式。這種模式被稱為混入。
const withAuthentication = (cls) =>
class extends cls {
authenticate() {
// …
}
};
class Admin extends withAuthentication(Person) {
// …
}
靜態屬性透過字首 static 建立。私有欄位和方法透過字首雜湊符號 # 建立(不是 private)。雜湊符號是元素名稱不可或缺的一部分,並將其與常規字串鍵屬性區分開來。(可以將 # 想象成 Python 中的 _。)與其他大多數語言不同,絕對無法在類體外部讀取私有元素——即使在派生類中也不行。
有關各種類功能的詳細指南,你可以閱讀指南頁面。
非同步程式設計
JavaScript 本質上是單執行緒的。沒有並行;只有併發。非同步程式設計由一個事件迴圈驅動,它允許將一組任務排隊並輪詢以完成。
JavaScript 中有三種慣用的非同步程式碼編寫方式
- 基於回撥的(例如
setTimeout()) - 基於
Promise的 async/await,它是 Promise 的語法糖
例如,檔案讀取操作在 JavaScript 中可能看起來像這樣
// Callback-based
fs.readFile(filename, (err, content) => {
// This callback is invoked when the file is read, which could be after a while
if (err) {
throw err;
}
console.log(content);
});
// Code here will be executed while the file is waiting to be read
// Promise-based
fs.readFile(filename)
.then((content) => {
// What to do when the file is read
console.log(content);
})
.catch((err) => {
throw err;
});
// Code here will be executed while the file is waiting to be read
// Async/await
async function readFile(filename) {
const content = await fs.readFile(filename);
console.log(content);
}
核心語言沒有指定任何非同步程式設計功能,但在與外部環境互動時至關重要——從請求使用者許可權,到獲取資料,再到讀取檔案。保持可能長時間執行的操作非同步可確保在等待期間其他程序仍可執行——例如,瀏覽器在等待使用者單擊按鈕授予許可權時不會凍結。
如果你有一個非同步值,就不可能同步獲取它的值。例如,如果你有一個 Promise,你只能透過then() 方法訪問最終結果。類似地,await 只能在非同步上下文中使用,這通常是非同步函式或模組。Promise 永遠不會阻塞——只有依賴於 Promise 結果的邏輯才會被延遲;其他一切都會同時繼續執行。如果你是函式式程式設計師,你可能會將 Promise 識別為單子,它可以用 then() 對映(然而,它們不是真正的單子,因為它們會自動扁平化;也就是說,你不能擁有 Promise<Promise<T>>)。
事實上,單執行緒模型使得 Node.js 因其非阻塞 IO 而成為伺服器端程式設計的流行選擇,這使得處理大量的資料庫或檔案系統請求具有非常高的效能。然而,純 JavaScript 的 CPU 密集型(計算密集型)任務仍然會阻塞主執行緒。為了實現真正的並行化,你可能需要使用Workers。
要了解更多關於非同步程式設計的資訊,你可以閱讀使用 Promise 或遵循非同步 JavaScript 教程。
模組
JavaScript 還指定了一個由大多數執行時支援的模組系統。模組通常是一個檔案,由其檔案路徑或 URL 標識。你可以使用import 和export 語句在模組之間交換資料
import { foo } from "./foo.js";
// Unexported variables are local to the module
const b = 2;
export const a = 1;
與 Haskell、Python、Java 等不同,JavaScript 模組解析完全由宿主定義——它通常基於 URL 或檔案路徑,因此相對檔案路徑“只需工作”,並且相對於當前模組的路徑而不是某個專案根路徑。
然而,JavaScript 語言不提供標準庫模組——所有核心功能都由諸如Math 和Intl 等全域性變數提供。這是由於 JavaScript 長期缺乏模組系統,以及選擇使用模組系統需要對執行時設定進行一些更改。
不同的執行時可能使用不同的模組系統。例如,Node.js 使用包管理器 npm,並且主要基於檔案系統,而 Deno 和瀏覽器則完全基於 URL,模組可以從 HTTP URL 解析。
欲瞭解更多資訊,請參閱模組指南頁面。
語言和執行時
在本頁面中,我們不斷提到某些功能是語言級別的,而另一些是執行時級別的。
JavaScript 是一種通用指令碼語言。核心語言規範側重於純粹的計算邏輯。它不處理任何輸入/輸出——事實上,如果沒有額外的執行時級 API(最顯著的是console.log()),JavaScript 程式的行為是完全無法觀察的。
執行時(或宿主)是向 JavaScript 引擎(直譯器)提供資料、提供額外全域性屬性以及提供引擎與外部世界互動的鉤子的東西。模組解析、讀取資料、列印訊息、傳送網路請求等都是執行時級別的操作。自誕生以來,JavaScript 已被各種環境採用,例如瀏覽器(提供諸如DOM 之類的 API)、Node.js(提供諸如檔案系統訪問之類的 API)等。JavaScript 已成功整合到 Web(這是其主要目的)、移動應用程式、桌面應用程式、伺服器端應用程式、無伺服器、嵌入式系統等。在學習 JavaScript 核心功能的同時,瞭解宿主提供的功能以將知識付諸實踐也很重要。例如,你可以閱讀所有Web 平臺 API,這些 API 由瀏覽器(有時是非瀏覽器)實現。
進一步探索
本頁面提供了關於各種 JavaScript 功能與其他語言相比的非常基本的見解。如果你想了解更多關於語言本身和每個功能的細微差別,你可以閱讀JavaScript 指南和JavaScript 參考。
由於篇幅和複雜性,我們省略了語言的一些基本部分,但你可以自行探索