第三篇:JavaScript 基础语法全解 + TaskFlow 逻辑实现

前两篇我们完成了 HTML5 骨架和 CSS3 样式。本篇系统讲解 JavaScript 的基础语法,并让 TaskFlow 具备真正的数据与业务逻辑能力(下一篇接入 DOM 事件)。


目录

  1. [JavaScript 是什么 & 引入方式](#JavaScript 是什么 & 引入方式)
  2. 变量、作用域与提升
  3. 数据类型全解
  4. 类型转换
  5. 运算符全解
  6. 流程控制
  7. 函数全解
  8. 数组全解
  9. 对象全解
  10. [this 与 call / apply / bind](#this 与 call / apply / bind)
  11. 错误处理与严格模式
  12. [模块化(ES Modules)](#模块化(ES Modules))
  13. [项目实战:TaskFlow 数据层](#项目实战:TaskFlow 数据层)
  14. 本篇小结与练习

一、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

  1. 装插件 Live Server
  2. 右键 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、动态导入

动手练习

  1. 基础 :写一个 groupBy(arr, fn),把数组按规则分组,返回对象。
  2. 进阶 :手写 debounce、throttle、deepClone,并用 console.time 对比性能。
  3. 挑战 :给 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 上,实现完整增删改查 + 主题切换 + 弹窗编辑

准备好让页面真正"活"起来。🚀

相关推荐
after you1 小时前
【无标题】
前端·javascript
酷炫码神1 小时前
Day 6|ES6+ 语法:把现代 JS 写顺手,从解构到 map / filter / reduce
前端·javascript·es6
晓得迷路了2 小时前
栗子前端技术周刊第 150 期 - Stack Overflow 开发者调查结果、TanStack Start 1.0...
javascript·css·后端
TYC92 小时前
Mac创建Vue3+TypeScript或者React+TypeScript项目
前端·javascript·typescript
持敬chijing12 小时前
白盒测试与代码审计-js登录逆向实战
javascript·网络·安全·web安全·网络安全·网络攻击模型
优选资源分享13 小时前
PicGo v3.0.3 新版 | 开源免费图床工具
javascript·笔记·electron
ShineWinsu14 小时前
对于JavaScript:JavaScript基础语法的解析
开发语言·前端·javascript·面试·web·基础语法·循环
普通人614 小时前
php+html+canvas实现树状打开的结构
开发语言·javascript·html5·1024程序员节
猫头_15 小时前
AI 流式传输工程指南:模型吐出半截 JSON,前端如何边收边渲染?
前端·javascript·llm