前两篇我们完成了 HTML5 骨架和 CSS3 样式。本篇系统讲解 JavaScript 的基础语法,并让 TaskFlow 具备真正的数据与业务逻辑能力(下一篇接入 DOM 事件)。
目录
- [JavaScript 是什么 & 引入方式](#JavaScript 是什么 & 引入方式)
- 变量、作用域与提升
- 数据类型全解
- 类型转换
- 运算符全解
- 流程控制
- 函数全解
- 数组全解
- 对象全解
- [this 与 call / apply / bind](#this 与 call / apply / bind)
- 错误处理与严格模式
- [模块化(ES Modules)](#模块化(ES Modules))
- [项目实战:TaskFlow 数据层](#项目实战:TaskFlow 数据层)
- 本篇小结与练习
一、JavaScript 是什么 & 引入方式
JavaScript(简称 JS) 是一门动态类型、解释执行、基于原型、多范式的编程语言,是浏览器唯一的原生脚本语言,也通过 Node.js 运行在服务器端。
引入方式
html
<!-- 1. 内联脚本(不推荐) -->
<script>
console.log('hello');
</script>
<!-- 2. 外部脚本(推荐),defer 让 DOM 解析完再执行 -->
<script src="./js/main.js" defer></script>
<!-- 3. ES Module,支持 import/export -->
<script type="module" src="./js/main.js"></script>
模块脚本特性:
- 自动
defer - 默认严格模式
- 支持
import/export - 有自己的作用域(不会污染全局)
常用调试手段
javascript
console.log('普通日志');
console.info('信息');
console.warn('警告');
console.error('错误');
console.table([{ name: 'Tom', age: 18 }]);
console.time('timer');
// ... 代码
console.timeEnd('timer'); // timer: 12.34ms
console.group('分组');
console.log('组内');
console.groupEnd();
// 断言
console.assert(1 === 2, '这个断言会报错');
二、变量、作用域与提升
2.1 三种声明方式
javascript
var a = 1; // 函数作用域,可重复声明,会提升
let b = 2; // 块级作用域,不可重复声明,暂时性死区
const c = 3; // 块级作用域,必须初始化,不可重新赋值
| 特性 | var |
let |
const |
|---|---|---|---|
| 作用域 | 函数/全局 | 块级 {} |
块级 {} |
| 重复声明 | ✅ | ❌ | ❌ |
| 变量提升 | ✅ 初始化为 undefined |
✅ 但进 TDZ | ✅ 但进 TDZ |
| 必须初始化 | ❌ | ❌ | ✅ |
| 可重新赋值 | ✅ | ✅ | ❌ |
挂到 window |
是(顶层) | 否 | 否 |
现代规范 :默认用
const,需要重新赋值再改let,永远不要用var。
2.2 const 并非"内容不可变"
javascript
const user = { name: 'Tom' };
user.name = 'Jerry'; // ✅ 可以改属性
user = {}; // ❌ 报错:Assignment to constant variable
const list = [1, 2, 3];
list.push(4); // ✅ 可以
list = []; // ❌ 报错
// 想彻底冻结:
const frozen = Object.freeze({ a: 1 });
frozen.a = 2; // 静默失败(严格模式报错)
2.3 作用域(Scope)
全局作用域 :最外层,任何地方可访问。
函数作用域 :函数内部。
块级作用域 :{} 内部(let / const)。
javascript
let g = 'global';
function outer() {
let o = 'outer';
function inner() {
let i = 'inner';
console.log(g, o, i); // 都能访问(作用域链)
}
inner();
// console.log(i); // ❌ i 是 inner 的
}
outer();
// console.log(o); // ❌ o 是 outer 的
2.4 变量提升(Hoisting)与 TDZ
javascript
console.log(a); // undefined(var 提升但不赋值)
var a = 1;
console.log(b); // ❌ ReferenceError: Cannot access 'b' before initialization
let b = 2;
// 实际等价于:
var a;
console.log(a);
a = 1;
let / const 也存在提升,但被"冻结"在**暂时性死区(TDZ)**里,声明之前访问直接报错。
2.5 闭包(Closure)
闭包 = 函数 + 它定义时所处的词法作用域。函数可以"记住"并访问定义时的变量,即使外部函数已经返回。
javascript
function counter() {
let count = 0; // 外层变量
return function () { // 内层函数
count++;
return count;
};
}
const c1 = counter();
console.log(c1()); // 1
console.log(c1()); // 2
console.log(c1()); // 3
const c2 = counter(); // 独立的作用域
console.log(c2()); // 1
闭包的应用:
javascript
// 1. 数据私有化(无法从外部访问 count)
// 2. 函数工厂
function multiplier(factor) {
return (n) => n * factor;
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// 3. 经典的循环陷阱(var 版本)
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3 3 3
}
// 用 let 解决
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0 1 2
}
三、数据类型全解
3.1 两大分类
原始类型(Primitive)------ 7 种:
| 类型 | 说明 | 示例 |
|---|---|---|
string |
字符串 | 'hello'、"a"、tpl |
number |
数字(整数/浮点/NaN/Infinity) | 3.14、NaN、-0 |
boolean |
布尔 | true、false |
undefined |
未定义 | let x; → x |
null |
空 | let y = null; |
symbol |
唯一标识符(ES6) | Symbol('id') |
bigint |
大整数(ES2020) | 123n、9007199254740993n |
引用类型(Reference):
Object(含Array、Function、Date、RegExp、Map、Set等)
关键区别:
javascript
// 原始类型按值传递/比较
let a = 1;
let b = a;
b = 2;
console.log(a); // 1(a 不受影响)
// 引用类型按引用传递/比较
let obj1 = { x: 1 };
let obj2 = obj1;
obj2.x = 99;
console.log(obj1.x); // 99(同一引用)
let o1 = { x: 1 };
let o2 = { x: 1 };
console.log(o1 === o2); // false(不同引用)
console.log(0.1 + 0.2 === 0.3); // false(浮点精度问题)
3.2 typeof 与类型检测
javascript
typeof 'abc'; // 'string'
typeof 123; // 'number'
typeof true; // 'boolean'
typeof undefined; // 'undefined'
typeof null; // 'object' ⚠️ 历史遗留 bug
typeof Symbol(); // 'symbol'
typeof 123n; // 'bigint'
typeof {}; // 'object'
typeof []; // 'object' ⚠️ 数组也是对象
typeof function(){}; // 'function'(特例)
更精确的检测:
javascript
Array.isArray([]); // true
Number.isNaN(NaN); // true(比全局 isNaN 更严格)
Number.isFinite(Infinity); // false
Number.isInteger(3.0); // true
Object.prototype.toString.call([]); // '[object Array]'
Object.prototype.toString.call(null); // '[object Null]'
[] instanceof Array; // true
3.3 数字(number)
javascript
// 字面量
123
3.14
1_000_000 // 数字分隔符
0xff // 255(十六进制)
0b1010 // 10(二进制)
0o777 // 511(八进制)
1e3 // 1000
1.5e-3 // 0.0015
// 特殊值
NaN // Not a Number
Infinity / -Infinity
-0 // 负零
// 常量
Number.MAX_SAFE_INTEGER // 2^53 - 1
Number.MIN_SAFE_INTEGER
Number.EPSILON // 最小精度
Number.MAX_VALUE
// 常用方法
(3.1415).toFixed(2); // "3.14"
parseInt('42px'); // 42
parseFloat('3.14abc'); // 3.14
Number('123'); // 123
Number.isNaN(NaN); // true
NaN 特性:不等于任何值,包括它自己。
javascript
NaN === NaN; // false
Number.isNaN(NaN); // true(推荐)
3.4 字符串(string)
javascript
// 三种写法
'单引号'
"双引号"
`模板字符串,可以换行,可以 ${变量}`
// 不可变:所有"修改"都是返回新串
let s = 'abc';
s[0] = 'x'; // 静默失败
s.toUpperCase(); // 'ABC'(原串不变)
// 常用属性和方法
s.length // 3
s[0] // 'a'
s.charAt(0) // 'a'
s.charCodeAt(0) // 97
s.at(-1) // 'c'(支持负索引)
s.includes('b') // true
s.indexOf('b') // 1
s.startsWith('ab') // true
s.endsWith('bc') // true
s.slice(0, 2) // 'ab'
s.slice(-2) // 'bc'
s.substring(0, 2) // 'ab'
s.substr(0, 2) // 'ab'(已废弃)
s.split('') // ['a','b','c']
s.replace('a', 'X') // 'Xbc'
s.replaceAll('a', 'X') // 全部替换
s.trim() // 去两端空白
s.padStart(5, '0') // '00abc'
s.padEnd(5, '0') // 'abc00'
s.repeat(2) // 'abcabc'
s.concat('d') // 'abcd'
'abc'.localeCompare('abd') // -1
// 模板字符串进阶
const name = 'Tom';
const age = 18;
const msg = `我叫 ${name},今年 ${age} 岁`;
const multi = `
第一行
第二行
`;
// 标签模板
function tag(strings, ...values) {
console.log(strings); // ['你好 ', ',今年 ', ' 岁']
console.log(values); // ['Tom', 18]
}
tag`你好 ${name},今年 ${age} 岁`;
3.5 布尔与假值
假值(falsy)------ 只有 8 个:
javascript
false
0
-0
0n
""
null
undefined
NaN
其他一切为真值(truthy) ,包括 []、{}、"0"、"false"。
javascript
if ([]) console.log('空数组是真值'); // 会打印
if ('0') console.log('字符串 0 也是真值'); // 会打印
3.6 null vs undefined
javascript
let a; // undefined:系统给的"未赋值"
let b = null; // null:人为给的"空"
typeof undefined; // 'undefined'
typeof null; // 'object'
undefined == null; // true(宽松相等)
undefined === null; // false(严格相等)
约定:
undefined:变量未初始化、函数无返回值、访问不存在的属性null:主动表示"空",如let el = null(等待赋值)
3.7 Symbol
唯一且不可变,适合做对象的唯一键:
javascript
const id1 = Symbol('id');
const id2 = Symbol('id');
id1 === id2; // false
const obj = {
[id1]: '私有数据'
};
obj[id1]; // '私有数据'
// 使用场景:给对象加元数据、避免命名冲突
const SIZE = Symbol('size');
class MyArray {
[SIZE] = 0;
}
// 全局注册表
const s1 = Symbol.for('key');
const s2 = Symbol.for('key');
s1 === s2; // true
3.8 BigInt
用于超过 Number.MAX_SAFE_INTEGER 的整数:
javascript
const big = 9007199254740993n;
big + 1n; // 9007199254740994n
// ⚠️ 不能和普通 number 混算
1n + 1; // ❌ TypeError
1n + BigInt(1); // ✅
typeof big; // 'bigint'
四、类型转换
4.1 显式转换
javascript
// 转字符串
String(123); // '123'
(123).toString(); // '123'
(255).toString(2); // '11111111'(二进制)
// 转数字
Number('123'); // 123
Number(''); // 0
Number('abc'); // NaN
Number(true); // 1
Number(null); // 0
Number(undefined); // NaN
parseInt('42px', 10); // 42
parseFloat('3.14abc'); // 3.14
+'123'; // 123(一元加号)
// 转布尔
Boolean(0); // false
Boolean(''); // false
Boolean('0'); // true
Boolean([]); // true
!!'abc'; // true
4.2 隐式转换(重点)
+ 运算符:只要有一边是字符串,就变成字符串拼接。
javascript
1 + 2; // 3
'1' + 2; // '12'
1 + '2'; // '12'
1 + true; // 2
1 + null; // 1
1 + undefined; // NaN
[] + []; // ''(空串)
[] + {}; // '[object Object]'
{} + []; // '[object Object]'(不推荐记,太坑)
其他算术运算符:都尝试把两边转数字。
javascript
'6' - 1; // 5
'6' * '2'; // 12
'6' / '2'; // 3
'abc' - 1; // NaN
true + true; // 2
比较运算符:
javascript
// 宽松 ==:会做类型转换
1 == '1'; // true
0 == false; // true
null == undefined; // true
null == 0; // false(特例)
NaN == NaN; // false
'' == 0; // true
[] == 0; // true([] → '' → 0)
[] == ![]; // true(这个很坑)
// 严格 ===:不转换
1 === '1'; // false
NaN === NaN; // false
// 建议:永远用 ===
toString() 与 valueOf() :对象转原始值时,先调用 valueOf,若返回对象则再调 toString。
javascript
const obj = {
valueOf() { return 42; },
toString() { return 'hi'; }
};
console.log(obj + 1); // 43(用了 valueOf)
console.log(`${obj}`); // 'hi'(用了 toString)
4.3 显式 vs 隐式对比表
| 表达式 | 结果 | 说明 |
|---|---|---|
'5' - 1 |
4 |
隐式转数字 |
'5' + 1 |
'51' |
隐式转字符串 |
+'5' |
5 |
一元加号转数字 |
!!'abc' |
true |
双重取反转布尔 |
Boolean([]) |
true |
空数组也是真值 |
五、运算符全解
5.1 算术运算符
javascript
5 + 3; // 8
5 - 3; // 2
5 * 3; // 15
5 / 3; // 1.666...
5 % 3; // 2(取余)
5 ** 3; // 125(幂)
let x = 5;
x++; // 后加,返回 5,x 变 6
++x; // 前加,x 先变 7,返回 7
x += 5; // x = x + 5
x -= 3;
x *= 2;
x /= 2;
x %= 4;
x **= 2;
取余的应用:
javascript
// 判断奇偶
n % 2 === 0;
// 循环索引
const colors = ['red', 'green', 'blue'];
colors[i % colors.length];
// 时间格式化
const seconds = 3661;
const h = Math.floor(seconds / 3600);
const m = Math.floor(seconds % 3600 / 60);
const s = seconds % 60;
5.2 比较运算符
javascript
3 > 2; // true
3 >= 3; // true
2 < 3; // true
2 <= 2; // true
// 用 === 和 !==
1 === '1'; // false
1 !== '1'; // true
对象比较:比较引用地址。
javascript
{} === {}; // false
[] === []; // false
const a = {};
const b = a;
a === b; // true
5.3 逻辑运算符
javascript
a && b; // a 真返回 b,a 假返回 a
a || b; // a 真返回 a,a 假返回 b
!a; // 取反
// 短路求值(重要)
const user = { name: 'Tom' };
user && user.name; // 'Tom'
user?.name ?? '匿名'; // 空值合并
// 默认值(旧的 || 陷阱)
const count = 0;
const n1 = count || 10; // 10(0 被当成假值)
const n2 = count ?? 10; // 0(推荐)
// 条件执行
isLogin && showWelcome();
isLogin || redirectToLogin();
5.4 空值合并 ?? 与可选链 ?.
javascript
// ??:只有 null / undefined 才用默认值
null ?? 'default'; // 'default'
undefined ?? 'default'; // 'default'
0 ?? 'default'; // 0
'' ?? 'default'; // ''
false ?? 'default'; // false
// ?.:安全访问,避免 undefined 报错
const user = { profile: { name: 'Tom' } };
user?.profile?.name; // 'Tom'
user?.address?.city; // undefined(不报错)
// 可选链调用
fn?.(); // fn 存在才调用
obj.method?.(); // method 存在才调用
// 可选链索引
arr?.[0];
map?.[key];
5.5 位运算符(了解)
javascript
5 & 3; // 1(按位与)
5 | 3; // 7(按位或)
5 ^ 3; // 6(按位异或)
~5; // -6(按位非)
5 << 1; // 10(左移)
5 >> 1; // 2(右移)
5 >>> 1; // 2(无符号右移)
实用技巧:
javascript
// 快速取整
~~3.7; // 3
3.7 | 0; // 3
3.7 >> 0; // 3
// 快速乘 2 的幂
n << 1; // n * 2
n << 3; // n * 8
5.6 三元运算符
javascript
const age = 18;
const label = age >= 18 ? '成年' : '未成年';
// 嵌套(慎用,可读性差)
const level = score >= 90 ? 'A'
: score >= 80 ? 'B'
: score >= 60 ? 'C'
: 'D';
5.7 逗号运算符与展开
javascript
// 逗号:取最后一个值
let a = (1, 2, 3); // a = 3
// 展开运算符
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; // [1,2,3,4,5]
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // { a:1, b:2, c:3 }
// 剩余参数
function sum(...nums) {
return nums.reduce((s, n) => s + n, 0);
}
sum(1, 2, 3, 4); // 10
// 解构
const [first, ...rest] = [1, 2, 3, 4]; // first=1, rest=[2,3,4]
const { name, ...others } = { name: 'Tom', age: 18 }; // others={age:18}
六、流程控制
6.1 if / else if / else
javascript
const score = 85;
if (score >= 90) {
console.log('优秀');
} else if (score >= 60) {
console.log('及格');
} else {
console.log('不及格');
}
6.2 switch
javascript
const day = 3;
switch (day) {
case 1:
case 2:
case 3:
case 4:
case 5:
console.log('工作日');
break;
case 6:
case 7:
console.log('周末');
break;
default:
console.log('无效');
}
// 用严格相等判断,注意 fallthrough(不写 break 会继续执行下一个 case)
现代替代:用对象映射更清晰。
javascript
const actions = {
add: () => console.log('add'),
remove: () => console.log('remove'),
default: () => console.log('unknown'),
};
(actions[type] ?? actions.default)();
6.3 for 循环
javascript
// 基本 for
for (let i = 0; i < 5; i++) {
console.log(i); // 0 1 2 3 4
}
// 倒序
for (let i = 5; i > 0; i--) { }
// 多个变量
for (let i = 0, j = 10; i < j; i++, j--) { }
// 无限循环(配合 break)
for (;;) {
if (condition) break;
}
6.4 while / do...while
javascript
// 先判断后执行
let i = 0;
while (i < 5) {
console.log(i);
i++;
}
// 先执行后判断(至少执行一次)
let j = 10;
do {
console.log(j);
j++;
} while (j < 5); // 打印一次 10
6.5 for...in vs for...of
javascript
// for...in:遍历对象的可枚举键(字符串),也遍历数组的索引
const obj = { a: 1, b: 2 };
for (const key in obj) {
console.log(key, obj[key]); // a 1, b 2
}
// 遍历数组不推荐用 for...in(会拿到字符串索引)
for (const i in ['x', 'y']) {
console.log(typeof i); // string
}
// for...of:遍历可迭代对象(数组、字符串、Map、Set、NodeList)
for (const v of [1, 2, 3]) {
console.log(v); // 1 2 3
}
for (const ch of 'abc') {
console.log(ch); // a b c
}
// 获取索引
for (const [i, v] of ['a', 'b'].entries()) {
console.log(i, v);
}
| 循环 | 适用 | 遍历内容 |
|---|---|---|
for |
需要索引/步长 | 任意 |
for...in |
对象 | 键名 |
for...of |
数组、字符串、可迭代 | 值 |
6.6 break / continue / label
javascript
for (let i = 0; i < 10; i++) {
if (i === 3) continue; // 跳过本次
if (i === 7) break; // 跳出整个循环
console.log(i);
}
// label:跳出嵌套循环
outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === 1 && j === 1) break outer;
console.log(i, j);
}
}
七、函数全解
7.1 三种定义方式
javascript
// 1. 函数声明(会提升)
function add(a, b) {
return a + b;
}
// 2. 函数表达式(不提升)
const add2 = function (a, b) {
return a + b;
};
// 3. 箭头函数(ES6,无自身 this)
const add3 = (a, b) => a + b;
const square = n => n * n; // 单参数可省括号
const noop = () => {}; // 无参数
const getObj = () => ({ a: 1 }); // 返回对象需包一层括号
函数声明的提升:
javascript
console.log(add(1, 2)); // 3(正常)
function add(a, b) { return a + b; }
console.log(sub(3, 1)); // ❌ TypeError: sub is not a function
var sub = function (a, b) { return a - b; };
7.2 参数
javascript
// 默认参数
function greet(name = '游客') {
return `你好,${name}`;
}
greet(); // '你好,游客'
greet(undefined); // '你好,游客'
greet(null); // '你好,null'
// 剩余参数(必须放最后)
function sum(...nums) {
return nums.reduce((s, n) => s + n, 0);
}
sum(1, 2, 3); // 6
// arguments(旧写法,不推荐)
function oldSum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
// 解构参数
function createUser({ name, age = 18, role = 'user' } = {}) {
return { name, age, role };
}
createUser({ name: 'Tom' }); // { name:'Tom', age:18, role:'user' }
7.3 返回值
javascript
function fn() {
return 42; // 结束函数并返回值
}
function noReturn() {
// 隐式返回 undefined
}
// 返回多个值用对象或数组
function minMax(arr) {
return { min: Math.min(...arr), max: Math.max(...arr) };
}
const { min, max } = minMax([3, 1, 4, 1, 5]);
// 箭头函数的隐式返回
const double = n => n * 2;
7.4 箭头函数 vs 普通函数
| 特性 | 普通函数 | 箭头函数 |
|---|---|---|
this |
调用时决定 | 定义时所在作用域 |
arguments |
有 | 无(用 ...rest) |
new 调用 |
✅ 可当构造函数 | ❌ 报错 |
prototype |
有 | 无 |
yield |
可作生成器 | 不可 |
| 适合 | 方法、构造函数 | 回调、简单逻辑 |
javascript
const obj = {
name: 'Tom',
// 普通函数:this 指向 obj
greet() {
console.log(this.name);
},
// 箭头函数:this 指向外层(可能是 window 或 undefined)
greetArrow: () => {
console.log(this.name);
}
};
obj.greet(); // 'Tom'
obj.greetArrow(); // undefined
7.5 回调函数(Callback)
javascript
function fetchData(callback) {
setTimeout(() => {
const data = { id: 1, name: 'Tom' };
callback(data);
}, 1000);
}
fetchData((data) => console.log(data));
// 回调地狱(后续用 Promise / async 解决)
step1((r1) => {
step2(r1, (r2) => {
step3(r2, (r3) => { });
});
});
7.6 高阶函数
接收函数或返回函数的函数。
javascript
// 接收
[1, 2, 3].map(n => n * 2);
// 返回
function logger(prefix) {
return (msg) => console.log(`[${prefix}] ${msg}`);
}
const log = logger('INFO');
log('启动成功'); // [INFO] 启动成功
// 组合
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const addOne = n => n + 1;
const double = n => n * 2;
compose(addOne, double)(5); // (5*2)+1 = 11
7.7 纯函数与副作用
纯函数:相同输入永远得到相同输出,且不修改外部状态。
javascript
// 纯
function add(a, b) { return a + b; }
// 不纯(修改了外部数组)
let total = 0;
function addToTotal(n) {
total += n;
return total;
}
// 不纯(有副作用)
function log(msg) {
console.log(msg);
}
为什么强调纯函数:可预测、易测试、可缓存、利于并行。
7.8 IIFE(立即执行函数表达式)
javascript
(function () {
const privateVar = 'only inside';
console.log('立即执行');
})();
// 箭头版
(() => {
console.log('箭头版 IIFE');
})();
// 用途:创建独立作用域、避免污染全局
7.9 递归
javascript
function factorial(n) {
if (n <= 1) return 1; // 终止条件
return n * factorial(n - 1); // 递归调用
}
factorial(5); // 120
// 深拷贝示例
function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (map.has(obj)) return map.get(obj); // 处理循环引用
const clone = Array.isArray(obj) ? [] : {};
map.set(obj, clone);
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
clone[key] = deepClone(obj[key], map);
}
}
return clone;
}
7.10 防抖与节流
javascript
// 防抖:等停止操作后再执行一次
function debounce(fn, delay = 300) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 节流:固定时间内只执行一次
function throttle(fn, interval = 300) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn.apply(this, args);
}
};
}
// 使用
const onSearch = debounce((kw) => console.log('搜索', kw), 500);
const onScroll = throttle(() => console.log('滚动'), 200);
八、数组全解
8.1 创建与访问
javascript
const arr1 = [1, 2, 3];
const arr2 = new Array(3); // [empty × 3]
const arr3 = Array.of(3); // [3]
const arr4 = Array.from('abc'); // ['a','b','c']
const arr5 = Array.from({ length: 5 }, (_, i) => i); // [0,1,2,3,4]
arr1[0]; // 1
arr1.at(-1); // 3(负索引)
arr1.length; // 3
arr1.length = 2; // 截断:只剩 [1,2]
8.2 增删改(会修改原数组)
javascript
const arr = [1, 2, 3];
// 尾部
arr.push(4); // 返回新长度 4,[1,2,3,4]
arr.pop(); // 返回 4,[1,2,3]
// 头部
arr.unshift(0); // 返回新长度 4,[0,1,2,3]
arr.shift(); // 返回 0,[1,2,3]
// 任意位置
arr.splice(1, 1); // 从索引 1 删 1 个,返回 [2],arr=[1,3]
arr.splice(1, 0, 'a'); // 索引 1 插入 'a',不删,arr=[1,'a',3]
arr.splice(1, 1, 'b'); // 替换,arr=[1,'b',3]
// 排序与反转
[3, 1, 2].sort(); // [1,2,3](默认按字符串)
[10, 2, 33].sort(); // [10,2,33](⚠️ 字符串排序)
[10, 2, 33].sort((a, b) => a - b); // [2,10,33](数字排序)
[1, 2, 3].reverse(); // [3,2,1]
// 填值
[1, 2, 3].fill(0); // [0,0,0]
[1, 2, 3].fill(0, 1, 2); // [1,0,3]
// 复制到自身
[1, 2, 3, 4].copyWithin(0, 2); // [3,4,3,4]
8.3 不修改原数组的方法
javascript
const arr = [1, 2, 3];
// 拼接(返回新数组)
arr.concat([4, 5]); // [1,2,3,4,5]
[...arr, 4, 5]; // 同上(推荐)
// 截取
arr.slice(1); // [2,3]
arr.slice(1, 2); // [2]
arr.slice(-2); // [2,3]
// 转字符串
arr.join('-'); // '1-2-3'
arr.toString(); // '1,2,3'
// 查找
arr.indexOf(2); // 1
arr.lastIndexOf(2); // 1
arr.includes(2); // true
arr.find(n => n > 1); // 2(返回第一个匹配值)
arr.findIndex(n => n > 1); // 1
arr.findLast(n => n > 1); // 3
arr.findLastIndex(n => n > 1); // 2
8.4 遍历与转换(重点)
javascript
const nums = [1, 2, 3, 4, 5];
// forEach:遍历,无返回值
nums.forEach((n, i) => console.log(i, n));
// map:映射,返回等长新数组
const doubled = nums.map(n => n * 2); // [2,4,6,8,10]
// filter:过滤,返回符合条件的
const evens = nums.filter(n => n % 2 === 0); // [2,4]
// reduce:归约,把数组变成一个值
const sum = nums.reduce((acc, n) => acc + n, 0); // 15
const max = nums.reduce((a, b) => a > b ? a : b); // 5
// reduce 统计
const words = ['a', 'b', 'a', 'c', 'b', 'a'];
const count = words.reduce((acc, w) => {
acc[w] = (acc[w] || 0) + 1;
return acc;
}, {}); // { a:3, b:2, c:1 }
// reduceRight:从右往左
[1, 2, 3].reduceRight((a, b) => a + b); // 6
// some / every
nums.some(n => n > 4); // true(任一满足)
nums.every(n => n > 0); // true(全部满足)
// flat / flatMap
[1, [2, [3, [4]]]].flat(); // [1,2,[3,[4]]]
[1, [2, [3, [4]]]].flat(Infinity); // [1,2,3,4]
[1, 2].flatMap(n => [n, n * 2]); // [1,2,2,4]
// 新方法(ES2023)
[1, 2, 3, 4].findLast(n => n < 4); // 3
[1, 2, 3].toSorted((a, b) => b - a); // [3,2,1](不修改原数组)
[1, 2, 3].toReversed(); // [3,2,1]
[1, 2, 3].with(0, 99); // [99,2,3]
8.5 链式调用(实战重点)
javascript
const users = [
{ name: 'Tom', age: 25, active: true },
{ name: 'Jerry', age: 30, active: false },
{ name: 'Alice', age: 22, active: true },
{ name: 'Bob', age: 35, active: true },
];
// 需求:找活跃用户,按年龄升序,取出名字,用逗号连接
const result = users
.filter(u => u.active)
.sort((a, b) => a.age - b.age)
.map(u => u.name)
.join(', ');
console.log(result); // 'Alice, Tom, Bob'
// 分组统计
const byAge = users.reduce((groups, u) => {
const key = u.age >= 30 ? '30+' : '30以下';
(groups[key] ??= []).push(u);
return groups;
}, {});
8.6 数组解构与展开
javascript
const [a, b, ...rest] = [1, 2, 3, 4, 5];
// a=1, b=2, rest=[3,4,5]
// 交换变量
let x = 1, y = 2;
[x, y] = [y, x];
// 跳过
const [, second] = [1, 2, 3]; // second=2
// 默认值
const [p = 10] = []; // p=10
// 展开
Math.max(...[3, 1, 4, 1, 5]); // 5
[...new Set([1, 1, 2, 2])]; // [1,2](去重技巧)
九、对象全解
9.1 创建对象
javascript
// 1. 字面量(推荐)
const user = {
name: 'Tom',
age: 18,
'full-name': 'Tom Smith', // 特殊字符加引号
greet() { return `hi ${this.name}`; },
get info() { return `${this.name}, ${this.age}`; }, // getter
set nickname(v) { this._nick = v; }, // setter
};
// 2. 构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
const p = new Person('Tom', 18);
// 3. Object.create(指定原型)
const proto = { greet() { return 'hi'; } };
const obj = Object.create(proto);
obj.greet(); // 'hi'
// 4. class(语法糖)
class User {
#private = 'secret'; // 私有字段(ES2022)
constructor(name) { this.name = name; }
greet() { return `hi ${this.name}`; }
static create(name) { return new User(name); }
}
9.2 访问与修改
javascript
const user = { name: 'Tom', age: 18 };
// 点号
user.name; // 'Tom'
// 中括号(支持变量键)
const key = 'age';
user[key]; // 18
// 新增/修改
user.email = 'tom@example.com';
user['phone'] = '13800000000';
// 删除
delete user.email;
// 检查是否存在
'name' in user; // true
user.hasOwnProperty('name'); // true(只查自身)
Object.hasOwn(user, 'name'); // true(ES2022,推荐)
// 访问不存在的属性
user.unknown; // undefined
9.3 遍历
javascript
const user = { name: 'Tom', age: 18 };
// 键
Object.keys(user); // ['name','age']
// 值
Object.values(user); // ['Tom', 18]
// 键值对
Object.entries(user); // [['name','Tom'],['age',18]]
// for...in
for (const key in user) {
console.log(key, user[key]);
}
// 遍历 entries
for (const [k, v] of Object.entries(user)) {
console.log(k, v);
}
9.4 合并与拷贝
javascript
const a = { x: 1, y: 2 };
const b = { y: 3, z: 4 };
// 浅合并
Object.assign({}, a, b); // { x:1, y:3, z:4 }
{ ...a, ...b }; // 同上(推荐)
// 浅拷贝
const shallow = { ...a };
Object.assign({}, a);
const arrClone = [...arr];
// 深拷贝
const deep1 = structuredClone(obj); // 现代浏览器原生(推荐)
const deep2 = JSON.parse(JSON.stringify(obj)); // 有局限:丢失函数、undefined、Symbol
9.5 解构
javascript
const user = { name: 'Tom', age: 18, role: 'admin' };
// 基础
const { name, age } = user;
// 重命名
const { name: userName, age: userAge } = user;
// 默认值
const { name, city = 'Beijing' } = user;
// 剩余
const { name: n, ...rest } = user; // rest={age, role}
// 嵌套
const data = { user: { profile: { name: 'Tom' } } };
const { user: { profile: { name: nm } } } = data;
// 参数解构
function render({ name, age = 0 }) {
return `${name}(${age})`;
}
render(user);
9.6 常用 Object 静态方法
javascript
const obj = { a: 1, b: 2 };
Object.keys(obj); // ['a','b']
Object.values(obj); // [1,2]
Object.entries(obj); // [['a',1],['b',2]]
Object.fromEntries([['a',1],['b',2]]); // { a:1, b:2 }
Object.assign({}, obj); // 浅拷贝
Object.freeze(obj); // 冻结(不可增删改)
Object.isFrozen(obj); // true
Object.seal(obj); // 密封(可改不可增删)
Object.isSealed(obj); // true
Object.getPrototypeOf(obj);
Object.setPrototypeOf(obj, null);
Object.hasOwn(obj, 'a'); // true
Object.getOwnPropertyNames(obj); // 含不可枚举
Object.getOwnPropertyDescriptor(obj, 'a');
9.7 Map 与 Set
Map:键可以是任意类型,有序。
javascript
const map = new Map();
map.set('name', 'Tom');
map.set(1, 'one');
map.set({ id: 1 }, 'object key');
map.get('name'); // 'Tom'
map.has('name'); // true
map.size; // 3
map.delete('name');
map.clear();
// 遍历
for (const [k, v] of map) { }
map.forEach((v, k) => { });
// 从数组构造
const m = new Map([['a', 1], ['b', 2]]);
const obj = Object.fromEntries(m);
Set:唯一值集合。
javascript
const set = new Set([1, 2, 2, 3]); // Set { 1, 2, 3 }
set.add(4);
set.has(2); // true
set.size; // 4
set.delete(1);
// 去重(最常用)
const unique = [...new Set([1, 1, 2, 2, 3])]; // [1,2,3]
// 集合运算
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
const union = new Set([...a, ...b]); // {1,2,3,4}
const intersect = new Set([...a].filter(x => b.has(x))); // {2,3}
const diff = new Set([...a].filter(x => !b.has(x))); // {1}
9.8 JSON
javascript
const user = { name: 'Tom', age: 18, hobbies: ['code'] };
// 序列化
const str = JSON.stringify(user);
// '{"name":"Tom","age":18,"hobbies":["code"]}'
// 美化输出
JSON.stringify(user, null, 2);
// 反序列化
const parsed = JSON.parse(str);
// 陷阱:丢失 undefined、函数、Symbol;Date 变字符串
JSON.stringify({ a: undefined, b: () => {}, c: Symbol() }); // '{}'
// 安全解析
function safeParse(str, fallback = null) {
try { return JSON.parse(str); } catch { return fallback; }
}
十、this 与 call / apply / bind
10.1 this 的五种指向
javascript
// 1. 全局作用域:window(浏览器) / undefined(模块、严格模式)
console.log(this);
// 2. 对象方法:调用者对象
const obj = {
name: 'Tom',
greet() { console.log(this.name); }
};
obj.greet(); // 'Tom'
// 3. 普通函数调用:window / undefined
function fn() { console.log(this); }
fn();
// 4. 构造函数:新创建的对象
function Person() { this.name = 'Tom'; }
const p = new Person();
console.log(p.name); // 'Tom'
// 5. 事件监听:绑定事件的元素
button.addEventListener('click', function () {
console.log(this); // button 元素
});
// 箭头函数:继承外层 this
const obj2 = {
name: 'Tom',
greet: () => console.log(this.name) // 外层(window/undefined)
};
10.2 改变 this:call / apply / bind
javascript
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const user = { name: 'Tom' };
// call:参数逐个传
greet.call(user, '你好', '!'); // '你好, Tom!'
// apply:参数以数组传
greet.apply(user, ['你好', '!']); // '你好, Tom!'
// bind:返回绑定了 this 的新函数(不立即执行)
const boundGreet = greet.bind(user, '你好');
boundGreet('!'); // '你好, Tom!'
手写 bind(理解原理):
javascript
Function.prototype.myBind = function (context, ...args) {
const fn = this;
return function (...args2) {
return fn.apply(context, [...args, ...args2]);
};
};
10.3 常见 this 陷阱与解决
javascript
const counter = {
count: 0,
start() {
// ❌ this 丢失
setInterval(function () {
this.count++; // this 是 window
}, 1000);
// ✅ 方案一:箭头函数
setInterval(() => {
this.count++; // 继承外层 this
}, 1000);
// ✅ 方案二:bind
setInterval(function () {
this.count++;
}.bind(this), 1000);
// ✅ 方案三:缓存 this
const self = this;
setInterval(function () {
self.count++;
}, 1000);
}
};
十一、错误处理与严格模式
11.1 try / catch / finally
javascript
try {
JSON.parse('invalid json');
} catch (err) {
console.error('解析失败:', err.message);
} finally {
console.log('无论成功失败都执行');
}
可省略 catch(现代):
javascript
try {
doSomething();
} catch {
// 不关心错误对象
fallback();
}
11.2 抛出错误
javascript
function divide(a, b) {
if (b === 0) {
throw new Error('除数不能为 0');
}
return a / b;
}
// 自定义错误类型
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
try {
throw new ValidationError('年龄必须大于 0', 'age');
} catch (e) {
if (e instanceof ValidationError) {
console.log(`字段 ${e.field}: ${e.message}`);
}
}
11.3 错误类型
| 类型 | 触发场景 |
|---|---|
Error |
基类 |
TypeError |
类型错误,如 undefined.foo |
ReferenceError |
引用未声明变量 |
RangeError |
数值超出范围 |
SyntaxError |
语法错误(一般捕获不到) |
URIError |
URI 编解码错误 |
11.4 严格模式
javascript
'use strict'; // 必须放在脚本/函数第一行
// 主要变化:
// 1. 未声明变量直接赋值 → 报错
// 2. this 在普通函数中是 undefined(不是 window)
// 3. 禁止删除变量、禁止参数重名
// 4. 禁止八进制字面量 0123
// 5. 禁止给只读属性赋值
ES Module 默认严格模式,无需手动声明。
十二、模块化(ES Modules)
12.1 基本用法
导出(math.js):
javascript
// 具名导出
export const PI = 3.14;
export function add(a, b) { return a + b; }
export class Calculator { }
// 统一导出
const multiply = (a, b) => a * b;
const divide = (a, b) => a / b;
export { multiply, divide };
// 重命名导出
export { multiply as mul };
// 默认导出(一个模块只能一个)
export default function main() { }
// 或
// export default { ... }
导入(main.js):
javascript
// 具名导入
import { PI, add } from './math.js';
// 重命名
import { add as sum } from './math.js';
// 全部导入
import * as math from './math.js';
math.add(1, 2);
// 默认导入
import main from './math.js';
// 混合
import main, { add } from './math.js';
// 副作用导入(只执行模块)
import './polyfill.js';
// 动态导入(返回 Promise)
const module = await import('./heavy.js');
12.2 在 HTML 中使用
html
<!-- 必须 type="module" -->
<script type="module" src="./js/main.js"></script>
<!-- 也可以内联 -->
<script type="module">
import { add } from './js/math.js';
console.log(add(1, 2));
</script>
12.3 关键规则
- 路径必须写完整(包括
.js后缀),或用打包工具 import必须在模块顶层(静态)- 需通过 HTTP 服务器访问(
file://协议会被 CORS 拦截)
十三、项目实战:TaskFlow 数据层
现在实现 TaskFlow 的数据与业务逻辑,为下一篇的 DOM 事件铺路。
13.1 目录结构
task-manager/
├── index.html
├── css/
│ ├── reset.css
│ ├── variables.css
│ └── main.css
└── js/
├── constants.js # 常量
├── utils.js # 工具函数
├── storage.js # 持久化
├── task.js # Task 模型与业务逻辑
├── taskService.js # 服务层(CRUD)
└── main.js # 入口
13.2 js/constants.js
javascript
// 集中管理,避免魔法字符串
export const STORAGE_KEY = 'taskflow.tasks';
export const THEME_KEY = 'taskflow.theme';
export const PRIORITY = Object.freeze({
LOW: 'low',
NORMAL: 'normal',
HIGH: 'high',
});
export const FILTER = Object.freeze({
ALL: 'all',
ACTIVE: 'active',
DONE: 'done',
});
export const MAX_TITLE_LENGTH = 80;
export const SCHEMA_VERSION = 1;
13.3 js/utils.js
javascript
/** 生成短 ID */
export function uid() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}
/** 格式化日期:2025-01-20 → 1月20日 */
export function formatDate(iso) {
if (!iso) return '';
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return '';
const today = new Date();
const isToday = d.toDateString() === today.toDateString();
if (isToday) return '今天';
return `${d.getMonth() + 1}月${d.getDate()}日`;
}
/** 判断是否逾期 */
export function isOverdue(iso) {
if (!iso) return false;
const d = new Date(iso);
d.setHours(23, 59, 59, 999); // 当天结束前不算逾期
return d < new Date();
}
/** 防抖 */
export function debounce(fn, delay = 300) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
/** 排序:优先级 → 截止日期 */
export function sortTasks(tasks) {
const order = { high: 0, normal: 1, low: 2 };
return [...tasks].sort((a, b) => {
// 已完成沉底
if (a.done !== b.done) return a.done ? 1 : -1;
// 优先级
const po = order[a.priority] - order[b.priority];
if (po !== 0) return po;
// 截止日期近的靠前
if (a.due && b.due) return new Date(a.due) - new Date(b.due);
if (a.due) return -1;
if (b.due) return 1;
// 创建时间
return b.createdAt - a.createdAt;
});
}
/** 统计 */
export function summarize(tasks) {
const total = tasks.length;
const done = tasks.filter(t => t.done).length;
return {
total,
done,
active: total - done,
percent: total === 0 ? 0 : Math.round((done / total) * 100),
};
}
13.4 js/storage.js
javascript
import { STORAGE_KEY, SCHEMA_VERSION } from './constants.js';
/**
* 本地持久化封装
* 数据结构:{ version, tasks: [] }
*/
export const storage = {
load() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
const data = JSON.parse(raw);
// 兼容旧版本:直接是数组
if (Array.isArray(data)) return data;
// 版本不符时,走迁移逻辑
if (data.version !== SCHEMA_VERSION) {
return migrate(data);
}
return Array.isArray(data.tasks) ? data.tasks : [];
} catch (err) {
console.warn('读取本地数据失败:', err);
return [];
}
},
save(tasks) {
try {
const payload = { version: SCHEMA_VERSION, tasks };
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
return true;
} catch (err) {
// 可能超出配额(QuotaExceededError)
console.error('保存本地数据失败:', err);
return false;
}
},
clear() {
localStorage.removeItem(STORAGE_KEY);
},
};
/** 数据迁移:把旧结构转成新结构 */
function migrate(oldData) {
const tasks = Array.isArray(oldData) ? oldData : (oldData.tasks ?? []);
return tasks.map(task => ({
id: task.id ?? String(Date.now()),
title: task.title ?? '',
priority: task.priority ?? 'normal',
due: task.due ?? '',
done: Boolean(task.done),
createdAt: task.createdAt ?? Date.now(),
}));
}
13.5 js/task.js
javascript
import { PRIORITY, MAX_TITLE_LENGTH } from './constants.js';
import { uid } from './utils.js';
/**
* Task 模型:负责创建、校验、转换
* 用工厂函数而非 class,便于序列化
*/
export function createTask({ title, priority = PRIORITY.NORMAL, due = '' }) {
const cleanTitle = String(title ?? '').trim();
if (!cleanTitle) {
throw new ValidationError('任务标题不能为空', 'title');
}
if (cleanTitle.length > MAX_TITLE_LENGTH) {
throw new ValidationError(`标题不能超过 ${MAX_TITLE_LENGTH} 个字符`, 'title');
}
if (!Object.values(PRIORITY).includes(priority)) {
throw new ValidationError('优先级不合法', 'priority');
}
return {
id: uid(),
title: cleanTitle,
priority,
due: due || '',
done: false,
createdAt: Date.now(),
};
}
/** 更新任务(返回新对象,不修改原对象) */
export function updateTask(task, patch) {
const next = { ...task, ...patch };
if (patch.title !== undefined) {
next.title = String(patch.title).trim();
if (!next.title) throw new ValidationError('标题不能为空', 'title');
if (next.title.length > MAX_TITLE_LENGTH) {
throw new ValidationError(`标题不能超过 ${MAX_TITLE_LENGTH} 字`, 'title');
}
}
if (patch.priority !== undefined &&
!Object.values(PRIORITY).includes(patch.priority)) {
throw new ValidationError('优先级不合法', 'priority');
}
return next;
}
/** 自定义错误类型 */
export class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
13.6 js/taskService.js
javascript
import { storage } from './storage.js';
import { createTask, updateTask } from './task.js';
import { FILTER } from './constants.js';
/**
* 服务层:管理任务集合状态,暴露纯业务方法
* 不涉及 DOM,可单测
*/
export function createTaskService() {
let tasks = storage.load();
const listeners = new Set();
/** 通知订阅者(发布订阅模式) */
function emit() {
storage.save(tasks);
listeners.forEach(fn => fn(tasks));
}
return {
/** 订阅变化,返回取消订阅函数 */
subscribe(fn) {
listeners.add(fn);
fn(tasks); // 立即推送一次
return () => listeners.delete(fn);
},
/** 获取全部(拷贝,防止外部直接改内部数组) */
getAll() {
return [...tasks];
},
/** 按条件过滤 */
getByFilter(filter) {
switch (filter) {
case FILTER.ACTIVE: return tasks.filter(t => !t.done);
case FILTER.DONE: return tasks.filter(t => t.done);
default: return [...tasks];
}
},
/** 新增 */
add(data) {
const task = createTask(data);
tasks = [task, ...tasks];
emit();
return task;
},
/** 修改 */
update(id, patch) {
const index = tasks.findIndex(t => t.id === id);
if (index === -1) return null;
tasks = tasks.map(t => (t.id === id ? updateTask(t, patch) : t));
emit();
return tasks[index];
},
/** 切换完成状态 */
toggle(id) {
const task = tasks.find(t => t.id === id);
if (!task) return null;
return this.update(id, { done: !task.done });
},
/** 删除 */
remove(id) {
const before = tasks.length;
tasks = tasks.filter(t => t.id !== id);
if (tasks.length !== before) emit();
return tasks.length !== before;
},
/** 清除已完成 */
clearDone() {
const before = tasks.length;
tasks = tasks.filter(t => !t.done);
if (tasks.length !== before) emit();
return before - tasks.length;
},
/** 全部清空 */
clearAll() {
tasks = [];
emit();
},
};
}
13.7 js/main.js(当前仅做自测)
javascript
import { createTaskService } from './taskService.js';
import { summarize, sortTasks } from './utils.js';
import { FILTER } from './constants.js';
const service = createTaskService();
// 订阅:数据变化时打印(下一篇替换为 DOM 渲染)
service.subscribe((tasks) => {
const stats = summarize(tasks);
console.table(sortTasks(tasks).map(t => ({
标题: t.title,
优先级: t.priority,
截止: t.due,
完成: t.done ? '✅' : '⬜',
})));
console.log('统计:', stats);
});
// ============ 自测 ============
console.group('TaskFlow 数据层自测');
// 1. 新增
const t1 = service.add({ title: '学习 HTML5', priority: 'high', due: '2025-01-20' });
const t2 = service.add({ title: '阅读 CSS3 文档', priority: 'normal' });
const t3 = service.add({ title: '练习 JavaScript', priority: 'low', due: '2025-02-01' });
console.log('新增 3 条:', service.getAll().length);
// 2. 切换状态
service.toggle(t2.id);
console.log('切换后已完成数:', summarize(service.getAll()).done);
// 3. 修改
service.update(t1.id, { title: '学完 HTML5 全部知识点', priority: 'high' });
// 4. 过滤
console.log('进行中:', service.getByFilter(FILTER.ACTIVE).map(t => t.title));
console.log('已完成:', service.getByFilter(FILTER.DONE).map(t => t.title));
// 5. 校验错误
try {
service.add({ title: ' ' });
} catch (e) {
console.warn('校验生效:', e.name, e.message, '字段:', e.field);
}
// 6. 删除
service.remove(t3.id);
console.log('删除后:', service.getAll().length);
// 7. 清除已完成
service.clearDone();
console.log('清除已完成:', service.getAll().length);
console.groupEnd();
13.8 用到的知识点索引
| 知识点 | 出现位置 |
|---|---|
const / let |
全局 |
| 模板字符串 | utils.js 的 formatDate |
| 箭头函数 | 各处回调 |
| 默认参数 | createTask({ title, priority = ... }) |
| 解构赋值 | const { version, tasks } = data |
| 剩余 / 展开 | { ...task, ...patch }、[...tasks] |
可选链 ?. |
data.tasks ?? [] |
空值合并 ?? |
raw ?? '' |
Set |
listeners = new Set() |
Map 式思维 |
Object.freeze 常量 |
| 数组方法 | filter/map/some/find/sort/reduce |
| 对象方法 | Object.entries / Object.values / Object.freeze |
try/catch |
存储读写 |
| 自定义 Error | ValidationError extends Error |
| 闭包 | createTaskService 里的 tasks |
| 发布订阅 | subscribe / emit |
| ES Module | 全部文件 import/export |
| 严格模式 | 模块默认严格 |
| 短路求值 | data.tasks ?? [] |
| 高阶函数 | subscribe(fn) |
| 不可变更新 | updateTask 返回新对象 |
13.9 运行与调试
因为用了 ES Module,必须通过 HTTP 服务访问:
方式一:VS Code + Live Server
- 装插件 Live Server
- 右键
index.html→ Open with Live Server
方式二:Node 内置 http-server
bash
npx http-server . -p 8080 -o
方式三:Python
bash
python3 -m http.server 8080
打开 http://localhost:8080,F12 看控制台,会输出:
TaskFlow 数据层自测
┌─────────┬──────────────────────────┬─────────┬───────────┬──────┐
│ (index) │ 标题 │ 优先级 │ 截止 │ 完成 │
├─────────┼──────────────────────────┼─────────┼───────────┼──────┤
│ 0 │ '学完 HTML5 全部知识点' │ 'high' │ '2025-01-20' │ '⬜' │
│ 1 │ '练习 JavaScript' │ 'low' │ '2025-02-01' │ '⬜' │
│ 2 │ '阅读 CSS3 文档' │ 'normal'│ '' │ '✅' │
└─────────┴──────────────────────────┴─────────┴───────────┴──────┘
统计: { total: 3, done: 1, active: 2, percent: 33 }
在 DevTools → Application → Local Storage → http://localhost:8080 可看到:
json
{
"taskflow.tasks": "{\"version\":1,\"tasks\":[...]}"
}
刷新页面,数据自动恢复 ------ 这就是持久化闭环。
十四、本篇小结与练习
知识点回顾
| 模块 | 核心 |
|---|---|
| 引入方式 | defer / type="module" |
| 变量 | const 优先、块级作用域、TDZ、闭包 |
| 数据类型 | 7 种原始 + 引用、typeof 局限 |
| 类型转换 | + 拼接、- 转数字、== 陷阱、永远用 === |
| 运算符 | ??、?.、展开、解构、位运算技巧 |
| 流程控制 | if/switch/for/for...in/for...of |
| 函数 | 声明提升、参数、箭头、回调、高阶、纯函数、防抖节流 |
| 数组 | 会改/不改原数组方法、map/filter/reduce 链式 |
| 对象 | 创建、遍历、合并、拷贝、Map/Set、JSON |
| this | 5 种指向、call/apply/bind |
| 错误处理 | try/catch/finally、自定义错误 |
| 模块化 | import/export、动态导入 |
动手练习
- 基础 :写一个
groupBy(arr, fn),把数组按规则分组,返回对象。 - 进阶 :手写
debounce、throttle、deepClone,并用console.time对比性能。 - 挑战 :给
taskService增加"撤销/重做"功能(用栈保存历史状态),并用subscribe打印每次变化。
常见坑清单
- ❌ 用
==比较 → 隐式转换埋雷 - ❌
for...in遍历数组 → 拿到字符串索引 - ❌ 箭头函数当对象方法 →
this丢失 - ❌
JSON.parse不 try/catch → 崩溃 - ❌ 直接改数组 / 对象 → 引用污染
- ❌
sort()不传比较函数 → 字符串排序 - ❌
const arr = []; arr.push()以为能改 → 其实可以,但arr = []不行 - ❌ 用
var循环里创建闭包 → 全变最后值 - ❌ 忘记
export或路径不带.js→ 模块报错 - ❌ 用
file://打开模块脚本 → CORS 拦截
下一篇预告
第四篇:JavaScript 进阶(DOM / BOM / 事件)
- DOM 树结构、节点类型、遍历
- 选择元素的全部方法(
querySelector家族) - 元素的增删改查、
classList、dataset、style - 表单操作、
FormData、原生校验 API - 事件模型(捕获 / 冒泡)、事件对象、
addEventListener选项 - 事件委托(性能关键)
- 常用事件:click / input / change / submit / keydown / scroll / resize...
- 定时器、
requestAnimationFrame - BOM:
location/history/navigator/screen - 实战:把 TaskFlow 的数据层接到 UI 上,实现完整增删改查 + 主题切换 + 弹窗编辑
准备好让页面真正"活"起来。🚀