JavaScript 语言概述

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

此页面简要概述了各种 JavaScript 语言功能,专为熟悉其他语言(如 C 或 Java)的读者撰写。

数据类型

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

  • 数字: 用于所有数字值(整数和浮点数),除了非常大的整数。
  • BigInt: 用于任意大的整数。
  • 字符串: 用于存储文本。
  • 布尔值: truefalse - 通常用于条件逻辑。
  • 符号: 用于创建不会发生冲突的唯一标识符。
  • 未定义: 表示变量尚未赋值。
  • : 表示一个故意非值。

其他所有内容都称为 对象。常见的对象类型包括

函数在 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 的整数类型(例如,除法截断为零),只是它可以无限增长。BigInt 使用数字字面量和 n 后缀指定。

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

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

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

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

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

  • parseInt(),它解析字符串以获取整数。
  • parseFloat(),它解析字符串以获取浮点数。
  • Number() 函数,它解析字符串,就好像它是一个数字字面量,并支持许多不同的数字表示。

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

数字值还包括 NaN(“非数字”的缩写)和 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-字符串或 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 的神奇属性。这始终比数组中的最高索引多 1。

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

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 保存传递给函数的所有值。

剩余参数将存储在其声明后的所有参数,而不是之前的参数。换句话说,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 运算符实例化的函数。每次实例化类时,它都会返回一个包含类指定的函数和属性的对象。类不会强制执行任何代码组织 - 例如,您可以有函数返回类,或者您可以在每个文件中拥有多个类。以下是如何随意创建类的示例:它只是从箭头函数返回的表达式。这种模式被称为 mixin

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 成为服务器端编程的热门选择,因为它具有非阻塞 I/O,这使得处理大量数据库或文件系统请求的性能非常高。但是,纯 JavaScript 的 CPU 密集型(计算密集型)任务仍然会阻塞主线程。要实现真正的并行化,您可能需要使用 工作线程

要了解更多关于异步编程的信息,您可以阅读有关 使用 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,它们由浏览器实现,有时也由非浏览器实现。

进一步探索

此页面提供了对各种 JavaScript 功能与其他语言的比较的非常基本的了解。 如果您想了解更多关于语言本身以及每个功能的细微差别,您可以阅读 JavaScript 指南JavaScript 参考

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