语句(Statement) ≠ 表达式(Expression) 。脚本与模块最终都是“语句列表”。这一篇,我们不站在教科书的冷门划分,而是以工程实用视角,把 JS 语句一次讲清:怎么用、易错点、最佳实践。
目录速览
- 语句的两大门类:普通语句 vs 声明型语句
- 普通语句全家桶与坑点
- 声明型语句与“预处理/提升”行为
- 速查清单 + 练习
1. 两大门类:普通语句 vs 声明型语句
- 普通语句:执行流程相关(块、条件、分支、循环、异常、调试、跳转等)。
- 声明型语句:引入绑定(变量、常量、函数、类等),关键在作用域与提升行为。
记忆法: “普通语句跑流程,声明语句造名字” 。
2. 普通语句全家桶
2.1 语句块(Block)
{
var x, y;
x = 10;
y = 20;
}
- 块会产生块级作用域(
let/const/class/function*等在块内有效)。 var不受块限制(函数/全局作用域)。
{
let x = 1;
}
console.log(x); // ReferenceError
2.2 空语句(Empty Statement)
;
- 只是为了语法完备性;偶尔用在 for 的占位里,但工程中几乎没有必要单独使用。
2.3 条件语句:if / else
if (a < 10) { /* ... */ }
else if (a < 20) { /* ... */ }
else { /* ... */ }
- 建议总是配合块
{}使用,避免悬挂else等可读性陷阱。
2.4 分支语句:switch
switch (num) {
case 1: console.log(1); break;
case 2: console.log(2); break;
case 3: console.log(3); break;
}
- 没有
break会“贯穿” (fall-through)。 - 在 JS 中性能与
if/else无本质差异,更多是可读性/结构化选择。
2.5 循环语句
2.5.1 while / do...while
let a = 100;
while (a--) console.log('*');
let b = 101;
do { console.log(b); } while (b < 100); // 至少执行一次
2.5.2 经典 for
for (let i = 0; i < 100; i++) console.log(i);
let每次迭代都会新绑定,闭包不再“全部打印同一个值”的老坑。const只适合“非迭代控制变量”的占位(不常见,不建议)。
2.5.3 for...in(枚举可枚举属性名)
const o = { a: 10, b: 20 };
Object.defineProperty(o, 'c', { enumerable: false, value: 30 });
for (const k in o) console.log(k); // 仅 a、b
- 遍历可枚举自有/继承属性名,不适合数组(顺序与稀疏位置不稳定)。
2.5.4 for...of(遍历可迭代值)
for (const e of [1, 2, 3]) console.log(e);
- 基于 Iterator 协议;数组、字符串、Map、Set、TypedArray 等原生可迭代。
- 自定义可迭代:
const o = {
[Symbol.iterator]: () => ({
_v: 0,
next() { return this._v === 3 ? { done: true } : { value: this._v++, done: false } }
})
};
for (const v of o) console.log(v); // 0,1,2
2.5.5 for await...of(异步可迭代)
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function* gen() { let i = 0; while (true) { await sleep(1000); yield i++; } }
for await (const n of gen()) console.log(n); // 每秒一个数(无限)
- 适用于流式/异步数据(网络、文件、计时器)。
2.6 跳转:break / continue(可带标签)
outer: for (let i = 0; i < 100; i++) {
inner: for (let j = 0; j < 100; j++) {
if (i === 50 && j === 50) break outer;
}
}
- 标签让跳转目标可控,但会降低可读性;仅在深度嵌套时酌情使用。
2.7 with(了解即可)
const o = { a:1, b:2 };
with (o) { console.log(a, b); }
- 破坏静态可分析性,被广泛认为是糟粕;严格模式下禁用。
2.8 异常:try / catch / finally 与 throw
try {
throw new Error('error');
} catch (e) {
console.log(e.message);
} finally {
console.log('finally'); // 一定执行
}
finally必执行,且其return/throw会覆盖try/catch的结果。catch (e)会创建块级作用域,其中不可再次声明同名e。
2.9 return
function square(x) { return x * x; }
- 只能在函数体中使用。
- 注意 ASI:
return后不允许换行([no LineTerminator here])。
2.10 debugger
debugger; // 仅在有调试器挂载时生效
3. 声明型语句与“预处理/提升”行为
关键问题:何时引入绑定?绑定是否可访问?是否已赋值?作用域边界?
3.1 var
- 作用于脚本/函数体(不受块限制)。
- 预处理阶段声明,值为
undefined;可重复声明。
console.log(x); // undefined
var x = 1;
- 历史遗留 + 可穿透块 → 容易出错。现代代码建议用
let/const。
3.2 let / const
- 作用于块级作用域;不可重复声明。
- 存在 TDZ(暂时性死区) :在声明前访问会抛错。
if (true) {
console.log(a); // ReferenceError(TDZ)
const a = 1;
}
const必须立即初始化;绑定不可变,但值可变(若是对象)。
3.3 function 声明
- 顶层/函数体中:预处理时声明 + 赋值(可提前调用)。
- 块级(如
if中) :规范近年调整 → 预声明,赋值在执行阶段,细节依宿主/模式而异。工程上避免在块中声明函数,改用函数表达式:
let f;
if (cond) {
f = function() { /* ... */ };
}
3.4 class 声明
- 块级作用域,存在 TDZ,不可在声明前使用,更贴近直觉:
console.log(C); // ReferenceError
class C {}
class体内方法默认严格模式;构造器、getter/setter、静态成员可用。
4. 速查清单(收藏)
- 循环:数组/迭代器 →
for...of;对象键枚举 →for...in(注意可枚举性与原型链)。 - 跳转:尽量避免标签;复杂嵌套用函数拆解更清晰。
- 异常:
finally必执行且可能覆盖结果;catch作用域独立。 - 声明:优先
const,其次let;避免var。 - 函数/类:避免在块中用
function声明;class有 TDZ,不能提前用。 - ASI:
return / throw / yield / await / ++/--(后置)/ 箭头 => 前位置不能换行。
5. 实战建议(工程落地)
-
打开 ESLint 规则:
no-constant-condition(循环条件)、no-fallthrough(switch 贯穿)no-empty(空块/空语句)、no-labels(禁止/限制标签)no-with(禁用 with)、no-unreachable、no-unsafe-finally
-
统一
let/const策略:默认const,需要变更再用let。 -
函数声明一律放在模块/文件顶层或函数体顶层;块内改用表达式。
-
遍历与异步流:优先
for...of/for await...of,减少手写下标与回调地狱。
6. 练习(动手秒记)
找出所有内置可迭代对象并演示其
for...of行为(数组、字符串、Map、Set、TypedArray、arguments、NodeList等),并写一个包含try/catch/finally + return的最小示例,观察finally覆盖行为。