八、变量提升与函数提升

8.1 理论背景:变量实例化(Variable Instantiation)

8.1.1 ECMAScript 规范中的执行上下文创建

ECMAScript 规范(ECMA-262 ES5)详细描述了执行上下文的创建过程。理解这个过程是掌握"提升"(Hoisting)的关键。

规范级两阶段模型

① 创建阶段(Creation Phase)——建立绑定(Variable Instantiation)

当进入任何执行上下文时(全局或函数),引擎会先执行"预处理"(Preprocessing):

全局

函数

进入执行上下文

全局 or 函数?

创建全局 EC

创建函数 EC

扫描 var 关键字

创建绑定, 初始化为 undefined

扫描 function 声明

同名 var 已存在?

不重复创建, 共用绑定

创建绑定, 初始化为完整函数值

绑定 function 值(覆盖)

进入执行阶段

详细步骤

  1. 扫描 var 声明

    • 查找当前作用域中的所有 var 关键字
    • 为每个 var 变量创建绑定(Binding)
    • 初始化绑定值为 undefined
    • 注意:只扫描 var,不扫描 let/const(它们有 TDZ)
  2. 扫描 function 声明

    • 查找当前作用域中的所有函数声明(function foo() {}
    • 为每个函数创建绑定
    • 初始化绑定值为完整的函数对象
    • 如果同名 var 已存在,共用同一个绑定,用函数值覆盖
  3. 建立词法环境

    • 创建环境记录(Environment Record)
    • 记录变量名到值的映射
    • 建立外部词法环境引用(形成作用域链)

② 执行阶段(Execution Phase)——逐行运行

预处理完成后,开始按代码顺序逐行执行:

遇到的语句 引擎行为 说明
var x = 10 只执行 x = 10 声明已在创建阶段完成,此处只赋值
function foo(){} 直接跳过 创建阶段已处理,无需重复
let y = 20 创建绑定 + 初始化 let 在执行阶段才创建,有 TDZ
const z = 30 创建绑定 + 初始化 const 同样在执行阶段创建
其他语句 正常执行 表达式、if/for/while 等

② 执行阶段(Execution Phase)——逐行运行

按代码顺序逐行执行:

  • 遇到 var x = val只执行赋值,变量已在创建阶段登记,此处仅更新其值
  • 遇到 function foo() {}直接跳过,创建阶段已处理
  • 遇到其他表达式/语句:正常执行
8.1.2 提升的底层机制

什么是提升(Hoisting)?

"提升"不是 JavaScript 语言规范中的术语,而是开发者对"变量和函数声明在代码执行前被处理"这一现象的描述。

提升的本质

你写的代码:           引擎看到的"等效视角"(非真实操作):
─────────────         ──────────────────────────────────────
console.log(x);  →    var x;           // 创建阶段:声明被"提升"
var x = 10;           console.log(x);  // undefined
                       x = 10;          // 执行阶段:赋值

三种声明的提升行为对比

声明类型 创建阶段 执行到声明行 TDZ 提升后的值
var x 创建绑定,初始化 undefined 赋值 undefined
function foo(){} 创建绑定,初始化完整函数值 跳过 函数对象
let x 创建绑定,不初始化(TDZ) 初始化 + 赋值 访问报 ReferenceError
const x 创建绑定,不初始化(TDZ) 初始化 + 赋值 访问报 ReferenceError
8.1.3 关键理论概念
概念 规范定义 作用
Lexical Environment 环境记录 + 外部环境引用 实现作用域的抽象概念
Environment Record { [[Type]], [[EnvironmentRecord]] } 存储变量绑定
Binding 变量名到值的映射 核心数据结构
TDZ 暂时性死区 let/const 声明前的区域
Completion { [[Type]], [[Value]], [[Target]] } 语句执行结果

执行阶段

var x = val → 执行赋值
(声明已在创建阶段完成)

function foo(){} → 跳过
(创建阶段已处理)

其他语句 → 正常执行

创建阶段

扫描 var → 创建绑定
初始化为 undefined

扫描 function → 创建绑定
初始化为完整函数值

var 与同名 function 合并
不重复创建,以 function 值为初值

8.2 课堂案例完整注释

案例 12:变量提升(对应 05-变量提升/01-变量提升

// =============================================
// 案例目标:通过三个子案例理解 var 变量提升的全部细节
// 核心概念:var 声明会被"提升"到作用域顶部,但只提升声明,不提升赋值
// =============================================

// ───────────────────────────────────────────────
// 子案例 1:全局变量提升
// ───────────────────────────────────────────────
// 预处理阶段(在代码执行前):
//   引擎扫描全局代码,发现 var address
//   创建绑定:address → undefined
//   相当于在代码最顶部写了:var address;

// 执行阶段开始:
console.log(address);  // → undefined
// 解释:
//   1. 此时代码还没有执行到 var address = '目标地址';
//   2. 但 address 已经在预处理阶段被声明并初始化为 undefined
//   3. 所以读取 address 得到 undefined,而不是报错
//   4. 这就是"变量提升"的体现

// 继续执行到赋值语句:
var address = '目标地址';
// 执行流程:
//   1. var address 是声明,但预处理阶段已经处理过了
//   2. 引擎跳过声明部分(创建阶段已完成)
//   3. 执行赋值:address = '目标地址'
//   4. address 的值从 undefined 更新为 '目标地址'

console.log(address);  // → 目标地址(赋值后的值)

// ───────────────────────────────────────────────
// 对比:没有 var 的赋值(隐式全局)
// ───────────────────────────────────────────────
age = 10;
// 解释:
//   1. age 没有用 var/let/const 声明
//   2. 创建阶段不会处理 age(不会被提升)
//   3. 这是直接给全局对象(window)的属性赋值
//   4. 在非严格模式下,会自动创建全局属性 window.age = 10
//   5. 在严格模式下,这会报错:ReferenceError: age is not defined

console.log(age);  // → 10
// age 现在是全局对象的属性,可以访问

// ⚠️ 反模式警告:
// 不写 var 的赋值会创建"隐式全局变量",这是常见的 Bug 来源
// 建议始终使用 var/let/const 显式声明变量
// 或者使用严格模式 'use strict' 来防止此类错误

console.log('');
console.log('');
// 空行分隔

// ───────────────────────────────────────────────
// 子案例 2:函数内部变量提升
// ───────────────────────────────────────────────
// 理论:函数调用时也会经历"创建阶段"和"执行阶段"

function func() {
    // ───────────────────────────────────────────────
    // func 调用时的创建阶段(在执行函数体之前):
    //   1. 创建新的执行上下文(EC)
    //   2. 扫描函数体,查找 var 声明
    //   3. 发现 var num
    //   4. 创建局部绑定:num → undefined
    //   5. 相当于在函数体顶部写了:var num;
    // ───────────────────────────────────────────────

    // ───────────────────────────────────────────────
    // func 调用时的执行阶段:
    // ───────────────────────────────────────────────

    // 第一行代码:
    console.log(num);  // → undefined
    // 解释:
    //   1. 此时还没有执行到 var num = 100;
    //   2. 但 num 已经在创建阶段被声明并初始化为 undefined
    //   3. 所以读取 num 得到 undefined
    //   4. 这就是"函数内部变量提升"

    // 第二行代码:
    var num = 100;
    // 执行流程:
    //   1. var num 的声明部分已在创建阶段处理
    //   2. 引擎只执行赋值:num = 100
    //   3. num 的值从 undefined 更新为 100

    // 第三行代码:
    console.log(num);  // → 100
    // 解释:num 现在的值是 100
}

// 调用函数,观察变量提升的效果:
func();
// 执行流程:
//   1. 遇到 func() 调用
//   2. 创建 func 的执行上下文
//   3. 预处理:发现 var num,创建绑定 num → undefined
//   4. 执行函数体:
//      - console.log(num) → 输出 undefined
//      - var num = 100 → 赋值
//      - console.log(num) → 输出 100
//   5. 函数结束,EC 被销毁

console.log('');
console.log('');
// 空行分隔

// ───────────────────────────────────────────────
// 子案例 3:局部 var 提升 vs 全局变量同名(最重要!)
// ───────────────────────────────────────────────
// 这是变量提升最容易混淆的场景

// 全局作用域:
var count = 100;    // 全局变量 count,值为 100
var message = 'What'; // 全局变量 message,值为 'What'

function fn() {
    // ───────────────────────────────────────────────
    // fn 创建阶段(预处理):
    //   1. 扫描函数体,查找 var 声明
    //   2. 发现 var count,创建局部绑定:count → undefined
    //   3. 没有发现 var message(message = 'Where' 是赋值,不是声明)
    //   4. 局部 count 遮蔽(Shadow)了全局 count
    // ───────────────────────────────────────────────

    // ───────────────────────────────────────────────
    // fn 执行阶段:
    // ───────────────────────────────────────────────

    // 第一行代码:
    console.log(count);    // → undefined
    // 解释(关键!):
    //   1. fn 内部有局部 var count
    //   2. 局部 count 在创建阶段被声明并初始化为 undefined
    //   3. 局部 count 遮蔽了全局 count
    //   4. 读取 count 时,优先使用局部的 undefined
    //   5. 很多人误以为会输出 100(全局的值),这是错误的!

    // 第二行代码:
    console.log(message);  // → What
    // 解释:
    //   1. fn 内部没有 var message(没有声明)
    //   2. message 沿着作用域链向上查找
    //   3. 找到全局的 message = 'What'
    //   4. 输出 'What'

    // 第三行代码:
    var count = 200;
    // 执行流程:
    //   1. var count 的声明已在创建阶段处理
    //   2. 只执行赋值:count = 200
    //   3. 这是修改局部的 count,不影响全局的 count

    // 第四行代码:
    message = 'Where';
    // 执行流程:
    //   1. message = 'Where' 是赋值,没有 var
    //   2. fn 内部没有局部 message
    //   3. 沿作用域链找到全局的 message
    //   4. 修改全局的 message = 'Where'
    //   5. 全局 message 被改变了!

    // 第五行代码:
    console.log(count);    // → 200
    // 解释:局部的 count 现在是 200

    // 第六行代码:
    console.log(message);  // → Where
    // 解释:全局的 message 现在是 'Where'
}

// 调用函数:
fn();
// 执行流程总结:
//   - fn 内部创建了局部 count(遮蔽全局)
//   - fn 内部修改了全局 message(没有遮蔽)

// 函数结束后,回到全局作用域:
console.log(count);    // → 100
// 解释:
//   1. 全局的 count 是 100
//   2. fn 内部的局部 count 是独立的,不会影响全局
//   3. fn 结束后,局部 count 被销毁

console.log(message);  // → Where
// 解释:
//   1. fn 内部没有局部 message,修改的是全局的
//   2. 全局 message 被修改为 'Where'
//   3. 所以这里输出 'Where'

// ───────────────────────────────────────────────
// 子案例 3 的关键总结
// ───────────────────────────────────────────────
// 1. 局部 var 提升:局部变量会在函数调用时被声明并初始化为 undefined
// 2. 变量遮蔽(Shadowing):局部变量会遮蔽同名全局变量
// 3. 作用域链查找:没有局部声明时,会沿作用域链向上查找
// 4. 隐式全局:没有 var 的赋值会修改全局变量(容易出错)

📌 案例 12 代码解释说明(变量提升)

三个子案例的输出汇总

子案例 1(全局提升)

console.log(address)  → undefined   ← var 已提升,但未赋值
var address = '目标地址'             ← 此处才赋值
console.log(address)  → '目标地址'  ← 赋值后读取
age = 10                             ← 隐式全局(没有 var)
console.log(age)      → 10

子案例 2(函数内部提升)

进入 func() 时,预处理扫描函数体,发现 var num → num = undefined
console.log(num) → undefined   ← 提升了,但未赋值
var num = 100                   ← 此处才赋值
console.log(num) → 100         ← 赋值后读取

子案例 3(最复杂!局部提升遮蔽全局)

时机 count 的值 message 的值 说明
进入 fn() 全局 = 100 全局 = 'What' 全局变量初始值
fn() 创建阶段 局部 = undefined (无局部声明) 局部 count 被提升
fn() 第1行打印 undefined 'What' count 是局部的 undefined;message 来自全局
执行 var count = 200 局部 = 200 局部 count 被赋值
执行 message = 'Where' 全局 = 'Where' 无局部 message,修改的是全局
fn() 最后打印 200 'Where' 局部 count;全局 message 被改了
fn() 结束后 全局 = 100 全局 = 'Where' 全局 count 未变;全局 message 被改了

最容易犯的错误:看到函数内 console.log(count) 以为会输出全局的 100,其实因为函数内有 var count,局部提升遮蔽了全局,所以是 undefined

心智模型(引擎如何看这段代码):

// 你写的代码:
console.log(x);
var x = 5;
console.log(x);

// 引擎在执行时的「等效视角」(非真实操作,仅用于理解):
var x;           // 创建阶段:var 声明被提升,初始化为 undefined
console.log(x);  // → undefined
x = 5;           // 执行阶段:赋值(原 var x = 5 的赋值部分)
console.log(x);  // → 5

案例 13:函数提升(对应 05-变量提升/02-函数提升

// =============================================
// 案例目标:理解函数声明式提升 vs 表达式赋值提升的根本区别
// 核心概念:函数声明会被"完全提升"(包括函数体),函数表达式只提升变量声明
// =============================================

// ───────────────────────────────────────────────
// 理论背景:两种函数定义方式的区别
// ───────────────────────────────────────────────

// 方式 1:函数声明(Function Declaration)
//   语法:function 函数名() { ... }
//   提升:整个函数(包括函数体)被提升
//   特点:可以在声明前调用

// 方式 2:函数表达式(Function Expression)
//   语法:var 变量名 = function() { ... }
//   提升:只提升 var 变量声明(值为 undefined)
//   特点:不能在声明前调用

// ───────────────────────────────────────────────
// 步骤 1:函数声明式提升(完全提升)
// ───────────────────────────────────────────────

// 在代码最顶部,还没有看到 function fn01() {...} 的声明
// 但在预处理阶段,引擎已经处理了函数声明

// 创建阶段(预处理):
//   1. 扫描全局代码,发现 function fn01() {...}
//   2. 创建绑定:fn01 → 完整的函数对象
//   3. 函数对象包含:函数名、参数列表、函数体、作用域链等
//   4. 此时 fn01 已经是一个可调用的函数

// 执行阶段开始:
console.log(fn01);  // → ƒ fn01() { console.log('fn01'); }
// 解释:
//   1. 读取 fn01 变量
//   2. fn01 在创建阶段已被绑定到完整的函数对象
//   3. 输出函数的源代码(字符串表示)
//   4. 注意:不是 undefined,而是完整的函数!

// 尝试在声明前调用:
fn01();             // → fn01
// 解释:
//   1. fn01() 是调用表达式
//   2. fn01 的值是完整的函数对象,可以调用
//   3. 执行函数体:console.log('fn01')
//   4. 输出 "fn01"
//   5. 这就是"函数提升"的威力:可以先调用,后声明

// ───────────────────────────────────────────────
// 步骤 2:函数表达式提升(只提升变量)
// ───────────────────────────────────────────────

// 在代码顶部,还没有看到 var fn02 = function() {...} 的赋值
// 但在预处理阶段,引擎已经处理了 var 声明

// 创建阶段(预处理):
//   1. 扫描全局代码,发现 var fn02 = ...
//   2. 只处理 var fn02 部分(变量声明)
//   3. 创建绑定:fn02 → undefined
//   4. 注意:此时 fn02 的值是 undefined,不是函数对象!

// 执行阶段开始:
console.log(fn02);  // → undefined
// 解释:
//   1. 读取 fn02 变量
//   2. fn02 在创建阶段被声明并初始化为 undefined
//   3. 输出 undefined
//   4. 这与函数声明不同:函数声明此时已经是完整的函数对象

// 尝试在赋值前调用:
// fn02();          // ← 如果取消注释,会报错!
// 错误信息:TypeError: fn02 is not a function
// 解释:
//   1. fn02() 是调用表达式
//   2. fn02 的当前值是 undefined
//   3. undefined 不能作为函数调用
//   4. 引擎抛出 TypeError

// ───────────────────────────────────────────────
// 步骤 3:执行到实际的声明/赋值语句
// ───────────────────────────────────────────────

// ───────────────────────────────────────────────
// 函数声明:function fn01() { ... }
// ───────────────────────────────────────────────
// 引擎处理:
//   1. 遇到 function fn01() {...}
//   2. 检查:fn01 已在创建阶段被声明并绑定到函数对象
//   3. 引擎直接跳过这行(已经处理过了)
//   4. 不执行任何操作
function fn01() {
    console.log('fn01');
}

// ───────────────────────────────────────────────
// 函数表达式:var fn02 = function() { ... }
// ───────────────────────────────────────────────
// 引擎处理:
//   1. 遇到 var fn02 = function() {...}
//   2. var fn02 的声明已在创建阶段处理
//   3. 只执行赋值部分:fn02 = function() {...}
//   4. 创建匿名函数对象,赋给 fn02
//   5. 此时 fn02 的值从 undefined 变为函数对象
var fn02 = function() {
    console.log('fn02');
};

// ───────────────────────────────────────────────
// 步骤 4:赋值后使用
// ───────────────────────────────────────────────

console.log(fn01);  // → ƒ fn01() { console.log('fn01'); }
// 解释:fn01 一直是完整的函数对象

fn01();             // → fn01
// 解释:fn01 可以正常调用

console.log(fn02);  // → ƒ fn02() { console.log('fn02'); }
// 解释:
//   1. 现在 fn02 已经被赋值为函数对象
//   2. 输出函数的源代码

fn02();             // → fn02
// 解释:
//   1. fn02 现在是函数对象,可以调用
//   2. 执行函数体,输出 "fn02"

// ───────────────────────────────────────────────
// 理论总结:两种方式的提升差异
// ───────────────────────────────────────────────

// 对比表:
//
// | 特性                | 函数声明              | 函数表达式 (var)        |
// |---------------------|---------------------|----------------------|
// | 语法                | function fn() {}    | var fn = function() {} |
// | 提升内容            | 完整函数对象          | 只提升 var 变量        |
// | 创建阶段的值        | ƒ fn() {...}        | undefined            |
// | 声明前能否调用      | ✓ 可以              | ✗ 不行              |
// | 声明前的 typeof     | "function"          | "undefined"          |
// | 执行到声明行的行为  | 跳过(已处理)       | 执行赋值              |
// | 适用场景            | 工具函数、主逻辑     | 条件赋值、回调        |
// | 箭头函数            | 不适用              | 可以使用              |

// ───────────────────────────────────────────────
// 实际应用场景
// ───────────────────────────────────────────────

// 场景 1:工具函数(使用函数声明,可以后置)
// 优点:主逻辑在顶部,工具函数在底部,代码可读性更好
// 主逻辑在顶部:
var result = processData([1, 2, 3]);
console.log('结果:', result);

// 工具函数在底部(仍然可以调用,因为函数提升)
function processData(arr) {
    return arr.map(function(x) { return x * 2; });
}

// 场景 2:条件赋值(使用函数表达式)
// 优点:根据条件选择不同的实现
var strategy;
if (condition) {
    strategy = function() { return 'A'; };
} else {
    strategy = function() { return 'B'; };
}
strategy();

// 场景 3:回调函数(使用函数表达式)
// 优点:直接传递匿名函数,简洁
button.addEventListener('click', function() {
    console.log('按钮被点击');
});

// ⚠️ 注意:箭头函数也有提升规则
// 箭头函数本质是函数表达式,只提升变量声明,不提升函数值

// 示例:箭头函数提升
// console.log(arrowFn);  // → undefined
// arrowFn();             // → TypeError: arrowFn is not a function
var arrowFn = () => {
    console.log('箭头函数');
};
arrowFn();              // → 箭头函数

📌 案例 13 代码解释说明(函数提升)

两种方式在不同阶段的值对比

时机 fn01(函数声明) fn02(函数表达式)
创建阶段(预处理) ƒ fn01() {...} ✅ 完整函数 undefined ⚠️ 只有声明
声明前 console.log 打印函数体 打印 undefined
声明前 调用() ✅ 正常执行 ❌ TypeError
执行到声明行 引擎跳过(已处理) 赋值:fn02 = function(){}
声明后 console.log 打印函数体 打印函数体
声明后 调用() ✅ 正常执行 ✅ 正常执行

引擎"等效视角"对比

// 你写的(函数声明):        引擎看到的等效:
fn01();                       function fn01() {...}  // 创建阶段完全提升
function fn01() { ... }fn01();                // 执行

// 你写的(函数表达式):       引擎看到的等效:
fn02();                       var fn02;              // 创建阶段:只提升声明
var fn02 = function(){...}fn02();               // ❌ TypeError(undefined不能调用)
                              fn02 = function(){};   // 执行阶段才赋值

选择建议

  • 需要在定义前使用 → 用函数声明
  • 根据条件决定函数内容 → 用函数表达式
  • 箭头函数 → 始终用函数表达式(没有函数声明形式)

案例 14:同名 var 与 function(陷阱案例)(对应 05-变量提升/03-var和function

// =============================================
// 案例目标:理解 var 与 function 同名时的提升覆盖规则
// =============================================

// 创建阶段:
//   - 发现 var address → 创建绑定
//   - 发现 function address → 同名,function 优先,绑定覆盖为完整函数值
//   最终创建阶段结束时:address = function address() {...}

// 此时 address 是函数(创建阶段 function 优先)
console.log(address);  // → ƒ address() {...}(完整函数对象)
address();             // → 输出 "我是函数 address"

// 执行阶段第一步:执行 var address = '目标城市'
// 这是一个赋值语句,将 address 从函数值更改为字符串 '目标城市'
var address = '目标城市';

// 执行阶段第二步:遇到 function address(){...} 声明 → 直接跳过(创建阶段已处理)
function address() {
    console.log('我是函数 address');
}

// 此时 address 已被赋值为字符串
console.log(address);  // → '目标城市'(字符串)
// address();          // ← TypeError: address is not a function(字符串不能调用)

📌 案例 14 代码解释说明(同名 var + function 陷阱)

整个执行过程的时间轴

── 创建阶段(预处理)──────────────────────────────
扫描到 var address          → 创建绑定 address
扫描到 function address(){} → 同名!用函数值覆盖
最终创建阶段结束:address = ƒ address(){...}

── 执行阶段(逐行)────────────────────────────────
第1行:console.log(address)  → ƒ address(){...}  ← 函数优先,此时仍是函数
第2行:address()             → '我是函数 address' ← 函数可以调用
第3行:var address = '目标城市'  → 赋值!address 变为字符串 '目标城市'
第4行:function address(){...}  → 引擎跳过(创建阶段已处理)
第5行:console.log(address)  → '目标城市'         ← 字符串
第6行:address()             → ❌ TypeError        ← 字符串不能调用

关键规则记忆

创建阶段:function 优先 > var(function 赢)
执行阶段:var 赋值语句会覆盖函数值(var 赋值后赢)

口诀:提升时函数赢,赋值后变量赢

三种声明提升优先级排列

优先级 声明类型 创建阶段初始值
🥇 最高 function foo(){} 完整函数对象
🥈 中 var foo undefined(但若同名function存在,共用绑定)
🥉 无提升 let/const TDZ(暂时性死区)

⚠️ 工程建议:永远避免变量名与函数名同名,这会产生难以追踪的 Bug。

提升优先级规则图:

同名 function + var address

创建阶段

function 优先
address = 完整函数值

var address 与 function address
共用同一绑定(不重复创建)

执行阶段

执行 var address = '目标城市'
→ 赋值覆盖,address 变为字符串

执行到 function address(){}
→ 直接跳过(已处理)

结果:address 是字符串
调用会报 TypeError ⚠

8.3 经典应用场景

场景 1:函数声明式提升的合理利用——工具函数后置

// 利用函数提升,可以把主逻辑写在顶部,工具函数定义在底部
// 这让代码读者先看到「做了什么」,再看「怎么实现的」

// 主逻辑在顶部(清晰)
var result = processData([1, 2, 3, 4, 5]);
console.log('求和:', result.sum);
console.log('平均:', result.avg);
console.log('最大:', result.max);

// 工具函数在底部(实现细节)
function processData(arr) {
    return {
        sum: sumArr(arr),       // sumArr 虽然定义在后面,但因为函数提升,这里可以使用
        avg: avgArr(arr),
        max: maxArr(arr)
    };
}

function sumArr(arr) {
    return arr.reduce(function(a, b) { return a + b; }, 0);
}

function avgArr(arr) {
    return sumArr(arr) / arr.length;
}

function maxArr(arr) {
    return Math.max.apply(null, arr);
}

场景 2:利用提升理解代码块条件函数声明(陷阱)

// ⚠ 不同引擎对「块内 function 声明」的提升行为不一致,要避免此写法
// 严格模式下,if/for 中的 function 声明只在块内有效

// ❌ 危险写法(非严格模式,行为因引擎而异)
if (true) {
    function maybeHoisted() { return 'yes'; }
}

// ✅ 安全写法:块内用 let + 函数表达式
if (true) {
    let safeFunc = function() { return 'yes'; };
}

场景 3:理解 TDZ(暂时性死区)——let/const 的提升

// let/const 也有提升(创建阶段也会登记),但不会初始化
// 在声明语句之前的区域叫「暂时性死区(Temporal Dead Zone, TDZ)」
// 访问 TDZ 中的 let/const 会抛出 ReferenceError(与 var 的 undefined 完全不同)

console.log(typeof x); // → "undefined"(var 提升,可安全检查类型)
var x = 10;

// console.log(typeof y); // → ReferenceError!(let 有 TDZ,typeof 也不安全)
let y = 20;

// 工程价值:TDZ 能帮助提前发现「声明前使用」的错误,而不是默默返回 undefined

8.4 完整可运行示例(含 CSS display: gridplace-itemsradial-gradient

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>变量提升与函数提升演示</title>
  <style>
    * { box-sizing: border-box; }
    body {
      margin: 0;
      min-height: 100vh;
      /*
       * display: grid + place-items: center
       * 作用:网格布局将唯一子项在水平和垂直两个轴方向居中
       * 真实产品:登录页表单居中(Vercel/GitHub/Stripe 等所有 SaaS 登录页)
       *           404 页面居中提示;Empty State 空状态页
       * 对比 flex:place-items 是 align-items + justify-items 的简写,grid 专属
       */
      display: grid;
      place-items: center;
      /*
       * radial-gradient 径向渐变
       * 作用:从指定位置向外发散的渐变效果
       * 真实产品:Vercel/Linear 深色官网背景;Stripe 产品页 Hero 区渐变光效
       */
      background: radial-gradient(circle at 20% 20%, #312e81, #020617 55%);
      color: #e0e7ff;
      font-family: system-ui, sans-serif;
      padding: 24px;
    }
    .shell {
      width: min(94vw, 680px);
      padding: 28px;
      /*
       * border-radius: 16px
       * 作用:圆角使卡片/容器视觉柔和,符合现代设计语言
       * 真实产品:所有现代 UI 框架(MUI/Ant Design/Chakra)的弹窗、卡片
       * 取值规律:模态框 12-16px;按钮 6-8px;头像 50%(正圆)
       */
      border-radius: 16px;
      background: rgba(15, 23, 42, 0.88);
      border: 1px solid rgba(129, 140, 248, 0.4);
      /*
       * box-shadow 多层阴影
       * 作用:叠加多组阴影,同时实现「发光轮廓」和「深度投影」两种效果
       * 真实产品:Material Design 的 elevation 层级系统
       *           弹窗的背景遮挡阴影;代码编辑器主题预览页的终端窗口
       */
      box-shadow:
        0 0 0 1px rgba(99,102,241,0.15),
        0 25px 80px rgba(0,0,0,0.5),
        0 4px 16px rgba(99,102,241,0.15);
    }
    h1 { font-size: 1.2rem; color: #a5b4fc; margin: 0 0 16px; }
    pre {
      background: #030712; border-radius: 8px; padding: 16px;
      font-family: ui-monospace, "SF Mono", Menlo, monospace;
      font-size: 12px; line-height: 1.65; color: #86efac;
      overflow-x: auto; white-space: pre-wrap; margin: 0;
    }
    .label { font-size:11px; color:#818cf8; margin:16px 0 6px; text-transform:uppercase; letter-spacing:.08em; }
  </style>
</head>
<body>
  <div class="shell">
    <h1>Hoisting 提升 — 运行日志</h1>
    <div class="label">① 变量提升(var)</div>
    <pre id="out-var"></pre>
    <div class="label">② 函数提升(声明式 vs 表达式)</div>
    <pre id="out-fn"></pre>
    <div class="label">③ 同名 var + function 覆盖顺序</div>
    <pre id="out-both"></pre>
  </div>
  <script>
    function log(id, msg) { document.getElementById(id).textContent += msg + '\n'; }

    /* ① 变量提升 */
    (function() {
      var type = (typeof city === 'undefined') ? 'undefined ✓ (var 已提升)' : city;
      log('out-var', 'var city 声明前 typeof city: ' + type);
      var city = '晴空城';
      log('out-var', '赋值后 city = "' + city + '"');
      log('out-var', '→ 创建阶段 var 登记为 undefined,执行阶段才赋值');
    })();

    /* ② 函数提升 */
    (function() {
      log('out-fn', '声明前 typeof fn01: ' + typeof fn01 + ' ← function(函数提升✓)');
      fn01(); // 调用成功(函数提升,创建阶段已绑定完整函数值)

      log('out-fn', '声明前 typeof fn02: ' + typeof fn02 + ' ← undefined(仅 var 提升)');
      // fn02(); // ← 这里调用:TypeError: fn02 is not a function

      function fn01() { log('out-fn', '  fn01 执行了!(声明前调用成功)'); }
      var fn02 = function() { log('out-fn', '  fn02 执行了!'); };

      log('out-fn', '声明后 typeof fn02: ' + typeof fn02 + '(现在有函数值了)');
      fn02();
    })();

    /* ③ 同名 var + function */
    (function() {
      var isFunc = typeof label === 'function';
      log('out-both', '声明前 typeof label: ' + typeof label
          + (isFunc ? ' ← 函数(function 优先于 var 提升)✓' : ''));
      var label = '字符串值'; // 赋值覆盖
      function label() { return '函数返回值'; } // 执行到此:跳过
      log('out-both', '赋值后 typeof label: ' + typeof label + ' ← 字符串覆盖了函数 ⚠');
      log('out-both', 'label 的值: "' + label + '"');
      log('out-both', '→ 此后 label() 会报 TypeError,这是典型的同名陷阱!');
    })();
  </script>
</body>
</html>

在这里插入图片描述

8.5 本节知识总结

三种声明的提升规则对比:

声明方式 创建阶段 执行到声明行 使用时机
var x 创建绑定,初始化 undefined 赋值(更新绑定值) 声明前读取 → undefined
function foo(){} 创建绑定,初始化完整函数值 跳过 声明前调用 → 正常执行 ✓
var fn = function(){} 创建绑定(fn),初始化 undefined 赋值(绑定函数值) 声明前调用 → TypeError
let/const 创建绑定,不初始化(TDZ) 初始化并赋值 声明前访问 → ReferenceError

同名 function + var 优先级:

  • 创建阶段:function 值优先(共用同一绑定)
  • 执行阶段:var = value 赋值覆盖函数值

九、算法综合案例(全部课堂案例详解)

9.1 翻转字符串(课堂案例 01、02)

思路分析: 字符串可以用下标访问(类数组),从最后一个下标向前遍历,依次拼接到新字符串。

时间复杂度: O(n),n 为字符串长度

// =============================================
// 案例 1:基础版(不封装,直观理解)
// =============================================
var msg = 'Hello, World!';
var newMsg = '';

// 从最后一个下标(msg.length-1)倒序遍历到 0
for (var i = msg.length - 1; i >= 0; i--) {
    newMsg += msg[i];  // 每次把当前字符追加到新字符串末尾
}
console.log('原字符串:', msg);    // Hello, World!
console.log('翻转结果:', newMsg); // !dlroW ,olleH

// =============================================
// 案例 2:封装版(可复用,有 JSDoc 注释)
// =============================================
/**
 * 翻转字符串
 * @param {string} str - 要翻转的字符串
 * @returns {string} 翻转后的新字符串(不修改原字符串)
 */
function reverseString(str) {
    var newStr = '';
    // 倒序遍历:从最后一个字符到第一个字符
    for (var i = str.length - 1; i >= 0; i--) {
        newStr += str[i];  // 将当前字符追加到结果字符串
    }
    return newStr;  // 返回翻转好的字符串
}

// 测试多种输入
console.log(reverseString('stressed'));     // → desserts
console.log(reverseString('racecar'));      // → racecar(回文:翻转后等于自身)
console.log(reverseString('Hello World')); // → dlroW olleH
console.log(reverseString(''));             // → ''(空字符串翻转还是空字符串)

// 扩展:现代 JS 写法(理解即可)
// 先 split('') 变成数组,再 reverse() 翻转数组,再 join('') 拼回字符串
console.log('stressed'.split('').reverse().join('')); // → desserts

📌 翻转字符串代码解释说明

核心算法思路(倒序遍历拼接):

原字符串:H e l l o
下标:     0 1 2 3 4

i=4: newStr = '' + 'o' = 'o'
i=3: newStr = 'o' + 'l' = 'ol'
i=2: newStr = 'ol' + 'l' = 'oll'
i=1: newStr = 'oll' + 'e' = 'olle'
i=0: newStr = 'olle' + 'H' = 'olleH'

结果:'olleH'

两种实现方式对比

方式 代码 时间复杂度 可读性 适合场景
for 循环(基础版) for(i=str.length-1; i>=0; i--) O(n) 面试手写、初学者
封装函数版 function reverseString(str){...} O(n) 工程复用
数组方法链 split('').reverse().join('') O(n) 最高 日常开发

关键知识点

  • 字符串的 length 属性返回字符数
  • 字符串可以用 [index] 访问单个字符(类数组)
  • str.length - 1 是最后一个字符的下标
  • 循环条件 i >= 0,不能写成 i > 0(否则漏掉下标0的字符)
  • 回文字符串(palindrome)翻转后等于自身,如 'racecar'

9.2 水仙花数(课堂案例 03)

定义: 三位数,每位数字的三次方之和等于该数本身。例:153 = 1³ + 5³ + 3³ = 1 + 125 + 27 = 153 ✓

四个水仙花数: 153、370、371、407

// =============================================
// 方法 1:朴素循环(课堂原版)
// =============================================
for (var num = 100; num <= 999; num++) {
    // 分解三位数的各位数字
    var n1 = parseInt(num / 100);      // 百位:整除100取整数部分
    var n2 = parseInt(num % 100 / 10); // 十位:先取后两位,再整除10
    var n3 = num % 10;                 // 个位:取余10

    // 判断三次方之和是否等于自身
    if (n1*n1*n1 + n2*n2*n2 + n3*n3*n3 === num) {
        console.log(num);  // 输出:153 370 371 407
    }
}

// =============================================
// 方法 2:封装函数(推荐——便于测试和复用)
// =============================================
/**
 * 判断一个三位数是否是水仙花数
 * @param {number} n - 要判断的三位数
 * @returns {boolean}
 */
function isNarcissistic(n) {
    var n1 = Math.floor(n / 100);       // Math.floor 比 parseInt 语义更清晰
    var n2 = Math.floor(n % 100 / 10);
    var n3 = n % 10;
    return n1**3 + n2**3 + n3**3 === n; // ES7 的幂运算符,等价于 Math.pow(n1, 3)
}

// 筛选出所有水仙花数
var narcissisticNums = [];
for (var i = 100; i <= 999; i++) {
    if (isNarcissistic(i)) {
        narcissisticNums.push(i);
    }
}
console.log('水仙花数:', narcissisticNums.join(', ')); // 153, 370, 371, 407

// 手动验证 153
console.log('验证 153: 1³+5³+3³ =', 1 + 125 + 27, '=== 153?', 1 + 125 + 27 === 153);

📌 水仙花数代码解释说明

位数分解核心计算(以 num = 753 为例):

n1 = parseInt(753 / 100)      = parseInt(7.53) = 7   ← 百位
n2 = parseInt(753 % 100 / 10) = parseInt(53 / 10)
                              = parseInt(5.3)  = 5   ← 十位
n3 = 753 % 10                 = 3                    ← 个位

验证:7³ + 5³ + 3³ = 343 + 125 + 27 = 495 ≠ 753 → 不是水仙花数

四个水仙花数验证

百位³ 十位³ 个位³ 总和 是否相等
153 1³=1 5³=125 3³=27 153
370 3³=27 7³=343 0³=0 370
371 3³=27 7³=343 1³=1 371
407 4³=64 0³=0 7³=343 407

关键运算符解析

  • parseInt(num / 100):整除100取整数部分 → 百位
  • num % 100:取最后两位数(如 753 % 100 = 53)
  • parseInt(num % 100 / 10):最后两位再整除10 → 十位
  • num % 10:取余10 → 个位

n³ 的三种写法(效果相同):

n * n * n       // ES5,最基础
Math.pow(n, 3)  // ES5,语义清晰
n ** 3          // ES7,简洁

9.3 质数判断与区间筛选(课堂案例 04、05、06)

质数(素数)定义: 大于 1 的正整数,且只能被 1 和自身整除。如:2、3、5、7、11、13…

// =============================================
// 方法 1:基础判断(不封装,单个数字)
// 课堂案例:用 prompt 获取用户输入
// =============================================
var num = +prompt('请输入一个数字:');  // + 号将字符串转数字

var isPrime = true;  // 标记:默认假设是质数

// 只需检验 2 到 num/2 之间的因子(优化:试除到 num/2 而非 num-1)
for (var i = 2; i <= num / 2; i++) {
    if (num % i === 0) {   // 如果能被 i 整除,则 i 是 num 的因子
        isPrime = false;   // 推翻质数假设
        break;             // 找到一个因子就够了,立即跳出循环(优化:提前终止)
    }
}
alert(isPrime ? num + ' 是质数!' : num + ' 不是质数!');

// =============================================
// 方法 2:不封装函数,直接输出 100-200 的质数
// =============================================
for (var num = 100; num <= 200; num++) {
    var isPrime = true;   // 每次循环重置标记

    for (var i = 2; i <= num / 2; i++) {
        if (num % i === 0) {
            isPrime = false;
            break;
        }
    }
    if (isPrime) {
        console.log(num);  // 打印质数
    }
}

// =============================================
// 方法 3:封装版(推荐——返回 boolean,可复用)
// =============================================
/**
 * 判断一个整数是否是质数
 * @param {number} num - 要判断的整数
 * @returns {boolean} true=质数,false=非质数
 */
function isPrimeNum(num) {
    if (num < 2) return false;        // 小于 2 的数不是质数
    for (var i = 2; i <= num / 2; i++) {
        if (num % i === 0) return false; // 找到因子,不是质数
    }
    return true;  // 遍历完没找到因子,是质数
}

// 使用封装函数筛选 100-200 的质数
var primes = [];
for (var n = 100; n <= 200; n++) {
    if (isPrimeNum(n)) primes.push(n);
}
console.log('100-200 的质数(' + primes.length + '个):', primes.join(', '));

// =============================================
// 进阶优化:试除到 sqrt(n) 即可(数学原理:因子总是成对出现)
// =============================================
function isPrimeFast(n) {
    if (n < 2) return false;
    if (n === 2) return true;           // 2 是唯一的偶数质数
    if (n % 2 === 0) return false;      // 偶数(除 2 外)直接排除
    for (var i = 3; i <= Math.sqrt(n); i += 2) {  // 只试除奇数因子
        if (n % i === 0) return false;
    }
    return true;
}
// 对于大数,isPrimeFast 比 isPrimeNum 快很多
console.log('isPrimeFast(997):', isPrimeFast(997)); // → true(997 是质数)

📌 质数判断代码解释说明

核心算法原理(标记法)

假设 num 是质数(isPrime = true)
      ↓
循环 i 从 2 到 num/2:
  如果 num % i === 0(i 能整除 num)
    → 推翻假设:isPrime = false
    → break(提前退出,无需继续)
      ↓
循环结束后,isPrime 还是 true?
  是 → num 是质数
  否 → num 不是质数

为什么只需循环到 num/2 而不是 num-1

若 i 是 num 的因子,那么 num/i 也是因子
因子总是成对出现,较小的那个一定 ≤ √num ≤ num/2

例:num = 36
  因子对:(2,18), (3,12), (4,9), (6,6)
  较小因子:2, 3, 4, 6 — 都 ≤ 6(= 36/6 ≈ √36)
  所以只需检测到 num/2(甚至只需到 √num,更快)

三种实现方式的效率对比(以判断 1000 为例):

方式 循环次数 效率
循环到 num-1 ~999 次 最慢
循环到 num/2 ~499 次 中等 ✅ 课堂版本
循环到 √num(进阶) ~31 次 最快

break 的重要性:找到一个因子就可以确定不是质数,立刻 break 跳出循环,避免无谓的后续计算。

100-200 之间的质数(共 21 个):

101 103 107 109 113 127 131 137 139 149
151 157 163 167 173 179 181 191 193 197 199

9.4 控制台输出等腰三角形(课堂案例 07)

规律分析(rows 为总行数):

行号 n 前导空格数 星号数
1 rows-1 1(= 2×1-1)
2 rows-2 3(= 2×2-1)
3 rows-3 5(= 2×3-1)
n rows-n 2n-1
var rows = 15;  // 总行数(可修改)

// 外层循环:控制行(n 从 1 到 rows)
for (var n = 1; n <= rows; n++) {
    var str = '';

    // 内层循环 1:拼接前导空格(使三角形居中对齐)
    // 第 n 行需要 rows-n 个空格
    for (var i = 0; i < rows - n; i++) {
        str += ' ';  // 追加一个空格
    }

    // 内层循环 2:拼接星号
    // 第 n 行需要 2n-1 个星号(等差数列:1, 3, 5, 7, ...)
    for (var i = 0; i < 2 * n - 1; i++) {
        str += '*';  // 追加一个星号
    }

    // 输出当前行(console.log 自动换行)
    console.log(str);
}
/*
 * rows=5 时输出:
 *     *
 *    ***
 *   *****
 *  *******
 * *********
 */

📌 等腰三角形代码解释说明

规律公式推导(总行数 rows = 5):

行号 n 前导空格数 rows-n 星号数 2n-1 该行字符串
1 5-1=4 个空格 2×1-1=1 个 ★ *
2 5-2=3 个空格 2×2-1=3 个 ★ ***
3 5-3=2 个空格 2×3-1=5 个 ★ *****
4 5-4=1 个空格 2×4-1=7 个 ★ *******
5 5-5=0 个空格 2×5-1=9 个 ★ *********

双重循环的执行结构

外层循环(n = 1 to rows):控制行
  内层循环1(i = 0 to rows-n):拼接前导空格
  内层循环2(i = 0 to 2n-1):拼接星号
  console.log(str):输出当前行

代码要点

  • 每次进入外层循环都要把 str 重置为 ''(否则多行内容会累加)
  • 两个内层循环共用变量名 i,不会冲突(各自独立的计数器)
  • console.log 自动换行,不需要手动加 \n

9.5 数字阵列输出到页面(课堂案例 08)

// =============================================
// 目标:页面输出 01-50 的数字阵列,每行 10 个
// =============================================
for (var i = 1; i <= 50; i++) {
    // 个位数前面补零(三元表达式):i < 10 ? '0'+i : i
    var num = i < 10 ? '0' + i : i;

    document.write(num + ' ');  // 输出当前数字和一个空格

    // 每 10 个数字换行(10 的倍数时输出 <br>)
    if (i % 10 === 0) {
        document.write('<br>');
    }
}
/*
 * 输出效果:
 * 01 02 03 04 05 06 07 08 09 10
 * 11 12 13 14 15 16 17 18 19 20
 * 21 22 23 24 25 26 27 28 29 30
 * 31 32 33 34 35 36 37 38 39 40
 * 41 42 43 44 45 46 47 48 49 50
 */

📌 数字阵列代码解释说明

关键技巧逐行拆解

var num = i < 10 ? '0' + i : i;
  • i < 10 → 个位数,前面补零:'0' + 1 = '01''0' + 9 = '09'
  • i >= 10 → 两位数,不补零:101150
  • 三元运算符 条件 ? 真值 : 假值 简洁替代 if-else
if (i % 10 === 0) {
    document.write('<br>');
}
  • i % 10 === 0:i 是10的倍数时换行(第10、20、30、40、50个数后换行)
  • document.write('<br>'):向页面写入换行标签

逐行执行轨迹(前几步)

i=1:  num='01', 输出'01 ', 1%10≠0 不换行
i=2:  num='02', 输出'02 ', 2%10≠0 不换行
...
i=9:  num='09', 输出'09 ', 9%10≠0 不换行
i=10: num=10,   输出'10 ', 10%10=0 → 换行
i=11: num=11,   输出'11 ', 11%10≠0 不换行
...

三种补零方式对比

i < 10 ? '0' + i : i           // 三元运算符(课堂版)
String(i).padStart(2, '0')     // ES6 padStart(现代写法)
('0' + i).slice(-2)            // slice 取后两位(老写法)

9.6 综合算法可运行页面(含 CSS Grid + 图片)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>算法案例综合演示</title>
  <style>
    * { box-sizing: border-box; }
    body { margin:0; font-family:system-ui,sans-serif; background:#f1f5f9; color:#0f172a; padding:24px; }
    h1 { font-size:1.5rem; margin-bottom:16px; }
    .intro { display:flex; align-items:center; gap:16px; margin-bottom:24px; }
    .intro img { border-radius:8px; }
    /*
     * display: grid + repeat(auto-fit, minmax(260px, 1fr))
     * 作用:响应式网格,不写媒体查询就能自动适配宽度
     * 真实产品:电商商品列表、图片画廊、仪表盘统计卡片、博客文章列表
     * 原理:auto-fit 自动填充列;minmax 确保每列不小于 260px;1fr 等分剩余空间
     */
    .grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
      gap: 20px;
    }
    .section {
      background: #fff; border-radius: 12px; padding: 20px;
      box-shadow: 0 4px 20px rgba(15,23,42,0.07);
      border: 1px solid #e2e8f0;
    }
    .section h2 { font-size:1rem; margin:0 0 12px; color:#4338ca; border-bottom:2px solid #e0e7ff; padding-bottom:8px; }
    pre {
      margin:0; white-space:pre-wrap; font-size:12px; line-height:1.6;
      font-family:ui-monospace,monospace; background:#f8fafc;
      border-radius:6px; padding:10px; color:#334155;
      max-height:200px; overflow-y:auto;
    }
    .matrix { letter-spacing:.1em; }
    .tri    { letter-spacing:.15em; }
  </style>
</head>
<body>
  <h1>算法案例综合展示</h1>
  <div class="intro">
    <img src="images/figure-demo.svg" width="100" height="70" alt="示意图" />
    <p style="color:#64748b;font-size:14px;margin:0;">以下算法均用纯 JS 实现,输出结果实时计算。</p>
  </div>
  <div class="grid">
    <div class="section"><h2>① 翻转字符串</h2><pre id="rev"></pre></div>
    <div class="section"><h2>② 水仙花数(100–999)</h2><pre id="narc"></pre></div>
    <div class="section"><h2>③ 100–200 质数</h2><pre id="prime"></pre></div>
    <div class="section tri"><h2>④ 等腰三角形(10 行)</h2><pre id="tri"></pre></div>
    <div class="section matrix"><h2>⑤ 数字阵列(1–50)</h2><pre id="mat"></pre></div>
    <div class="section"><h2>⑥ 九九乘法表</h2><pre id="mul"></pre></div>
  </div>
  <script>
    /* ① 翻转字符串 */
    function revStr(s) { var r=''; for(var i=s.length-1;i>=0;i--) r+=s[i]; return r; }
    var revEl = document.getElementById('rev');
    ['Hello World','racecar','stressed','algorithm'].forEach(function(s) {
      revEl.textContent += '"'+s+'" → "'+revStr(s)+'"\n';
    });

    /* ② 水仙花数 */
    var narcs = [];
    for (var n = 100; n <= 999; n++) {
      var a=Math.floor(n/100), b=Math.floor(n%100/10), c=n%10;
      if (a**3+b**3+c**3===n) narcs.push(n);
    }
    document.getElementById('narc').textContent =
      '共 '+narcs.length+' 个:'+narcs.join(', ')+'\n\n'
      +'验证 153:1³+5³+3³='+(1+125+27);

    /* ③ 质数 */
    function isPrime(n) {
      if(n<2) return false;
      for(var i=2;i<=n/2;i++) if(n%i===0) return false;
      return true;
    }
    var primes = [];
    for (var p=100;p<=200;p++) if(isPrime(p)) primes.push(p);
    document.getElementById('prime').textContent = '共 '+primes.length+' 个:\n'+primes.join(' ');

    /* ④ 三角形 */
    var rows=10, lines=[];
    for (var i=1;i<=rows;i++) {
      var line='';
      for(var s=0;s<rows-i;s++) line+=' ';
      for(var t=0;t<2*i-1;t++) line+='*';
      lines.push(line);
    }
    document.getElementById('tri').textContent = lines.join('\n');

    /* ⑤ 数字阵列 */
    var buf='';
    for(var i=1;i<=50;i++) {
      buf += (i<10?'0'+i:String(i)) + (i%10===0?'\n':' ');
    }
    document.getElementById('mat').textContent = buf.trim();

    /* ⑥ 九九乘法表 */
    var mul='';
    for(var i=1;i<=9;i++) {
      for(var j=1;j<=i;j++) mul+=j+'×'+i+'='+(i*j)+(j<i?' ':'');
      mul+='\n';
    }
    document.getElementById('mul').textContent = mul;
  </script>
</body>
</html>

在这里插入图片描述


十、强化练习题(全部九题)

10.1 练习清单(来自配套练习文档)

  1. while 循环写出九九乘法表的四种形态(左下角、右下角、左上角、右上角三角)
  2. 打印 100–200 中能被 3 或 7 整除的数
  3. 打印三位数(100–999)中,数位上含 3 或 7 的数
  4. 计算 100 的阶乘1 × 2 × 3 × … × 100
  5. 1! + 2! + 3! + … + 20! 的值
  6. 求 100–999 之间的水仙花数(见第九章)
  7. 输出 100–200 之间所有的质数(见第九章)
  8. 控制台用 * 打印等腰三角形(见第九章)
  9. 页面输出 01–50 的矩阵阵列(见第九章)

10.2 四种九九乘法表(完整可运行示例)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>九九乘法表 — 四种形态</title>
  <style>
    * { box-sizing: border-box; }
    body { font-family:system-ui,sans-serif; background:#0f172a; color:#f1f5f9; margin:0; padding:24px; }
    h1 { font-size:1.3rem; color:#a5b4fc; margin-bottom:20px; }
    .grid4 {
      display:grid;
      grid-template-columns:repeat(auto-fit, minmax(300px, 1fr));
      gap:16px;
    }
    .card { background:#1e293b; border-radius:12px; padding:16px; border:1px solid #334155; }
    .card h2 { font-size:.9rem; color:#818cf8; margin:0 0 10px; text-transform:uppercase; letter-spacing:.06em; }
    pre { font-family:ui-monospace,monospace; font-size:11px; line-height:1.65; color:#86efac; margin:0; white-space:pre; overflow-x:auto; }
  </style>
</head>
<body>
  <h1>九九乘法表 — 四种形态(while 循环)</h1>
  <div class="grid4">
    <div class="card"><h2>① 左下三角(1×1 到 9×9)</h2><pre id="t1"></pre></div>
    <div class="card"><h2>② 右下三角(j 从 i 到 9)</h2><pre id="t2"></pre></div>
    <div class="card"><h2>③ 左上三角(i 从 9 到 1)</h2><pre id="t3"></pre></div>
    <div class="card"><h2>④ 右上三角(i 从 9 到 1,j 从 i 到 9)</h2><pre id="t4"></pre></div>
  </div>
  <script>
    /* ① 左下三角:j 从 1 到 i(标准九九乘法表) */
    var s1 = '', i = 1;
    while (i <= 9) {
      var j = 1;
      while (j <= i) {
        // 格式化:确保等号后的数字对齐(乘积最多两位数)
        s1 += j + 'x' + i + '=' + (i*j) + (j < i ? '  ' : '');
        j++;
      }
      s1 += '\n';
      i++;
    }
    document.getElementById('t1').textContent = s1;

    /* ② 右下三角:j 从 i 到 9(每行从对角线开始) */
    var s2 = '';
    for (var i2 = 1; i2 <= 9; i2++) {
      // 前面补空格,让每行的起始位置对齐对角线
      var pad = '';
      for (var k = 1; k < i2; k++) pad += '        '; // 每个格子约 8 字符宽
      s2 += pad;
      for (var j2 = i2; j2 <= 9; j2++) {
        s2 += i2 + 'x' + j2 + '=' + (i2*j2) + (j2 < 9 ? '  ' : '');
      }
      s2 += '\n';
    }
    document.getElementById('t2').textContent = s2;

    /* ③ 左上三角:i 从 9 到 1(标准表倒序) */
    var s3 = '';
    for (var i3 = 9; i3 >= 1; i3--) {
      for (var j3 = 1; j3 <= i3; j3++) {
        s3 += j3 + 'x' + i3 + '=' + (i3*j3) + (j3 < i3 ? '  ' : '');
      }
      s3 += '\n';
    }
    document.getElementById('t3').textContent = s3;

    /* ④ 右上三角:i 从 9 到 1,j 从 i 到 9 */
    var s4 = '';
    for (var i4 = 9; i4 >= 1; i4--) {
      var pad4 = '';
      for (var k4 = 1; k4 < i4; k4++) pad4 += '        ';
      s4 += pad4;
      for (var j4 = i4; j4 <= 9; j4++) {
        s4 += i4 + 'x' + j4 + '=' + (i4*j4) + (j4 < 9 ? '  ' : '');
      }
      s4 += '\n';
    }
    document.getElementById('t4').textContent = s4;
  </script>
</body>
</html>

在这里插入图片描述

📌 10.2 九九乘法表代码解释说明

四种形态的规律推导:

形态 外层循环 内层循环 对齐方式 视觉效果
① 左下三角 i: 1→9 j: 1→i 无需补空格 标准九九乘法表(教科书形式)
② 右下三角 i: 1→9 j: i→9 前置补空格((i-1)*8 字符) 每行从对角线位置开始
③ 左上三角 i: 9→1 j: 1→i 无需补空格 标准表倒序输出
④ 右上三角 i: 9→1 j: i→9 前置补空格 右上角形态,两种变换叠加

嵌套循环的核心逻辑:

外层控制「行」(i):决定输出多少行
内层控制「列」(j):决定每行输出多少个等式
关键规律:j 的起点和终点与 i 的关系决定了三角形状
  - j 从 1 到 i → 左三角(每行等式数递增)
  - j 从 i 到 9 → 右三角(每行等式数递减,但从对角线位置开始)

技术要点:

  • while 循环改写为 for 循环:逻辑等价,for 更简洁(初始化/条件/步进三合一)
  • 前置空格补齐(pad):用字符串拼接模拟表格对齐,确保视觉上的矩形对称
  • textContent vs innerHTML:纯文本用 textContent(安全,不解析 HTML 标签)

10.3 其余练习参考解答(完整注释版)

/* ─── 练习 2:100-200 中被 3 或 7 整除 ─── */
var results2 = [];
for (var i = 100; i <= 200; i++) {
    // % 是取余运算符;=== 0 表示能整除;|| 表示"或"(满足其一即可)
    if (i % 3 === 0 || i % 7 === 0) {
        results2.push(i);
    }
}
console.log('练习2:', results2.join(' '));

/* ─── 练习 3:三位数中数位含 3 或 7 ─── */
var results3 = [];
for (var n = 100; n <= 999; n++) {
    var a = Math.floor(n / 100);       // 百位
    var b = Math.floor(n % 100 / 10); // 十位
    var c = n % 10;                    // 个位
    // 任意一位等于 3 或 7
    if (a===3||a===7 || b===3||b===7 || c===3||c===7) {
        results3.push(n);
    }
}
console.log('练习3: 共', results3.length, '个,首5个:', results3.slice(0,5).join(','));

/* ─── 练习 4:100 的阶乘(BigInt 精确计算) ─── */
// JavaScript 的 Number 精度有限(53 位整数),100! 是一个非常大的数
// 使用 ES10 的 BigInt 类型可以精确计算任意大整数
var factorial = 1n;  // BigInt 字面量以 n 结尾
for (var fi = 1n; fi <= 100n; fi++) {
    factorial *= fi;
}
// 100! 是一个 158 位的数,这里只展示前 20 位
console.log('100! 的前20位:', factorial.toString().slice(0, 20) + '...');

/* ─── 练习 5:1! + 2! + ... + 20! ─── */
// 优化:不重复计算阶乘,每次在上一次结果的基础上累乘
var total = 0;
var fac = 1;
for (var i = 1; i <= 20; i++) {
    fac *= i;    // fac 始终是当前 i 的阶乘(利用了上一次的 fac)
    total += fac; // 将 i! 累加到总和
}
console.log('1!+...+20! =', total);

📌 10.3 练习解答代码解释说明

练习 2:被 3 或 7 整除

  • i % 3 === 0:取余为 0 即整除。=== 严格相等,不做类型转换
  • || 逻辑或:左右任一为真则整体为真,两个条件满足其一即入选
  • 输出示例:102 105 105 ...(注:105 同时被 3 和 7 整除,只收录一次,因为 pushif 判断内)

练习 3:数位含 3 或 7

  • 提取三位数各位的通用算法:
    百位 a = Math.floor(n / 100)        → 例:n=371,a=3
    十位 b = Math.floor(n % 100 / 10)   → n=371,n%100=71,b=7
    个位 c = n % 10                      → n=371,c=1
    
  • 条件检查 a===3||a===7 || b===3||b===7 || c===3||c===7:三位任一为 3 或 7 均成立

练习 4:100 的阶乘(BigInt)

  • Number 类型只有 53 位整数精度(Number.MAX_SAFE_INTEGER = 2^53-1 ≈ 9千万亿),100! ≈ 10^157,远超精度上限,会损失精度
  • BigInt(ES2020):以 n 结尾的字面量,支持任意精度整数运算
  • factorial.toString().slice(0, 20):先转字符串再截取,只展示前 20 位供人阅读

练习 5:阶乘累加的优化技巧

  • 朴素方法:每次重新从 1 连乘到 i,共 1+2+...+20=210 次乘法
  • 优化方法(本代码采用):复用上一次的 fac,每次只做 1 次乘法,共 20 次——效率提升 10 倍
  • 核心思路:i! = (i-1)! × i,用 fac *= i 替代每次从头计算
练习 关键算子/方法 考核的 JS 知识点
练习 2 %===|| 取余判断整除、逻辑运算符
练习 3 Math.floor%、多重 || 数位提取算法
练习 4 BigIntn 后缀) 大整数精度问题、ES2020 新特性
练习 5 循环优化、变量复用 累加与累乘的数学关系

十一、配套题目详解

11.1 题目 1:数组操作推演

原题:

var arr = [100, 200, 300, 400];
arr.unshift(1000);       // 向数组头部插入
arr.push(2000);          // 向数组尾部追加
arr.splice(4, 0, 3000);  // 在指定位置插入

console.log(arr[arr.length - 1] + arr[arr.length - 2]);  // = ?

逐步推演:

步骤 操作 数组状态 说明
初始 [100, 200, 300, 400] 4 个元素
unshift(1000) [1000, 100, 200, 300, 400] 头部插入,所有元素右移
push(2000) [1000, 100, 200, 300, 400, 2000] 尾部追加
splice(4, 0, 3000) [1000, 100, 200, 300, 3000, 400, 2000] 下标4处插入(不删除)
结果 arr[6]+arr[5] 2000 + 400 = 2400

📌 题目 1 代码解释说明(数组操作推演)

三个数组方法的行为对比:

方法 作用 改变长度 返回值
arr.unshift(1000) 头部插入元素,原有元素全部右移一位 变长 新数组的 length
arr.push(2000) 尾部追加元素 变长 新数组的 length
arr.splice(4, 0, 3000) 在下标 4 处插入(第二个参数 0 表示不删除) 变长 被删除的元素数组(空数组)

arr[arr.length - 1] 的计算逻辑:

  • 操作后数组:[1000, 100, 200, 300, 3000, 400, 2000],长度 = 7
  • arr.length - 1 = 6,即最后一个元素:2000
  • arr.length - 2 = 5,即倒数第二个元素:400
  • 最终:2000 + 400 = 2400

易错点: splice(4, 0, 3000) 插入在下标 4 的位置之前,使原下标 4(400)变成新下标 5。
初学者常误认为 splice(4, 0, 3000) 是「替换下标 4 的元素」,但第二参数为 0 表示不删除

11.2 题目 2:函数调用 + 数组访问

// 定义一个将参数乘以 3 的函数
function func(n) {
    return n * 3;  // 返回 n * 3
}

var arr = [100, 200, 300];
//   下标:   0    1    2

// arr[0] = 100
// func(arr[0]) = func(100) = 100 * 3 = 300
// arr[2] = 300
// func(arr[0]) + arr[2] = 300 + 300 = 600

console.log(func(arr[0]) + arr[2]);  // → 600

📌 题目 2 代码解释说明(函数调用 + 数组访问)

逐步求值过程(由内向外):

表达式:func(arr[0]) + arr[2]

第一步:arr[0] → 数组下标 0 的值 → 100
第二步:func(100) → 调用 func,n = 100
                 → 执行 return n * 3 → 100 * 3 → 返回 300
第三步:arr[2]  → 数组下标 2 的值 → 300
第四步:300 + 300 → 600

最终输出:600

考查的三个知识点:

知识点 涉及代码 说明
数组下标访问 arr[0]arr[2] 下标从 0 开始,arr[0]=100arr[2]=300
函数调用与返回值 func(arr[0]) 实参是表达式,先求值再传入;return 把结果送回
表达式求值顺序 func(...) + arr[2] 加法运算符两侧均先独立求值,再相加

常见误区:

  • ❌ 看到 func(arr[0]) 误以为 arr[0] 被修改 → ✅ 按值传递,原数组不变
  • ❌ 误读下标:arr[1]200arr[2]300(不是 200) → ✅ 仔细对应数组初始值

十二、面试题深度解析

12.1 面试题 1:作用域链与调用位置无关

var num = 10;

function fun() {
    var num = 20;  // fun 的局部变量,不影响 fun2 的查找
    fun2();        // 在 fun 内部调用 fun2
}

function fun2() {
    // fun2 定义在全局,外层词法环境是全局作用域
    // 查找 num 时,沿 fun2 的作用域链向上找:全局中 num = 10
    console.log(num);  // → 10(不是 20!)
}

fun();

关键推理: fun2 的词法外层是全局(因为 fun2 定义在全局)。在 fun 内部调用 fun2fun2 的作用域链不会经过 fun,只按词法嵌套关系查找。

📌 面试题 1 解释说明

答题步骤(三步推理法)

步骤1:找出所有 var 声明 → var num = 20(在 fun 内部)
步骤2:找出所有 function 声明 → function fun(){...}, function fun2(){...}
步骤3:跟踪作用域链 → fun2 定义在全局,外层指向全局

核心推理:
  - fun 内调用 fun2
  - fun2 的外层作用域 = 全局(不是 fun!)
  - fun2 查找 num:fun2 没有 → 去全局找 → 全局 num = 10
  - 输出:10

考点提炼:作用域链在函数定义时就已固定,与调用位置无关。


12.2 面试题组(提升综合分析)

题目 1:未声明变量

alert(a);  // ReferenceError: a is not defined
a = 0;

分析: a 从未用 var 声明,创建阶段无法登记,执行时找不到 a → 抛出 ReferenceError

📌 解释:没有 var 声明 → 创建阶段不会登记 a → 执行 alert(a) 时找不到 a → ReferenceError。
区分:有 var → undefined;无 var → ReferenceError(两者完全不同!)


题目 2:var 基础提升

alert(a);    // → undefined(var a 已提升,但尚未赋值)
var a = 0;
alert(a);    // → 0(赋值后)

等效视角:

var a;         // 创建阶段:提升,初始值 undefined
alert(a);      // undefined
a = 0;         // 执行阶段:赋值
alert(a);      // 0

📌 解释var a 被提升到顶部,初始值为 undefined,所以第1行输出 undefined 而非报错。执行到 var a = 0 时只执行赋值,第2行输出 0


题目 3:同名 var + function(第一版)

alert(a);              // → function a(){...}(function 优先提升)
var a = '字符串A';     // 执行阶段:赋值覆盖函数
function a() { alert('函数A'); }  // 执行阶段:跳过
alert(a);              // → 字符串A

📌 解释

  • 创建阶段:var afunction a 同名 → function 优先,a 初始值为函数
  • 第1行:a 是函数对象 → 输出函数
  • 执行到 var a = '字符串A':只执行赋值 → a 变为字符串
  • 执行到 function a(){...}:跳过(创建阶段已处理)
  • 第2行:a 是字符串 → 输出 '字符串A'

题目 4:function + var + 运算

alert(a);              // → function a(){...}
a++;                   // 函数对象与数字运算:函数转数字为 NaN,NaN+1=NaN
alert(a);              // → NaN
var a = '字符串B';
function a() { alert('函数B'); }
alert(a);              // → 字符串B

📌 解释

  • 创建阶段:function a 优先 → a = 函数对象
  • 第1行:输出函数
  • a++:函数对象先被 Number() 转换 → NaNNaN + 1 = NaNa = NaN
  • 第2行:输出 NaN
  • var a = '字符串B':赋值覆盖 → a = '字符串B'
  • 第3行:输出 '字符串B'

关键:函数对象转数字(Number(function(){})))= NaNNaN 参与任何算术运算结果还是 NaN


题目 5:嵌套函数内的 var 提升(最复杂)

alert(a);    // 全局:undefined(全局 var a 提升)
var a = 0;
alert(a);    // 全局:0

function fn() {
    alert(a);   // fn 内:undefined(局部 var a 提升,遮蔽全局)
    var a = 1;  // 局部 a 赋值
    alert(a);   // fn 内:1
}
fn();
alert(a);    // 全局:0(全局 a 未被 fn 修改)

📌 解释(逐行分析 6 次 alert 的输出)

序号 代码 输出 原因
全局 alert(a) undefined 全局 var a 提升,未赋值
全局 alert(a) 0 var a = 0 赋值后
fn 内 alert(a) undefined fn 内有局部 var a,提升后遮蔽全局,值为 undefined
fn 内 alert(a) 1 局部 var a = 1 赋值后
全局 alert(a) 0 fn 内是局部变量,全局 a 未被修改

最易出错的是③:很多人以为会输出全局的 0,实则 fn 内有局部 var a,提升后值为 undefined,遮蔽了全局。

推演时序图:

fn EC 全局 EC fn EC 全局 EC 创建阶段: var a → undefined 创建阶段: var a(局部)→ undefined alert(a) → "undefined" a = 0(赋值) alert(a) → "0" 调用 fn() alert(a) → "undefined"(局部遮蔽全局) a = 1(局部 a 赋值) alert(a) → "1" fn 结束,EC 弹出 alert(a) → "0"(全局 a 未变)

12.3 面试题规律总结

场景 结论 原因
未声明变量 ReferenceError 创建阶段未登记绑定
var 声明前读取 undefined 创建阶段登记但未赋值
function 声明前读取/调用 完整函数值,调用成功 创建阶段绑定完整函数
同名 var + function 函数优先;var 赋值覆盖 函数在创建阶段先绑定
局部 var 与全局同名 函数内读到 undefined 局部 var 提升遮蔽全局
嵌套函数自由变量 沿词法链查找 词法作用域,与调用位置无关

十三、CSS 样式速查与产品中的对应关系

13.1 字体与排版

CSS 属性 示例值 作用 真实产品使用场景
font-family: system-ui system-ui, -apple-system, ... 使用操作系统默认字体 GitHub、Notion、Linear、Bootstrap 5
line-height 1.5 / 1.6 / 1.7 行间距(影响可读性) Medium(1.6)、MDN(1.5)、掘金(1.7)
letter-spacing 0.08em 字符间距 小号标签、全大写文字、Logo 字体
font-size 13px / 1.1rem 字体大小 正文 16px、代码 13px、辅助说明 12px
text-align: center center / right / left 文本对齐 标题居中、数字列右对齐、按钮居中
text-transform uppercase 大小写转换 导航标签、章节标题装饰性大写

text-align 完整可运行示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>text-align 示例</title>
  <style>
    * { box-sizing: border-box; }
    body { font-family:system-ui,sans-serif; padding:24px; background:#f8fafc; }
    /* text-align: center → 标题居中(电商、内容平台标配) */
    h1 { text-align:center; color:#4338ca; margin-bottom:20px; }
    table { width:100%; border-collapse:collapse; }
    th, td { border:1px solid #e2e8f0; padding:10px 14px; }
    th { background:#f1f5f9; text-align:left; }
    /* text-align: right → 数字列右对齐(所有财务/数据表格)*/
    .right  { text-align:right; }
    /* text-align: center → 状态列居中(后台管理系统)*/
    .center { text-align:center; }
  </style>
</head>
<body>
  <h1>商品列表(text-align 演示)</h1>
  <table>
    <thead>
      <tr><th>产品名称</th><th class="center">状态</th><th class="right">价格(元)</th><th class="right">库存</th></tr>
    </thead>
    <tbody>
      <tr><td>产品 Alpha</td><td class="center">上架</td><td class="right">299.00</td><td class="right">1,024</td></tr>
      <tr><td>产品 Beta</td><td class="center">下架</td><td class="right">1,599.00</td><td class="right">0</td></tr>
      <tr><td>产品 Gamma</td><td class="center">上架</td><td class="right">49.90</td><td class="right">8,763</td></tr>
    </tbody>
  </table>
</body>
</html>

在这里插入图片描述

13.2 盒模型

CSS 属性 作用 真实产品使用场景
box-sizing: border-box 宽高包含 padding/border 所有主流框架默认;消除宽度计算陷阱
margin 元素外边距 块间距、margin:0 auto 居中
padding 元素内边距 按钮点击区域、卡片内容区
border-radius 圆角 按钮、卡片、头像(50%=圆形)
border-left 左边框高亮 MDN Note/Warning 块、引用块
max-width 最大宽度 内容区限宽(博客文章页)

margin / padding / border-radius 完整示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>盒模型属性示例</title>
  <style>
    * { box-sizing: border-box; }
    body { font-family:system-ui,sans-serif; background:#eef2ff; padding:32px; color:#1e1b4b; }
    h1 { text-align:center; margin:0 0 24px; }
    .flex { display:flex; flex-wrap:wrap; gap:20px; justify-content:center; }
    .card {
      width:220px;
      padding:20px;           /* 内容区与边框之间的空间 */
      background:#fff;
      border-radius:16px;     /* 圆角 */
      border:2px solid #c7d2fe;
      box-shadow:0 8px 24px rgba(99,102,241,0.12);
      text-align:center;
    }
    .avatar {
      width:60px; height:60px;
      background:linear-gradient(135deg,#818cf8,#6366f1);
      border-radius:50%;      /* 50% → 正圆 */
      margin:0 auto 12px;
      display:grid; place-items:center;
      color:#fff; font-size:22px; font-weight:700;
    }
    .name { font-weight:600; margin:0 0 4px; }
    .role { color:#6366f1; font-size:13px; margin:0 0 12px; }
    .btn {
      padding:7px 18px;
      background:#6366f1; color:#fff;
      border-radius:99px; /* 胶囊按钮 */
      font-size:13px; border:none; cursor:pointer;
    }
    .quote {
      max-width:460px; margin:24px auto 0;
      background:#fff;
      border-left:4px solid #6366f1;  /* 左边框高亮 */
      border-radius:0 12px 12px 0;
      padding:16px 20px;
      font-style:italic; color:#374151;
    }
    .quote cite { display:block; font-style:normal; color:#6b7280; font-size:12px; margin-top:8px; }
  </style>
</head>
<body>
  <h1>盒模型属性演示</h1>
  <div class="flex">
    <div class="card">
      <div class="avatar">A</div>
      <p class="name">Alice Chen</p>
      <p class="role">Frontend Dev</p>
      <button class="btn">关注</button>
    </div>
    <div class="card">
      <div class="avatar">B</div>
      <p class="name">Bob Smith</p>
      <p class="role">UI Designer</p>
      <button class="btn">关注</button>
    </div>
  </div>
  <div class="quote">
    理解词法作用域,是掌握闭包、模块化与异步编程的基石。
    <cite>— JavaScript 工程实践</cite>
  </div>
</body>
</html>

在这里插入图片描述

13.3 颜色与视觉效果

CSS 属性 作用 真实产品使用场景
radial-gradient 径向渐变 Vercel/Linear/Stripe 登录页背景
linear-gradient 线性渐变 按钮、进度条、头图渐变文字
box-shadow 阴影/发光 Material 层级系统;弹窗遮挡
opacity 整体透明度 禁用态(0.45)、遮罩层(0.7)

opacity / linear-gradient 完整示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>opacity 与 linear-gradient</title>
  <style>
    * { box-sizing:border-box; }
    body { font-family:system-ui,sans-serif; background:#0f172a; color:#f1f5f9; padding:32px; }
    h1 { margin-bottom:20px; }
    .row { display:flex; gap:12px; flex-wrap:wrap; margin-bottom:20px; }
    .btn { padding:10px 22px; border-radius:8px; border:none; font-size:14px; cursor:pointer; font-weight:500; }
    /* linear-gradient 按钮:比纯色更有层次感,被大量产品使用 */
    .btn-primary { background:linear-gradient(135deg,#6366f1,#8b5cf6); color:#fff; }
    .btn-success { background:linear-gradient(135deg,#10b981,#059669); color:#fff; }
    /* opacity 禁用态:整棵子树变半透明,比改 color 更简单 */
    .btn-disabled { background:linear-gradient(135deg,#6366f1,#8b5cf6); color:#fff; opacity:.45; cursor:not-allowed; }
    /* 进度条 */
    .bar-wrap { height:8px; background:#1e293b; border-radius:4px; overflow:hidden; margin-bottom:6px; }
    .bar-fill { height:100%; border-radius:4px; }
    .bar-label { font-size:12px; color:#94a3b8; display:flex; justify-content:space-between; margin-bottom:14px; }
    .card { background:#1e293b; border-radius:12px; padding:20px; }
  </style>
</head>
<body>
  <h1>opacity + linear-gradient 演示</h1>
  <div class="card">
    <p style="margin:0 0 12px;font-size:.9rem;color:#a5b4fc;">按钮状态(opacity 禁用)</p>
    <div class="row">
      <button class="btn btn-primary">主操作</button>
      <button class="btn btn-success">成功提交</button>
      <button class="btn btn-disabled" disabled>已禁用(opacity:.45)</button>
    </div>
    <p style="margin:16px 0 10px;font-size:.9rem;color:#a5b4fc;">学习进度(linear-gradient 进度条)</p>
    <div class="bar-wrap"><div class="bar-fill" style="width:82%;background:linear-gradient(90deg,#6366f1,#a78bfa)"></div></div>
    <div class="bar-label"><span>JavaScript 基础</span><span>82%</span></div>
    <div class="bar-wrap"><div class="bar-fill" style="width:58%;background:linear-gradient(90deg,#f59e0b,#fbbf24)"></div></div>
    <div class="bar-label"><span>CSS 进阶</span><span>58%</span></div>
    <div class="bar-wrap"><div class="bar-fill" style="width:34%;background:linear-gradient(90deg,#10b981,#34d399)"></div></div>
    <div class="bar-label"><span>框架与工程化</span><span>34%</span></div>
  </div>
</body>
</html>

在这里插入图片描述

13.4 布局系统

CSS 属性 作用 真实产品使用场景
display: flex 弹性布局 导航栏、工具条、表单行、卡片内部
gap 子项间距 替代 margin,更简洁的间距控制
justify-content 主轴对齐 导航 space-between、按钮 center
align-items 交叉轴对齐 图标+文字垂直居中
display: grid 网格布局 卡片列表、仪表盘、响应式布局
repeat(auto-fit, minmax) 响应式列 无媒体查询的响应式商品/文章列表
place-items: center Grid 双轴居中 登录页、404、空状态

13.5 滚动与溢出

CSS 属性 作用 真实产品使用场景
overflow-y: auto 纵向滚动 日志面板、侧边栏导航、通知列表
overflow-x: auto 横向滚动 宽表格、代码块
white-space: pre-wrap 保留格式+折行 代码块、终端输出、diff
max-height + overflow 固定高度内滚动 下拉列表、通知面板

overflow: auto + max-height 完整示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>overflow 滚动示例</title>
  <style>
    * { box-sizing:border-box; }
    body { font-family:system-ui,sans-serif; background:#f8fafc; padding:32px; }
    h1 { margin-bottom:20px; font-size:1.3rem; }
    .label { font-size:12px; color:#64748b; margin-bottom:6px; text-transform:uppercase; letter-spacing:.05em; }
    /* max-height + overflow-y: auto = 固定高度滚动容器 */
    .log-panel {
      background:#0f172a; color:#86efac;
      border-radius:12px; padding:16px;
      max-height:200px;
      overflow-y:auto;   /* 内容超出后出现滚动条 */
      font-family:ui-monospace,monospace; font-size:12px; line-height:1.6;
    }
    /* overflow-x: auto + white-space: pre = 代码块横向滚动 */
    .code-block {
      background:#1e293b; color:#e2e8f0;
      border-radius:8px; padding:16px; margin-top:16px;
      overflow-x:auto;
      white-space:pre;   /* 保留格式,不折行,需要滚动时横向滚动 */
      font-family:ui-monospace,monospace; font-size:13px;
    }
  </style>
</head>
<body>
  <h1>overflow 属性演示</h1>
  <div class="label">日志面板(max-height + overflow-y: auto)</div>
  <div class="log-panel" id="logs"></div>
  <div class="label" style="margin-top:16px">代码块(overflow-x: auto,长代码不自动折行)</div>
  <div class="code-block">// 作用域链完整示例:全局 → func01 → func02 → func03 的四层嵌套查找
function createNestedScopeExample(globalVar, level1Config, level2Config, level3Config) {
    return { globalVar, level1Config, level2Config, level3Config };
}</div>
  <script>
    var logsEl = document.getElementById('logs');
    var levels = ['INFO','DEBUG','WARN','ERROR','INFO'];
    for (var i = 1; i <= 20; i++) {
      var lvl = levels[i % levels.length];
      logsEl.innerHTML += '['+String(i).padStart(3,'0')+'] ['+lvl+'] 执行上下文 #'+i+' — 变量绑定完成<br>';
    }
    logsEl.scrollTop = logsEl.scrollHeight; // 滚动到底部
  </script>
</body>
</html>

在这里插入图片描述


十四、工程实践中的最佳规范

14.1 变量声明选择策略

否(常量)

是(变量)

是(现代代码)

维护旧代码

需要声明变量

是否需要重新赋值?

const
意图明确,最安全
无提升陷阱

是否需要块级作用域?

let
块级作用域,TDZ 保护

var
理解提升行为后谨慎使用

14.2 函数参数设计规范

// ❌ 不推荐:arguments 处理可变参数
function sumBad() {
    var total = 0;
    for (var i = 0; i < arguments.length; i++) total += arguments[i];
    return total;
}

// ✅ 推荐:剩余参数,语义清晰,有 Array 方法
function sumGood(...nums) {
    return nums.reduce((a, b) => a + b, 0);
}

// ❌ 不推荐:ES5 手动判断 undefined
function createUserBad(name, role, active) {
    if (role === undefined) role = 'user';
    if (active === undefined) active = true;
    // ...
}

// ✅ 推荐:ES6 默认参数,代码更少,语义更清晰
function createUserGood(name, role = 'user', active = true) {
    return { name, role, active };
}

14.3 作用域与提升规范

// ❌ var 在循环中的陷阱(高频 Bug 来源)
for (var i = 0; i < 5; i++) {
    setTimeout(() => console.log(i), 100); // 全部打印 5!
}

// ✅ let 块级作用域,每次迭代独立
for (let i = 0; i < 5; i++) {
    setTimeout(() => console.log(i), 100); // 0, 1, 2, 3, 4 ✓
}

// ❌ 函数声明与变量同名(提升陷阱)
var config = '字符串';
function config() { return {}; }  // 同名冲突!

// ✅ 避免同名,语义清晰
function getConfig() { return {}; }
const config = getConfig();

14.4 纯函数与副作用控制

// ❌ 有副作用(修改外部状态)
var cart = [];
function addToCart(item) {
    cart.push(item); // 直接修改全局变量 cart(副作用)
    return cart;
}

// ✅ 纯函数(返回新数组,不修改原数组)
function addToCart(cart, item) {
    return [...cart, item];  // 返回新数组,不改变原来的 cart
}
const cart1 = [];
const cart2 = addToCart(cart1, {id: 1, name: '商品A'});
// cart1 完全不变,cart2 是新数组

14.5 严格模式防御

'use strict'; // 放在脚本或函数顶部

// 严格模式下以下操作会报错,帮助提前发现 Bug:
// 1. 未声明变量赋值 → ReferenceError(非严格模式只是创建全局属性)
x = 10; // ← ReferenceError in strict mode

// 2. 删除变量/函数 → SyntaxError
delete x; // ← SyntaxError in strict mode

// 3. 重复形参 → SyntaxError
function f(a, a) {} // ← SyntaxError in strict mode

14.6 规范要点七条

  1. 默认 const,需要重绑定才用 let——避免 var 的提升和函数作用域陷阱
  2. 可变参数用 ...rest——arguments 仅用于理解历史代码
  3. 函数声明 vs 表达式要清晰——需要提升用声明式;需要控制可见性用 const fn = () => {}
  4. 理解词法作用域——闭包、模块化、异步回调都建立在此基础上
  5. 避免函数名与变量名同名——会触发提升覆盖的不确定行为
  6. 启用严格模式——让隐式全局变量赋值提前报错
  7. 面试中的怪写法是陷阱题——工程代码中绝对不应这样写

十五、CSS 盒模型与可读性综合演示

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CSS 属性综合演示</title>
  <style>
    * { box-sizing: border-box; }
    body { margin:0; padding:32px; font-family:system-ui,sans-serif; background:#eef2ff; color:#1e1b4b; }
    /* text-align: center → 标题居中 */
    h1 { text-align:center; margin:0 0 6px; font-size:1.5rem; letter-spacing:.02em; }
    /* opacity → 弱化辅助文案,视觉层次分离 */
    .note { text-align:center; margin-bottom:28px; font-size:14px; opacity:.6; }
    /* Flex + gap → 卡片排列 */
    .deck { display:flex; flex-wrap:wrap; gap:20px; justify-content:center; }
    /* margin + padding + border-radius + box-shadow 综合卡片 */
    .tile {
      width:280px;
      padding:20px;
      margin:0;          /* 间距由父级 gap 控制,无需设 margin */
      background:#fff;
      border-radius:14px;
      border:1px solid #c7d2fe;
      box-shadow:0 6px 20px rgba(99,102,241,0.1);
    }
    .tile h2 { margin:0 0 10px; font-size:1rem; color:#4338ca; }
    .tile p  { margin:0 0 10px; font-size:14px; line-height:1.6; color:#374151; }
    /* overflow-y: auto → 固定高度滚动容器 */
    .scrollbox {
      max-height:120px;
      overflow-y:auto;
      padding:10px; margin-top:8px;
      font-family:ui-monospace,monospace; font-size:12px;
      line-height:1.5; background:#f5f3ff;
      border-radius:8px; color:#4c1d95;
    }
    /* border-left → 引用块高亮条 */
    .tip {
      border-left:3px solid #818cf8;
      padding-left:12px;
      color:#4338ca; font-size:13px;
      margin-top:10px; opacity:.85;
    }
    img { display:block; margin:10px auto 0; border-radius:8px; }
  </style>
</head>
<body>
  <h1>CSS 盒模型 + 可读性属性综合示例</h1>
  <p class="note">
    opacity · overflow · text-align · border-radius · padding/margin · border-left
  </p>
  <div class="deck">
    <div class="tile">
      <h2>margin / padding</h2>
      <p><code>padding:20px</code> 撑开内容区;卡片间距由父级 <code>gap:20px</code> 统一管理。</p>
      <img src="images/figure-demo.svg" width="90" alt="示意图" />
    </div>
    <div class="tile">
      <h2>overflow-y:auto 滚动区</h2>
      <p>日志、通知列表等场景:</p>
      <div class="scrollbox">
        [001] 变量提升:创建阶段登记 var = undefined<br>
        [002] 函数提升:创建阶段绑定完整函数值<br>
        [003] 作用域链:词法嵌套,由内向外查找<br>
        [004] 词法作用域:与调用位置无关<br>
        [005] arguments:类数组,非 Array<br>
        [006] 剩余参数 ...rest:真 Array<br>
        [007] var 无块级作用域:let/const 有<br>
        [008] 严格模式:隐式全局赋值报错
      </div>
    </div>
    <div class="tile">
      <h2>opacity + text-align</h2>
      <p style="text-align:center;font-size:1.5rem;margin:8px 0"></p>
      <p style="text-align:center;font-weight:600">核心记忆点</p>
      <div class="tip">作用域只与函数声明位置有关,与调用位置无关!</div>
      <p style="margin-top:8px;opacity:.5;font-size:13px">
        opacity:.5 弱化次要文字,比改 color 更简单
      </p>
    </div>
  </div>
</body>
</html>

在这里插入图片描述


十六、知识体系总结(三张归纳表 + 学习路径)

16.1 全部知识点难度与面试频率

知识点 核心内容 难度 面试频率
函数调用与返回值 fn vs fn()return 行为 ★☆☆ ★★☆
形参与实参 按位置绑定、数量不匹配 ★☆☆ ★☆☆
ES5/ES6 默认参数 === undefined 判断 vs 语法糖 ★★☆ ★★☆
arguments 对象 类数组、箭头函数无 arguments ★★☆ ★★★
剩余参数 ...rest 真数组、必须在最后 ★★☆ ★★☆
全局作用域 var 成为 window 属性 ★☆☆ ★★☆
局部作用域 形参/var 的函数级隔离 ★☆☆ ★★☆
var 无块级作用域 for/ifvar 泄漏 ★★☆ ★★★
变量遮蔽(Shadowing) 内层同名绑定遮蔽外层 ★★☆ ★★☆
作用域链 由内向外查找,链顶是全局 ★★★ ★★★
词法作用域 声明位置决定,与调用无关 ★★★ ★★★
变量提升(var 创建阶段 undefined → 执行阶段赋值 ★★★ ★★★
函数提升 创建阶段绑定完整函数值 ★★★ ★★★
同名 var + function 覆盖顺序与陷阱 ★★★ ★★★
翻转字符串 倒序遍历 + 封装 ★☆☆ ★☆☆
水仙花数 位分解 + 立方和 ★★☆ ★☆☆
质数判断 试除法 + 封装 ★★☆ ★★☆
等腰三角形 双重循环 + 字符串拼接 ★★☆ ★☆☆
数字阵列 补零 + 取模换行 ★★☆ ★☆☆

16.2 var vs let vs const 作用域对比

特性 var let const
作用域 函数/全局 块级 块级
提升 提升,初始化为 undefined 提升,但有 TDZ 提升,但有 TDZ
重复声明 允许 不允许(同作用域) 不允许
重新赋值 允许 允许 不允许(不可重新绑定)
全局 window 属性
推荐使用 维护旧代码 需要重赋值时 默认选择

16.3 函数声明 vs 函数表达式

特性 声明式 function f(){} 表达式 var f = function(){} 箭头函数 const f = () => {}
提升行为 完整函数提升 var f 提升(值为 undefined const f 提升(TDZ)
声明前调用 ✓ 可以 ✗ TypeError ✗ ReferenceError
arguments 对象 ✓ 有 ✓ 有 ✗ 无
this 绑定 动态绑定 动态绑定 继承外层 this
适用场景 工具函数(可提升) 条件赋值、回调 简短回调、保留 this

16.4 学习路径图(掌握程度递进)

Level 1
能跑起来
写出代码不报错

Level 2
能解释原理
知道为什么这样运行

Level 3
能规避陷阱
不写有 Bug 的代码

Level 4
能灵活应用
闭包/模块/异步回调

掌握基础语法
变量/函数/循环

理解执行上下文
作用域链/提升规则

用 let/const 替代 var
严格模式/纯函数

闭包/IIFE/回调/递归
下一单元重点


十七、下一单元知识预告(Day07 预习大纲)

知识点 核心问题 与本单元的联系
匿名函数 没有函数名的函数表达式如何使用? 函数表达式的特殊形式,不触发函数提升
IIFE 为什么立即执行?解决什么问题? 利用函数作用域创建独立环境,防全局污染
回调函数 满足哪三个条件才算回调? 函数是一等公民的直接应用
递归函数 终止条件如何设计? 依赖作用域链、调用栈,注意栈溢出

具名函数
function fn(){}

匿名函数
function(){}

函数表达式
var fn = function(){}

IIFE
(function(){})()

回调函数
eventListener/setTimeout

递归函数
函数调用自身

独立作用域
防全局污染

事件系统
异步编程基础

分治思想
树/图遍历

ES6 模块(import/export)
终极解决方案

简要介绍:

  • 匿名函数:没有函数名的函数表达式,常用于回调和 IIFE
  • IIFE(function() { ... })(),创建独立作用域,模块化的前身
  • 回调函数:满足「我定义 + 没直接调用 + 最终执行」三个条件
  • 递归函数:需要基本情况(终止条件)+ 递归情况(规模递减)

十八、延伸阅读与参考资源

官方规范与文档

深度社区资源


十九、内存管理与垃圾回收(GC)

JavaScript 是一门自动管理内存的语言,但「自动」并不意味着「无忧」。理解 GC 机制,有助于写出不泄漏、不卡顿的代码,尤其是在使用闭包和事件监听器时。

19.1 执行上下文与内存生命周期

JavaScript 引擎使用**堆(Heap)存储对象(包括函数对象、数组、词法环境),使用栈(Stack)**存储执行上下文帧。

解析 + 编译

调用函数

函数返回

无引用

有闭包持有引用

闭包函数也不再引用

JS 源代码

全局执行上下文
压入调用栈
(持续存在直到脚本结束)

函数执行上下文
(每次调用创建新实例)

弹出调用栈
执行上下文帧销毁

词法环境被 GC
局部变量释放

词法环境继续存活
内存不释放

词法环境最终被 GC
内存释放

内存分配与释放的规律:

时机 分配 释放
声明变量 var x = {} 在堆上分配对象,在环境记录中创建绑定 环境记录中的绑定被销毁后,若对象无其他引用则 GC
调用函数 新建词法环境(堆)+ 执行上下文帧(栈) 函数返回 → EC 帧出栈 → 词法环境若无闭包引用则 GC
创建闭包 内部函数的 [[Environment]] 指向外部词法环境,引用计数+1 内部函数自身也不可达时,引用计数-1,最终 GC
DOM 事件监听 监听器函数(及其闭包)被 DOM 节点持有 需手动 removeEventListener 或节点从 DOM 移除(且无其他引用)

19.2 闭包与内存泄漏

闭包是内存泄漏的高发区——因为它使词法环境脱离了执行上下文的生命周期:

// 潜在内存泄漏示例
function setup() {
  const largeData = new Array(1_000_000).fill('data'); // 占用大量内存

  document.getElementById('btn').addEventListener('click', function handler() {
    // handler 通过闭包持有 largeData 所在的词法环境
    // 即使 handler 只用到 largeData 的一小部分,整个词法环境都不会被 GC
    console.log(largeData[0]);
  });
  // setup() 返回后,largeData 理应被回收,但由于 handler 闭包,它不会!
}

// 修复:只保留需要的数据,让 largeData 可以被回收
function setupFixed() {
  const largeData = new Array(1_000_000).fill('data');
  const needed = largeData[0]; // 提取需要的数据

  document.getElementById('btn').addEventListener('click', function handler() {
    console.log(needed); // 只持有 needed,largeData 可以被 GC
  });
}

19.3 常见内存泄漏场景与修复

场景 1:忘记移除事件监听器

// 泄漏
function addHandler() {
  const btn = document.getElementById('btn');
  btn.addEventListener('click', () => { /* 处理逻辑 */ });
  // 每次调用 addHandler 都添加一个新监听器,永远不被移除
}

// 修复:保存引用,在适当时机移除
function addHandlerFixed() {
  const btn = document.getElementById('btn');
  const handler = () => { /* 处理逻辑 */ };
  btn.addEventListener('click', handler);
  // 在组件销毁时:btn.removeEventListener('click', handler);
  return () => btn.removeEventListener('click', handler); // 返回清理函数
}

场景 2:定时器未清除

// 泄漏:interval 持有闭包,闭包持有外部状态
function startPolling(url) {
  let data = null;
  const intervalId = setInterval(async () => {
    data = await fetch(url).then(r => r.json());
  }, 1000);
  // 如果不调用 clearInterval(intervalId),闭包和 data 永远不会被释放
}

// 修复:在合适时机清除定时器
function startPollingFixed(url) {
  let data = null;
  const intervalId = setInterval(async () => {
    data = await fetch(url).then(r => r.json());
  }, 1000);
  return () => clearInterval(intervalId); // 返回清理函数
}

场景 3:全局变量积累

// 泄漏:意外创建全局变量(非严格模式)
function process() {
  result = computeResult(); // 忘记 var/let/const,创建全局 result
}

// 修复:使用严格模式或明确声明
function processFixed() {
  'use strict'; // 严格模式下上述代码会直接抛 ReferenceError
  const result = computeResult();
}

内存管理黄金法则:

规则 说明
最小化闭包捕获范围 只捕获(引用)需要的变量,不要让闭包持有大型对象
配对添加/移除 addEventListenerremoveEventListenersetIntervalclearInterval
使用 WeakRef / WeakMap 需要持有对象引用但不阻止其 GC 时,用弱引用
善用开发者工具 Chrome DevTools → Memory → Take Heap Snapshot 检查内存
优先用 const/let 避免意外全局变量,严格模式是最后一道防线

二十、函数式编程范式核心理论

函数式编程(Functional Programming,FP)是与面向对象编程并列的主流编程范式。JavaScript 天然支持 FP 的核心特性(函数是一等公民、高阶函数、闭包)。掌握 FP 思维,能让代码更可预测、更易测试、更易组合。

20.1 纯函数与引用透明性

纯函数(Pure Function) 的两个必要条件:

  1. 确定性(Determinism):相同输入永远产生相同输出
  2. 无副作用(No Side Effects):不修改外部状态,不进行 I/O
// 纯函数示例
const add = (a, b) => a + b;                    // ✓ 纯函数
const double = arr => arr.map(x => x * 2);      // ✓ 纯函数(map 返回新数组)
const getMax = (...nums) => Math.max(...nums);   // ✓ 纯函数

// 非纯函数示例
let total = 0;
const addToTotal = n => { total += n; };         // ✗ 修改外部状态(副作用)
const getRandom = () => Math.random();           // ✗ 不确定性(不同调用结果不同)
const fetchUser = id => fetch(`/api/users/${id}`); // ✗ I/O 副作用(但这是必要的副作用)

引用透明性(Referential Transparency):如果一个表达式可以被其求值结果替换,而不改变程序行为,则称之为引用透明。纯函数保证引用透明。

// 引用透明:add(2, 3) 可以在任何地方替换为 5,不影响程序行为
const result = add(2, 3) * add(2, 3);
// 等价于:
const result2 = 5 * 5; // 25,完全等效

纯函数的工程价值:

优势 说明
可测试性 不需要 mock 外部状态,输入输出即测试全部
可缓存(Memoization) 相同输入 → 相同输出,可以安全地缓存计算结果
并发安全 无共享状态,天然线程安全(JS 单线程,但 Worker 场景重要)
可组合 纯函数可以任意组合,不用担心执行顺序带来的副作用

20.2 柯里化与函数组合

柯里化(Currying):将接受多个参数的函数,转化为一系列接受单一参数的函数。

// 普通函数
const add3 = (a, b, c) => a + b + c;
add3(1, 2, 3); // 6

// 柯里化版本
const curriedAdd = a => b => c => a + b + c;
curriedAdd(1)(2)(3); // 6

// 实用场景:预填充参数(Partial Application)
const add10 = curriedAdd(10); // 固定第一个参数
const add10And20 = add10(20); // 固定前两个参数
console.log(add10And20(5));   // 35 = 10 + 20 + 5

// 实用示例:通用的 map 柯里化
const map = fn => arr => arr.map(fn);
const doubleAll = map(x => x * 2);
const tripleAll = map(x => x * 3);
console.log(doubleAll([1, 2, 3])); // [2, 4, 6]
console.log(tripleAll([1, 2, 3])); // [3, 6, 9]

函数组合(Function Composition):将多个函数串联,前一个函数的输出作为后一个函数的输入。

// 数学上:compose(f, g)(x) = f(g(x))
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
const pipe    = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
// compose:从右到左执行(数学惯例)
// pipe:从左到右执行(更符合阅读习惯)

// 示例:数据处理管道
const trim    = str => str.trim();
const toLower = str => str.toLowerCase();
const split   = str => str.split(' ');
const join    = arr => arr.join('-');

// compose(从右到左)
const toSlug = compose(join, split, toLower, trim);
// pipe(从左到右,更直观)
const toSlugPipe = pipe(trim, toLower, split, join);

console.log(toSlug('  Hello World  '));     // 'hello-world'
console.log(toSlugPipe('  Hello World  ')); // 'hello-world'

20.3 函数式思想在 JavaScript 中的实践

记忆化(Memoization):利用纯函数的确定性,缓存计算结果,避免重复计算:

function memoize(fn) {
  const cache = new Map(); // 闭包持有缓存
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key); // 缓存命中,直接返回
    }
    const result = fn.apply(this, args);
    cache.set(key, result); // 存入缓存
    return result;
  };
}

// 斐波那契数列(未记忆化:指数级复杂度)
const fib = n => n <= 1 ? n : fib(n - 1) + fib(n - 2);

// 记忆化版本(线性复杂度)
const memoFib = memoize(function fib(n) {
  return n <= 1 ? n : memoFib(n - 1) + memoFib(n - 2);
});
console.log(memoFib(40)); // 快速计算,无重复递归

**高阶函数三巨头(map/filter/reduce)**与命令式对比:

const orders = [
  { product: 'apple',  price: 3,  qty: 2 },
  { product: 'banana', price: 1,  qty: 5 },
  { product: 'cherry', price: 10, qty: 1 },
];

// 命令式(Imperative):关注「如何做」
let total = 0;
for (const order of orders) {
  if (order.price > 2) {
    total += order.price * order.qty;
  }
}
console.log(total); // 16

// 函数式(Declarative):关注「要做什么」
const totalFP = orders
  .filter(o => o.price > 2)         // 筛选高价商品
  .map(o => o.price * o.qty)        // 计算每单小计
  .reduce((sum, sub) => sum + sub, 0); // 求总和
console.log(totalFP); // 16 —— 语义更清晰,无中间变量

函数式编程 vs 命令式编程核心对比:

维度 命令式(Imperative) 函数式(Functional)
关注点 如何做(步骤) 做什么(描述)
状态 可变状态(mutable) 不可变数据(immutable)
循环 for/while map/filter/reduce
条件 if/switch 三元表达式/模式匹配
副作用 允许副作用 隔离副作用到边界
调试 需要追踪状态变化 函数输入输出即全部信息
适用场景 性能敏感的底层逻辑 数据转换、业务逻辑

JavaScript 的最佳实践:不必强行全函数式,而是在合适场景(数据处理、工具函数、状态管理)采用函数式思维,将副作用(DOM 操作、网络请求)隔离到代码边界,保持核心逻辑纯净可测。


深度社区资源


二十一、经典业务场景完整实战

本章将函数、作用域、闭包、参数设计等知识点综合运用于真实业务场景。每个场景均包含:业务背景 → 实现代码 → 技术要点标注 → 业务价值说明


21.1 防抖与节流——性能优化的核心手段

已在 5.4 节详细介绍,参见「场景 4:防抖」和「场景 5:节流」。


21.2 表单验证框架——规则驱动的参数设计

已在 6.5 节详细介绍,参见「场景 4:规则驱动的表单验证框架」。


21.3 API 请求统一封装——错误处理与 Loading 状态

业务背景:大型前端项目中,接口调用分散在各处,如果每个地方都各自处理 loading、错误、token 过期,代码会非常冗余。将 fetch 封装为统一函数,集中处理公共逻辑,是工程化的必要实践。

业务价值

  • 只需在一处修改 baseURL、请求头、错误处理逻辑,全项目生效
  • 任何请求都自动携带 token,无需每处重复编写
  • loading 状态统一管理,防止重复请求
  • 错误码(401/403/500)统一拦截,集中跳转
/**
 * API 请求封装
 * 综合运用:闭包(持久化配置)、默认参数(ES6)、函数返回值(Promise)
 */

// ── 全局配置(通过闭包保持私有)──
var apiConfig = (function() {
    var _baseURL = 'https://api.example.com';
    var _timeout = 10000; // 10秒超时
    var _token   = null;  // 登录后设置

    return {
        setBaseURL: function(url)   { _baseURL = url; },
        setToken:   function(token) { _token = token; },
        getBaseURL: function()      { return _baseURL; },
        getToken:   function()      { return _token; }
    };
})();

/**
 * 核心请求函数
 * @param {string} url - 接口路径(相对路径)
 * @param {Object} options - 请求配置
 * @param {string} [options.method='GET'] - HTTP 方法
 * @param {Object} [options.body=null]   - 请求体(POST/PUT 时使用)
 * @param {Object} [options.headers={}]  - 自定义请求头
 * @returns {Promise} 解析后的响应数据
 */
function request(url, options) {
    // ES5 默认参数处理
    options = options || {};
    var method  = options.method  || 'GET';
    var body    = options.body    || null;
    var headers = options.headers || {};

    // 自动拼接 baseURL
    var fullURL = apiConfig.getBaseURL() + url;

    // 自动注入 Authorization Token
    var token = apiConfig.getToken();
    if (token) {
        headers['Authorization'] = 'Bearer ' + token;
    }
    headers['Content-Type'] = headers['Content-Type'] || 'application/json';

    // 构造 fetch 配置
    var fetchOptions = {
        method:  method,
        headers: headers
    };
    if (body && method !== 'GET') {
        fetchOptions.body = JSON.stringify(body); // 自动序列化请求体
    }

    // 超时控制:Promise.race 让 fetch 和 timeout 竞争
    var timeoutPromise = new Promise(function(_, reject) {
        setTimeout(function() {
            reject(new Error('请求超时(' + _timeout + 'ms)'));
        }, _timeout);
    });

    return Promise.race([fetch(fullURL, fetchOptions), timeoutPromise])
        .then(function(response) {
            // 统一处理 HTTP 错误状态码
            if (response.status === 401) {
                apiConfig.setToken(null); // 清除失效 token
                window.location.href = '/login'; // 跳转登录
                return Promise.reject(new Error('登录已过期,请重新登录'));
            }
            if (response.status === 403) {
                return Promise.reject(new Error('没有权限访问此资源'));
            }
            if (!response.ok) {
                return Promise.reject(new Error('请求失败:' + response.status));
            }
            return response.json(); // 自动解析 JSON
        })
        .catch(function(error) {
            // 统一错误上报(实际项目中可接入 Sentry 等错误监控)
            console.error('[API Error]', url, error.message);
            return Promise.reject(error); // 继续向上抛出,让调用方处理 UI 反馈
        });
}

// ── 语义化封装:GET / POST / PUT / DELETE ──
var api = {
    get:    function(url, params)         { return request(url + '?' + new URLSearchParams(params)); },
    post:   function(url, body)           { return request(url, { method: 'POST', body: body }); },
    put:    function(url, body)           { return request(url, { method: 'PUT',  body: body }); },
    delete: function(url)                 { return request(url, { method: 'DELETE' }); }
};

// ── 业务层调用(简洁、无重复逻辑)──
apiConfig.setToken('eyJhbGci...');   // 登录后设置 token

api.get('/users', { page: 1, size: 20 })
    .then(function(data) { renderUserList(data.list); })
    .catch(function(err) { showToast(err.message, 'error'); });

api.post('/orders', { productId: 123, qty: 2 })
    .then(function(order) { showToast('下单成功!订单号:' + order.id); })
    .catch(function(err) { showToast('下单失败:' + err.message, 'error'); });
技术点 对应知识 效果
apiConfig 是 IIFE 返回的对象 闭包 + 作用域 配置私有,只能通过方法修改
options = options || {} ES5 默认参数惯用法 调用方可省略第二参数
request 返回 Promise 函数返回值 链式调用 .then().catch()
统一错误处理 高阶函数封装 业务层不再写重复的错误逻辑

21.4 购物车状态管理——闭包模块模式实战

业务背景:购物车是电商系统最核心的功能之一,需要管理商品列表、数量、总价,并对外提供操作接口。使用闭包模块模式,可以在不依赖任何框架的情况下实现可靠的状态封装。

业务价值

  • 购物车数据完全私有,防止被外部代码意外篡改
  • 所有操作通过方法调用,行为可预期、可追踪
  • 可在任意框架(原生 JS / Vue / React)中复用核心逻辑
/**
 * 购物车模块
 * 综合运用:IIFE、闭包、对象方法、return 多值、作用域
 */
var ShoppingCart = (function() {
    // ── 私有状态(外部无法直接访问)──
    var _items  = [];     // 商品列表:[{ id, name, price, qty }]
    var _userId = null;   // 当前用户 ID(用于区分不同用户的购物车)

    // ── 私有辅助函数 ──
    function findItemIndex(productId) {
        for (var i = 0; i < _items.length; i++) {
            if (_items[i].id === productId) return i;
        }
        return -1; // 未找到返回 -1
    }

    function calcTotal() {
        return _items.reduce(function(sum, item) {
            return sum + item.price * item.qty;
        }, 0);
    }

    // ── 公开接口 ──
    return {
        /**
         * 初始化购物车(登录后调用)
         * @param {number} userId
         */
        init: function(userId) {
            _userId = userId;
            _items  = [];
            console.log('购物车已为用户', userId, '初始化');
        },

        /**
         * 添加商品(商品已存在则增加数量)
         * @param {{ id, name, price }} product
         * @param {number} [qty=1]
         */
        addItem: function(product, qty) {
            qty = qty === undefined ? 1 : qty;

            var idx = findItemIndex(product.id);
            if (idx !== -1) {
                _items[idx].qty += qty; // 商品已在购物车,累加数量
            } else {
                _items.push({ id: product.id, name: product.name,
                              price: product.price, qty: qty });
            }
        },

        /**
         * 修改商品数量(qty 为 0 则移除)
         */
        updateQty: function(productId, qty) {
            if (qty <= 0) {
                this.removeItem(productId); // 数量为 0:直接移除
                return;
            }
            var idx = findItemIndex(productId);
            if (idx !== -1) _items[idx].qty = qty;
        },

        /**
         * 移除商品
         */
        removeItem: function(productId) {
            var idx = findItemIndex(productId);
            if (idx !== -1) _items.splice(idx, 1);
        },

        /** 清空购物车 */
        clear: function() { _items = []; },

        /**
         * 获取购物车摘要(只读副本,防止外部修改内部状态)
         * @returns {{ items: Array, total: number, count: number }}
         */
        getSummary: function() {
            return {
                items: _items.map(function(item) {
                    return { id: item.id, name: item.name,
                             price: item.price, qty: item.qty,
                             subtotal: item.price * item.qty };
                }), // 返回副本,不暴露原始引用
                total: calcTotal(),
                count: _items.reduce(function(sum, item) { return sum + item.qty; }, 0)
            };
        },

        /** 结算:获取订单数据并清空购物车 */
        checkout: function() {
            var order = {
                userId:    _userId,
                items:     this.getSummary().items,
                total:     calcTotal(),
                createdAt: new Date().toISOString()
            };
            this.clear(); // 下单后清空
            return order;
        }
    };
})();

// ── 业务层使用 ──
ShoppingCart.init(1001);
ShoppingCart.addItem({ id: 'A1', name: '无线耳机', price: 299 }, 1);
ShoppingCart.addItem({ id: 'B2', name: '手机壳',   price: 39  }, 2);
ShoppingCart.addItem({ id: 'A1', name: '无线耳机', price: 299 }, 1); // 累加,qty 变 2

console.log(ShoppingCart.getSummary());
// {
//   items: [
//     { id:'A1', name:'无线耳机', price:299, qty:2, subtotal:598 },
//     { id:'B2', name:'手机壳',   price:39,  qty:2, subtotal:78  }
//   ],
//   total: 676,
//   count: 4
// }

var order = ShoppingCart.checkout();
console.log('订单:', order); // 完整订单数据
console.log(ShoppingCart.getSummary().count); // 0 —— 购物车已清空

21.5 权限守卫——作用域与高阶函数的组合应用

业务背景:前端路由权限控制是中后台系统的标配需求。需要根据用户角色判断是否有权访问某个页面或执行某个操作,同时避免权限逻辑侵入业务代码。

业务价值

  • 权限检查逻辑集中管理,业务函数本身无需关心权限
  • 新增权限规则只需修改配置,不改业务逻辑
  • 高阶函数 withPermission 可装饰任意函数,像「插件」一样按需启用
// ── 权限数据(通常来自后端接口)──
var currentUser = {
    id:   1001,
    name: '张三',
    role: 'editor',                          // 角色:admin / editor / viewer
    permissions: ['read', 'write', 'export'] // 权限列表
};

// ── 权限检查工具函数(纯函数)──
function hasPermission(user, requiredPermission) {
    if (user.role === 'admin') return true;  // admin 拥有所有权限
    return user.permissions.indexOf(requiredPermission) !== -1;
}

function hasRole(user, requiredRole) {
    var roleHierarchy = { admin: 3, editor: 2, viewer: 1 };
    return (roleHierarchy[user.role] || 0) >= (roleHierarchy[requiredRole] || 0);
}

/**
 * 高阶函数:权限守卫装饰器
 * 包裹目标函数,调用前先检查权限,无权限时执行 onDenied 回调
 *
 * @param {Function} fn          - 需要保护的业务函数
 * @param {string}   permission  - 所需权限
 * @param {Function} [onDenied]  - 无权限时的回调(默认弹出提示)
 * @returns {Function} 带权限检查的新函数
 */
function withPermission(fn, permission, onDenied) {
    onDenied = onDenied || function() {
        alert('您没有权限执行此操作(需要:' + permission + ')');
    };

    return function() {              // 返回包裹后的新函数
        if (hasPermission(currentUser, permission)) {
            return fn.apply(this, arguments); // 有权限:执行原函数
        } else {
            onDenied(permission);             // 无权限:执行拒绝回调
        }
    };
}

// ── 业务函数(本身不含权限逻辑,职责单一)──
function deleteArticle(articleId) {
    console.log('删除文章:', articleId);
    // 调用 API 删除
}

function exportReport(format) {
    console.log('导出报表为:', format);
    // 生成并下载文件
}

function publishArticle(articleId) {
    console.log('发布文章:', articleId);
    // 改变文章状态为已发布
}

// ── 包裹权限守卫(一行代码给函数加权限)──
var guardedDelete  = withPermission(deleteArticle,  'delete');   // 需要 delete 权限
var guardedExport  = withPermission(exportReport,   'export');   // 需要 export 权限
var guardedPublish = withPermission(publishArticle, 'publish', function() {
    console.log('只有管理员才能发布文章,请联系管理员');          // 自定义拒绝回调
});

// ── 调用(调用方无需关心权限逻辑)──
guardedDelete(42);      // currentUser 有 delete?没有 → 弹出提示
guardedExport('PDF');   // currentUser 有 export?有 → 执行导出
guardedPublish(99);     // currentUser 有 publish?没有 → 显示自定义提示

// ── 批量给按钮绑定权限守卫 ──
var actionButtons = [
    { id: 'btn-delete',  fn: deleteArticle,  permission: 'delete'  },
    { id: 'btn-export',  fn: exportReport,   permission: 'export'  },
    { id: 'btn-publish', fn: publishArticle, permission: 'publish' }
];

actionButtons.forEach(function(btn) {
    var el = document.getElementById(btn.id);
    if (el) {
        el.addEventListener('click', withPermission(btn.fn, btn.permission));
        // 无权限时按钮变灰
        if (!hasPermission(currentUser, btn.permission)) {
            el.disabled = true;
            el.title = '权限不足:需要 ' + btn.permission;
        }
    }
});
技术点 对应知识 业务效果
withPermission 返回新函数 高阶函数 + 闭包 业务函数与权限解耦
闭包捕获 permissiononDenied 词法作用域 每个守卫有独立配置
fn.apply(this, arguments) 函数调用、arguments 完整转发调用者的参数
onDenied = onDenied || function... 默认参数(ES5 方式) 可选的自定义回调

21.6 数据格式化工具库——纯函数与组合的工程实践

业务背景:前端展示层需要大量数据格式化:金额显示、时间格式化、手机号脱敏、文件大小转换等。将这些格式化函数设计为纯函数,可以在任何组件、任何框架中直接复用,也方便单元测试。

业务价值

  • 纯函数:相同输入永远相同输出,极易测试
  • 工具函数库统一管理,全项目格式一致,改动一处全局生效
  • 组合使用:小函数 → 管道 → 复杂转换
/**
 * 前端数据格式化工具库
 * 设计原则:所有函数均为纯函数(无副作用,输入决定输出)
 */
var Formatter = (function() {

    // ── 金额格式化 ──
    function currency(amount, options) {
        options = options || {};
        var symbol   = options.symbol   !== undefined ? options.symbol   : '¥';
        var decimals = options.decimals !== undefined ? options.decimals : 2;
        var separator = options.separator !== undefined ? options.separator : ',';

        if (isNaN(amount)) return symbol + '--';

        var fixed = Number(amount).toFixed(decimals);
        var parts = fixed.split('.');
        // 整数部分每三位加分隔符
        parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, separator);
        return symbol + parts.join('.');
    }

    // ── 时间格式化 ──
    function datetime(date, format) {
        format = format || 'YYYY-MM-DD HH:mm:ss';
        var d = date instanceof Date ? date : new Date(date);
        if (isNaN(d.getTime())) return '无效时间';

        var pad = function(n) { return n < 10 ? '0' + n : String(n); };
        return format
            .replace('YYYY', d.getFullYear())
            .replace('MM',   pad(d.getMonth() + 1))
            .replace('DD',   pad(d.getDate()))
            .replace('HH',   pad(d.getHours()))
            .replace('mm',   pad(d.getMinutes()))
            .replace('ss',   pad(d.getSeconds()));
    }

    // ── 相对时间(几分钟前 / 几小时前)──
    function relativeTime(date) {
        var d       = date instanceof Date ? date : new Date(date);
        var now     = Date.now();
        var diff    = now - d.getTime(); // 毫秒差

        var second = 1000;
        var minute = 60   * second;
        var hour   = 60   * minute;
        var day    = 24   * hour;
        var week   = 7    * day;
        var month  = 30   * day;

        if (diff < minute)     return '刚刚';
        if (diff < hour)       return Math.floor(diff / minute)  + ' 分钟前';
        if (diff < day)        return Math.floor(diff / hour)    + ' 小时前';
        if (diff < week)       return Math.floor(diff / day)     + ' 天前';
        if (diff < month)      return Math.floor(diff / week)    + ' 周前';
        return datetime(d, 'YYYY-MM-DD'); // 超过一个月:显示具体日期
    }

    // ── 手机号脱敏 ──
    function maskPhone(phone) {
        var str = String(phone);
        if (str.length !== 11) return str; // 非标准手机号不处理
        return str.slice(0, 3) + '****' + str.slice(7); // 139****8888
    }

    // ── 文件大小格式化 ──
    function fileSize(bytes) {
        if (bytes === 0) return '0 B';
        var units = ['B', 'KB', 'MB', 'GB', 'TB'];
        var i = Math.floor(Math.log(bytes) / Math.log(1024));
        return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
    }

    // ── 数字缩写(1.2万 / 3.5亿)──
    function numberAbbr(num) {
        if (num < 10000)       return String(num);
        if (num < 100000000)   return (num / 10000).toFixed(1) + '万';
        return (num / 100000000).toFixed(1) + '亿';
    }

    // ── 字符串截断(防止超长文本撑破布局)──
    function truncate(str, maxLen, suffix) {
        suffix = suffix !== undefined ? suffix : '...';
        if (!str || str.length <= maxLen) return str || '';
        return str.slice(0, maxLen) + suffix;
    }

    // ── 暴露公开接口 ──
    return { currency, datetime, relativeTime, maskPhone, fileSize, numberAbbr, truncate };
})();

// ── 使用示例 ──
console.log(Formatter.currency(9876543.1));                 // ¥9,876,543.10
console.log(Formatter.currency(99.9, { symbol: '$' }));     // $99.90
console.log(Formatter.datetime(new Date(), 'YYYY/MM/DD'));  // 2026/05/10
console.log(Formatter.relativeTime(Date.now() - 3600000)); // 1 小时前
console.log(Formatter.maskPhone('13912345678'));            // 139****5678
console.log(Formatter.fileSize(1536000));                  // 1.46 MB
console.log(Formatter.numberAbbr(125000));                 // 12.5万
console.log(Formatter.truncate('这是一段很长的标题文字', 8)); // 这是一段很长...

业务场景对应关系:

函数 典型页面 没有它会怎样
currency 商品详情、订单、账单 每个组件自己写格式化,格式不统一
datetime 订单时间、评论时间 各处时间格式杂乱
relativeTime 消息通知、朋友圈、评论 只能显示绝对时间,体验差
maskPhone 收货地址、订单详情 用户隐私泄露风险
fileSize 文件管理、上传进度 显示原始字节数,用户无法理解
numberAbbr 数据看板、点赞数 10000000 比 1000万 难读得多
truncate 商品标题、列表描述 超长文本撑坏布局

二十一章知识总结

函数基础知识

防抖/节流
高频事件优化

表单验证框架
规则驱动参数设计

API 封装
统一错误处理

购物车模块
闭包状态封装

权限守卫
高阶函数装饰器

格式化工具库
纯函数组合

降低服务端压力
防止页面卡顿

验证逻辑可复用
新规则零改动

消除重复代码
集中鉴权/错误处理

数据私有安全
状态可追踪

业务与权限解耦
权限灵活配置

显示一致性
易测试易维护

业务场景 核心技术 业务价值
防抖/节流 闭包持久化 timer、高阶函数包装 减少无效请求,防止页面卡顿
表单验证 纯函数规则库、参数设计、Early Return 验证逻辑复用,零侵入业务代码
API 封装 闭包持有配置、默认参数、Promise 链 消灭重复 header/错误处理代码
购物车 IIFE 私有作用域、闭包状态、对象接口 数据安全私有,操作可控可追踪
权限守卫 高阶函数装饰器、闭包捕获权限、arguments 转发 权限与业务彻底解耦
格式化库 纯函数、默认参数、IIFE 封装 全局展示一致,极易单元测试

本文总结
本文在完整保留课堂笔记、练习题(9 题)、面试题(6 组)、配套题目与预习大纲的基础上:
补充理论:执行上下文/词法环境/调用栈/词法作用域的 ECMAScript 规范背景
代码注释:所有课堂案例(14 个)每行均有解释性注释,说明「为什么」而非「是什么」
经典场景:每节补充 2-3 个真实工程应用场景(Early Return、IIFE 封装、闭包计数器、var 循环陷阱修复等)
知识总结:每节末有总结表;全文末有三张归纳表 + 学习路径递进图
7 个可运行 HTML:含 images/figure-demo.svg 引用,每个 CSS 属性附真实产品使用场景
业务实战:防抖/节流、表单验证、API 封装、购物车、权限守卫、格式化库六大完整业务场景

学习路径:能跑的代码 → 能解释的原理 → 能规避的陷阱 → 能灵活应用的工程规范 → 能独立完成业务功能

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐