js 复习四月 3, 2026语言核心基础# - 语言核心基础 - 变量与数据类型 - 变量声明 - JavaScript 有三种声明变量的关键字,它们的核心区别在于作用域和可变性 - var - var 是 ES5 时代的声明方式,它的作用域是函数级别的,而不是块级的。 - 这意味着在 if 或 for 的大括号里用 var 声明的变量,在外面依然可以访问 - 另外 var 存在"变量提升"(hoisting)——声明会被提升到函数顶部,但赋值不会,所以在声明前访问会得到 undefined 而不是报错 - ```js function example() { console.log(a); // undefined(不会报错,因为声明被提升了) var a = 10; if (true) { var b = 20; } console.log(b); // 20(var 没有块级作用域) } ``` - let - let 是 ES6 引入的,拥有块级作用域,不会被提升到可访问状态(存在"暂时性死区" TDZ),在声明前访问会直接报 ReferenceError。 - ```js 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 指向一个对象或数组,对象内部的属性是可以修改的。 - ```js const PI = 3.14159; // PI = 3; // TypeError: Assignment to constant variable const user = { name: 'An' }; user.name = 'Bob'; // ✅ 没问题,修改的是对象属性,不是重新赋值 // user = {}; // ❌ TypeError,不能重新绑定 ``` - 七种原始类型 - JavaScript 的数据类型分为原始类型和引用类型。 - 原始类型有 7 种,它们的值是不可变的,按值传递。 - string 字符串 - ```js const s1 = 'hello'; // 单引号 const s2 = "world"; // 双引号 const s3 = `你好 ${s1}`; // 模板字符串,支持插值和换行 ``` - 字符串是不可变的。s1[0] = 'H' 不会报错但也不会生效。所有字符串方法都返回新字符串。 - number 数字 - JavaScript 只有一种数字类型,使用 IEEE 754 双精度浮点数。这带来了经典问题 - ```js 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)时使用 - ```js const big = 123456789012345678901234567890n; // 末尾加 n console.log(typeof big); // "bigint" // big + 1; // ❌ TypeError,bigint 不能和 number 混合运算 big + 1n; // ✅ ``` - boolean 布尔值 - 只有 true 和 false。但在条件判断中,其他类型会被隐式转换为布尔值。 - 记住这些 falsy 值(转换为 false 的值): - ```js // 以下全部是 falsy false, 0, -0, 0n, "", null, undefined, NaN // 其他一切都是 truthy,包括: // "0", " ", [], {}, function(){} 都是 truthy! ``` - null 空值 - 表示"有意为空",通常用于显式地标记一个变量"目前没有值"。 - ```js let user = null; // 明确表示:当前没有用户 ``` - undefined 未定义 - 表示变量已声明但未赋值,或者函数没有返回值 - ```js let x; console.log(x); // undefined function foo() {} console.log(foo()); // undefined ``` - symbol 符号 - ES6 引入,每个 Symbol 都是唯一的,主要用作对象属性的唯一标识符 - ```js const id1 = Symbol('id'); const id2 = Symbol('id'); console.log(id1 === id2); // false,每次调用都创建新的 const obj = { [id1]: '这个属性名是唯一的' }; ``` - 引用类型(Reference Types) - 除了原始类型,剩下的都是引用类型(对象)。 - 包括普通对象 {}、数组 []、函数 function、Date、RegExp 等等。 - 引用类型按引用传递,变量存的是内存地址而不是值本身 - ```js 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 返回一个字符串,表示值的类型 - ```js 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 - 类型转换 - 显式转换 - ```js // 转字符串 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 ``` - 隐式转换 - ```js // + 号:只要有一边是字符串,就做字符串拼接 '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 ``` - 运算符与流程控制 - 算术运算符 - 基本的 +、-、*、/ 之外,还有几个需要注意的 ```js // 取余(求模) 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(先加再返回) ``` - 复合赋值运算符就是简写:+=、-=、*=、/=、%=、**=、??=、||=、&&= ```js 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" ``` - 比较运算符 - ==(宽松相等) 会在比较前做隐式类型转换,规则非常复杂: - ```js 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: - ```js 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,返回左侧;否则返回右侧。 - ```js 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,返回左侧;否则返回右侧。 - ```js console.log(0 || 'default'); // "default" console.log('hello' || 'default'); // "hello" // 经典用法:提供默认值 const port = config.port || 3000; // ⚠️ 陷阱:如果 config.port 是 0,也会被跳过! ``` - ??(空值合并,ES2020):只有左侧是 null 或 undefined 时才返回右侧。这解决了 || 的陷阱。 - ```js 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'; // "" ``` - ! (逻辑非) - ```js 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 而不是报错。 - ```js 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 循环中见到 - ```js const result = (1, 2, 3); // result === 3 for (let i = 0, j = 10; i < j; i++, j--) { /* ... */ } ``` - 条件语句 - if/else if/else - ```js const score = 85; if (score >= 90) { console.log('优秀'); } else if (score >= 70) { console.log('良好'); } else if (score >= 60) { console.log('及格'); } else { console.log('不及格'); } ``` - 三元表达式:适合简单的二选一,不要嵌套太深 - ```js const status = score >= 60 ? '及格' : '不及格'; // ❌ 不推荐:嵌套三元,可读性很差 const grade = score >= 90 ? 'A' : score >= 70 ? 'B' : score >= 60 ? 'C' : 'D'; // ✅ 这种情况用 if/else 或函数更清晰 ``` - switch:适合对同一个值做多重精确匹配,注意 break 不能忘 - ```js 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 循环:最经典的循环,适合需要精确控制索引的场景: ```js 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: ```js // 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 等)的值: ```js 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:遍历对象的可枚举属性名(键名): ```js const user = { name: 'An', age: 25, city: 'Tokyo' }; for (const key in user) { console.log(`${key}: ${user[key]}`); } // ⚠️ 不推荐用 for...in 遍历数组! // 它会遍历原型链上的属性,顺序也不保证 // 遍历数组请用 for...of 或数组方法 ``` - break、continue 与标签 - ```js // 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):整个函数会被提升到作用域顶部,因此可以在声明之前调用。 - ```js // ✅ 可以在声明之前调用 greet('An'); function greet(name) { console.log(`你好,${name}!`); } ``` - 函数表达式(Function Expression):把一个匿名或具名函数赋值给变量。变量声明会提升,但赋值不会,所以在赋值之前调用会报错。 - ```js // ❌ 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 引入的简洁写法,但它不仅仅是语法糖,和普通函数有几个本质区别。 - 基本语法 ```js // 完整写法 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 }); ``` - 箭头函数与普通函数的区别 ```js // 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): - ```js 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 类数组对象): - ```js 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 }); ``` - 返回值 - 函数只能返回一个值。如果需要返回多个值,用对象或数组: ```js // 不写 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 换行陷阱 ```js function gotcha() { return // ← JS 在这里自动插入分号,函数返回 undefined { name: 'An' }; } console.log(gotcha()); // undefined! // 正确写法:左大括号跟 return 同一行 function correct() { return { name: 'An' }; } ``` - 函数是一等公民 - 在 JavaScript 中,函数和其他值(数字、字符串)没有区别,可以被赋值、传递、返回。 - 赋值给变量: ```js const greet = function(name) { return `Hello, ${name}`; }; const sayHi = greet; // 函数可以像值一样赋给另一个变量 sayHi('An'); // "Hello, An" ``` - 作为参数传递(回调函数): ```js 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 ``` - 作为返回值 ```js function multiplier(factor) { return (number) => number * factor; // 返回一个新函数 } const double = multiplier(2); const triple = multiplier(3); double(5); // 10 triple(5); // 15 // 这就是闭包的雏形——返回的函数"记住"了 factor 的值 // Day 4 会深入讲解 ``` - 存储在数据结构中: ```js 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 模块化之前,它是避免全局变量污染的主要手段。 - ```js // 经典写法 (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 对象上。 - ```js 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 在函数内都有这个特性。 - ```js 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 没有块级作用域。 - ```js { 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 引擎会沿着作用域链由内向外逐层查找,直到找到或到达全局作用域。 - ```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 使用词法作用域(也叫静态作用域),意思是函数的作用域在定义时就确定了,而不是在调用时。 - ```js const x = 'global'; function printX() { console.log(x); // 这里的 x 在定义时就绑定了全局的 x } function wrapper() { const x = 'wrapper'; printX(); // 虽然在 wrapper 内部调用,但打印的是 "global" } wrapper(); // "global",不是 "wrapper" ``` - 这个特性是闭包的基础。函数"记住"的是它被创建时的环境,而不是被调用时的环境。 - ```js function createPrinter() { const message = '来自 createPrinter'; return function() { console.log(message); // 词法作用域:绑定的是 createPrinter 内的 message }; } const printer = createPrinter(); function someOtherPlace() { const message = '来自 someOtherPlace'; printer(); // "来自 createPrinter",不受调用位置影响 } someOtherPlace(); ``` - 闭包 - 闭包是指一个函数能够访问其词法作用域中的变量,即使这个函数在其词法作用域之外执行。 - ```js 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 引擎就保留了这个变量。返回的函数 + 它引用的外部变量 = 闭包。 - 闭包应用场景 - 数据私有化 / 模块模式 ```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 被封闭在闭包中 ``` - 工厂函数 / 函数生成器 ```js 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):把接受多个参数的函数,变成一系列只接受一个参数的函数 ```js // 普通函数 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) ```js 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) ```js 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 中最基础的数据结构,用来存储键值对。 - 创建与属性访问: ```js // 对象字面量 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): ```js 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'; } } }; ``` - 属性的增删改查: ```js 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 ``` - 对象的遍历与常用静态方法 - ```js 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() 与冻结: ```js // 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 ``` - 解构赋值 — 对象 - 解构让你用简洁的语法从对象中提取值。 ```js 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: [...] } ``` - 函数参数中的解构(非常常用): ```js // ❌ 不解构:参数含义不清晰 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(); // 因为有 = {},不传也不报错 ``` - 数组基础与常用方法 - 创建数组: ```js 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] ``` - 增删元素(会修改原数组): ```js 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'] ``` - 不修改原数组的方法: ```js 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" ``` - 解构赋值-数组 - ```js 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) - 数组展开 ```js 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 ``` - 对象展开 ```js 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):只拷贝第一层,嵌套的对象/数组仍然是共享引用。 ```js 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):递归拷贝所有层级,完全独立。 ```js // 方法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 - 映射转换 - 对数组每个元素执行回调,将返回值收集成一个等长的新数组。不修改原数组。 - ```js 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'] ``` - 常见错误 ```js // ❌ 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 的元素,返回一个新数组(长度 ≤ 原数组)。 - ```js 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) - ```js // 最基本:求和 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。 - ```js 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(类似逻辑与)。 - ```js 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 码点排序,这对数字来说通常不是你想要的。 - ```js // ❌ 默认排序的坑 [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)或先拷贝: ```js 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),但只扁平一层。它特别适合一对多的映射场景。 - ```js // 普通 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)。它是纯粹用于副作用的方法。 - ```js 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。 - ```js function showThis() { console.log(this); } showThis(); // 浏览器:window,Node:global // 严格模式 function strictShow() { 'use strict'; console.log(this); } strictShow(); // undefined ``` - 规则二:隐式绑定 - 当函数作为对象的方法调用时(obj.method()),this 指向调用它的那个对象。 - ```js 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 — 立即调用,逐个传参 ```js 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 — 立即调用,参数以数组形式传入 ```js 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(不立即调用): ```js 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 绑定到这个新对象上。 - ```js 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),且永远不可改变。 - ```js 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()) - 原型与原型链 - 为什么需要原型 - 在没有原型机制的情况下,每次用构造函数创建实例,方法都会被重复创建 ```js 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()。 ```js 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 属性,指回函数本身 - ```js 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 - ```js 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 是原型继承的语法糖。 - 它不引入新的对象模型,底层仍然是原型机制,但写法更清晰、更接近传统面向对象语言。 - ```js // 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 和构造函数的区别: - ```js // 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 只能有一个。 - ```js 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: 必须是正数 ``` - 实例方法、访问器与类字段 - ```js 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 定义在类本身上,不是在实例上。通过类名调用,不能通过实例调用。 - ```js 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 引入 # 前缀表示真正的私有属性和方法,只能在类内部访问。 ```js 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 用于调用父类。 - ```js 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 ``` - 字符串与正则表达式 - 模板字符串深入 - ```js // 基本插值 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> `; ``` - 模板标签 - 标签模板允许你用一个函数来处理模板字符串,这是一个强大但不太常见的特性: - ```js // 标签函数接收两个参数: // 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 就是用标签模板实现的) ``` - 常用字符串方法 - ```js 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 高级用法 - ```js // 函数替换:每次匹配时调用函数,返回值作为替换内容 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" ``` - 正则表达式基础语法 - 正则表达式用于模式匹配,由模式和标志组成。 - 创建方式 ```js // 字面量(推荐,不需要转义反斜杠) const re1 = /hello/i; // 构造函数(适用于动态构建正则) const re2 = new RegExp('hello', 'i'); const keyword = 'world'; const re3 = new RegExp(keyword, 'gi'); // 动态模式 ``` - 常用标志 ```js /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 ``` - 正则核心语法 - 字符类 ```js // 普通字符:匹配自身 /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(合法的变量名) ``` - 量词 ```js * // 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>"](匹配第一个完整标签就停) ``` - 位置锚点 ```js ^ // 字符串开头(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"](两个都匹配) ``` - 分组与或 ```js // 分组 () /(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 的捕获组 ``` - 捕获组与反向引用 - ```js // 编号捕获组 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 引入的键值对集合,和普通对象的核心区别在于:任何类型都可以作为键。 - ```js 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 ``` - 用可迭代对象初始化: ```js // 用二维数组初始化 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 普通对象 ```js // 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 - 唯一值集合 - ```js 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 方法,并且该方法返回一个迭代器,这个对象就是可迭代的。 - ```js // 内置可迭代对象 // 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 }。 - ```js // 手动使用迭代器 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(); } ``` - 自定义可迭代对象 - ```js // 实现一个范围对象 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 暂停并产出值。它自动实现了迭代器协议。 - ```js // 基本语法 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] ``` - 错误处理 - 错误类型 - ```js // 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 - ```js // 基本结构 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 - ```js // 基本自定义错误 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): - ```js // 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 }; ``` - 命名导入 - ```js // 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; ``` - 默认导出 - 每个模块只能有一个默认导出。 - ```js // logger.js export default class Logger { log(msg) { console.log(`[LOG] ${msg}`); } error(msg) { console.error(`[ERR] ${msg}`); } } // 也可以导出函数或值 // export default function() { ... } // export default 42; ``` - ```js // 导入默认导出(不需要花括号,名字可以随意取) import Logger from './logger.js'; import MyLogger from './logger.js'; // 不同名字也行 const logger = new Logger(); logger.log('hello'); ``` - 混合使用 ```js // 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'; ``` - 高级模块用法 - 重新导出 ```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'; ``` - 动态导入 ```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 有重要区别。 - ```js // 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 是单线程语言——同一时刻只能执行一段代码 - 现实中很多操作是耗时的(网络请求、文件读写、定时器),如果同步等待,整个程序就会卡住 - 事件循环 -  - 宏任务与微任务 - ```js 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 — 延迟执行: - ```js // 基本用法 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 最早的异步处理方式——把"完成后要做的事"作为函数传进去。 - ```js // 最简单的回调 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,就不可再改变: - ```js // 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 — 处理成功和失败: - ```js 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 的状态: - ```js // 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 错误处理 - ```js // 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 — 无论成败都执行: - ```js 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 — 快速创建: - ```js // 创建一个立即 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 — 全部成功才成功: - ```js 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 的语法糖,让异步代码看起来像同步代码一样。 - ```js // 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 的值。 - ```js 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 链: ```js // 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 ```js 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 }; } ```