13、你真的知道 JavaScript 语句吗?

44 阅读5分钟

语句(Statement) ≠ 表达式(Expression) 。脚本与模块最终都是“语句列表”。这一篇,我们不站在教科书的冷门划分,而是以工程实用视角,把 JS 语句一次讲清:怎么用、易错点、最佳实践。


目录速览

  1. 语句的两大门类:普通语句 vs 声明型语句
  2. 普通语句全家桶与坑点
  3. 声明型语句与“预处理/提升”行为
  4. 速查清单 + 练习

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 / finallythrow

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,不能提前用。
  • ASIreturn / throw / yield / await / ++/--(后置)/ 箭头 => 前 位置不能换行

5. 实战建议(工程落地)

  • 打开 ESLint 规则:

    • no-constant-condition(循环条件)、no-fallthrough(switch 贯穿)
    • no-empty(空块/空语句)、no-labels(禁止/限制标签)
    • no-with(禁用 with)、no-unreachableno-unsafe-finally
  • 统一 let/const 策略:默认 const,需要变更再用 let

  • 函数声明一律放在模块/文件顶层函数体顶层块内改用表达式

  • 遍历与异步流:优先 for...of / for await...of,减少手写下标与回调地狱。


6. 练习(动手秒记)

找出所有内置可迭代对象并演示其 for...of 行为(数组、字符串、Map、Set、TypedArray、argumentsNodeList 等),并写一个包含 try/catch/finally + return 的最小示例,观察 finally 覆盖行为。