语言核心基础

  • 语言核心基础
    • 变量与数据类型
      • 变量声明
        • JavaScript 有三种声明变量的关键字,它们的核心区别在于作用域和可变性
        • var
          • var 是 ES5 时代的声明方式,它的作用域是函数级别的,而不是块级的。
          • 这意味着在 if 或 for 的大括号里用 var 声明的变量,在外面依然可以访问
          • 另外 var 存在"变量提升"(hoisting)——声明会被提升到函数顶部,但赋值不会,所以在声明前访问会得到 undefined 而不是报错
          function example() {
              console.log(a); // undefined(不会报错,因为声明被提升了)
              var a = 10;
          
              if (true) {
                  var b = 20;
              }
              console.log(b); // 20(var 没有块级作用域)
          }
          
        • let
          • let 是 ES6 引入的,拥有块级作用域,不会被提升到可访问状态(存在"暂时性死区" TDZ),在声明前访问会直接报 ReferenceError。
          function example() {
              // console.log(a); // ReferenceError!
              let a = 10;
          
              if (true) {
                  let b = 20;
              }
              // console.log(b); // ReferenceError! b 只存在于 if 块内
          }
          
        • const
          • const 和 let 一样有块级作用域和 TDZ,但它要求声明时必须赋值,且绑定不可重新赋值。
          • 注意"不可重新赋值"不等于"不可变"——如果 const 指向一个对象或数组,对象内部的属性是可以修改的。
          const PI = 3.14159;
          // PI = 3; // TypeError: Assignment to constant variable
          
          const user = { name: 'An' };
          user.name = 'Bob'; // ✅ 没问题,修改的是对象属性,不是重新赋值
          // user = {};      // ❌ TypeError,不能重新绑定
          
      • 七种原始类型
        • JavaScript 的数据类型分为原始类型和引用类型。
        • 原始类型有 7 种,它们的值是不可变的,按值传递。
        • string 字符串

          const s1 = 'hello';         // 单引号
          const s2 = "world";         // 双引号
          const s3 = `你好 ${s1}`;    // 模板字符串,支持插值和换行
          
          • 字符串是不可变的。s1[0] = ‘H’ 不会报错但也不会生效。所有字符串方法都返回新字符串。
        • number 数字
          • JavaScript 只有一种数字类型,使用 IEEE 754 双精度浮点数。这带来了经典问题
          console.log(0.1 + 0.2);           // 0.30000000000000004
          console.log(0.1 + 0.2 === 0.3);   // false
          
          // 特殊值
          console.log(Infinity);    // 正无穷
          console.log(-Infinity);   // 负无穷
          console.log(NaN);         // Not a Number
          console.log(NaN === NaN); // false!NaN 不等于自身
          console.log(Number.isNaN(NaN)); // true,用这个判断
          
        • bigint 大整数
          • 当数字超过 Number.MAX_SAFE_INTEGER(2⁵³ - 1)时使用
          const big = 123456789012345678901234567890n; // 末尾加 n
          console.log(typeof big); // "bigint"
          // big + 1;  // ❌ TypeError,bigint 不能和 number 混合运算
          big + 1n;    // ✅
          
        • boolean 布尔值
          • 只有 true 和 false。但在条件判断中,其他类型会被隐式转换为布尔值。
          • 记住这些 falsy 值(转换为 false 的值):
          // 以下全部是 falsy
          false, 0, -0, 0n, "", null, undefined, NaN
          
          // 其他一切都是 truthy,包括:
          // "0", " ", [], {}, function(){}  都是 truthy!
          
        • null 空值
          • 表示"有意为空",通常用于显式地标记一个变量"目前没有值"。
          let user = null; // 明确表示:当前没有用户
          
        • undefined 未定义
          • 表示变量已声明但未赋值,或者函数没有返回值
          let x;
          console.log(x); // undefined
          
          function foo() {}
          console.log(foo()); // undefined
          
        • symbol 符号
          • ES6 引入,每个 Symbol 都是唯一的,主要用作对象属性的唯一标识符
          const id1 = Symbol('id');
          const id2 = Symbol('id');
          console.log(id1 === id2); // false,每次调用都创建新的
          
          const obj = {
              [id1]: '这个属性名是唯一的'
          };
          
      • 引用类型(Reference Types)
        • 除了原始类型,剩下的都是引用类型(对象)。
        • 包括普通对象 {}、数组 []、函数 function、Date、RegExp 等等。
        • 引用类型按引用传递,变量存的是内存地址而不是值本身
        const a = { name: 'An' };
        const b = a;        // b 和 a 指向同一个对象
        b.name = 'Bob';
        console.log(a.name); // "Bob" —— a 也被改了!
        
        // 数组同理
        const arr1 = [1, 2, 3];
        const arr2 = arr1;
        arr2.push(4);
        console.log(arr1); // [1, 2, 3, 4]
        
      • typeof 操作符
        • typeof 返回一个字符串,表示值的类型
        typeof 42;          // "number"
        typeof 'hello';     // "string"
        typeof true;        // "boolean"
        typeof undefined;   // "undefined"
        typeof Symbol();    // "symbol"
        typeof 10n;         // "bigint"
        
        // ⚠️ 两个著名的"坑"
        typeof null;        // "object" —— 历史遗留 bug,null 其实是原始类型
        typeof function(){};// "function" —— 虽然函数也是对象
        typeof [];          // "object"
        typeof {};          // "object"
        
        • 判断数组应该用 Array.isArray(),判断 null 用 === null
      • 类型转换
        • 显式转换

          // 转字符串
          String(123);     // "123"
          (123).toString();// "123"
          
          // 转数字
          Number("42");    // 42
          Number("");      // 0
          Number("hello"); // NaN
          Number(true);    // 1
          Number(null);    // 0
          Number(undefined);// NaN
          parseInt("42px");// 42 —— 会尽量解析
          parseFloat("3.14abc"); // 3.14
          
          // 转布尔
          Boolean(0);      // false
          Boolean("");     // false
          Boolean("0");    // true ← 注意!非空字符串都是 true
          Boolean([]);     // true ← 注意!空数组也是 true
          
        • 隐式转换

          // + 号:只要有一边是字符串,就做字符串拼接
          '5' + 3;      // "53"
          5 + '3';      // "53"
          
          // 其他算术运算符会把字符串转为数字
          '5' - 3;      // 2
          '5' * 2;      // 10
          '5' / '2';    // 2.5
          
          // == 的隐式转换(不推荐使用)
          0 == false;    // true
          "" == false;   // true
          null == undefined; // true
          null == 0;     // false ← 特殊规则
          
          // === 严格相等,不做类型转换(推荐始终使用)
          0 === false;   // false
          "" === false;  // false
          
    • 运算符与流程控制
      • 算术运算符
        • 基本的 +、-、*、/ 之外,还有几个需要注意的
        // 取余(求模)
        console.log(10 % 3);  // 1
        console.log(-10 % 3); // -1(结果符号跟被除数一致)
        
        // 幂运算(ES2016)
        console.log(2 ** 10); // 1024,等价于 Math.pow(2, 10)
        
        // 自增/自减
        let a = 5;
        console.log(a++); // 5(先返回再加)
        console.log(a);   // 6
        console.log(++a); // 7(先加再返回)
        
        • 复合赋值运算符就是简写:+=、-=、*=、/=、%=、**=、??=、||=、&&=
        let count = 10;
        count += 5;  // 等价于 count = count + 5
        count **= 2; // 等价于 count = count ** 2
        
        // 逻辑赋值(ES2021)
        let name = null;
        name ??= 'Anonymous'; // 如果 name 是 null/undefined,才赋值
        console.log(name);     // "Anonymous"
        
        let config = '';
        config ||= 'default';  // 如果 config 是 falsy,就赋值
        console.log(config);    // "default"
        
      • 比较运算符
        • ==(宽松相等) 会在比较前做隐式类型转换,规则非常复杂:

          0 == false;         // true(false 转为 0)
          '' == false;        // true(两边都转为 0)
          '1' == 1;           // true(字符串转为数字)
          null == undefined;  // true(特殊规则,它俩只跟对方相等)
          null == 0;          // false(null 不会被转为数字)
          NaN == NaN;         // false(NaN 不等于任何东西包括自己)
          [] == false;        // true([] → '' → 0,false → 0)
          [] == ![];          // true(这个最离谱,但确实是 true)
          
        • ===(严格相等) 不做类型转换,类型不同直接返回 false:

          0 === false;   // false(number vs boolean)
          '' === false;  // false
          '1' === 1;     // false
          null === undefined; // false
          
        • 实际开发原则:永远使用 === 和 !==。
        • 唯一可能用 == 的场景是 x == null 来同时判断 null 和 undefined
        • 更推荐写成 x === null || x === undefined,或者用 x ?? defaultValue
      • 逻辑运算符与短路求值
        • 逻辑运算符在 JS 中的行为和很多语言不同——它们返回的不是布尔值,而是操作数本身。
        • &&(逻辑与):如果左侧是 falsy,返回左侧;否则返回右侧。

          console.log(0 && 'hello');     // 0(左侧 falsy,直接返回左侧)
          console.log(1 && 'hello');     // "hello"(左侧 truthy,返回右侧)
          console.log('a' && 'b' && 0); // 0(逐个检查,遇到第一个 falsy 就返回)
          console.log('a' && 'b' && 'c'); // "c"(全 truthy,返回最后一个)
          
          // 实际用法:条件执行
          user && user.login(); // 如果 user 存在才调用 login
          
        • ||(逻辑或):如果左侧是 truthy,返回左侧;否则返回右侧。

          console.log(0 || 'default');   // "default"
          console.log('hello' || 'default'); // "hello"
          
          // 经典用法:提供默认值
          const port = config.port || 3000;
          // ⚠️ 陷阱:如果 config.port 是 0,也会被跳过!
          
        • ??(空值合并,ES2020):只有左侧是 null 或 undefined 时才返回右侧。这解决了 || 的陷阱。

          const port = config.port ?? 3000;
          // config.port 为 0 → 返回 0 ✅
          // config.port 为 '' → 返回 '' ✅
          // config.port 为 null/undefined → 返回 3000
          
          // 对比
          0 || 3000;  // 3000(0 是 falsy)
          0 ?? 3000;  // 0(0 不是 null/undefined)
          '' || 'default'; // "default"
          '' ?? 'default'; // ""
          
        • ! (逻辑非)

          console.log(!0);    // true
          console.log(!'');   // true
          console.log(!null); // true
          
          // 双重取反 !! 可以将任意值转为布尔值
          console.log(!!0);       // false
          console.log(!!'hello'); // true
          console.log(!!null);    // false
          
      • 可选链与其他实用运算符
        • ?.(可选链,Optional Chaining):安全地访问嵌套属性,遇到 null/undefined 时短路返回 undefined 而不是报错。
        const user = {
            name: 'An',
            address: {
                city: 'Tokyo'
            }
        };
        
        // 不用可选链
        const zip = user && user.address && user.address.zip;
        
        // 用可选链
        const zip2 = user?.address?.zip;   // undefined,不报错
        const nothing = null?.foo?.bar;     // undefined
        
        // 也可以用在方法调用和下标访问上
        user.greet?.();       // 如果 greet 方法存在才调用
        const first = arr?.[0]; // 如果 arr 存在才取下标
        
        • 逗号运算符:依次执行所有表达式,返回最后一个。用得不多,偶尔在 for 循环中见到
        const result = (1, 2, 3); // result === 3
        for (let i = 0, j = 10; i < j; i++, j--) { /* ... */ }
        
      • 条件语句
        • if/else if/else
        const score = 85;
        
        if (score >= 90) {
            console.log('优秀');
        } else if (score >= 70) {
            console.log('良好');
        } else if (score >= 60) {
            console.log('及格');
        } else {
            console.log('不及格');
        }
        
        • 三元表达式:适合简单的二选一,不要嵌套太深
        const status = score >= 60 ? '及格' : '不及格';
        
        // ❌ 不推荐:嵌套三元,可读性很差
        const grade = score >= 90 ? 'A' : score >= 70 ? 'B' : score >= 60 ? 'C' : 'D';
        
        // ✅ 这种情况用 if/else 或函数更清晰
        
        • switch:适合对同一个值做多重精确匹配,注意 break 不能忘
        const day = 'Monday';
        
        switch (day) {
            case 'Monday':
            case 'Tuesday':
            case 'Wednesday':
            case 'Thursday':
            case 'Friday':
                console.log('工作日');
                break;   // ← 不写 break 会"穿透"到下一个 case
            case 'Saturday':
            case 'Sunday':
                console.log('周末');
                break;
            default:
                console.log('无效日期');
        }
        
        • switch 使用 === 进行比较,不会做隐式类型转换。
      • 循环语句
        • for 循环:最经典的循环,适合需要精确控制索引的场景:
        for (let i = 0; i < 5; i++) {
            console.log(i); // 0, 1, 2, 3, 4
        }
        
        // 倒序遍历
        for (let i = arr.length - 1; i >= 0; i--) {
            console.log(arr[i]);
        }
        
        • while 与 do…while:
        // while:先判断再执行
        let n = 5;
        while (n > 0) {
            console.log(n);
            n--;
        }
        
        // do...while:先执行一次再判断,至少执行一次
        let input;
        do {
            input = prompt('请输入密码');
        } while (input !== 'secret');
        
        • for…of:遍历可迭代对象(数组、字符串、Map、Set 等)的值:
        const fruits = ['苹果', '香蕉', '橘子'];
        for (const fruit of fruits) {
            console.log(fruit); // "苹果", "香蕉", "橘子"
        }
        
        // 遍历字符串的每个字符(对中文友好)
        for (const char of '你好世界') {
            console.log(char); // "你", "好", "世", "界"
        }
        
        // 如果同时需要索引,用 entries()
        for (const [index, fruit] of fruits.entries()) {
            console.log(`${index}: ${fruit}`);
        }
        
        • for…in:遍历对象的可枚举属性名(键名):
        const user = { name: 'An', age: 25, city: 'Tokyo' };
        for (const key in user) {
            console.log(`${key}: ${user[key]}`);
        }
        
        // ⚠️ 不推荐用 for...in 遍历数组!
        // 它会遍历原型链上的属性,顺序也不保证
        // 遍历数组请用 for...of 或数组方法
        
      • break、continue 与标签

        // break:立即退出整个循环
        for (let i = 0; i < 10; i++) {
            if (i === 5) break;
            console.log(i); // 0, 1, 2, 3, 4
        }
        
        // continue:跳过本次迭代,进入下一次
        for (let i = 0; i < 10; i++) {
            if (i % 2 === 0) continue; // 跳过偶数
            console.log(i); // 1, 3, 5, 7, 9
        }
        
        // 标签(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);
            }
        }
        
    • 函数基础
      • 函数声明 vs 函数表达式
        • JavaScript 中定义函数有两种基本方式,它们最大的区别在于提升(hoisting)行为。
        • 函数声明(Function Declaration):整个函数会被提升到作用域顶部,因此可以在声明之前调用。
        // ✅ 可以在声明之前调用
        greet('An');
        
        function greet(name) {
            console.log(`你好,${name}!`);
        }
        
        • 函数表达式(Function Expression):把一个匿名或具名函数赋值给变量。变量声明会提升,但赋值不会,所以在赋值之前调用会报错。
        // ❌ TypeError: sayHi is not a function
        // sayHi('An');
        
        const sayHi = function(name) {
            console.log(`Hi, ${name}!`);
        };
        
        sayHi('An'); // ✅
        
        // 具名函数表达式:函数名只在函数内部可见,主要用于递归和调试
        const factorial = function fact(n) {
            return n <= 1 ? 1 : n * fact(n - 1);
        };
        // fact(5);      // ❌ ReferenceError,外部不可见
        factorial(5);    // ✅ 120
        
      • 箭头函数
        • ES6 引入的简洁写法,但它不仅仅是语法糖,和普通函数有几个本质区别。
        • 基本语法
        // 完整写法
        const add = (a, b) => {
            return a + b;
        };
        
        // 单个表达式可以省略 {} 和 return(隐式返回)
        const add2 = (a, b) => a + b;
        
        // 只有一个参数可以省略括号
        const double = x => x * 2;
        
        // 没有参数必须写空括号
        const getRandom = () => Math.random();
        
        // ⚠️ 返回对象字面量必须加括号,否则 {} 会被当成函数体
        const makeUser = (name) => ({ name, active: true });
        
        • 箭头函数与普通函数的区别
        // 1. 没有自己的 this,继承外层作用域的 this
        const team = {
            name: 'Frontend',
            members: ['An', 'Bob'],
        
            // ❌ 普通函数:this 指向调用者,forEach 的回调中 this 不是 team
            showBad() {
                this.members.forEach(function(member) {
                    console.log(`${member} belongs to ${this.name}`); // this.name 是 undefined
                });
            },
        
            // ✅ 箭头函数:继承 showGood 的 this,即 team
            showGood() {
                this.members.forEach(member => {
                    console.log(`${member} belongs to ${this.name}`); // "Frontend"
                });
            }
        };
        
        // 2. 没有 arguments 对象
        function normalFn() {
            console.log(arguments); // ✅ [1, 2, 3]
        }
        const arrowFn = () => {
            // console.log(arguments); // ❌ ReferenceError
        };
        normalFn(1, 2, 3);
        
        // 3. 不能用作构造函数
        const Person = (name) => { this.name = name; };
        // new Person('An'); // ❌ TypeError: Person is not a constructor
        
        // 4. 没有 prototype 属性
        
        • 回调函数、数组方法的参数优先用箭头函数
        • 对象方法用普通函数(需要自己的 this)
        • 永远不要用箭头函数定义对象方法或构造函数。
      • 参数处理
        • 默认参数(Default Parameters):

          function createUser(name, role = 'viewer', active = true) {
              return { name, role, active };
          }
          
          createUser('An');                    // { name: 'An', role: 'viewer', active: true }
          createUser('An', 'admin');           // { name: 'An', role: 'admin', active: true }
          createUser('An', undefined, false);  // { name: 'An', role: 'viewer', active: false }
          // 注意:传 undefined 会触发默认值,传 null 不会
          createUser('An', null);              // { name: 'An', role: null, active: true }
          
          // 默认值可以是表达式,甚至引用前面的参数
          function createId(prefix, timestamp = Date.now(), id = `${prefix}-${timestamp}`) {
              return id;
          }
          
        • 剩余参数(Rest Parameters):
          • 用 … 收集剩余的参数到一个真正的数组中(不同于 arguments 类数组对象):
          function sum(first, ...rest) {
              console.log(first);  // 1
              console.log(rest);   // [2, 3, 4, 5](真正的数组)
              return rest.reduce((acc, val) => acc + val, first);
          }
          
          sum(1, 2, 3, 4, 5); // 15
          
          // 剩余参数必须是最后一个参数
          // function bad(a, ...b, c) {} // ❌ SyntaxError
          
          // 搭配解构使用
          function processConfig({ host, port, ...otherOptions }) {
              console.log(host);          // "localhost"
              console.log(port);          // 3000
              console.log(otherOptions);  // { debug: true, timeout: 5000 }
          }
          
          processConfig({ host: 'localhost', port: 3000, debug: true, timeout: 5000 });
          
      • 返回值
        • 函数只能返回一个值。如果需要返回多个值,用对象或数组:
        // 不写 return 或 return 后没有值,返回 undefined
        function noReturn() {
            console.log('hello');
        }
        console.log(noReturn()); // undefined
        
        // 返回多个值的模式
        function getMinMax(arr) {
            return {
                min: Math.min(...arr),
                max: Math.max(...arr)
            };
        }
        const { min, max } = getMinMax([3, 1, 4, 1, 5, 9]);
        
        // 用数组返回,配合解构
        function divide(a, b) {
            return [Math.floor(a / b), a % b]; // [商, 余数]
        }
        const [quotient, remainder] = divide(17, 5); // 3, 2
        
        • return 换行陷阱
        function gotcha() {
            return   // ← JS 在这里自动插入分号,函数返回 undefined
            {
                name: 'An'
            };
        }
        console.log(gotcha()); // undefined!
        
        // 正确写法:左大括号跟 return 同一行
        function correct() {
            return {
                name: 'An'
            };
        }
        
      • 函数是一等公民
        • 在 JavaScript 中,函数和其他值(数字、字符串)没有区别,可以被赋值、传递、返回。
        • 赋值给变量:
        const greet = function(name) { return `Hello, ${name}`; };
        const sayHi = greet; // 函数可以像值一样赋给另一个变量
        sayHi('An');         // "Hello, An"
        
        • 作为参数传递(回调函数):
        function repeat(n, action) {
            for (let i = 0; i < n; i++) {
                action(i);
            }
        }
        
        repeat(3, console.log);          // 0, 1, 2
        repeat(3, i => console.log(i * i)); // 0, 1, 4
        
        // 数组方法中大量使用回调
        const nums = [1, 2, 3, 4, 5];
        const evens = nums.filter(n => n % 2 === 0);    // [2, 4]
        const doubled = nums.map(n => n * 2);             // [2, 4, 6, 8, 10]
        const sum = nums.reduce((acc, n) => acc + n, 0);  // 15
        
        • 作为返回值
        function multiplier(factor) {
            return (number) => number * factor; // 返回一个新函数
        }
        
        const double = multiplier(2);
        const triple = multiplier(3);
        
        double(5);  // 10
        triple(5);  // 15
        
        // 这就是闭包的雏形——返回的函数"记住"了 factor 的值
        // Day 4 会深入讲解
        
        • 存储在数据结构中:
        const strategies = {
            add: (a, b) => a + b,
            subtract: (a, b) => a - b,
            multiply: (a, b) => a * b,
        };
        
        function calculate(strategy, a, b) {
        return strategies[strategy](a, b);
        }
        
        calculate('add', 10, 5);      // 15
        calculate('multiply', 10, 5); // 50
        
      • IIFE(立即调用函数表达式)
        • IIFE 是一种定义后立即执行的函数模式。在 ES6 模块化之前,它是避免全局变量污染的主要手段。
        // 经典写法
        (function() {
            const secret = '只在这里可见';
            console.log(secret);
        })();
        // console.log(secret); // ❌ ReferenceError
        
        // 带参数
        (function(global) {
            global.myLib = { version: '1.0' };
        })(window);
        
        // 箭头函数版
        (() => {
            console.log('立即执行');
        })();
        
        // 带返回值
        const result = (() => {
            const a = 10;
            const b = 20;
            return a + b;
        })();
        console.log(result); // 30
        
        • 现在有了 ES Modules 和块级作用域(let/const),IIFE 用得少了,但在一些老代码库和特定场景(如隔离作用域执行异步代码)中仍然常见。
    • 作用域与闭包
      • 作用域的三种层级
        • 作用域决定了变量在哪里可以被访问。JavaScript 有三种作用域层级。
        • 全局作用域:在所有函数和块之外声明的变量,任何地方都能访问。在浏览器中,全局变量会挂载到 window 对象上。

          const APP_NAME = 'MyApp'; // 全局作用域
          
          function foo() {
              console.log(APP_NAME); // ✅ 可以访问
          }
          
          // 浏览器环境下
          var x = 10;
          console.log(window.x); // 10(var 声明的全局变量挂到 window)
          // let/const 声明的不会挂到 window
          let y = 20;
          console.log(window.y); // undefined
          
        • 函数作用域:在函数内部声明的变量,外部不可见。var、let、const 在函数内都有这个特性。

          function createSecret() {
              var secret1 = 'var secret';
              let secret2 = 'let secret';
              const secret3 = 'const secret';
              return 'done';
          }
          
          createSecret();
          // console.log(secret1); // ❌ ReferenceError
          // console.log(secret2); // ❌ ReferenceError
          // console.log(secret3); // ❌ ReferenceError
          
        • 块级作用域:let 和 const 在 {}(if、for、while 或单独的块)中声明时,只在该块内可见。var 没有块级作用域。

          {
              let a = 1;
              const b = 2;
              var c = 3;
          }
          // console.log(a); // ❌ ReferenceError
          // console.log(b); // ❌ ReferenceError
          console.log(c);    // 3 ✅ var 穿透了块
          
          // 经典面试题:for 循环中的 var vs let
          for (var i = 0; i < 3; i++) {
              setTimeout(() => console.log(i), 100);
          }
          // 输出:3, 3, 3(var 没有块级作用域,循环结束后 i 是 3)
          
          for (let j = 0; j < 3; j++) {
              setTimeout(() => console.log(j), 100);
          }
          // 输出:0, 1, 2(let 每次迭代创建新的块级作用域)
          
      • 作用域链
        • 当代码访问一个变量时,JS 引擎会沿着作用域链由内向外逐层查找,直到找到或到达全局作用域。
        const global = 'global';
        
        function outer() {
            const outerVar = 'outer';
        
            function middle() {
                const middleVar = 'middle';
        
                function inner() {
                    const innerVar = 'inner';
        
                    // inner 可以访问所有外层变量
                    console.log(innerVar);   // "inner"    ← 自身作用域
                    console.log(middleVar);  // "middle"   ← 父级作用域
                    console.log(outerVar);   // "outer"    ← 祖父级作用域
                    console.log(global);     // "global"   ← 全局作用域
                }
        
                inner();
                // console.log(innerVar); // ❌ 外层不能访问内层
            }
        
            middle();
        }
        
        outer();
        
      • 词法作用域
        • JavaScript 使用词法作用域(也叫静态作用域),意思是函数的作用域在定义时就确定了,而不是在调用时。
        const x = 'global';
        
        function printX() {
            console.log(x); // 这里的 x 在定义时就绑定了全局的 x
        }
        
        function wrapper() {
            const x = 'wrapper';
            printX(); // 虽然在 wrapper 内部调用,但打印的是 "global"
        }
        
        wrapper(); // "global",不是 "wrapper"
        
        • 这个特性是闭包的基础。函数"记住"的是它被创建时的环境,而不是被调用时的环境。
        function createPrinter() {
            const message = '来自 createPrinter';
        
            return function() {
                console.log(message); // 词法作用域:绑定的是 createPrinter 内的 message
            };
        }
        
        const printer = createPrinter();
        
        function someOtherPlace() {
            const message = '来自 someOtherPlace';
            printer(); // "来自 createPrinter",不受调用位置影响
        }
        
        someOtherPlace();
        
      • 闭包
        • 闭包是指一个函数能够访问其词法作用域中的变量,即使这个函数在其词法作用域之外执行。
        function createCounter() {
            let count = 0; // 这个变量被"封闭"在闭包中
        
            return function() {
                count++;
                return count;
            };
        }
        
        const counter = createCounter();
        console.log(counter()); // 1
        console.log(counter()); // 2
        console.log(counter()); // 3
        
        // count 变量从外部完全无法访问
        // console.log(count); // ❌ ReferenceError
        
        // 每次调用 createCounter 创建独立的闭包
        const counter2 = createCounter();
        console.log(counter2()); // 1(独立的 count)
        
        • 闭包的本质:createCounter 执行完后,正常来说内部变量 count 应该被垃圾回收。但因为返回的函数仍然引用着 count,JS 引擎就保留了这个变量。返回的函数 + 它引用的外部变量 = 闭包。
      • 闭包应用场景
        • 数据私有化 / 模块模式
        function createWallet(initialBalance) {
            let balance = initialBalance; // 私有变量,外部无法直接修改
        
            return {
                deposit(amount) {
                if (amount <= 0) throw new Error('金额必须大于 0');
                balance += amount;
                return balance;
                },
                withdraw(amount) {
                if (amount > balance) throw new Error('余额不足');
                balance -= amount;
                return balance;
                },
                getBalance() {
                return balance;
                }
            };
        }
        
        const wallet = createWallet(100);
        wallet.deposit(50);      // 150
        wallet.withdraw(30);     // 120
        wallet.getBalance();     // 120
        // wallet.balance;       // undefined,无法直接访问
        // balance = 999999;     // 不可能,balance 被封闭在闭包中
        
        • 工厂函数 / 函数生成器
        function createGreeter(greeting) {
            return function(name) {
                return `${greeting}, ${name}!`;
            };
        }
        
        const hello = createGreeter('Hello');
        const nihao = createGreeter('你好');
        
        hello('An');  // "Hello, An!"
        nihao('An');  // "你好, An!"
        
        // 更实用的例子:创建带预设配置的函数
        function createLogger(prefix) {
            return function(message) {
                console.log(`[${prefix}] ${new Date().toISOString()}: ${message}`);
            };
        }
        
        const dbLog = createLogger('DB');
        const apiLog = createLogger('API');
        
        dbLog('连接成功');  // [DB] 2026-04-05T...: 连接成功
        apiLog('请求收到'); // [API] 2026-04-05T...: 请求收到
        
        • 柯里化(Currying):把接受多个参数的函数,变成一系列只接受一个参数的函数
        // 普通函数
        function add(a, b, c) {
            return a + b + c;
        }
        add(1, 2, 3); // 6
        
        // 柯里化版本(手动)
        function curriedAdd(a) {
            return function(b) {
                return function(c) {
                return a + b + c; // 三层闭包,每层记住一个参数
                };
            };
        }
        
        curriedAdd(1)(2)(3); // 6
        
        // 拆开用
        const addOne = curriedAdd(1);
        const addOneAndTwo = addOne(2);
        addOneAndTwo(3); // 6
        
        // 箭头函数简写
        const curriedAdd2 = a => b => c => a + b + c;
        
        // 实际应用:事件处理器工厂
        const handleClick = (action) => (id) => (event) => {
            console.log(`Action: ${action}, ID: ${id}, Target: ${event.target}`);
        };
        
        // button.addEventListener('click', handleClick('delete')(42));
        
        • 防抖(Debounce)
        function debounce(fn, delay) {
            let timerId = null; // 闭包保存定时器 ID
        
            return function(...args) {
                clearTimeout(timerId); // 每次调用都清除上一次的定时器
                timerId = setTimeout(() => {
                fn.apply(this, args); // delay 毫秒后才真正执行
                }, delay);
            };
        }
        
        // 使用:搜索框输入时,停止输入 300ms 后才发请求
        const search = debounce((query) => {
            console.log(`搜索: ${query}`);
        }, 300);
        
        search('h');     // 被取消
        search('he');    // 被取消
        search('hel');   // 被取消
        search('hello'); // 300ms 后执行 → "搜索: hello"
        
        • 节流(Throttle)
        function throttle(fn, interval) {
            let lastTime = 0; // 闭包保存上次执行时间
        
            return function(...args) {
                const now = Date.now();
                if (now - lastTime >= interval) {
                    lastTime = now;
                    fn.apply(this, args);
                }
            };
        }
        
        // 使用:滚动事件每 200ms 最多触发一次
        // window.addEventListener('scroll', throttle(handleScroll, 200));
        
      • 对象与数组
        • 对象字面量与属性
          • 对象是 JavaScript 中最基础的数据结构,用来存储键值对。
          • 创建与属性访问:
          // 对象字面量
          const user = {
              name: 'An',
              age: 25,
              'favorite-color': 'blue', // 包含特殊字符的键名需要引号
              1: 'one'                  // 数字键会被转为字符串
          };
          
          // 点语法访问
          console.log(user.name); // "An"
          
          // 方括号访问(适用于动态键名或特殊字符)
          console.log(user['favorite-color']); // "blue"
          console.log(user[1]);                // "one"
          
          const key = 'age';
          console.log(user[key]); // 25(动态访问)
          
          • 属性简写与计算属性名(ES6):
          const name = 'An';
          const age = 25;
          
          // 属性简写:变量名和属性名相同时可以简写
          const user = { name, age };
          // 等价于 { name: name, age: age }
          
          // 方法简写
          const calculator = {
              // ES6 之前:add: function(a, b) { return a + b; }
              add(a, b) {
                  return a + b;
              },
              subtract(a, b) {
                  return a - b;
              }
          };
          
          // 计算属性名:用表达式作为键名
          const field = 'email';
          const obj = {
              [field]: 'an@example.com',              // { email: 'an@example.com' }
              [`${field}Verified`]: true,              // { emailVerified: true }
              ['get' + 'Name']() { return 'An'; }     // { getName() { return 'An'; } }
          };
          
          • 属性的增删改查:
          const user = { name: 'An' };
          
          // 增
          user.age = 25;
          user['city'] = 'Tokyo';
          
          // 改
          user.name = 'Bob';
          
          // 删
          delete user.city;
          console.log(user.city); // undefined
          
          // 查(判断属性是否存在)
          console.log('name' in user);           // true(包括原型链)
          console.log(user.hasOwnProperty('name')); // true(只查自身)
          console.log(user.toString !== undefined);  // true(原型链上的)
          console.log(user.hasOwnProperty('toString')); // false
          
        • 对象的遍历与常用静态方法

          const user = { name: 'An', age: 25, city: 'Tokyo' };
          
          // Object.keys() — 返回键名数组
          Object.keys(user);   // ['name', 'age', 'city']
          
          // Object.values() — 返回值数组
          Object.values(user);  // ['An', 25, 'Tokyo']
          
          // Object.entries() — 返回 [key, value] 二维数组
          Object.entries(user);  // [['name','An'], ['age',25], ['city','Tokyo']]
          
          // 用 for...of 配合 entries 遍历
          for (const [key, value] of Object.entries(user)) {
              console.log(`${key}: ${value}`);
          }
          
          // Object.fromEntries() — entries 的反操作
          const entries = [['a', 1], ['b', 2]];
          const obj = Object.fromEntries(entries); // { a: 1, b: 2 }
          
          // 实用场景:过滤对象属性
          const filtered = Object.fromEntries(
              Object.entries(user).filter(([key]) => key !== 'age')
          );
          // { name: 'An', city: 'Tokyo' }
          
          • Object.assign() 与冻结:
          // Object.assign() — 合并对象(浅拷贝)
          const defaults = { theme: 'light', lang: 'zh', debug: false };
          const userPrefs = { theme: 'dark', debug: true };
          const config = Object.assign({}, defaults, userPrefs);
          // { theme: 'dark', lang: 'zh', debug: true }
          // 后面的属性覆盖前面的
          
          // Object.freeze() — 冻结对象,不能增删改
          const frozen = Object.freeze({ name: 'An', age: 25 });
          frozen.name = 'Bob';  // 静默失败(严格模式下报错)
          frozen.city = 'NYC';  // 静默失败
          console.log(frozen);   // { name: 'An', age: 25 }
          
          // ⚠️ freeze 是浅冻结
          const obj = Object.freeze({ inner: { value: 1 } });
          obj.inner.value = 999; // ✅ 生效了!内层对象没有被冻结
          
          // Object.isFrozen()
          console.log(Object.isFrozen(frozen)); // true
          
        • 解构赋值 — 对象
          • 解构让你用简洁的语法从对象中提取值。
          const user = {
              name: 'An',
              age: 25,
              address: {
                  city: 'Tokyo',
                  zip: '100-0001'
              },
              hobbies: ['coding', 'reading']
          };
          
          // 基本解构
          const { name, age } = user;
          console.log(name); // "An"
          console.log(age);  // 25
          
          // 重命名
          const { name: userName, age: userAge } = user;
          console.log(userName); // "An"
          
          // 默认值
          const { name: n, role = 'viewer' } = user;
          console.log(role); // "viewer"(user 中没有 role)
          
          // 嵌套解构
          const { address: { city, zip } } = user;
          console.log(city); // "Tokyo"
          
          // 剩余属性
          const { name: nm, ...rest } = user;
          console.log(rest); // { age: 25, address: {...}, hobbies: [...] }
          
          • 函数参数中的解构(非常常用):
          // ❌ 不解构:参数含义不清晰
          function createUser(name, age, role, active) {
              // name 是第几个参数?容易搞混
          }
          
          // ✅ 解构:清晰且支持默认值、可选参数
          function createUser({ name, age, role = 'viewer', active = true } = {}) {
              return { name, age, role, active };
          }
          
          // 调用时不需要关心参数顺序
          createUser({ age: 25, name: 'An' });
          createUser({}); // 全部使用默认值
          createUser();   // 因为有 = {},不传也不报错
          
      • 数组基础与常用方法
        • 创建数组:
        const arr1 = [1, 2, 3];
        const arr2 = new Array(5);        // [empty × 5](5个空位,不推荐)
        const arr3 = Array.from({ length: 5 }, (_, i) => i); // [0, 1, 2, 3, 4]
        const arr4 = Array.of(1, 2, 3);   // [1, 2, 3]
        
        • 增删元素(会修改原数组):
        const arr = [1, 2, 3];
        
        // 尾部操作
        arr.push(4);        // [1, 2, 3, 4],返回新长度 4
        arr.pop();          // [1, 2, 3],返回被删除的元素 4
        
        // 头部操作
        arr.unshift(0);     // [0, 1, 2, 3],返回新长度 4
        arr.shift();        // [1, 2, 3],返回被删除的元素 0
        
        // splice — 万能的增删改
        const items = ['a', 'b', 'c', 'd', 'e'];
        // splice(起始索引, 删除个数, ...插入的元素)
        
        items.splice(2, 1);         // 删除:items = ['a','b','d','e'],返回 ['c']
        items.splice(1, 0, 'x');    // 插入:items = ['a','x','b','d','e']
        items.splice(1, 2, 'y','z');// 替换:items = ['a','y','z','d','e']
        
        • 不修改原数组的方法:
        const arr = [1, 2, 3, 4, 5];
        
        // slice(start, end) — 切片,左闭右开
        arr.slice(1, 3);    // [2, 3]
        arr.slice(-2);      // [4, 5](最后两个)
        arr.slice();        // [1, 2, 3, 4, 5](浅拷贝)
        
        // concat — 拼接
        arr.concat([6, 7]); // [1, 2, 3, 4, 5, 6, 7]
        
        // indexOf / lastIndexOf — 查找索引
        [1, 2, 3, 2].indexOf(2);     // 1(第一个)
        [1, 2, 3, 2].lastIndexOf(2); // 3(最后一个)
        [1, 2, 3].indexOf(99);       // -1(未找到)
        
        // includes — 是否包含
        [1, 2, 3].includes(2);   // true
        [1, 2, 3].includes(99);  // false
        
        // ⚠️ indexOf 和 includes 对引用类型无效
        const obj = { id: 1 };
        [{ id: 1 }].includes(obj); // false(不同的引用)
        
        // join — 数组转字符串
        ['a', 'b', 'c'].join('-'); // "a-b-c"
        ['a', 'b', 'c'].join('');  // "abc"
        
        // flat — 扁平化
        [1, [2, [3, [4]]]].flat();      // [1, 2, [3, [4]]](默认一层)
        [1, [2, [3, [4]]]].flat(2);     // [1, 2, 3, [4]]
        [1, [2, [3, [4]]]].flat(Infinity); // [1, 2, 3, 4](完全扁平)
        
        // at — 支持负索引(ES2022)
        const letters = ['a', 'b', 'c', 'd'];
        letters.at(0);   // "a"
        letters.at(-1);  // "d"(最后一个)
        letters.at(-2);  // "c"
        
      • 解构赋值-数组

        const rgb = [255, 128, 0];
        
        // 基本解构
        const [r, g, b] = rgb;
        console.log(r, g, b); // 255 128 0
        
        // 跳过元素
        const [first, , third] = [1, 2, 3];
        console.log(first, third); // 1 3
        
        // 默认值
        const [a, b, c, d = 0] = [1, 2, 3];
        console.log(d); // 0
        
        // 剩余元素
        const [head, ...tail] = [1, 2, 3, 4, 5];
        console.log(head); // 1
        console.log(tail); // [2, 3, 4, 5]
        
        // 交换变量(不需要临时变量)
        let x = 1, y = 2;
        [x, y] = [y, x];
        console.log(x, y); // 2 1
        
        // 从函数返回值解构
        function getCoords() {
        return [35.6762, 139.6503];
        }
        const [lat, lng] = getCoords();
        
        // 嵌套解构
        const matrix = [[1, 2], [3, 4]];
        const [[a1, a2], [b1, b2]] = matrix;
        
      • 扩展运算符(Spread Operator)
        • … 在不同上下文中扮演不同角色。用在函数参数中是"收集"(rest),用在数组/对象字面量中是"展开"(spread)
        • 数组展开
        const arr1 = [1, 2, 3];
        const arr2 = [4, 5, 6];
        
        // 合并数组
        const merged = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
        
        // 浅拷贝
        const copy = [...arr1]; // [1, 2, 3](新数组,不同引用)
        
        // 在特定位置插入
        const withInsert = [...arr1.slice(0, 1), 99, ...arr1.slice(1)];
        // [1, 99, 2, 3]
        
        // 展开字符串
        const chars = [..."hello"]; // ['h', 'e', 'l', 'l', 'o']
        
        // 配合 Math 使用
        const nums = [3, 1, 4, 1, 5, 9];
        Math.max(...nums); // 9
        
        • 对象展开
        const base = { a: 1, b: 2 };
        const extra = { b: 3, c: 4 };
        
        // 合并对象(后面覆盖前面)
        const merged = { ...base, ...extra };
        // { a: 1, b: 3, c: 4 }
        
        // 浅拷贝 + 修改部分属性(React 中常用)
        const user = { name: 'An', age: 25, city: 'Tokyo' };
        const updated = { ...user, age: 26 };
        // { name: 'An', age: 26, city: 'Tokyo' }
        
        // 条件展开
        const isAdmin = true;
        const config = {
            theme: 'dark',
            ...(isAdmin && { adminPanel: true, debugMode: true })
        };
        // isAdmin 为 true 时:{ theme: 'dark', adminPanel: true, debugMode: true }
        // isAdmin 为 false 时:{ theme: 'dark' }(false 被展开为空)
        
      • 浅拷贝与深拷贝
        • JavaScript 中引用类型赋值只是复制地址,理解拷贝层级非常重要。
        • 浅拷贝(Shallow Copy):只拷贝第一层,嵌套的对象/数组仍然是共享引用。
        const original = {
            name: 'An',
            scores: [90, 85, 95],
            address: { city: 'Tokyo' }
        };
        
        // 三种浅拷贝方式
        const copy1 = { ...original };
        const copy2 = Object.assign({}, original);
        const copy3 = structuredClone ? null : null; // 这个是深拷贝,后面说
        
        // 浅拷贝的问题:嵌套对象是共享的
        copy1.name = 'Bob';           // ✅ 不影响 original
        copy1.scores.push(100);       // ❌ original.scores 也变了!
        copy1.address.city = 'Osaka'; // ❌ original.address.city 也变了!
        
        console.log(original.scores);       // [90, 85, 95, 100]
        console.log(original.address.city); // "Osaka"
        
        • 深拷贝(Deep Copy):递归拷贝所有层级,完全独立。
        // 方法1:structuredClone(现代浏览器和 Node 17+ 原生支持,推荐)
        const deep1 = structuredClone(original);
        deep1.scores.push(100);
        deep1.address.city = 'Osaka';
        console.log(original.scores);       // [90, 85, 95] 不受影响 ✅
        console.log(original.address.city); // "Tokyo" 不受影响 ✅
        
        // 方法2:JSON 序列化(简单但有限制)
        const deep2 = JSON.parse(JSON.stringify(original));
        // ⚠️ 限制:
        //   - undefined、function、Symbol 会丢失
        //   - Date 会变成字符串
        //   - RegExp 会变成空对象
        //   - 不支持循环引用
        
        // 示例:JSON 方法的数据丢失
        const problematic = {
            fn: () => 'hello',   // 函数
            date: new Date(),     // Date
            undef: undefined,     // undefined
            regex: /abc/g         // 正则
        };
        const jsonCopy = JSON.parse(JSON.stringify(problematic));
        console.log(jsonCopy);
        // { date: "2026-04-05T...", regex: {} }
        // fn 和 undef 直接丢了,date 变成了字符串,regex 变成了空对象
        
    • 数组高阶方法
      • 什么是高阶方法
        • 高阶方法就是接受函数作为参数的方法
        • 每个方法的回调函数都接收三个参数:(当前元素, 索引, 原数组),大多数情况只用第一个。
      • map - 映射转换
        • 对数组每个元素执行回调,将返回值收集成一个等长的新数组。不修改原数组。
        const nums = [1, 2, 3, 4, 5];
        
        const doubled = nums.map(n => n * 2);
        // [2, 4, 6, 8, 10]
        
        const strings = nums.map(n => `第${n}项`);
        // ['第1项', '第2项', '第3项', '第4项', '第5项']
        
        // 实际场景:从 API 数据中提取需要的字段
        const users = [
            { id: 1, name: 'An', email: 'an@test.com', age: 25 },
            { id: 2, name: 'Bob', email: 'bob@test.com', age: 30 },
            { id: 3, name: 'Cat', email: 'cat@test.com', age: 22 }
        ];
        
        const names = users.map(u => u.name);
        // ['An', 'Bob', 'Cat']
        
        const cards = users.map(({ id, name, age }) => ({
            id,
            label: `${name} (${age}岁)`
        }));
        // [{ id: 1, label: 'An (25岁)' }, ...]
        
        // 使用索引参数
        const indexed = ['a', 'b', 'c'].map((item, i) => `${i}: ${item}`);
        // ['0: a', '1: b', '2: c']
        
        • 常见错误
        // ❌ map 中不 return(箭头函数加了 {} 就必须写 return)
        nums.map(n => { n * 2 });  // [undefined, undefined, ...]
        
        // ❌ 用 map 做副作用操作(不关心返回值时应该用 forEach)
        users.map(u => console.log(u.name)); // 能跑,但语义错误
        
        // ❌ parseInt 的经典陷阱
        ['1', '2', '3'].map(parseInt);
        // [1, NaN, NaN]
        // 原因:map 传了 (元素, 索引, 数组) 三个参数给 parseInt
        // parseInt('1', 0) → 1
        // parseInt('2', 1) → NaN(1进制无效)
        // parseInt('3', 2) → NaN(2进制没有3)
        
        // ✅ 正确写法
        ['1', '2', '3'].map(s => parseInt(s, 10)); // [1, 2, 3]
        ['1', '2', '3'].map(Number);               // [1, 2, 3]
        
      • filter-过滤筛选
        • 对数组每个元素执行回调,保留返回值为 truthy 的元素,返回一个新数组(长度 ≤ 原数组)。
        const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        
        const evens = nums.filter(n => n % 2 === 0);
        // [2, 4, 6, 8, 10]
        
        const bigNums = nums.filter(n => n > 5);
        // [6, 7, 8, 9, 10]
        
        // 过滤对象数组
        const users = [
            { name: 'An', age: 25, active: true },
            { name: 'Bob', age: 17, active: true },
            { name: 'Cat', age: 30, active: false },
            { name: 'Dan', age: 22, active: true }
        ];
        
        const activeAdults = users.filter(u => u.active && u.age >= 18);
        // [{ name: 'An', ... }, { name: 'Dan', ... }]
        
        // 移除 falsy 值
        const messy = [0, 'hello', '', null, 42, undefined, false, 'world'];
        const clean = messy.filter(Boolean);
        // ['hello', 42, 'world']
        
        // 去重(Day 5 用的 Set 更简洁,这里展示 filter 做法)
        const arr = [1, 2, 2, 3, 3, 3];
        const unique = arr.filter((item, index) => arr.indexOf(item) === index);
        // [1, 2, 3]
        
      • reduce — 归约累积
        • reduce 是最强大也最灵活的数组方法,它将数组"归约"为一个值(可以是任何类型)。
        • 语法:array.reduce((accumulator, currentValue, index, array) => {…}, initialValue)
        // 最基本:求和
        const sum = [1, 2, 3, 4, 5].reduce((acc, cur) => acc + cur, 0);
        // 执行过程:
        // acc=0, cur=1 → 1
        // acc=1, cur=2 → 3
        // acc=3, cur=3 → 6
        // acc=6, cur=4 → 10
        // acc=10, cur=5 → 15
        // 结果:15
        
        // 求最大值
        const max = [3, 1, 4, 1, 5, 9].reduce((a, b) => a > b ? a : b);
        // 没有初始值时,第一个元素作为初始值
        // 结果:9
        
      • find 与 findIndex - 查找
        • find 返回第一个满足条件的元素本身,找不到返回 undefined。
        • findIndex 返回第一个满足条件的元素索引,找不到返回 -1。
        const users = [
            { id: 1, name: 'An', role: 'dev' },
            { id: 2, name: 'Bob', role: 'admin' },
            { id: 3, name: 'Cat', role: 'dev' }
        ];
        
        const admin = users.find(u => u.role === 'admin');
        // { id: 2, name: 'Bob', role: 'admin' }
        
        const notFound = users.find(u => u.role === 'ceo');
        // undefined
        
        const adminIndex = users.findIndex(u => u.role === 'admin');
        // 1
        
        // findLast / findLastIndex(ES2023)从后往前找
        const lastDev = users.findLast(u => u.role === 'dev');
        // { id: 3, name: 'Cat', role: 'dev' }
        
      • some 与 every — 条件判断
        • some 只要有一个元素满足条件就返回 true(类似逻辑或)。
        • every 要求所有元素都满足条件才返回 true(类似逻辑与)。
        const nums = [1, 2, 3, 4, 5];
        
        nums.some(n => n > 4);   // true(5 > 4)
        nums.some(n => n > 10);  // false
        
        nums.every(n => n > 0);  // true(全部大于0)
        nums.every(n => n > 3);  // false(1, 2, 3 不满足)
        
        // 空数组的行为
        [].some(() => true);   // false(没有元素满足)
        [].every(() => false);  // true(没有元素不满足 —— 空真)
        
        // 实际场景
        const cart = [
        { name: '键盘', inStock: true },
        { name: '鼠标', inStock: true },
        { name: '显示器', inStock: false }
        ];
        
        const allInStock = cart.every(item => item.inStock);  // false
        const anyOutOfStock = cart.some(item => !item.inStock); // true
        
        // 表单验证
        const fields = [
        { name: 'email', valid: true },
        { name: 'password', valid: true },
        { name: 'phone', valid: false }
        ];
        const canSubmit = fields.every(f => f.valid); // false
        
      • sort 排序
        • sort 会修改原数组,并且默认按字符串的 Unicode 码点排序,这对数字来说通常不是你想要的。
        // ❌ 默认排序的坑
        [10, 9, 2, 21, 3].sort();
        // [10, 2, 21, 3, 9]
        // 因为 "10" < "2"(按字符串比较,'1' 的码点 < '2' 的码点)
        
        // ✅ 数字排序:提供比较函数
        // 返回负数 → a 排前面
        // 返回正数 → b 排前面
        // 返回 0 → 保持原序
        [10, 9, 2, 21, 3].sort((a, b) => a - b);
        // [2, 3, 9, 10, 21](升序)
        
        [10, 9, 2, 21, 3].sort((a, b) => b - a);
        // [21, 10, 9, 3, 2](降序)
        
        // 对象数组排序
        const users = [
        { name: 'Cat', age: 30 },
        { name: 'An', age: 25 },
        { name: 'Bob', age: 22 }
        ];
        
        // 按年龄升序
        users.sort((a, b) => a.age - b.age);
        
        // 按名字字母序
        users.sort((a, b) => a.name.localeCompare(b.name));
        // localeCompare 支持中文拼音排序:
        // ['张三','李四','王五'].sort((a,b) => a.localeCompare(b, 'zh-CN'))
        
        • 不想修改原数组? 用 toSorted()(ES2023)或先拷贝:
        const original = [3, 1, 2];
        
        // ES2023
        const sorted = original.toSorted((a, b) => a - b); // [1, 2, 3]
        console.log(original); // [3, 1, 2](不变)
        
        // 兼容写法
        const sorted2 = [...original].sort((a, b) => a - b);
        
      • flatMap — 映射 + 扁平化
        • flatMap 等价于先 map 再 flat(1),但只扁平一层。它特别适合一对多的映射场景。
        // 普通 map 产生嵌套数组
        const sentences = ['hello world', 'goodbye moon'];
        sentences.map(s => s.split(' '));
        // [['hello', 'world'], ['goodbye', 'moon']]
        
        // flatMap 自动扁平一层
        sentences.flatMap(s => s.split(' '));
        // ['hello', 'world', 'goodbye', 'moon']
        
        // 一对多:每个用户有多个标签
        const users = [
        { name: 'An', tags: ['dev', 'js'] },
        { name: 'Bob', tags: ['design', 'css'] }
        ];
        const allTags = users.flatMap(u => u.tags);
        // ['dev', 'js', 'design', 'css']
        
        // 条件性映射:过滤 + 转换一步完成
        const nums = [1, 2, 3, 4, 5, 6];
        const doubledEvens = nums.flatMap(n => n % 2 === 0 ? [n * 2] : []);
        // [4, 8, 12]
        // 偶数返回 [n*2](包含一个元素的数组),奇数返回 [](空数组被扁平掉了)
        
      • forEach — 遍历执行
        • forEach 对每个元素执行回调,没有返回值(返回 undefined)。它是纯粹用于副作用的方法。
        const users = ['An', 'Bob', 'Cat'];
        users.forEach((name, index) => {
            console.log(`${index + 1}. ${name}`);
        });
        // 1. An
        // 2. Bob
        // 3. Cat
        

面向对象与核心机制

  • 面向对象与核心机制
    • this 指向
      • 为什么 this 这么重要
        • 和大多数语言不同,JS 中 this 的值不取决于函数在哪里定义,而取决于函数怎么被调用(箭头函数除外)
      • 规则一:默认绑定
        • 当函数作为独立函数调用(不通过对象、不用 call/apply/bind、不用 new),this 指向全局对象(浏览器中是 window,Node 中是 global)。严格模式下 this 是 undefined。
        function showThis() {
            console.log(this);
        }
        
        showThis(); // 浏览器:window,Node:global
        
        // 严格模式
        function strictShow() {
            'use strict';
            console.log(this);
        }
        
        strictShow(); // undefined
        
      • 规则二:隐式绑定
        • 当函数作为对象的方法调用时(obj.method()),this 指向调用它的那个对象。
        const user = {
            name: 'An',
            greet() {
                console.log(this.name);
            }
        };
        
        user.greet(); // "An",this === user
        
        // 链式对象:this 指向最近的调用者
        const company = {
            name: 'MiniMax',
            department: {
                name: 'Agent Team',
                getName() {
                    return this.name;
                }
            }
        };
        
        company.department.getName(); // "Agent Team",不是 "MiniMax"
        // this 指向 department,因为 department 是最近的调用者
        
      • 规则三:显式绑定 — call / apply / bind
        • 当你想手动指定 this 时,使用这三个方法
        • call — 立即调用,逐个传参
        function introduce(greeting, punctuation) {
            console.log(`${greeting}, I'm ${this.name}${punctuation}`);
        }
        
        const user = { name: 'An' };
        
        introduce.call(user, 'Hello', '!');
        // "Hello, I'm An!"
        
        // 第一个参数是 this 的值,后面依次是函数参数
        
        • apply — 立即调用,参数以数组形式传入
        introduce.apply(user, ['Hi', '~']);
        // "Hi, I'm An~"
        
        // call 和 apply 的唯一区别就是参数传递方式
        // 助记:Apply 用 Array
        
        // 经典用法:借用方法
        const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
        const realArray = Array.prototype.slice.call(arrayLike);
        // ['a', 'b', 'c']
        
        // 求数组最大值(ES5 时代)
        Math.max.apply(null, [3, 1, 4, 1, 5]); // 5
        // 现在用展开运算符更好:Math.max(...[3, 1, 4, 1, 5])
        
        • bind — 返回新函数,永久绑定 this(不立即调用):
        const user = { name: 'An' };
        
        function greet(greeting) {
        console.log(`${greeting}, ${this.name}`);
        }
        
        const greetAn = greet.bind(user);
        greetAn('Hello');     // "Hello, An"
        greetAn('Hey');       // "Hey, An"
        
        // bind 也可以预设参数(偏函数/Partial Application)
        const helloAn = greet.bind(user, 'Hello');
        helloAn(); // "Hello, An"(greeting 已经被绑定为 'Hello')
        
        // 解决回调中 this 丢失的问题
        setTimeout(user.greet.bind(user), 100); // ✅ 不会丢失 this
        
        // bind 返回的函数,this 不可被再次修改
        const bound = greet.bind(user);
        bound.call({ name: 'Bob' }, 'Hi'); // "Hi, An"(仍然是 An,bind 的优先级更高)
        
      • 规则四:new 绑定
        • 当用 new 调用函数时,JS 会自动创建一个新对象,将 this 绑定到这个新对象上。
        function User(name, age) {
            // new 调用时,this 自动指向新创建的空对象 {}
            this.name = name;
            this.age = age;
            // 不需要 return,自动返回 this
        }
        
        const user = new User('An', 25);
        console.log(user); // User { name: 'An', age: 25 }
        
        // new 到底做了什么?等价于:
        function myNew(Constructor, ...args) {
            // 1. 创建空对象,原型指向构造函数的 prototype
            const obj = Object.create(Constructor.prototype);
            // 2. 执行构造函数,this 绑定到新对象
            const result = Constructor.apply(obj, args);
            // 3. 如果构造函数返回了对象,就用那个对象;否则返回新创建的对象
            return result instanceof Object ? result : obj;
        }
        
        const user2 = myNew(User, 'Bob', 30);
        console.log(user2); // User { name: 'Bob', age: 30 }
        
      • 箭头函数中的 this
        • 箭头函数没有自己的 this,它的 this 继承自定义时所在的外层作用域(词法 this),且永远不可改变。
        const user = {
            name: 'An',
        
            // 普通方法
            greetNormal() {
                console.log(this.name); // this 取决于调用方式
            },
        
            // 箭头函数作为方法 ❌
            greetArrow: () => {
                console.log(this.name); // this 是外层作用域的 this,不是 user
            }
        };
        
        user.greetNormal(); // "An" ✅
        user.greetArrow();  // undefined ❌(箭头函数的 this 是全局/模块作用域)
        
      • 绑定优先级
        • new 绑定 > 显式绑定(call/apply/bind) > 隐式绑定(obj.fn()) > 默认绑定(fn())
    • 原型与原型链
      • 为什么需要原型
        • 在没有原型机制的情况下,每次用构造函数创建实例,方法都会被重复创建
        function User(name) {
            this.name = name;
            // 每个实例都创建一个新的 greet 函数,浪费内存
            this.greet = function() {
                return `Hi, I'm ${this.name}`;
            };
        }
        
        const u1 = new User('An');
        const u2 = new User('Bob');
        
        console.log(u1.greet === u2.greet); // false —— 两个不同的函数对象
        
        // 如果创建 10000 个用户,就有 10000 份完全相同的 greet 函数
        // 原型就是用来解决这个问题的
        
      • prototype 与 proto
        • prototype:只有函数才拥有的属性,它是一个对象,当用 new 调用该函数时,新创建的实例的原型会指向这个对象。
        • proto(或者说 [[Prototype]]):每个对象都有的内部属性,指向它的原型对象。proto 是非标准但被广泛支持的访问方式,标准方式是 Object.getPrototypeOf()。
        function User(name) {
            this.name = name;
        }
        
        // 在 prototype 上定义方法,所有实例共享
        User.prototype.greet = function() {
            return `Hi, I'm ${this.name}`;
        };
        
        const u1 = new User('An');
        const u2 = new User('Bob');
        
        // 实例的 __proto__ 指向构造函数的 prototype
        console.log(u1.__proto__ === User.prototype);           // true
        console.log(Object.getPrototypeOf(u1) === User.prototype); // true(标准写法)
        
        // 所有实例共享 prototype 上的方法
        console.log(u1.greet === u2.greet); // true ✅ 同一个函数
        u1.greet(); // "Hi, I'm An"
        u2.greet(); // "Hi, I'm Bob"
        
      • constructor 属性
        • 每个函数的 prototype 对象默认有一个 constructor 属性,指回函数本身
        function User(name) {
        this.name = name;
        }
        
        console.log(User.prototype.constructor === User); // true
        
        const u = new User('An');
        console.log(u.constructor === User); // true(通过原型链找到的)
        
        // 可以用 constructor 判断实例是由哪个构造函数创建的
        // 但不如 instanceof 可靠,因为 constructor 可以被覆盖
        
      • 原型链(Prototype Chain)
        • 当你访问一个对象的属性时,JS 引擎会沿着原型链逐层查找,直到找到该属性或到达链的顶端 null
        function Animal(name) {
            this.name = name;
        }
        Animal.prototype.eat = function() {
            return `${this.name} is eating`;
        };
        
        function Dog(name, breed) {
            Animal.call(this, name); // 调用父构造函数
            this.breed = breed;
        }
        
        // 建立原型链:Dog.prototype 的原型指向 Animal.prototype
        Dog.prototype = Object.create(Animal.prototype);
        Dog.prototype.constructor = Dog; // 修复 constructor
        
        Dog.prototype.bark = function() {
            return `${this.name} says woof!`;
        };
        
        const dog = new Dog('Buddy', 'Golden');
        
        // 属性查找过程
        dog.name;    // "Buddy"      ← 在 dog 自身找到
        dog.bark();  // "Buddy says woof!" ← 在 Dog.prototype 上找到
        dog.eat();   // "Buddy is eating"  ← 在 Animal.prototype 上找到
        dog.toString(); // "[object Object]"  ← 在 Object.prototype 上找到
        dog.whatever;   // undefined  ← 一路找到 null,没找到
        
      • Object.create()
        • Object.create(proto) 创建一个新对象,将它的 proto 设为指定的对象。这是建立原型链最干净的方式。
    • Class 语法与继承
      • Class 的本质
        • ES6 的 class 是原型继承的语法糖。
        • 它不引入新的对象模型,底层仍然是原型机制,但写法更清晰、更接近传统面向对象语言。
        // ES5 写法
        function User(name, age) {
            this.name = name;
            this.age = age;
        }
        User.prototype.greet = function() {
            return `Hi, I'm ${this.name}`;
        };
        
        // ES6 class 写法(完全等价)
        class User {
            constructor(name, age) {
                this.name = name;
                this.age = age;
            }
        
            greet() {
                return `Hi, I'm ${this.name}`;
            }
        }
        
        // 验证底层仍然是原型
        console.log(typeof User);                         // "function"
        console.log(User.prototype.greet);                // function
        console.log(new User('An', 25).__proto__ === User.prototype); // true
        
      • class 和构造函数的区别:

        // 1. class 必须用 new 调用
        class Foo {}
        // Foo(); // ❌ TypeError: Class constructor Foo cannot be invoked without 'new'
        
        // 构造函数不强制
        function Bar() {}
        Bar(); // ✅ 不报错(但通常是 bug)
        
        // 2. class 内部默认严格模式
        class StrictExample {
            method() {
                // 这里自动处于严格模式
                // 未声明的变量赋值会报错等
            }
        }
        
        // 3. class 声明不会提升(存在 TDZ)
        // const u = new User(); // ❌ ReferenceError
        // class User {}
        
        // 4. class 中的方法不可枚举
        class MyClass {
            method() {}
        }
        console.log(Object.keys(MyClass.prototype)); // [](空,method 不可枚举)
        
        // 对比 ES5
        function MyFunc() {}
        MyFunc.prototype.method = function() {};
        console.log(Object.keys(MyFunc.prototype)); // ['method'](可枚举)
        
      • constructor 构造函数
        • constructor 是 class 中的特殊方法,在 new 时自动调用。每个 class 只能有一个。
        class User {
            constructor(name, age) {
                // this 指向新创建的实例
                this.name = name;
                this.age = age;
                this.createdAt = new Date();
            }
        }
        
        const u = new User('An', 25);
        console.log(u.name);      // "An"
        console.log(u.createdAt); // 当前时间
        
        // 如果不写 constructor,会有一个默认的空 constructor
        class Empty {}
            // 等价于
            class Empty {
            constructor() {}
        }
        
        // constructor 中可以做参数验证
        class PositiveNumber {
            constructor(value) {
                if (typeof value !== 'number' || value <= 0) {
                throw new Error('必须是正数');
                }
                this.value = value;
            }
        }
        
        new PositiveNumber(5);    // ✅
        // new PositiveNumber(-1); // ❌ Error: 必须是正数
        
      • 实例方法、访问器与类字段

        class User {
            // --- 类字段(Class Fields,ES2022)---
            // 直接在 class body 中声明,会成为实例自身的属性
            role = 'viewer';
            loginCount = 0;
        
            constructor(name, email) {
                this.name = name;
                this.email = email;
            }
        
            // --- 实例方法 ---
            // 定义在 User.prototype 上,所有实例共享
            greet() {
                return `Hi, I'm ${this.name}`;
            }
        
            login() {
                this.loginCount++;
                return `${this.name} logged in (${this.loginCount} times)`;
            }
        
            // --- getter / setter ---
            // 像属性一样访问,但实际上是方法调用
            get displayName() {
                return `${this.name} <${this.email}>`;
            }
        
            get isFrequent() {
                return this.loginCount > 10;
            }
        
            set nickname(value) {
                if (value.length < 2) {
                throw new Error('昵称至少2个字符');
                }
                this._nickname = value;
            }
        
            get nickname() {
                return this._nickname || this.name;
            }
        }
        
        const u = new User('An', 'an@test.com');
        
        // 类字段
        console.log(u.role);       // "viewer"
        console.log(u.loginCount); // 0
        
        // 实例方法
        u.login(); // "An logged in (1 times)"
        
        // getter(不加括号)
        console.log(u.displayName); // "An <an@test.com>"
        console.log(u.isFrequent);  // false
        
        // setter(像赋值一样使用)
        u.nickname = 'AnDev';
        console.log(u.nickname); // "AnDev"
        // u.nickname = 'A'; // ❌ Error: 昵称至少2个字符
        
      • 静态方法与静态字段
        • static 定义在类本身上,不是在实例上。通过类名调用,不能通过实例调用。
        class MathUtils {
            // 静态方法
            static add(a, b) {
                return a + b;
            }
        
            static multiply(a, b) {
                return a * b;
            }
        
            // 静态字段
            static PI = 3.14159;
        
            // 静态方法可以互相调用
            static circleArea(radius) {
                return MathUtils.PI * radius ** 2;
                // 或者用 this(this 在静态方法中指向类本身)
                // return this.PI * radius ** 2;
            }
        }
        
        MathUtils.add(1, 2);       // 3
        MathUtils.PI;              // 3.14159
        MathUtils.circleArea(5);   // 78.53975
        
        // 不能通过实例调用
        // const m = new MathUtils();
        // m.add(1, 2); // ❌ TypeError: m.add is not a function
        
      • 私有字段(Private Fields)
        • ES2022 引入 # 前缀表示真正的私有属性和方法,只能在类内部访问。
        class BankAccount {
            // 私有字段
            #balance;
            #owner;
            #transactionHistory = [];
        
            constructor(owner, initialBalance) {
                this.#owner = owner;
                this.#balance = initialBalance;
            }
        
            // 私有方法
            #recordTransaction(type, amount) {
                this.#transactionHistory.push({
                type,
                amount,
                date: new Date(),
                balance: this.#balance
                });
            }
        
            // 公开方法(通过它们间接访问私有数据)
            deposit(amount) {
                if (amount <= 0) throw new Error('金额必须大于0');
                this.#balance += amount;
                this.#recordTransaction('deposit', amount);
                return this;
            }
        
            withdraw(amount) {
                if (amount > this.#balance) throw new Error('余额不足');
                this.#balance -= amount;
                this.#recordTransaction('withdraw', amount);
                return this;
            }
        
            get balance() {
                return this.#balance;
            }
        
            get history() {
                // 返回副本,防止外部修改
                return [...this.#transactionHistory];
            }
        
            // 静态私有方法
            static #validateAmount(amount) {
                return typeof amount === 'number' && amount > 0;
            }
        }
        
        const account = new BankAccount('An', 1000);
        account.deposit(500).withdraw(200); // 链式调用
        
        console.log(account.balance); // 1300
        console.log(account.history); // [{...}, {...}]
        
        // ❌ 私有字段外部完全不可访问
        // account.#balance;           // SyntaxError
        // account.#recordTransaction; // SyntaxError
        
      • extends 与 super — 类继承
        • extends 建立继承关系,super 用于调用父类。
        class Animal {
            constructor(name) {
                this.name = name;
                this.energy = 100;
            }
        
            eat(amount) {
                this.energy += amount;
                return `${this.name} ate, energy: ${this.energy}`;
            }
        
            sleep(hours) {
                this.energy += hours * 10;
                return `${this.name} slept ${hours}h, energy: ${this.energy}`;
            }
        
            info() {
                return `${this.name} (energy: ${this.energy})`;
            }
            }
        
            class Dog extends Animal {
            // 子类的 constructor 必须在使用 this 之前调用 super()
            constructor(name, breed) {
                super(name); // 调用 Animal 的 constructor
                this.breed = breed;
            }
        
            // 新方法
            bark() {
                this.energy -= 5;
                return `${this.name} says Woof!`;
            }
        
            // 重写父类方法
            eat(amount) {
                // 调用父类的 eat 方法
                super.eat(amount * 2); // 狗吃东西恢复双倍能量
                return `${this.name} gobbled food! energy: ${this.energy}`;
            }
        
            // 扩展父类方法
            info() {
                return `${super.info()} [${this.breed}]`;
            }
        }
        
        const dog = new Dog('Buddy', 'Golden');
        dog.eat(10);  // "Buddy gobbled food! energy: 120"(10*2=20,100+20=120)
        dog.bark();   // "Buddy says Woof!"
        dog.sleep(2); // "Buddy slept 2h, energy: 135"(继承自 Animal)
        dog.info();   // "Buddy (energy: 135) [Golden]"
        
        // 原型链验证
        console.log(dog instanceof Dog);    // true
        console.log(dog instanceof Animal); // true
        
    • 字符串与正则表达式
      • 模板字符串深入

        // 基本插值
        const name = 'An';
        const greeting = `Hello, ${name}!`;
        
        // 插值中可以放任意表达式
        const price = 99;
        const qty = 3;
        console.log(`总价: ¥${price * qty}`);              // "总价: ¥297"
        console.log(`状态: ${qty > 0 ? '有货' : '缺货'}`); // "状态: 有货"
        
        // 多行字符串(保留换行和缩进)
        const html = `
        <div class="card">
            <h2>${name}</h2>
            <p>Price: ¥${price}</p>
        </div>
        `;
        
        // 嵌套模板
        const items = ['JS', 'CSS', 'HTML'];
        const list = `
        <ul>
            ${items.map(item => `<li>${item}</li>`).join('\n    ')}
        </ul>
        `;
        
      • 模板标签
        • 标签模板允许你用一个函数来处理模板字符串,这是一个强大但不太常见的特性:
        // 标签函数接收两个参数:
        // strings — 模板中的静态文本部分(数组)
        // values — 插值表达式的值(rest 参数)
        function highlight(strings, ...values) {
            return strings.reduce((result, str, i) => {
                const value = values[i] !== undefined ? `【${values[i]}】` : '';
                return result + str + value;
            }, '');
        }
        
        const lang = 'JavaScript';
        const exp = 5;
        console.log(highlight`我学了 ${lang} ${exp} 年`);
        // "我学了 【JavaScript】 【5】 年"
        
        // 实际应用1:SQL 防注入
        function sql(strings, ...values) {
            return {
                text: strings.join('$'),  // 用占位符替代
                values                     // 参数化查询
            };
        }
        const userId = "1; DROP TABLE users; --";
        const query = sql`SELECT * FROM users WHERE id = ${userId}`;
        // { text: "SELECT * FROM users WHERE id = $", values: ["1; DROP TABLE users; --"] }
        
        // 实际应用2:国际化
        // 实际应用3:CSS-in-JS(styled-components 就是用标签模板实现的)
        
      • 常用字符串方法

        const str = '  Hello, World! Hello, JavaScript!  ';
        
        // --- 查找类 ---
        str.indexOf('Hello');         // 2(第一次出现的位置)
        str.lastIndexOf('Hello');     // 17(最后一次出现的位置)
        str.indexOf('Python');        // -1(未找到)
        
        str.includes('World');        // true
        str.startsWith('  Hello');    // true
        str.endsWith('!  ');          // true
        
        str.search(/hello/i);         // 2(用正则查找,返回索引)
        
        // --- 提取类 ---
        str.slice(2, 7);              // "Hello"(起始, 结束索引,左闭右开)
        str.slice(-5);                // "t!  "(从倒数第5个到末尾)
        str.substring(2, 7);          // "Hello"(类似 slice,但不支持负数)
        
        str.at(2);                    // "H"(ES2022,支持负索引)
        str.at(-3);                   // "!  " → 不对,at 只取单个字符 → "!"
        // 更正:str.at(-3) 返回 '!'
        
        // --- 修改类(返回新字符串,原字符串不变)---
        str.trim();                   // "Hello, World! Hello, JavaScript!"
        str.trimStart();              // "Hello, World! Hello, JavaScript!  "
        str.trimEnd();                // "  Hello, World! Hello, JavaScript!"
        
        str.toUpperCase();            // "  HELLO, WORLD! HELLO, JAVASCRIPT!  "
        str.toLowerCase();            // "  hello, world! hello, javascript!  "
        
        'hello'.repeat(3);            // "hellohellohello"
        
        // --- 填充类 ---
        '42'.padStart(5, '0');        // "00042"(左填充到5位)
        '42'.padEnd(5, '.');          // "42..."(右填充到5位)
        '7'.padStart(2, '0');         // "07"(常用于格式化月份/日期)
        
        // --- 分割类 ---
        'a,b,c,d'.split(',');         // ['a', 'b', 'c', 'd']
        'hello'.split('');            // ['h', 'e', 'l', 'l', 'o']
        'a-b-c'.split('-', 2);       // ['a', 'b'](limit 参数)
        
        // --- 替换类 ---
        'hello world'.replace('world', 'JS');      // "hello JS"(只替换第一个)
        'aabbcc'.replace('b', 'X');                // "aaXbcc"
        'aabbcc'.replaceAll('b', 'X');             // "aaXXcc"(ES2021,替换全部)
        
      • replace 高级用法

        // 函数替换:每次匹配时调用函数,返回值作为替换内容
        const result = 'hello-world-foo'.replace(/\w+/g, (match) => {
            return match.charAt(0).toUpperCase() + match.slice(1);
        });
        // "Hello-World-Foo"
        
        // 替换函数的参数:(match, ...groups, offset, fullString)
        '2026-04-07'.replace(
            /(\d{4})-(\d{2})-(\d{2})/,
            (match, year, month, day) => `${day}/${month}/${year}`
        );
        // "07/04/2026"
        
        // 实际场景:模板引擎
        function template(str, data) {
            return str.replace(/\{\{(\w+)\}\}/g, (match, key) => {
                return data[key] ?? match; // 找不到就保留原文
            });
        }
        
        template('Hello, {{name}}! You have {{count}} messages.', {
            name: 'An',
            count: 5
        });
        // "Hello, An! You have 5 messages."
        
        // 驼峰转换
        function camelToKebab(str) {
            return str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
        }
        camelToKebab('backgroundColor'); // "background-color"
        camelToKebab('fontSize');        // "font-size"
        
        function kebabToCamel(str) {
            return str.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
        }
        kebabToCamel('background-color'); // "backgroundColor"
        
      • 正则表达式基础语法
        • 正则表达式用于模式匹配,由模式和标志组成。
        • 创建方式
        // 字面量(推荐,不需要转义反斜杠)
        const re1 = /hello/i;
        
        // 构造函数(适用于动态构建正则)
        const re2 = new RegExp('hello', 'i');
        const keyword = 'world';
        const re3 = new RegExp(keyword, 'gi'); // 动态模式
        
        • 常用标志
        /pattern/g   // g — global,匹配所有,不只第一个
        /pattern/i   // i — case insensitive,忽略大小写
        /pattern/m   // m — multiline,^ 和 $ 匹配每行的开头结尾
        /pattern/s   // s — dotAll,让 . 也匹配换行符
        /pattern/u   // u — unicode,正确处理 Unicode(如 emoji、中文)
        /pattern/d   // d — indices,返回匹配的位置信息(ES2022)
        
        // 可以组合
        /pattern/giu
        
      • 正则核心语法
        • 字符类
        // 普通字符:匹配自身
        /hello/          // 匹配 "hello"
        
        // 元字符
        .                // 任意单个字符(除换行符,除非用 s 标志)
        \d               // 数字 [0-9]
        \D               // 非数字 [^0-9]
        \w               // 单词字符 [a-zA-Z0-9_]
        \W               // 非单词字符
        \s               // 空白字符(空格、tab、换行等)
        \S               // 非空白字符
        \b               // 单词边界
        \B               // 非单词边界
        
        // 字符集合 []
        [abc]            // 匹配 a、b 或 c 中的任意一个
        [a-z]            // a 到 z
        [A-Z0-9]         // 大写字母或数字
        [^abc]           // 不是 a、b、c(^ 在 [] 内表示取反)
        
        // 示例
        /\d{3}-\d{4}/.test('123-4567');       // true
        /[aeiou]/i.test('Hello');              // true(有元音字母)
        /^[a-zA-Z_]\w*$/.test('myVar_1');     // true(合法的变量名)
        
        • 量词
        *        // 0 次或多次
        +        // 1 次或多次
        ?        // 0 次或 1 次
        {n}      // 恰好 n 次
        {n,}     // 至少 n 次
        {n,m}    // n 到 m 次
        
        // 示例
        /colou?r/        // "color" 或 "colour"(u 出现 0 或 1 次)
        /\d{3,4}/        // 3 位或 4 位数字
        /ha+/            // "ha", "haa", "haaa"...
        
        // 贪婪 vs 非贪婪
        // 默认贪婪:尽可能多地匹配
        '<div>hello</div>'.match(/<.+>/);    // ["<div>hello</div>"](匹配了整个)
        
        // 加 ? 变成非贪婪(懒惰):尽可能少地匹配
        '<div>hello</div>'.match(/<.+?>/);   // ["<div>"](匹配第一个完整标签就停)
        
        • 位置锚点
        ^        // 字符串开头(m 标志下匹配行开头)
        $        // 字符串结尾(m 标志下匹配行结尾)
        \b       // 单词边界
        
        /^hello/.test('hello world');  // true(以 hello 开头)
        /world$/.test('hello world');  // true(以 world 结尾)
        /^hello$/.test('hello');       // true(整个字符串就是 hello)
        
        // \b 单词边界
        'cat concatenate'.match(/\bcat\b/g);   // ["cat"](只匹配独立的 cat)
        'cat concatenate'.match(/cat/g);        // ["cat", "cat"](两个都匹配)
        
        • 分组与或
        // 分组 ()
        /(ab)+/.test('ababab');     // true(ab 重复多次)
        /(foo|bar)/.test('foobar'); // true
        
        // 或 |
        /cat|dog/.test('I have a cat'); // true
        /^(http|https):\/\//.test('https://example.com'); // true
        
        // 非捕获分组 (?:) — 分组但不捕获
        /(?:http|https):\/\/(\w+)/.exec('https://example.com');
        // 结果中不包含 http/https 的捕获组
        
      • 捕获组与反向引用

        // 编号捕获组
        const dateRe = /(\d{4})-(\d{2})-(\d{2})/;
        const match = dateRe.exec('2026-04-07');
        
        console.log(match[0]); // "2026-04-07"(完整匹配)
        console.log(match[1]); // "2026"(第1个捕获组)
        console.log(match[2]); // "04"(第2个捕获组)
        console.log(match[3]); // "07"(第3个捕获组)
        
        // 命名捕获组(ES2018,强烈推荐)
        const namedRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
        const m = namedRe.exec('2026-04-07');
        
        console.log(m.groups.year);  // "2026"
        console.log(m.groups.month); // "04"
        console.log(m.groups.day);   // "07"
        
        // 解构命名捕获组
        const { groups: { year, month, day } } = namedRe.exec('2026-04-07');
        
        // 在 replace 中使用捕获组
        '2026-04-07'.replace(
            /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/,
            '$<d>/$<m>/$<y>'
        );
        // "07/04/2026"
        
        // 编号引用
        '2026-04-07'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$3/$2/$1');
        // "07/04/2026"
        
        // 反向引用:在正则内部引用前面的捕获组
        // 匹配重复的单词
        /\b(\w+)\s+\1\b/.test('the the');  // true(\1 引用第一个捕获组)
        /\b(\w+)\s+\1\b/.test('the cat');  // false
        
    • Map、Set 与迭代器
      • Map — 键值映射
        • Map 是 ES6 引入的键值对集合,和普通对象的核心区别在于:任何类型都可以作为键。
        const map = new Map();
        
        // 设置键值对
        map.set('name', 'An');
        map.set(42, 'number key');
        map.set(true, 'boolean key');
        
        const objKey = { id: 1 };
        const fnKey = () => {};
        map.set(objKey, '对象作为键');
        map.set(fnKey, '函数作为键');
        
        // 获取
        map.get('name');    // "An"
        map.get(42);        // "number key"
        map.get(objKey);    // "对象作为键"
        map.get({ id: 1 }); // undefined!不是同一个引用
        
        // 检查与删除
        map.has('name');    // true
        map.delete(42);     // true(删除成功)
        map.size;           // 4
        
        // 清空
        map.clear();
        map.size;           // 0
        
        • 用可迭代对象初始化:
        // 用二维数组初始化
        const map = new Map([
            ['name', 'An'],
            ['age', 25],
            ['city', 'Tokyo']
        ]);
        
        // 从对象转换
        const obj = { a: 1, b: 2, c: 3 };
        const mapFromObj = new Map(Object.entries(obj));
        
        // 从 Map 转回对象
        const backToObj = Object.fromEntries(mapFromObj);
        // { a: 1, b: 2, c: 3 }
        
        • Map vs 普通对象
        // 1. 键的类型
        // 对象的键只能是 string 或 Symbol
        const obj = {};
        obj[1] = 'one';
        obj['1'] = 'string one';
        console.log(obj[1]);  // "string one"(数字键被转成了字符串,覆盖了)
        
        // Map 的键保持原始类型
        const map = new Map();
        map.set(1, 'number one');
        map.set('1', 'string one');
        map.get(1);   // "number one"
        map.get('1'); // "string one"(两个不同的键)
        
        // 2. 顺序保证
        // Map 始终按插入顺序遍历
        // 对象的键顺序在现代引擎中也基本有序,但规范上数字键会排在前面
        
        // 3. 性能
        // Map 在频繁增删键值对时性能更好
        // Map 有原生的 size 属性(O(1)),对象需要 Object.keys(obj).length
        
        // 4. 原型污染
        // 对象会继承 Object.prototype 上的属性
        const plain = {};
        console.log('toString' in plain); // true(原型上的)
        
        // Map 没有这个问题
        const safeMap = new Map();
        safeMap.has('toString'); // false
        
      • Set - 唯一值集合
      • const set = new Set();
        
        // 添加
        set.add(1);
        set.add(2);
        set.add(3);
        set.add(2); // 重复,被忽略
        set.add('2'); // 字符串 "2" 和数字 2 是不同的值
        
        console.log(set.size); // 4
        
        // 检查与删除
        set.has(2);     // true
        set.delete(2);  // true
        set.has(2);     // false
        
        // 初始化
        const set2 = new Set([1, 2, 3, 2, 1]); // Set { 1, 2, 3 }
        
        // 清空
        set.clear();
        
      • 可迭代协议
        • JavaScript 中 for…of、展开运算符 …、解构赋值等特性,都依赖可迭代协议。
        • 一个对象如果实现了 Symbol.iterator 方法,并且该方法返回一个迭代器,这个对象就是可迭代的。
        // 内置可迭代对象
        // Array, String, Map, Set, TypedArray, arguments, NodeList
        
        // 验证是否可迭代
        function isIterable(obj) {
        return obj != null && typeof obj[Symbol.iterator] === 'function';
        }
        
        isIterable([1, 2, 3]);     // true
        isIterable('hello');        // true
        isIterable(new Map());     // true
        isIterable(new Set());     // true
        isIterable({ a: 1 });      // false(普通对象不可迭代)
        isIterable(42);             // false
        
      • 迭代器协议:
        • 迭代器是一个对象,它有一个 next() 方法,每次调用返回 { value, done }。
        // 手动使用迭代器
        const arr = [10, 20, 30];
        const iterator = arr[Symbol.iterator]();
        
        iterator.next(); // { value: 10, done: false }
        iterator.next(); // { value: 20, done: false }
        iterator.next(); // { value: 30, done: false }
        iterator.next(); // { value: undefined, done: true }
        
        // for...of 本质就是自动调用迭代器
        // 等价于:
        const iter = arr[Symbol.iterator]();
        let result = iter.next();
        while (!result.done) {
            console.log(result.value);
            result = iter.next();
        }
        
      • 自定义可迭代对象

        // 实现一个范围对象
        class Range {
            constructor(start, end, step = 1) {
                this.start = start;
                this.end = end;
                this.step = step;
            }
        
            [Symbol.iterator]() {
                let current = this.start;
                const end = this.end;
                const step = this.step;
        
                return {
                next() {
                    if (current <= end) {
                    const value = current;
                    current += step;
                    return { value, done: false };
                    }
                    return { value: undefined, done: true };
                }
                };
            }
        }
        
        const range = new Range(1, 5);
        for (const n of range) {
        console.log(n); // 1, 2, 3, 4, 5
        }
        
        // 因为实现了迭代协议,所有相关语法都自动支持
        console.log([...range]);              // [1, 2, 3, 4, 5]
        const [a, b, c] = range;              // a=1, b=2, c=3
        const nums = Array.from(range);        // [1, 2, 3, 4, 5]
        const set = new Set(range);            // Set { 1, 2, 3, 4, 5 }
        
        // 带步长
        const evens = new Range(0, 10, 2);
        console.log([...evens]); // [0, 2, 4, 6, 8, 10]
        
      • 生成器函数
        • 生成器用 function* 声明,yield 暂停并产出值。它自动实现了迭代器协议。
        // 基本语法
        function* numberGenerator() {
            yield 1;
            yield 2;
            yield 3;
        }
        
        const gen = numberGenerator();
        gen.next(); // { value: 1, done: false }
        gen.next(); // { value: 2, done: false }
        gen.next(); // { value: 3, done: false }
        gen.next(); // { value: undefined, done: true }
        
        // 生成器自动可迭代
        for (const n of numberGenerator()) {
            console.log(n); // 1, 2, 3
        }
        
        console.log([...numberGenerator()]); // [1, 2, 3]
        
    • 错误处理
      • 错误类型

        // 1. SyntaxError — 语法错误,代码根本无法解析
        // const x = ;   // SyntaxError: Unexpected token ';'
        // 这种错误在代码运行前就会被发现
        
        // 2. ReferenceError — 引用了不存在的变量
        // console.log(notDefined); // ReferenceError: notDefined is not defined
        
        // 3. TypeError — 类型错误,对值进行了不支持的操作
        // null.toString();        // TypeError: Cannot read properties of null
        // (42)();                 // TypeError: 42 is not a function
        // const x = 1; x = 2;    // TypeError: Assignment to constant variable
        
        // 4. RangeError — 值超出有效范围
        // new Array(-1);          // RangeError: Invalid array length
        // (1.5).toFixed(200);     // RangeError: toFixed() digits argument must be between 0 and 100
        
        // 5. URIError — URI 编解码错误
        // decodeURIComponent('%'); // URIError: URI malformed
        
        // 6. EvalError — eval 相关(现代 JS 中很少见)
        
        // 所有错误都继承自 Error
        const err = new TypeError('出错了');
        console.log(err instanceof TypeError); // true
        console.log(err instanceof Error);     // true
        console.log(err.message);              // "出错了"
        console.log(err.name);                 // "TypeError"
        console.log(err.stack);                // 完整的调用栈信息
        
      • try/catch/finally

        // 基本结构
        try {
            // 可能出错的代码
            const data = JSON.parse('invalid json');
        } catch (error) {
        // 出错时执行
            console.log(error.message); // "Unexpected token i in JSON at position 0"
            console.log(error.name);    // "SyntaxError"
        } finally {
        // 无论是否出错都执行
            console.log('清理工作');
        }
        
        // catch 可以省略参数(ES2019)
        try {
            JSON.parse('{}');
        } catch {
        // 不需要 error 对象时可以省略
            console.log('解析失败');
        }
        
        // finally 即使有 return 也会执行
        function example() {
            try {
                return '来自 try';
            } finally {
                console.log('finally 仍然执行');
                // ⚠️ 如果 finally 里也有 return,会覆盖 try 的 return
                // return '来自 finally'; // 不推荐这样做
            }
        }
        example(); // 打印 "finally 仍然执行",返回 "来自 try"
        
      • throw 与自定义 Error

        // 基本自定义错误
        class AppError extends Error {
            constructor(message, code) {
                super(message);
                this.name = 'AppError';
                this.code = code;
            }
        }
        
        // 特定业务错误
        class ValidationError extends AppError {
            constructor(field, message) {
                super(message, 'VALIDATION_ERROR');
                this.name = 'ValidationError';
                this.field = field;
            }
        }
        
        class NotFoundError extends AppError {
            constructor(resource, id) {
                super(`${resource} with id ${id} not found`, 'NOT_FOUND');
                this.name = 'NotFoundError';
                this.resource = resource;
                this.id = id;
            }
        }
        
        class AuthenticationError extends AppError {
            constructor(message = '未认证') {
                super(message, 'UNAUTHORIZED');
                this.name = 'AuthenticationError';
            }
        }
        
        // 使用
        function getUser(id) {
            if (typeof id !== 'number') {
                throw new ValidationError('id', 'ID 必须是数字');
            }
            const user = database.find(u => u.id === id);
            if (!user) {
                throw new NotFoundError('User', id);
            }
            return user;
        }
        
    • 模块化
      • ES Modules — import / export
        • ES Modules 是 JavaScript 的官方模块系统,每个文件就是一个模块,有自己的作用域。
        • 命名导出(Named Exports):

          // utils.js
          // 声明时导出
          export const PI = 3.14159;
          
          export function add(a, b) {
              return a + b;
          }
          
          export class Calculator {
              multiply(a, b) { return a * b; }
          }
          
          // 也可以统一导出
          const subtract = (a, b) => a - b;
          const divide = (a, b) => a / b;
          export { subtract, divide };
          
        • 命名导入

          // main.js
          // 导入指定的导出
          import { add, PI, Calculator } from './utils.js';
          
          console.log(add(1, 2)); // 3
          console.log(PI);          // 3.14159
          
          // 重命名导入
          import { add as sum, subtract as sub } from './utils.js';
          sum(1, 2); // 3
          
          // 导入全部到一个命名空间
          import * as Utils from './utils.js';
          Utils.add(1, 2);
          Utils.PI;
          
        • 默认导出
          • 每个模块只能有一个默认导出。
          // logger.js
          export default class Logger {
              log(msg) { console.log(`[LOG] ${msg}`); }
              error(msg) { console.error(`[ERR] ${msg}`); }
          }
          
          // 也可以导出函数或值
          // export default function() { ... }
          // export default 42;
          
          // 导入默认导出(不需要花括号,名字可以随意取)
          import Logger from './logger.js';
          import MyLogger from './logger.js'; // 不同名字也行
          
          const logger = new Logger();
          logger.log('hello');
          
          • 混合使用
          // api.js
          export default class API {
              constructor(baseUrl) { this.baseUrl = baseUrl; }
          }
          
          export const VERSION = '2.0';
          export function createAPI(url) {
              return new API(url);
          }
          // 同时导入默认和命名导出
          import API, { VERSION, createAPI } from './api.js';
          
      • 高级模块用法
        • 重新导出
        // models/user.js
        export class User { /* ... */ }
        
        // models/post.js
        export class Post { /* ... */ }
        
        // models/comment.js
        export class Comment { /* ... */ }
        
        // models/index.js — 统一入口
        export { User } from './user.js';
        export { Post } from './post.js';
        export { Comment } from './comment.js';
        
        // 重新导出全部
        export * from './user.js';
        
        // 重新导出并重命名
        export { User as UserModel } from './user.js';
        
        // 重新导出默认导出
        export { default as User } from './user.js';
        
        • 动态导入
        // 按需加载
        async function loadChart() {
            const { Chart } = await import('./chart.js');
            const chart = new Chart('#container');
            chart.render(data);
        }
        
        // 条件加载
        async function loadLocale(lang) {
            const messages = await import(`./locales/${lang}.js`);
            return messages.default;
        }
        
        // 搭配 UI 事件
        button.addEventListener('click', async () => {
            const { openModal } = await import('./modal.js');
            openModal();
        });
        
        // 多个模块并行加载
        const [moduleA, moduleB] = await Promise.all([
            import('./a.js'),
            import('./b.js')
        ]);
        
      • CommonJS 对比
        • CommonJS 是 Node.js 传统的模块系统,和 ES Modules 有重要区别。
        // CommonJS 语法
        // 导出
        module.exports = { add, subtract };
        // 或
        exports.add = add;
        
        // 导入
        const { add, subtract } = require('./utils');
        const utils = require('./utils');
        
        // 主要区别:
        // 1. CJS 是同步加载,ESM 是异步的
        // 2. CJS 的 require 可以在任何地方调用,ESM 的 import 必须在顶层
        // 3. CJS 导出的是值的拷贝,ESM 导出的是活绑定
        // 4. CJS 用 .js 后缀,ESM 在 Node 中用 .mjs 或在 package.json 中设 "type": "module"
        
        // Node.js 中使用 ESM 的两种方式:
        // 方式1:文件后缀用 .mjs
        // 方式2:package.json 中添加 "type": "module"
        

异步编程与浏览器 API

  • 异步编程与浏览器 API
    • 异步基础与回调
      • 同步 vs 异步
        • JavaScript 是单线程语言——同一时刻只能执行一段代码
        • 现实中很多操作是耗时的(网络请求、文件读写、定时器),如果同步等待,整个程序就会卡住
      • 事件循环
      • 宏任务与微任务

        console.log('1. 同步');
        
        setTimeout(() => {
            console.log('2. 宏任务 - setTimeout');
        }, 0);
        
        Promise.resolve().then(() => {
            console.log('3. 微任务 - Promise.then');
        });
        
        queueMicrotask(() => {
            console.log('4. 微任务 - queueMicrotask');
        });
        
        console.log('5. 同步');
        
        // 输出顺序:
        // 1. 同步
        // 5. 同步
        // 3. 微任务 - Promise.then
        // 4. 微任务 - queueMicrotask
        // 2. 宏任务 - setTimeout
        
        // 解释:
        // 第一轮:执行所有同步代码 → 1, 5
        // 清空微任务队列 → 3, 4
        // 取一个宏任务 → 2
        
      • setTimeout 与 setInterval
        • setTimeout — 延迟执行:
        // 基本用法
        const timerId = setTimeout(() => {
            console.log('3 秒后执行');
        }, 3000);
        
        // 取消定时器
        clearTimeout(timerId);
        
        // 传递参数给回调
        setTimeout((name, age) => {
            console.log(`${name}, ${age}`);
        }, 1000, 'An', 25);
        // 1 秒后打印 "An, 25"
        
        // ⚠️ 延迟时间不是精确的
        // setTimeout(fn, 100) 意思是"至少 100ms 后执行"
        // 实际可能更长(调用栈忙、页面后台时浏览器会节流等)
        
        // 最小延迟:嵌套 setTimeout 超过 5 层后,最小延迟被限制为 4ms
        function nested(depth = 0) {
            const start = performance.now();
            setTimeout(() => {
                console.log(`depth ${depth}: ${(performance.now() - start).toFixed(1)}ms`);
                if (depth < 10) nested(depth + 1);
            }, 0);
        }
        // 前几层接近 0ms,深层后会变成 4ms+
        
      • 回调函数模式
        • 回调是 JavaScript 最早的异步处理方式——把"完成后要做的事"作为函数传进去。
        // 最简单的回调
        function fetchData(callback) {
            setTimeout(() => {
                const data = { name: 'An', age: 25 };
                callback(data);
            }, 1000);
        }
        
        fetchData((data) => {
            console.log('收到数据:', data);
        });
        
        // Node.js 风格的回调:错误优先(Error-first Callback)
        function readFile(path, callback) {
            setTimeout(() => {
                if (path === '') {
                callback(new Error('路径不能为空'), null);
                } else {
                callback(null, `${path} 的内容`); // 第一个参数是错误,第二个是结果
                }
            }, 500);
        }
        
        readFile('data.txt', (error, data) => {
            if (error) {
                console.error('读取失败:', error.message);
                return;
            }
            console.log('读取成功:', data);
        });
        
    • Promise
      • Promise 是什么
        • Promise 是一个代表异步操作最终结果的对象。它解决了回调地狱的问题,让异步代码像同步一样线性书写。
        • 一个 Promise 有三种状态,一旦从 pending 变为 fulfilled 或 rejected,就不可再改变:
        // pending(进行中)→ fulfilled(已成功)
        // pending(进行中)→ rejected(已失败)
        
        // 创建 Promise
        const promise = new Promise((resolve, reject) => {
        // 这个函数(executor)立即同步执行
        console.log('executor 执行了');
        
        // 异步操作
        setTimeout(() => {
            const success = true;
                if (success) {
                    resolve('成功的数据');  // pending → fulfilled
                } else {
                    reject(new Error('失败了')); // pending → rejected
                }
            }, 1000);
        });
        
        console.log('Promise 创建后');
        // 输出顺序:
        // "executor 执行了"(同步)
        // "Promise 创建后"(同步)
        // (1 秒后)promise 被 resolve
        
      • then/catch/finally
        • then — 处理成功和失败:

          const promise = new Promise((resolve) => {
              setTimeout(() => resolve(42), 1000);
          });
          
          // then 接受两个回调:onFulfilled 和 onRejected
          promise.then(
              (value) => console.log('成功:', value),   // 42
              (error) => console.log('失败:', error)
          );
          
          // 通常只传第一个参数,用 catch 处理错误
          promise
          .then(value => console.log('成功:', value))
          .catch(error => console.log('失败:', error));
          
        • then 的返回值——链式调用的基础:
          • then 始终返回一个新的 Promise。返回值决定了新 Promise 的状态:
          // 1. 返回普通值 → 新 Promise 以该值 resolve
          Promise.resolve(1)
          .then(v => v + 1)   // 返回 2
          .then(v => v * 3)   // 返回 6
          .then(v => console.log(v)); // 6
          
          // 2. 返回 Promise → 新 Promise 跟随返回的 Promise
          Promise.resolve(1)
              .then(v => {
                  return new Promise(resolve => {
                  setTimeout(() => resolve(v + 10), 500);
                  });
              })
          .then(v => console.log(v)); // 11(等待 500ms 后)
          
          // 3. 抛出异常 → 新 Promise 被 reject
          Promise.resolve(1)
          .then(v => {
              throw new Error('出错了');
          })
          .then(v => console.log('不会执行'))
          .catch(err => console.log(err.message)); // "出错了"
          
          // 4. 不写 return → 返回 undefined
          Promise.resolve(1)
          .then(v => { v + 1; })  // 忘了 return!
          .then(v => console.log(v)); // undefined
          
        • catch 错误处理

          // catch 是 .then(null, onRejected) 的语法糖
          // 它能捕获前面所有 then 中抛出的错误
          
          Promise.resolve(1)
          .then(v => {
              throw new Error('then 中的错误');
          })
          .then(v => {
              console.log('跳过'); // 不执行
          })
          .catch(err => {
              console.log('捕获:', err.message); // "then 中的错误"
              return '已恢复'; // catch 也可以返回值,让链继续
          })
          .then(v => {
              console.log(v); // "已恢复"(链恢复了)
          });
          
          // ⚠️ then 的第二个参数 vs catch 的区别
          Promise.reject(new Error('初始错误'))
          .then(
              v => console.log('成功'),
              err => {
              console.log('then 的 onRejected:', err.message);
              throw new Error('新错误');
              }
          );
          // "新错误" 无法被捕获!因为同一个 then 的 onRejected 不能捕获 onFulfilled 的错误
          
          // ✅ 用 catch 更安全
          Promise.reject(new Error('初始错误'))
          .then(v => console.log('成功'))
          .catch(err => console.log('catch 捕获:', err.message));
          
        • finally — 无论成败都执行:

          function fetchData() {
              showLoading();
          
              return fetch('/api/data')
                  .then(res => res.json())
                  .then(data => {
                  displayData(data);
                  return data;
                  })
                  .catch(err => {
                  showError(err.message);
                  })
                  .finally(() => {
                  hideLoading(); // 无论成功失败都要隐藏 loading
                  });
          }
          
          // finally 的特殊行为:
          // 1. 不接收参数(不知道是成功还是失败)
          // 2. 默认"透传"前一步的值
          Promise.resolve(42)
          .finally(() => {
              console.log('清理'); // 执行
              return 999;          // 返回值被忽略(除非抛错)
          })
          .then(v => console.log(v)); // 42(透传,不是 999)
          
          // 3. 如果 finally 中抛出错误,会覆盖之前的结果
          Promise.resolve(42)
          .finally(() => {
              throw new Error('finally 出错');
          })
          .catch(err => console.log(err.message)); // "finally 出错"
          
      • Promise 静态方法
        • Promise.resolve / Promise.reject — 快速创建:

          // 创建一个立即 resolve 的 Promise
          const p1 = Promise.resolve(42);
          p1.then(v => console.log(v)); // 42
          
          // 如果传入 Promise,原样返回
          const p2 = Promise.resolve(Promise.resolve('hello'));
          p2.then(v => console.log(v)); // "hello"(不会嵌套)
          
          // 如果传入 thenable(有 then 方法的对象),会展开
          const thenable = {
              then(resolve) {
                  resolve('from thenable');
              }
          };
          Promise.resolve(thenable).then(v => console.log(v)); // "from thenable"
          
          // 创建一个立即 reject 的 Promise
          const p3 = Promise.reject(new Error('失败'));
          p3.catch(err => console.log(err.message)); // "失败"
          
        • Promise.all — 全部成功才成功:

          const p1 = fetch('/api/users').then(r => r.json());
          const p2 = fetch('/api/posts').then(r => r.json());
          const p3 = fetch('/api/comments').then(r => r.json());
          
          // 并行执行,全部完成后返回结果数组(顺序与传入一致)
          Promise.all([p1, p2, p3])
          .then(([users, posts, comments]) => {
              console.log(users, posts, comments);
          })
          .catch(err => {
              // ⚠️ 任何一个失败,整个 all 就失败
              // 其他 Promise 仍然会执行完,但结果被丢弃
              console.error('有一个请求失败:', err);
          });
          
          // 传入空数组立即 resolve
          Promise.all([]).then(v => console.log(v)); // []
          
          // 非 Promise 值会被自动包装
          Promise.all([1, 'hello', Promise.resolve(42)])
          .then(v => console.log(v)); // [1, 'hello', 42]
          
          // 实际应用:批量操作
          const userIds = [1, 2, 3, 4, 5];
          const users = await Promise.all(
              userIds.map(id => getUser(id))
          );
          
    • async/await
      • async 函数基础
        • async/await 是 Promise 的语法糖,让异步代码看起来像同步代码一样。
        // async 函数始终返回 Promise
        async function greet() {
            return 'Hello'; // 自动包装为 Promise.resolve('Hello')
        }
        
        greet().then(v => console.log(v)); // "Hello"
        
        // 等价于
        function greet() {
            return Promise.resolve('Hello');
        }
        
        // 即使返回普通值,也是 Promise
        async function getNumber() {
            return 42;
        }
        console.log(getNumber() instanceof Promise); // true
        
        // 抛出的错误变成 rejected Promise
        async function fail() {
            throw new Error('出错了');
        }
        fail().catch(err => console.log(err.message)); // "出错了"
        
      • await 表达式
        • await 只能在 async 函数(或模块顶层)内部使用。它暂停函数执行,等待 Promise resolve,然后返回 resolve 的值。
        async function example() {
            console.log('1. 开始');
        
            // await 暂停,等 Promise resolve
            const result = await new Promise(resolve => {
                setTimeout(() => resolve('数据'), 1000);
            });
        
            console.log('2. 收到:', result); // 1 秒后
            console.log('3. 继续');
        }
        
        example();
        console.log('4. async 函数外部');
        
        // 输出:
        // 1. 开始
        // 4. async 函数外部(不等待 async 函数)
        // (1 秒后)
        // 2. 收到: 数据
        // 3. 继续
        
        • await 的本质——对比 then 链:
        // Promise 链
        function getData() {
            return getUser(1)
                .then(user => getOrders(user.id))
                .then(orders => getOrderDetail(orders[0].id))
                .then(detail => {
                    console.log(detail);
                    return detail;
                });
        }
        
        // async/await(完全等价,但可读性好得多)
        async function getData() {
            const user = await getUser(1);
            const orders = await getOrders(user.id);
            const detail = await getOrderDetail(orders[0].id);
            console.log(detail);
            return detail;
        }
        
        // 最大的优势:中间变量全部可用
        // 不需要像 Promise 链那样用嵌套或外部变量传递
        
      • 错误处理
        • try/catch
        async function fetchUserData(userId) {
            try {
                const user = await getUser(userId);
                const orders = await getOrders(user.id);
                const detail = await getOrderDetail(orders[0].id);
                return { user, orders, detail };
            } catch (error) {
                // 任何一步的 reject 或 throw 都会被捕获
                console.error('获取数据失败:', error.message);
                throw error; // 重新抛出或返回兜底数据
            }
        }
        
        // 精细化错误处理:每步单独 try/catch
        async function fetchWithFallback(userId) {
            let user;
            try {
                user = await getUser(userId);
            } catch (error) {
                console.warn('用户服务异常,使用默认数据');
                user = { id: userId, name: 'Unknown' };
            }
        
            let orders;
            try {
                orders = await getOrders(user.id);
            } catch (error) {
                console.warn('订单服务异常');
                orders = [];
            }
        
            return { user, orders };
        }