DAY_06JavaScript 函数、作用域与提升:从语言规范到工程实战(下)
八、变量提升与函数提升
8.1 理论背景:变量实例化(Variable Instantiation)
8.1.1 ECMAScript 规范中的执行上下文创建
ECMAScript 规范(ECMA-262 ES5)详细描述了执行上下文的创建过程。理解这个过程是掌握"提升"(Hoisting)的关键。
规范级两阶段模型:
① 创建阶段(Creation Phase)——建立绑定(Variable Instantiation)
当进入任何执行上下文时(全局或函数),引擎会先执行"预处理"(Preprocessing):
详细步骤:
-
扫描
var声明:- 查找当前作用域中的所有
var关键字 - 为每个
var变量创建绑定(Binding) - 初始化绑定值为
undefined - 注意:只扫描
var,不扫描let/const(它们有 TDZ)
- 查找当前作用域中的所有
-
扫描
function声明:- 查找当前作用域中的所有函数声明(
function foo() {}) - 为每个函数创建绑定
- 初始化绑定值为完整的函数对象
- 如果同名
var已存在,共用同一个绑定,用函数值覆盖
- 查找当前作用域中的所有函数声明(
-
建立词法环境:
- 创建环境记录(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]] } |
语句执行结果 |
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 fooundefined(但若同名function存在,共用绑定)🥉 无提升 let/constTDZ(暂时性死区) ⚠️ 工程建议:永远避免变量名与函数名同名,这会产生难以追踪的 Bug。
提升优先级规则图:
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: grid、place-items、radial-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→ 两位数,不补零:10、11…50- 三元运算符
条件 ? 真值 : 假值简洁替代 if-elseif (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 练习清单(来自配套练习文档)
- 用
while循环写出九九乘法表的四种形态(左下角、右下角、左上角、右上角三角) - 打印 100–200 中能被 3 或 7 整除的数
- 打印三位数(100–999)中,数位上含 3 或 7 的数
- 计算 100 的阶乘(
1 × 2 × 3 × … × 100) - 求
1! + 2! + 3! + … + 20!的值 - 求 100–999 之间的水仙花数(见第九章)
- 输出 100–200 之间所有的质数(见第九章)
- 控制台用
*打印等腰三角形(见第九章) - 页面输出 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→9j: 1→i无需补空格 标准九九乘法表(教科书形式) ② 右下三角 i: 1→9j: i→9前置补空格( (i-1)*8字符)每行从对角线位置开始 ③ 左上三角 i: 9→1j: 1→i无需补空格 标准表倒序输出 ④ 右上三角 i: 9→1j: i→9前置补空格 右上角形态,两种变换叠加 嵌套循环的核心逻辑:
外层控制「行」(i):决定输出多少行 内层控制「列」(j):决定每行输出多少个等式 关键规律:j 的起点和终点与 i 的关系决定了三角形状 - j 从 1 到 i → 左三角(每行等式数递增) - j 从 i 到 9 → 右三角(每行等式数递减,但从对角线位置开始)技术要点:
while循环改写为for循环:逻辑等价,for更简洁(初始化/条件/步进三合一)- 前置空格补齐(
pad):用字符串拼接模拟表格对齐,确保视觉上的矩形对称textContentvsinnerHTML:纯文本用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 整除,只收录一次,因为push在if判断内)练习 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 BigInt(n后缀)大整数精度问题、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)在头部插入元素,原有元素全部右移一位 变长 新数组的 lengtharr.push(2000)在尾部追加元素 变长 新数组的 lengtharr.splice(4, 0, 3000)在下标 4 处插入(第二个参数 0 表示不删除) 变长 被删除的元素数组(空数组)
arr[arr.length - 1]的计算逻辑:
- 操作后数组:
[1000, 100, 200, 300, 3000, 400, 2000],长度 = 7arr.length - 1 = 6,即最后一个元素:2000arr.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]=100,arr[2]=300函数调用与返回值 func(arr[0])实参是表达式,先求值再传入; return把结果送回表达式求值顺序 func(...) + arr[2]加法运算符两侧均先独立求值,再相加 常见误区:
- ❌ 看到
func(arr[0])误以为arr[0]被修改 → ✅ 按值传递,原数组不变- ❌ 误读下标:
arr[1]是200,arr[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 内部调用 fun2,fun2 的作用域链不会经过 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 a与function 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()转换 →NaN;NaN + 1 = NaN→a = NaN- 第2行:输出
NaNvar a = '字符串B':赋值覆盖 →a='字符串B'- 第3行:输出
'字符串B'关键:函数对象转数字(
Number(function(){})))=NaN,NaN参与任何算术运算结果还是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)0var a = 0赋值后③ fn 内 alert(a)undefinedfn 内有局部 var a,提升后遮蔽全局,值为 undefined④ fn 内 alert(a)1局部 var a = 1赋值后⑤ 全局 alert(a)0fn 内是局部变量,全局 a 未被修改 最易出错的是③:很多人以为会输出全局的
0,实则 fn 内有局部var a,提升后值为undefined,遮蔽了全局。
推演时序图:
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 变量声明选择策略
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 规范要点七条
- 默认
const,需要重绑定才用let——避免var的提升和函数作用域陷阱 - 可变参数用
...rest——arguments仅用于理解历史代码 - 函数声明 vs 表达式要清晰——需要提升用声明式;需要控制可见性用
const fn = () => {} - 理解词法作用域——闭包、模块化、异步回调都建立在此基础上
- 避免函数名与变量名同名——会触发提升覆盖的不确定行为
- 启用严格模式——让隐式全局变量赋值提前报错
- 面试中的怪写法是陷阱题——工程代码中绝对不应这样写
十五、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/if 中 var 泄漏 |
★★☆ | ★★★ |
| 变量遮蔽(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 学习路径图(掌握程度递进)
十七、下一单元知识预告(Day07 预习大纲)
| 知识点 | 核心问题 | 与本单元的联系 |
|---|---|---|
| 匿名函数 | 没有函数名的函数表达式如何使用? | 函数表达式的特殊形式,不触发函数提升 |
| IIFE | 为什么立即执行?解决什么问题? | 利用函数作用域创建独立环境,防全局污染 |
| 回调函数 | 满足哪三个条件才算回调? | 函数是一等公民的直接应用 |
| 递归函数 | 终止条件如何设计? | 依赖作用域链、调用栈,注意栈溢出 |
简要介绍:
- 匿名函数:没有函数名的函数表达式,常用于回调和 IIFE
- IIFE:
(function() { ... })(),创建独立作用域,模块化的前身 - 回调函数:满足「我定义 + 没直接调用 + 最终执行」三个条件
- 递归函数:需要基本情况(终止条件)+ 递归情况(规模递减)
十八、延伸阅读与参考资源
官方规范与文档
- ECMA-262:执行上下文、词法环境、变量实例化——提升行为的最终裁决来源
- MDN: Hoisting——提升最精炼的官方描述
- MDN: Closures——词法作用域在闭包中的经典应用
- MDN: Default parameters——ES6 默认参数的边界行为与 TDZ
- MDN: arguments——类数组对象完整说明
- MDN: Rest parameters——
...rest现代替代方案
深度社区资源
- Mozilla Hacks: ES6 In Depth - Rest parameters and defaults——设计动机深度讲解
- You Don’t Know JS: Scope & Closures——Kyle Simpson 对作用域提升的最权威开源书籍
- JavaScript.info: Variable scope, closure——配图丰富、例子清晰的现代 JS 教程
- DigitalOcean: Understanding Variables, Scope, and Hoisting——分步骤实践文章
十九、内存管理与垃圾回收(GC)
JavaScript 是一门自动管理内存的语言,但「自动」并不意味着「无忧」。理解 GC 机制,有助于写出不泄漏、不卡顿的代码,尤其是在使用闭包和事件监听器时。
19.1 执行上下文与内存生命周期
JavaScript 引擎使用**堆(Heap)存储对象(包括函数对象、数组、词法环境),使用栈(Stack)**存储执行上下文帧。
内存分配与释放的规律:
| 时机 | 分配 | 释放 |
|---|---|---|
声明变量 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();
}
内存管理黄金法则:
| 规则 | 说明 |
|---|---|
| 最小化闭包捕获范围 | 只捕获(引用)需要的变量,不要让闭包持有大型对象 |
| 配对添加/移除 | addEventListener 配 removeEventListener;setInterval 配 clearInterval |
| 使用 WeakRef / WeakMap | 需要持有对象引用但不阻止其 GC 时,用弱引用 |
| 善用开发者工具 | Chrome DevTools → Memory → Take Heap Snapshot 检查内存 |
优先用 const/let |
避免意外全局变量,严格模式是最后一道防线 |
二十、函数式编程范式核心理论
函数式编程(Functional Programming,FP)是与面向对象编程并列的主流编程范式。JavaScript 天然支持 FP 的核心特性(函数是一等公民、高阶函数、闭包)。掌握 FP 思维,能让代码更可预测、更易测试、更易组合。
20.1 纯函数与引用透明性
纯函数(Pure Function) 的两个必要条件:
- 确定性(Determinism):相同输入永远产生相同输出
- 无副作用(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 操作、网络请求)隔离到代码边界,保持核心逻辑纯净可测。
深度社区资源
- Mozilla Hacks: ES6 In Depth - Rest parameters and defaults——设计动机深度讲解
- You Don’t Know JS: Scope & Closures——Kyle Simpson 对作用域提升的最权威开源书籍
- JavaScript.info: Variable scope, closure——配图丰富、例子清晰的现代 JS 教程
- DigitalOcean: Understanding Variables, Scope, and Hoisting——分步骤实践文章
二十一、经典业务场景完整实战
本章将函数、作用域、闭包、参数设计等知识点综合运用于真实业务场景。每个场景均包含:业务背景 → 实现代码 → 技术要点标注 → 业务价值说明。
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 返回新函数 |
高阶函数 + 闭包 | 业务函数与权限解耦 |
闭包捕获 permission 和 onDenied |
词法作用域 | 每个守卫有独立配置 |
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 |
商品标题、列表描述 | 超长文本撑坏布局 |
二十一章知识总结
| 业务场景 | 核心技术 | 业务价值 |
|---|---|---|
| 防抖/节流 | 闭包持久化 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 封装、购物车、权限守卫、格式化库六大完整业务场景学习路径:能跑的代码 → 能解释的原理 → 能规避的陷阱 → 能灵活应用的工程规范 → 能独立完成业务功能
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)