JavaScript 语言概览

JavaScript 是一种多范式、动态语言,它具有类型和运算符、标准内置对象和方法。它的语法基于 Java 和 C 语言——这些语言中的许多结构也适用于 JavaScript。JavaScript 支持使用对象原型和类进行面向对象编程。它还支持函数式编程,因为函数是一等对象,可以通过表达式轻松创建并像任何其他对象一样传递。

本页面旨在为具有其他语言(如 C 或 Java)背景的读者快速概述各种 JavaScript 语言特性。

数据类型

让我们从任何语言的构建块开始:类型。JavaScript 程序操作值,这些值都属于一种类型。JavaScript 提供了七种原始类型

  • Number:用于所有数值(整数和浮点数),除了非常大的整数。
  • BigInt:用于任意大的整数。
  • String:用于存储文本。
  • Booleantruefalse——通常用于条件逻辑。
  • Symbol:用于创建不会冲突的唯一标识符。
  • Undefined:表示变量尚未赋值。
  • Null:表示一个刻意的非值。

其他一切都称为Object。常见的对象类型包括

函数在 JavaScript 中不是特殊的数据结构——它们只是可以调用的特殊类型的对象。

数字

JavaScript 有两种内置的数字类型:Number 和 BigInt。

Number 类型是 IEEE 754 64 位双精度浮点值,这意味着整数可以在 -(253 − 1)253 − 1 之间安全表示而不会丢失精度,浮点数可以存储到 1.79 × 10308。在数字内部,JavaScript 不区分浮点数和整数。

js
console.log(3 / 2); // 1.5, not 1

因此,一个看起来是整数的实际上隐式是浮点数。由于 IEEE 754 编码,有时浮点算术可能不精确。

js
console.log(0.1 + 0.2); // 0.30000000000000004

对于期望整数的操作,例如位运算,数字将被转换为 32 位整数。

数字字面量也可以带有前缀来指示基数(二进制、八进制、十进制或十六进制),或者带有指数后缀。

js
console.log(0b111110111); // 503
console.log(0o767); // 503
console.log(0x1f7); // 503
console.log(5.03e2); // 503

BigInt 类型是任意长度的整数。它的行为类似于 C 的整数类型(例如,除法向零截断),但它可以无限增长。BigInts 用数字字面量和 n 后缀指定。

js
console.log(-3n / 2n); // -1n

支持标准算术运算符,包括加法、减法、取余等。BigInts 和数字不能在算术运算中混合使用。

Math 对象提供了标准的数学函数和常量。

js
Math.sin(3.5);
const circumference = 2 * Math.PI * r;

有三种方法可以将字符串转换为数字

  • parseInt(),它解析字符串以获取整数。
  • parseFloat(),它解析字符串以获取浮点数。
  • Number() 函数,它将字符串解析为数字字面量,并支持多种不同的数字表示。

你也可以使用一元加号 + 作为 Number() 的简写。

数值还包括NaN(“Not a Number”的缩写)和Infinity。许多“无效数学”操作将返回 NaN——例如,如果尝试解析非数字字符串,或对负值使用Math.log()。除以零会产生 Infinity(正或负)。

NaN 具有传染性:如果将其作为任何数学运算的操作数,结果也将是 NaNNaN 是 JavaScript 中唯一不等于其自身的值(根据 IEEE 754 规范)。

字符串

JavaScript 中的字符串是 Unicode 字符序列。对于任何处理过国际化的人来说,这应该是个好消息。更准确地说,它们是UTF-16 编码的。

js
console.log("Hello, world");
console.log("你好,世界!"); // Nearly all Unicode characters can be written literally in string literals

字符串可以用单引号或双引号书写——JavaScript 不区分字符和字符串。如果你想表示单个字符,只需使用由该单个字符组成的字符串。

js
console.log("Hello"[1] === "e"); // true

要查找字符串的长度(以代码单元计),请访问其length 属性。

字符串具有实用方法来操作字符串和访问有关字符串的信息。由于所有原始类型本质上都是不可变的,这些方法会返回新字符串。

+ 运算符对字符串进行了重载:当其中一个操作数是字符串时,它执行字符串连接而不是数字加法。一种特殊的模板字面量语法允许你更简洁地编写带有嵌入表达式的字符串。与 Python 的 f-strings 或 C# 的插值字符串不同,模板字面量使用反引号(而不是单引号或双引号)。

js
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 有一个布尔类型,可能的值是 truefalse——两者都是关键字。任何值都可以根据以下规则转换为布尔值

  1. false0、空字符串("")、NaNnullundefined 都变为 false
  2. 所有其他值都变为 true

你可以使用Boolean() 函数显式执行此转换

js
Boolean(""); // false
Boolean(234); // true

然而,这很少是必要的,因为当 JavaScript 期望布尔值时,例如在 if 语句中,它会默默地执行此转换(参见控制结构)。因此,我们有时会谈论“真值”和“假值”,分别表示在布尔上下文中变为 truefalse 的值。

支持布尔运算,例如 &&(逻辑)、||(逻辑)和 !(逻辑);参见运算符

Symbol 类型通常用于创建唯一标识符。使用Symbol() 函数创建的每个符号都保证是唯一的。此外,还有注册符号,它们是共享常量,以及众所周知的符号,它们被语言用作某些操作的“协议”。你可以在符号参考中阅读更多相关信息。

变量

JavaScript 中的变量使用三个关键字之一声明:letconstvar

let 允许你声明块级变量。声明的变量在其封闭的内可用。

js
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 允许你声明其值永不更改的变量。该变量在其声明的内可用。

js
const Pi = 3.14; // Declare variable Pi
console.log(Pi); // 3.14

const 声明的变量不能重新赋值。

js
const Pi = 3.14;
Pi = 1; // will throw an error because you cannot change a constant variable.

const 声明只阻止重新赋值——如果变量是对象,它们不会阻止变量值的突变

js
const obj = {};
obj.a = 1; // no error
console.log(obj); // { a: 1 }

var 声明可能具有令人惊讶的行为(例如,它们不是块作用域),在现代 JavaScript 代码中不鼓励使用它们。

如果你声明一个变量但没有给它赋值,它的值是 undefined。你不能在没有初始化器的情况下声明 const 变量,因为你以后无论如何都无法更改它。

letconst 声明的变量仍然占据它们定义在的整个作用域,并且在实际声明行之前处于一个称为暂时性死区的区域。这与变量遮蔽有一些有趣的交互,这在其他语言中不会发生。

js
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 声明的变量,你总是可以通过重新赋值来更改其类型。

js
let a = 1;
a = "foo";

运算符

JavaScript 的数字运算符包括 +-*/%(余数)和 **(幂)。值使用 = 赋值。每个二元运算符也都有一个复合赋值对应物,例如 +=-=,它们扩展为 x = x operator y

js
x += 5;
x = x + 5;

你可以使用 ++-- 分别进行递增和递减。这些可以用作前缀或后缀运算符。

+ 运算符还执行字符串连接

js
"hello" + " world"; // "hello world"

如果你将一个字符串添加到数字(或其他值),所有内容都会首先转换为字符串。这可能会让你感到困惑

js
"3" + 4 + 5; // "345"
3 + 4 + "5"; // "75"

将空字符串添加到某些内容是将其本身转换为字符串的一种有用方法。

JavaScript 中的比较可以使用 <><=>= 进行,这些运算符适用于字符串和数字。对于相等性,如果你提供不同类型,双等号运算符会执行类型强制转换,有时会产生有趣的结果。另一方面,三等号运算符不尝试类型强制转换,通常更受青睐。

js
123 == "123"; // true
1 == true; // true

123 === "123"; // false
1 === true; // false

双等号和三等号也有它们的不等式对应物:!=!==

JavaScript 还具有位运算符逻辑运算符。值得注意的是,逻辑运算符不只适用于布尔值——它们通过值的“真值”来工作。

js
const a = 0 && "Hello"; // 0 because 0 is "falsy"
const b = "Hello" || "world"; // "Hello" because both "Hello" and "world" are "truthy"

&&|| 运算符使用短路逻辑,这意味着它们是否执行第二个操作数取决于第一个操作数。这对于在访问属性之前检查空对象很有用

js
const name = o && o.getName();

或者用于缓存值(当假值无效时)

js
const name = cachedName || (cachedName = getName());

有关运算符的完整列表,请参阅指南页面参考部分。你可能对运算符优先级特别感兴趣。

语法

JavaScript 语法与 C 家族非常相似。有几点值得一提

  • 标识符可以包含 Unicode 字符,但它们不能是保留字之一。
  • 注释通常是 ///* */,而许多其他脚本语言如 Perl、Python 和 Bash 使用 #
  • JavaScript 中的分号是可选的——语言会在需要时自动插入。然而,有一些需要注意的注意事项,因为与 Python 不同,分号仍然是语法的一部分。

要深入了解 JavaScript 语法,请参阅词法语法参考页面

控制结构

JavaScript 具有与 C 家族中其他语言相似的控制结构集。条件语句由ifelse 支持;你可以将它们链接在一起

js
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 循环。前者适用于基本循环;后者适用于你希望确保循环体至少执行一次的循环

js
while (true) {
  // an infinite loop!
}

let input;
do {
  input = get_input();
} while (inputIsNotValid(input));

JavaScript 的for 循环与 C 和 Java 中的相同:它允许你在单行上提供循环的控制信息。

js
for (let i = 0; i < 5; i++) {
  // Will execute 5 times
}

JavaScript 还包含另外两个著名的 for 循环:for...of,它迭代可迭代对象,最明显的是数组,以及for...in,它访问对象的所有可枚举属性。

js
for (const value of array) {
  // do something with value
}

for (const property in object) {
  // do something with object property
}

switch 语句可用于基于相等性检查的多个分支。

js
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 语句处理。

js
try {
  buildMySite("./website");
} catch (e) {
  console.error("Building site failed:", e);
}

可以使用throw 语句抛出错误。许多内置操作也可能抛出错误。

js
function buildMySite(siteDirectory) {
  if (!pathExists(siteDirectory)) {
    throw new Error("Site directory does not exist");
  }
}

一般来说,你无法判断刚刚捕获的错误的类型,因为任何东西都可以从 throw 语句中抛出。然而,你通常可以假设它是一个 Error 实例,如上例所示。有一些内置的 Error 子类,如 TypeErrorRangeError,你可以使用它们来提供有关错误的额外语义。JavaScript 中没有条件捕获——如果你只想处理一种类型的错误,你需要捕获所有内容,使用 instanceof 识别错误类型,然后重新抛出其他情况。

js
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 中的对象没有固定的形状——属性可以随时添加、删除、重新排序、修改或动态查询。对象键总是字符串符号——即使是数组索引,它们通常是整数,但在底层实际上是字符串。

对象通常使用字面量语法创建

js
const obj = {
  name: "Carrot",
  for: "Max",
  details: {
    color: "orange",
    size: 12,
  },
};

对象属性可以通过点(.)或方括号([])进行访问。使用点表示法时,键必须是有效的标识符。另一方面,方括号允许使用动态键值索引对象。

js
// 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?");

属性访问可以链式调用

js
obj.details.color; // orange
obj["details"]["size"]; // 12

对象始终是引用,因此除非有明确的对象复制操作,否则对对象的修改将对外部可见。

js
const obj = {};
function doSomething(o) {
  o.x = 1;
}
doSomething(obj);
console.log(obj.x); // 1

这也意味着两个独立创建的对象永远不会相等(!==),因为它们是不同的引用。如果你持有同一对象的两个引用,修改其中一个将通过另一个可见。

js
const me = {};
const stillMe = me;
me.x = 1;
console.log(stillMe.x); // 1

有关对象和原型的更多信息,请参阅Object 参考页面。有关对象初始化器语法的更多信息,请参阅其参考页面

本页面省略了关于对象原型和继承的所有细节,因为你通常可以使用实现继承,而无需触及底层机制(你可能听说过它很晦涩)。要了解它们,请参阅继承和原型链

数组

JavaScript 中的数组实际上是一种特殊类型的对象。它们的工作方式与常规对象非常相似(数字属性自然只能使用 [] 语法访问),但它们有一个神奇的属性叫做 length。这总是比数组中的最高索引大一。

数组通常用数组字面量创建

js
const a = ["dog", "cat", "hen"];
a.length; // 3

JavaScript 数组仍然是对象——你可以为它们分配任何属性,包括任意数字索引。唯一的“魔法”是当你设置特定索引时,length 会自动更新。

js
const a = ["dog", "cat", "hen"];
a[100] = "fox";
console.log(a.length); // 101
console.log(a); // ['dog', 'cat', 'hen', empty × 97, 'fox']

我们上面得到的数组被称为稀疏数组,因为中间存在未填充的槽,这将导致引擎将其从数组降级为哈希表。确保你的数组是密集填充的!

越界索引不会抛出错误。如果你查询一个不存在的数组索引,你将得到 undefined 值作为返回

js
const a = ["dog", "cat", "hen"];
console.log(typeof a[90]); // undefined

数组可以包含任何元素,并且可以任意增长或缩小。

js
const arr = [1, "foo", true];
arr.push({});
// arr = [1, "foo", true, {}]

数组可以使用 for 循环进行迭代,就像在其他类 C 语言中一样

js
for (let i = 0; i < a.length; i++) {
  // Do something with a[i]
}

或者,由于数组是可迭代的,你可以使用for...of 循环,它与 C++/Java 的 for (int x : arr) 语法同义

js
for (const currentValue of a) {
  // Do something with currentValue
}

数组附带了大量的数组方法。其中许多方法会迭代数组——例如,map() 会对每个数组元素应用回调,并返回一个新数组

js
const babies = ["dog", "cat", "hen"].map((name) => `baby ${name}`);
// babies = ['baby dog', 'baby cat', 'baby hen']

函数

与对象一样,函数是理解 JavaScript 的核心组件。最基本的函数声明如下所示

js
function add(x, y) {
  const total = x + y;
  return total;
}

JavaScript 函数可以接受 0 个或更多参数。函数体可以包含任意数量的语句,并且可以声明自己的变量,这些变量是该函数局部的。可以在任何时候使用return 语句返回值,终止函数。如果未使用 return 语句(或没有值的空 return),JavaScript 返回 undefined

函数可以以多于或少于其指定的参数进行调用。如果你在不传递函数期望的参数的情况下调用函数,它们将被设置为 undefined。如果你传递的参数多于函数期望的参数,函数将忽略多余的参数。

js
add(); // NaN
// Equivalent to add(undefined, undefined)

add(2, 3, 4); // 5
// added the first two; 4 was ignored

还有许多其他参数语法可用。例如,剩余参数语法允许将调用者传递的所有额外参数收集到一个数组中,类似于 Python 的 *args。(由于 JS 在语言级别上没有命名参数,因此没有 **kwargs。)

js
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 没有命名参数。不过,可以使用对象解构来实现它们,这使得对象可以方便地打包和解包。

js
// 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 传递的参数)具有默认值。

js
function avg(firstValue, secondValue, thirdValue = 0) {
  return (firstValue + secondValue + thirdValue) / 3;
}

avg(1, 2); // 1, instead of NaN

匿名函数

JavaScript 允许你创建匿名函数——即没有名称的函数。实际上,匿名函数通常用作其他函数的参数,立即赋值给可用于调用函数的变量,或者从另一个函数返回。

js
// 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() {} 声明语法声明函数。

还有另一种定义匿名函数的方法——使用箭头函数表达式

js
// 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)

js
(function () {
  // …
})();

关于 IIFE 的用例,你可以阅读使用闭包模拟私有方法

递归函数

JavaScript 允许你递归调用函数。这对于处理树结构特别有用,例如在浏览器 DOM 中找到的那些。

js
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;
}

函数表达式也可以命名,这使得它们可以递归。

js
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 开箱即用地支持闭包,无需显式捕获,让你方便地应用函数式编程风格。

js
// 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 中的一个重要细节是它们可以访问其父函数作用域中的变量

js
function parentFunc() {
  const a = 1;

  function nestedFunc() {
    const b = 4; // parentFunc can't use this
    return a + b;
  }
  return nestedFunc(); // 5
}

这在编写更易于维护的代码方面提供了极大的便利。如果一个被调用的函数依赖于一个或两个对代码其他部分无用的其他函数,你可以将这些实用函数嵌套在其中。这可以减少全局作用域中的函数数量。

这对于抵制全局变量的诱惑也是一个很好的方法。在编写复杂的代码时,经常会诱惑使用全局变量在多个函数之间共享值,这会导致难以维护的代码。嵌套函数可以共享其父级中的变量,因此你可以使用该机制将函数耦合在一起,而不会污染全局命名空间。

JavaScript 提供了与 Java 等语言非常相似的语法。

js
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 运算符实例化的函数。每次实例化一个类时,它都会返回一个包含该类指定的方法和属性的对象。类不强制任何代码组织——例如,你可以有函数返回类,或者每个文件中有多个类。这是一个说明类创建如何随意的示例:它只是一个从箭头函数返回的表达式。这种模式被称为混入

js
const withAuthentication = (cls) =>
  class extends cls {
    authenticate() {
      // …
    }
  };

class Admin extends withAuthentication(Person) {
  // …
}

静态属性通过前缀 static 创建。私有字段和方法通过前缀哈希符号 # 创建(不是 private)。哈希符号是元素名称不可或缺的一部分,并将其与常规字符串键属性区分开来。(可以将 # 想象成 Python 中的 _。)与其他大多数语言不同,绝对无法在类体外部读取私有元素——即使在派生类中也不行。

有关各种类功能的详细指南,你可以阅读指南页面

异步编程

JavaScript 本质上是单线程的。没有并行;只有并发。异步编程由一个事件循环驱动,它允许将一组任务排队并轮询以完成。

JavaScript 中有三种惯用的异步代码编写方式

例如,文件读取操作在 JavaScript 中可能看起来像这样

js
// 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 标识。你可以使用importexport 语句在模块之间交换数据

js
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 语言不提供标准库模块——所有核心功能都由诸如MathIntl 等全局变量提供。这是由于 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 参考

由于篇幅和复杂性,我们省略了语言的一些基本部分,但你可以自行探索