目录
JavaScript 1. 基础语法
JavaScript 程序由表达式、语句、声明和代码块组成。理解基础语法不仅意味着记住关键字和运算符,还需要区分三个层次:
- ECMAScript 语言规范规定语法、类型、运算和执行语义;
- 宿主环境提供浏览器 DOM、定时器、文件系统或网络接口;
- JavaScript 引擎负责解析、编译和执行代码,并可以采用不同的内部优化策略。
ECMAScript 规范通常使用环境记录、引用记录、内部槽和抽象操作描述程序行为。这些概念用于定义可观察语义,并不等同于某一种固定的栈、堆或指针布局。因此,除非明确讨论某个引擎,否则不应把规范抽象直接解释为物理内存结构。
1.1 ECMAScript、JavaScript 引擎与宿主环境
JavaScript 通常指以 ECMAScript 为语言核心,并运行在浏览器、Node.js 等宿主环境中的编程体系。
ECMAScript 本身定义了以下内容:
- 词法和语法规则;
- 内置数据类型;
- 表达式和语句的求值规则;
- 函数、对象、类和模块的语言语义;
Array、Map、Promise等标准内置对象。
浏览器则额外提供:
window、document和 DOM;fetch();setTimeout();- 用户交互事件;
- 页面渲染相关接口。
Node.js 也提供自己的文件系统、进程、网络和定时器接口。相同的 ECMAScript 代码可以在不同宿主中执行,但宿主 API 并不一定通用。
1.1.1 在 HTML 中加载脚本
HTML 可以通过 <script> 标签嵌入或引入 JavaScript:
<script>
console.log("inline script");
</script>
<script src="/scripts/main.js"></script>
当 <script> 标签具有 src 属性时,它表示加载外部脚本。此时标签内部的 JavaScript 文本不会作为该脚本的内容执行:
<script src="/scripts/main.js">
console.log("这段代码不会执行");
</script>
传统脚本与模块脚本具有不同的作用域和加载语义:
<script src="/scripts/legacy.js"></script>
<script type="module" src="/scripts/main.js"></script>
模块默认运行在严格模式下,并具有独立的模块作用域。模块系统将在第九章系统讨论。
1.1.2 表达式、语句与声明
表达式会求值得到一个值:
1 + 2;
user.name;
isReady ? "ready" : "waiting";
语句用于控制程序执行:
if (isReady) {
start();
}
声明用于创建变量、函数、类或模块绑定:
const language = "JavaScript";
function greet() {
return "Hello";
}
某些结构既包含声明,也包含表达式。理解二者的区别有助于判断一段代码能否出现在某个语法位置。
1.1.3 代码块与注释
代码块由一对花括号组成:
{
const value = 10;
console.log(value);
}
let、const 和类声明具有块级作用域。普通对象字面量也使用花括号,但它是表达式的一部分,不是代码块。
JavaScript 支持两种注释:
// 单行注释
/*
多行注释
*/
多行注释不能嵌套。注释应说明设计意图、边界条件或不明显的原因,而不是逐字复述代码。
1.2 自动分号插入与严格模式
1.2.1 自动分号插入
JavaScript 中的许多语句在语法上以分号结束。源代码省略分号时,解析器可以在规范规定的条件下执行自动分号插入(Automatic Semicolon Insertion,ASI)。ASI 并不是简单地“在每个换行处补上分号”。如果换行后的标记仍能与上一行构成合法表达式,解析器通常不会在两行之间插入分号。
const value = 10
(function () {
console.log("IIFE");
})();
这段代码可能被理解为尝试调用数值 10,而不是两条独立语句。类似风险常见于下一行以下列标记开始时:
(;[;- 模板字面量的反引号;
/;- 一元
+或-。
例如:
const numbers = [1, 2, 3]
[4, 5].forEach((value) => {
console.log(value);
});
如果上一行没有分号,后一行的方括号可能被解析为对上一表达式结果进行属性访问。
一种可靠风格是在所有可写分号的位置显式书写分号:
const numbers = [1, 2, 3];
[4, 5].forEach((value) => {
console.log(value);
});
另一种风格是整体省略分号,但在以危险标记开头的语句前添加防御性分号。无论选择哪种风格,都应保持一致,并交由格式化工具统一处理。
1.2.2 受限产生式中的换行
某些语法位置明确禁止换行。最常见的是 return:
function createUser() {
return
{
name: "Alice",
};
}
console.log(createUser()); // undefined
解析器会将其视为:
function createUser() {
return;
{
name: "Alice";
}
}
应让返回表达式与 return 位于同一行,或者在换行时使用圆括号:
function createUser() {
return {
name: "Alice",
};
}
function createAnotherUser() {
return (
{
name: "Bob",
}
);
}
类似的换行限制还会影响 throw、带标签的 break 和 continue 等结构。
1.2.3 严格模式
在传统脚本或普通函数体的指令序言中写入 "use strict",可以启用严格模式:
"use strict";
const value = 10;
指令序言必须位于脚本或函数体开头,并由字符串字面量表达式构成:
function run() {
"use strict";
// 函数体在严格模式下执行。
}
严格模式会改变部分早期 JavaScript 行为,例如:
- 给未声明的标识符赋值会抛出
ReferenceError; - 独立调用普通函数时,函数内部的
this保持为undefined; - 某些静默失败会改为抛出异常;
- 删除不可删除的绑定等非法操作会更早暴露。
ECMAScript 模块和类体默认处于严格模式,无需额外声明。严格模式不能在已经执行的代码中动态关闭。它改变的是相应源代码区域的解析与执行语义,而不是一个可以随时切换的全局开关。
1.3 变量声明、作用域与绑定
1.3.1 let、const 与 var
现代 JavaScript 主要使用 let 和 const 声明变量。三种声明方式的核心差异如下:
| 声明 | 作用域 | 声明前访问 | 同一作用域重复声明 | 是否允许重新赋值 |
|---|---|---|---|---|
var | 函数或全局作用域 | 返回 undefined | 允许 | 允许 |
let | 块级作用域 | 抛出 ReferenceError | 不允许 | 允许 |
const | 块级作用域 | 抛出 ReferenceError | 不允许 | 不允许 |
{
let count = 1;
const limit = 10;
count += 1;
console.log(count); // 2
console.log(limit); // 10
}
推荐默认使用 const。只有当变量绑定确实需要重新赋值时,才使用 let。现代代码通常不需要使用 var。
1.3.2 暂时性死区
包含 let、const 或类声明的作用域在开始执行时,会先建立相应绑定。但在执行到声明语句并完成初始化之前,这些绑定不能被访问。从作用域开始到初始化完成之间的区域称为暂时性死区(Temporal Dead Zone,TDZ):
{
console.log(value); // ReferenceError
const value = 10;
}
TDZ 并不意味着变量“完全不存在”,而是相应词法绑定已经属于当前作用域,但仍处于未初始化状态。
这一规则还会阻止代码错误地访问外层同名变量:
const value = "outer";
{
console.log(value); // ReferenceError
const value = "inner";
}
块内的 value 从块开始处就遮蔽了外层绑定,但在声明完成前不可访问。
1.3.3 var 提升
var 声明的绑定在进入函数或脚本执行阶段前已经初始化为 undefined:
console.log(value); // undefined
var value = 10;
它在行为上近似于:
var value;
console.log(value);
value = 10;
这不表示赋值语句也被移动。所谓“提升”是对环境初始化结果的通俗描述,而不是引擎必须实际重排源代码。
1.3.4 const 限制的是绑定
const 阻止标识符被重新赋值:
const score = 90;
// TypeError
score = 100;
如果绑定的值是对象,仍然可以修改对象自身允许修改的属性:
const student = {
name: "Alice",
score: 90,
};
student.score = 100;
console.log(student.score); // 100
下面的重新赋值仍然非法:
const student = {
name: "Alice",
};
// TypeError
student = {
name: "Bob",
};
不应把这一规则描述为“锁定栈内存中的指针”。ECMAScript 规范只保证绑定不可重新赋值,并不规定该值必须以某种指针形式存放在某个物理内存区域。
如需限制对象属性,可以使用 Object.freeze(),但它默认只进行浅层冻结:
const settings = Object.freeze({
theme: "dark",
});
// 严格模式下抛出 TypeError;非严格模式下通常静默失败。
settings.theme = "light";
1.3.5 标识符规则
JavaScript 标识符可以包含 Unicode 标识符字符、数字、美元符号 $ 和下划线 _,但数字不能作为首字符。
const userName = "Alice";
const _internalValue = 10;
const $element = document.querySelector("#app");
const 学科 = "Physics";
JavaScript 区分大小写:
const value = 1;
const Value = 2;
console.log(value); // 1
console.log(Value); // 2
关键字和保留字不能在不允许的位置作为标识符使用。
工程命名通常遵循以下约定:
- 变量和函数使用小驼峰:
currentUser、calculateTotal(); - 类使用大驼峰:
StudentRecord; - 已知且具有全局常量语义的值可以使用全大写下划线:
MAX_RETRY_COUNT; - 普通
const变量不需要全部使用大写。
1.4 JavaScript 数据类型
JavaScript 具有 7 种原始类型和对象类型。
原始类型包括:
undefined;null;boolean;number;bigint;string;symbol。
除原始值之外的语言值都属于对象。数组、函数、日期、正则表达式、Map 和 Set 都是对象的不同形式。
1.4.1 值语义与对象引用
原始值是不可变值。把原始值赋给另一个变量时,两个绑定分别持有相同的值:
let first = 10;
let second = first;
second = 20;
console.log(first); // 10
console.log(second); // 20
对象赋值会复制对象引用所代表的值,因此两个变量可以引用同一个对象:
const first = {
score: 90,
};
const second = first;
second.score = 100;
console.log(first.score); // 100
这里应表述为“两个绑定引用同一个对象”,而不是假定规范要求复制某个物理内存地址。对象的实际存储位置和优化方式属于引擎实现细节。
1.4.2 Number
JavaScript 的 Number 使用 IEEE 754 双精度浮点数语义表示普通数值。
const integer = 42;
const decimal = 3.14;
const scientific = 1.5e6;
Number 同时表示整数和浮点数。可安全精确表示的整数范围为:
对应常量为:
console.log(Number.MIN_SAFE_INTEGER);
console.log(Number.MAX_SAFE_INTEGER);
判断整数是否位于安全范围内,可以使用:
console.log(
Number.isSafeInteger(9007199254740991),
); // true
console.log(
Number.isSafeInteger(9007199254740992),
); // false
Infinity 与负无穷
某些数值运算会得到正无穷或负无穷:
console.log(1 / 0); // Infinity
console.log(-1 / 0); // -Infinity
Infinity 仍然属于 number:
console.log(typeof Infinity); // number
判断一个数值是否有限,推荐使用 Number.isFinite():
console.log(Number.isFinite(10)); // true
console.log(Number.isFinite(Infinity)); // false
console.log(Number.isFinite("10")); // false
全局 isFinite() 会先执行数字转换,语义更宽松:
console.log(isFinite("10")); // true
NaN
NaN 表示数值运算未能产生有效数值结果:
console.log(0 / 0); // NaN
console.log(Number("text")); // NaN
NaN 与包括自身在内的任何值进行 === 比较都返回 false:
console.log(NaN === NaN); // false
检测 NaN 应使用 Number.isNaN():
console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN("text")); // false
全局 isNaN() 会先进行数字转换,容易产生误解:
console.log(isNaN("text")); // true
大多数涉及 NaN 的普通算术运算仍会得到 NaN,但不能把它理解为所有操作中的绝对传播定律。例如字符串拼接可以把它转换为字符串:
console.log(NaN + 1); // NaN
console.log(`${NaN}`); // "NaN"
浮点误差
二进制浮点数不能精确表示所有十进制小数:
console.log(0.1 + 0.2); // 0.30000000000000004
判断浮点结果是否足够接近时,可以根据问题尺度选择容差:
const result = 0.1 + 0.2;
const expected = 0.3;
console.log(
Math.abs(result - expected) < Number.EPSILON,
); // true
Number.EPSILON 适合描述 1 附近相邻可表示数的间隔,并不是适用于所有数量级的通用误差阈值。实际计算应根据数据尺度构造相对或绝对容差。
1.4.3 BigInt
BigInt 用于表示任意精度整数:
const largeValue = 9007199254740993n;
console.log(typeof largeValue); // bigint
BigInt 不能与 Number 直接混合执行普通算术运算:
// TypeError
// console.log(1n + 1);
必须先显式转换到同一种类型:
console.log(1n + BigInt(1)); // 2n
console.log(Number(1n) + 1); // 2
把较大的 BigInt 转换为 Number 可能丢失精度:
const exact = 9007199254740993n;
const approximate = Number(exact);
console.log(approximate); // 9007199254740992
BigInt 除法会舍弃小数部分:
console.log(7n / 2n); // 3n
1.4.4 String
字符串是不可变的 UTF-16 代码单元序列:
const single = 'JavaScript';
const double = "JavaScript";
const template = `JavaScript`;
单引号和双引号没有语义差异。模板字面量支持多行文本和表达式插值:
const name = "Alice";
const score = 95;
const message = `${name}'s score is ${score}.`;
console.log(message);
字符串不能原地修改:
const language = "JavaScript";
language[0] = "j";
console.log(language); // JavaScript
某些 Unicode 字符由两个 UTF-16 代码单元组成,因此字符串的 length 不一定等于用户感知的字符数量:
console.log("😀".length); // 2
更完整的 Unicode 和国际化处理将在第七章讨论。
1.4.5 Boolean
布尔类型只有两个值:
const enabled = true;
const disabled = false;
布尔值主要用于条件判断,但 JavaScript 的条件表达式也允许其他类型,并通过布尔转换决定分支。
1.4.6 null 与 undefined
undefined 通常表示缺少值或尚未提供值:
let result;
console.log(result); // undefined
函数没有显式返回值时,也会返回 undefined。
null 通常由程序显式使用,表示有意设置的空值:
let selectedUser = null;
二者语义接近但类型不同:
console.log(null === undefined); // false
console.log(null == undefined); // true
在现代代码中,应根据接口语义明确选择 null 或 undefined,而不是随意混用。
1.4.7 Symbol
每次调用 Symbol() 都会创建一个唯一值:
const first = Symbol("id");
const second = Symbol("id");
console.log(first === second); // false
Symbol 可以用作对象属性键,以避免普通字符串键名冲突:
const internalId = Symbol("internalId");
const user = {
name: "Alice",
[internalId]: 1001,
};
console.log(user[internalId]); // 1001
全局 Symbol 注册表中的 Symbol.for() 具有不同语义:
const first = Symbol.for("shared");
const second = Symbol.for("shared");
console.log(first === second); // true
Symbol 的系统应用将在对象、迭代器和元编程章节中继续讨论。
1.4.8 Object
对象用于保存属性和行为:
const student = {
name: "Alice",
score: 95,
introduce() {
return `I am ${this.name}.`;
},
};
数组和函数也是对象:
console.log(typeof []); // object
console.log(typeof function() {}); // function
函数之所以返回 "function",是因为 typeof 对可调用对象具有专门分支。函数仍然属于对象,可以拥有属性并作为值传递。对象和数组的属性、复制、遍历及内存拓扑将在第五章系统讨论。
1.5 typeof 与更精确的类型检测
1.5.1 typeof 的返回结果
typeof 是一元运算符:
console.log(typeof 42); // number
写成 typeof(value) 也合法,但圆括号并不表示它是普通函数调用。
| 值 | typeof 结果 |
|---|---|
undefined | "undefined" |
null | "object" |
true | "boolean" |
42 | "number" |
42n | "bigint" |
"text" | "string" |
Symbol() | "symbol" |
| 普通函数 | "function" |
| 数组及其他普通对象 | "object" |
1.5.2 typeof null
console.log(typeof null); // object
这是 JavaScript 的历史遗留行为。它通常被追溯到早期实现使用类型标签表示值时,空指针式的 null 表示与对象标签发生冲突。现代 ECMAScript 规范直接保留了这一可观察结果,以维持既有网页兼容性。这一历史解释不应被当作现代引擎必须采用的物理内存布局。可靠结论只有一个:语言规定 typeof null 返回 "object"。检测 null 应直接使用严格相等:
console.log(value === null);
1.5.3 数组和对象检测
检测数组应使用:
console.log(Array.isArray([])); // true
检测某个值是否为非空对象,可以写成:
function isObject(value) {
return value !== null && typeof value === "object";
}
instanceof 检查原型链关系,不等同于通用类型检测,并且会受到跨 realm、原型修改和自定义 Symbol.hasInstance 的影响。其机制将在第六章讨论。
1.6 类型转换
JavaScript 既支持显式类型转换,也会在部分运算中进行隐式类型转换。
显式转换通常更容易阅读:
const input = "42";
const number = Number(input);
隐式转换则由运算符或语句语义触发:
console.log("1" + 2); // "12"
console.log("6" / 2); // 3
1.6.1 字符串转换
显式字符串转换使用 String():
console.log(String(123)); // "123"
console.log(String(true)); // "true"
console.log(String(null)); // "null"
console.log(String(undefined)); // "undefined"
模板字面量插值也会把值转换为字符串:
const value = 42;
console.log(`value = ${value}`);
直接把 Symbol 与普通字符串使用 + 拼接会抛出 TypeError:
const id = Symbol("id");
// TypeError
// console.log("id: " + id);
console.log(`id: ${String(id)}`);
1.6.2 数字转换
显式数字转换使用 Number():
console.log(Number("42")); // 42
console.log(Number(" ")); // 0
console.log(Number("3.14")); // 3.14
console.log(Number("3px")); // NaN
console.log(Number(true)); // 1
console.log(Number(false)); // 0
console.log(Number(null)); // 0
console.log(Number(undefined)); // NaN
一元 + 也会触发普通数字转换:
console.log(+"42"); // 42
console.log(+""); // 0
一元 + 不能用于 BigInt:
// TypeError
// console.log(+1n);
parseInt() 和 parseFloat() 用于从字符串开头解析数字,其语义与 Number() 不同:
console.log(Number("12px")); // NaN
console.log(Number.parseInt("12px", 10)); // 12
console.log(Number.parseFloat("3.14m")); // 3.14
使用 Number.parseInt() 时通常应显式提供进制。
1.6.3 布尔转换
显式布尔转换使用 Boolean():
console.log(Boolean(1)); // true
console.log(Boolean(0)); // false
条件语句和逻辑运算符会隐式执行布尔转换。
普通 ECMAScript 值中的假值包括:
false;0;-0;0n;"";null;undefined;NaN。
除此之外,浏览器中还存在历史兼容对象 document.all,它具有特殊的假值行为。一般程序不应依赖这一例外。所有其他值都是真值,包括:
Boolean("0"); // true
Boolean(" "); // true
Boolean([]); // true
Boolean({}); // true
空数组和空对象是真值,因为布尔转换判断的是值的类型和规则,而不是集合内部是否包含元素。
1.6.4 二元加法
二元 + 同时承担数值加法和字符串拼接。
求值时,操作数会先转换为原始值。如果任一转换结果是字符串,则执行字符串拼接;否则尝试执行数值加法。
console.log(1 + 2); // 3
console.log("1" + 2); // "12"
console.log(1 + "2"); // "12"
console.log(1 + 2 + "3"); // "33"
console.log("1" + 2 + 3); // "123"
对象也可能通过原始值转换参与运算:
console.log([] + 1); // "1"
这类隐式转换规则容易产生难以理解的结果。业务代码应避免依赖复杂的对象转换,优先显式转换数据类型。
Number 与 BigInt 不能直接混合执行加法:
// TypeError
// console.log(1 + 1n);
1.6.5 其他算术运算
减法、乘法、除法、取余和幂运算不会执行字符串拼接,而会尝试执行数值转换:
console.log("6" - 1); // 5
console.log("6" * 2); // 12
console.log("6" / 2); // 3
如果转换失败,结果通常为 NaN:
console.log("text" * 2); // NaN
1.7 运算符
1.7.1 算术运算符
常见算术运算符包括:
| 运算符 | 含义 |
|---|---|
+ | 加法或字符串拼接 |
- | 减法 |
* | 乘法 |
/ | 除法 |
% | 余数 |
** | 幂 |
JavaScript 的 % 返回余数,其符号通常与被除数一致:
console.log(7 % 3); // 1
console.log(-7 % 3); // -1
它不总是数学意义上的非负模运算。需要非负模时,可以封装:
function modulo(value, divisor) {
return ((value % divisor) + divisor) % divisor;
}
console.log(modulo(-7, 3)); // 2
1.7.2 赋值运算符
赋值表达式本身会返回所赋的值:
let value;
console.log(value = 10); // 10
链式赋值从右向左结合:
let first;
let second;
first = second = 5;
console.log(first); // 5
console.log(second); // 5
复合赋值运算符包括:
value += 1;
value -= 1;
value *= 2;
value /= 2;
value **= 2;
逻辑赋值运算符包括:
value ||= fallback;
value &&= nextValue;
value ??= fallback;
它们具有短路求值语义,不完全等价于不考虑副作用的简单展开式。
1.7.3 自增与自减
前置形式先修改变量,再返回新值:
let value = 1;
console.log(++value); // 2
console.log(value); // 2
后置形式先返回旧值,再修改变量:
let value = 1;
console.log(value++); // 1
console.log(value); // 2
自增和自减只能作用于可赋值引用,不能直接作用于字面量:
// SyntaxError
// 5++;
在复杂表达式中混合使用 ++、-- 和其他副作用会降低可读性。通常应将状态更新写成独立语句。
1.7.4 逗号运算符
逗号运算符从左到右求值多个表达式,并返回最后一个表达式的结果:
const result = (1 + 2, 3 + 4);
console.log(result); // 7
数组字面量、函数实参列表和变量声明中的逗号通常是语法分隔符,不是逗号运算符:
const values = [1 + 2, 3 + 4];
console.log(values); // [3, 7]
逗号运算符优先级很低,实际业务代码中应谨慎使用。
1.7.5 运算符优先级
const result = 2 + 3 * 4;
console.log(result); // 14
乘法优先于加法。圆括号可以显式改变求值顺序:
const result = (2 + 3) * 4;
console.log(result); // 20
即使熟悉优先级,在混合多个不同运算符时也应使用圆括号表达意图。可读性比省略字符更重要。
1.8 相等、关系与逻辑运算
1.8.1 严格相等
严格相等 === 不执行类型转换:
console.log(1 === 1); // true
console.log(1 === "1"); // false
对于对象,严格相等判断两个操作数是否引用同一个对象:
const first = {
value: 1,
};
const second = {
value: 1,
};
const third = first;
console.log(first === second); // false
console.log(first === third); // true
数值比较中:
console.log(NaN === NaN); // false
console.log(+0 === -0); // true
1.8.2 Object.is
Object.is() 使用 SameValue 语义:
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(+0, -0)); // false
其他常见值上,它通常与 === 结果一致。
1.8.3 宽松相等
宽松相等 == 会根据操作数类型执行规范规定的转换和特殊分支:
console.log(1 == "1"); // true
console.log(false == 0); // true
console.log(null == undefined); // true
它的行为不是简单地“把两边都转换为数字”。例如,null 与 undefined 之间有专门规则,对象与原始值的比较还会触发原始值转换。
现代业务代码通常优先使用 === 和 !==。一种常见且有明确意图的宽松相等用法是同时检测 null 与 undefined:
if (value == null) {
// value 为 null 或 undefined
}
使用这种写法时应确保团队理解其语义,否则也可以显式写出两个条件。
1.8.4 关系比较
关系运算符包括:
<;
>;
<=;
>=;
字符串与字符串之间按照 UTF-16 代码单元序列进行比较:
console.log("2" < "10"); // false
不同类型之间会发生相应转换:
console.log("2" < 10); // true
null 与 undefined 在关系比较和宽松相等中遵循不同规则:
console.log(null == 0); // false
console.log(null >= 0); // true
console.log(undefined > 0); // false
console.log(undefined <= 0); // false
不要通过这类边界行为表达业务逻辑。输入数据应先验证并转换为明确类型。
1.8.5 逻辑运算符
逻辑或 || 返回第一个真值;如果所有操作数都为假值,则返回最后一个操作数:
console.log("" || "default"); // default
console.log("value" || "default"); // value
逻辑与 && 返回第一个假值;如果所有操作数都为真值,则返回最后一个操作数:
console.log(0 && "next"); // 0
console.log(1 && "next"); // next
逻辑非 ! 先执行布尔转换再取反:
console.log(!0); // true
console.log(!""); // true
console.log(!"0"); // false
双重逻辑非可以转换为布尔值:
console.log(!!"text"); // true
这些运算符都会短路。确定结果后,右侧表达式不会继续执行:
const user = null;
user && console.log(user.name);
1.8.6 空值合并运算符
?? 只在左侧为 null 或 undefined 时使用右侧值:
console.log(0 ?? 10); // 0
console.log("" ?? "text"); // ""
console.log(false ?? true); // false
console.log(null ?? 10); // 10
与 || 相比,?? 不会把 0、空字符串和 false 当作缺失值。
?? 不能在没有圆括号的情况下直接与 && 或 || 混合:
// SyntaxError
// const result = a || b ?? c;
const result = (a || b) ?? c;
1.9 条件分支
1.9.1 if 语句
if 会对条件表达式执行布尔转换:
const score = 85;
if (score >= 90) {
console.log("A");
} else if (score >= 80) {
console.log("B");
} else {
console.log("C");
}
即使分支中只有一条语句,也推荐保留花括号,以减少修改代码时引入控制流错误的风险。
1.9.2 条件运算符
条件运算符是一个表达式,会返回两个候选表达式之一:
const age = 18;
const category = age >= 18 ? "adult" : "minor";
它适合根据简单条件选择一个值。复杂分支或包含大量副作用的逻辑应使用 if,避免嵌套三元表达式降低可读性。
1.9.3 switch 语句
switch 适合针对一个表达式进行多路匹配:
const status = "pending";
switch (status) {
case "pending":
console.log("Waiting");
break;
case "fulfilled":
console.log("Completed");
break;
case "rejected":
console.log("Failed");
break;
default:
console.log("Unknown status");
}
case 匹配采用严格相等语义:
const value = "2";
switch (value) {
case 2:
console.log("number");
break;
case "2":
console.log("string");
break;
}
输出为:
string
如果匹配分支没有通过 break、return 或 throw 等方式终止,控制流会继续执行后续分支,这称为穿透(Fall-through):
const role = "editor";
switch (role) {
case "admin":
case "editor":
console.log("Can edit");
break;
default:
console.log("Read only");
}
有意使用穿透时,应让代码结构清晰,必要时添加简短注释。
1.10 循环与迭代
1.10.1 while
while 在每轮开始前检查条件:
let count = 0;
while (count < 3) {
console.log(count);
count += 1;
}
如果条件初始就是假值,循环体不会执行。
1.10.2 do…while
do...while 先执行循环体,再检查条件,因此至少执行一次:
let count = 0;
do {
console.log(count);
count += 1;
} while (count < 3);
do...while 末尾通常保留分号。
1.10.3 for
for 适合具有初始化、继续条件和步进逻辑的循环:
for (let index = 0; index < 3; index += 1) {
console.log(index);
}
三个部分都可以省略:
let count = 0;
for (;;) {
if (count >= 3) {
break;
}
count += 1;
}
使用 let 声明循环变量时,规范为每轮迭代提供适当的词法绑定语义,使闭包可以捕获对应轮次的值:
const callbacks = [];
for (let index = 0; index < 3; index += 1) {
callbacks.push(() => index);
}
console.log(callbacks[0]()); // 0
console.log(callbacks[1]()); // 1
console.log(callbacks[2]()); // 2
闭包机制将在第二章详细讨论。
1.10.4 break 与 continue
break 立即终止当前循环:
for (let index = 0; index < 10; index += 1) {
if (index === 3) {
break;
}
console.log(index);
}
continue 跳过当前轮剩余代码,进入下一轮:
for (let index = 0; index < 5; index += 1) {
if (index % 2 === 0) {
continue;
}
console.log(index);
}
在 for 循环中,执行 continue 后会先运行步进表达式,再检查下一轮条件。
1.10.5 标签语句
标签可以与 break 或 continue 配合控制外层循环:
outerLoop:
for (let row = 0; row < 3; row += 1) {
for (let column = 0; column < 3; column += 1) {
if (row === 1 && column === 1) {
break outerLoop;
}
console.log(row, column);
}
}
break outerLoop 会终止被标记的外层循环。
标签不是函数跳转机制,也不能跨越函数边界。它适合少量明确的嵌套控制流;如果标签过多,通常应通过函数拆分或重新组织算法降低复杂度。
1.10.6 for…in
for...in 遍历对象自身及其原型链上的可枚举字符串属性名:
const user = {
name: "Alice",
score: 95,
};
for (const key in user) {
if (Object.hasOwn(user, key)) {
console.log(key, user[key]);
}
}
for...in 得到的是属性名,而不是属性值。
不推荐使用 for...in 遍历数组:
const values = [10, 20, 30];
// 不推荐
for (const key in values) {
console.log(key);
}
原因包括:
- 得到的是字符串属性名;
- 可能包含额外可枚举属性;
- 它表达的是对象属性枚举,而不是数组元素迭代。
1.10.7 for…of
for...of 通过可迭代协议读取值:
const values = [10, 20, 30];
for (const value of values) {
console.log(value);
}
字符串、数组、Map、Set 和类型化数组等内置对象可直接迭代。
普通对象默认不可迭代:
const user = {
name: "Alice",
};
// TypeError
// for (const value of user) {
// console.log(value);
// }
可以先使用对象静态方法取得数组:
for (const [key, value] of Object.entries(user)) {
console.log(key, value);
}
迭代器协议将在第七章进一步讨论。
1.11 位运算符
JavaScript 的普通数字位运算会先把 Number 转换为 32 位整数,再执行二进制运算。运算完成后,结果再以 Number 返回。
因此,位运算适合处理 32 位标志位、掩码和特定底层数据,不适合替代一般数值运算,也不应仅以“更高效”为理由使用。
1.11.1 常用位运算符
| 运算符 | 名称 | 示例 |
|---|---|---|
& | 按位与 | a & b |
| ` | ` | 按位或 |
^ | 按位异或 | a ^ b |
~ | 按位非 | ~a |
<< | 左移 | a << n |
>> | 有符号右移 | a >> n |
>>> | 无符号右移 | a >>> n |
1.11.2 按位与、或和异或
const first = 0b1100;
const second = 0b1010;
console.log((first & second).toString(2)); // "1000"
console.log((first | second).toString(2)); // "1110"
console.log((first ^ second).toString(2)); // "110"
按位与只保留两边都为 1 的位;按位或保留任一边为 1 的位;按位异或保留两边不同的位。
1.11.3 标志位
可以使用不同二进制位表示多个布尔权限:
const READ = 1 << 0; // 0001
const WRITE = 1 << 1; // 0010
const DELETE = 1 << 2; // 0100
let permissions = READ | WRITE;
console.log((permissions & READ) !== 0); // true
console.log((permissions & DELETE) !== 0); // false
permissions |= DELETE;
console.log((permissions & DELETE) !== 0); // true
清除标志位:
permissions &= ~WRITE;
console.log((permissions & WRITE) !== 0); // false
1.11.4 移位
console.log(5 << 1); // 10
console.log(8 >> 1); // 4
左移和右移有时类似乘除 ,但它们会先发生 32 位整数转换,溢出、符号位和截断行为与普通算术不同。
console.log(2 ** 31); // 2147483648
console.log((2 ** 31) | 0); // -2147483648
>>> 使用零填充右移,并把结果视为无符号 32 位模式:
console.log(-1 >>> 0); // 4294967295
它常被用于把一个数值转换为无符号 32 位结果,但会截断超出 32 位的部分:
console.log(2 ** 40 >>> 0); // 0
因此,不能把 >>> 1 普遍视为比 Math.floor(value / 2) 更安全的除二方法,也不应在可能超过 32 位范围的数组索引计算中使用它。
1.11.5 BigInt 位运算
BigInt 支持:
&;|;^;~;<<;>>。
但不支持无符号右移 >>>,因为 BigInt 没有固定宽度的最高符号位:
console.log(8n >> 1n); // 4n
// TypeError
// console.log(8n >>> 1n);
1.12 浏览器中的基础交互
浏览器提供 alert()、prompt() 和 confirm() 等模态对话框。它们属于 Web API,而不是 ECMAScript 语言本身。
alert("Hello");
const name = prompt("Your name:", "Alice");
const confirmed = confirm("Continue?");
典型返回值为:
alert()返回undefined;prompt()在确认时返回字符串,在取消时返回null;confirm()返回布尔值。
这些对话框会阻断当前页面的交互与脚本推进,而且浏览器可以对其显示方式和可用性施加限制。现代界面通常使用 HTML、CSS 和事件处理器构建非阻断式交互。
调试输出通常使用:
console.log("message");
console.warn("warning");
console.error("error");
console 同样由宿主环境提供,其显示格式不属于 ECMAScript 规范保证。
1.13 本章小结
本章建立了后续学习所需的语言基础:
- ECMAScript 定义语言语义,浏览器和 Node.js 提供宿主 API,引擎负责具体实现;
- 规范中的环境记录、引用和内部槽是行为模型,不等同于固定物理内存结构;
let和const具有块级作用域与暂时性死区,const限制的是绑定重新赋值;- JavaScript 包含 7 种原始类型和对象类型;
Number具有安全整数范围、浮点误差、Infinity和NaN等边界;typeof null是保留至今的历史行为,不能用于判断空值;- 隐式类型转换取决于具体运算符,二元
+同时支持数值加法和字符串拼接; - 严格相等通常比宽松相等更容易推理;
&&、||和??返回操作数并具有短路求值语义;for...in枚举属性名,for...of迭代可迭代对象的值;- 普通数值位运算采用 32 位整数语义,可能发生截断和符号变化;
- ASI 是语法规则的一部分,不能简单理解为在每个换行处自动补分号。
参考资料
- javascript.info: JavaScript Fundamentals
- MDN JavaScript Guide: Grammar and types
- MDN JavaScript Guide: Expressions and operators
- MDN JavaScript Guide: Control flow and error handling
- MDN JavaScript Guide: Loops and iteration
- MDN JavaScript Reference: Strict mode
- MDN JavaScript Reference: typeof
- ECMAScript 2026 Language Specification: Lexical Grammar
- ECMAScript 2026 Language Specification: ECMAScript Data Types and Values
- ECMAScript 2026 Language Specification: ECMAScript Language—Expressions
- ECMAScript 2026 Language Specification: Statements and Declarations
- HTML Standard: Simple dialogs