claude code plugin 实验
🎯 实验前:假设与目标 (Plan) 开发一个 Plugin 给 Claude Code 使用。 ...
js 复习
语言核心基础 - 语言核心基础 - 变量与数据类型 - 变量声明 - 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 }; } ```
OpenSpec 实验
实验元数据 (Meta Data) 用于日后检索和归档,建立知识索引。 实验编号/标题:OpenSpec 实验 ...
CSS 复习
基础核心 - 基础核心 - CSS 基础与选择器 - CSS 的三种引入方式 - ```html <!-- 1. 行内样式:直接写在标签上,优先级最高但不推荐大量使用 --> <p style="color: red; font-size: 16px;">这是行内样式</p> <!-- 2. 内嵌样式:写在 <head> 的 <style> 标签里 --> <head> <style> p { color: blue; } </style> </head> <!-- 3. 外链样式(推荐!):单独的 .css 文件 --> <link rel="stylesheet" href="style.css"> ``` - 一般使用外链样式,它能让 HTML 和 CSS 分离,方便维护和缓存 - 基础选择器 - ```css /* 元素选择器 —— 选中所有该标签 */ p { color: #333; } h1 { font-size: 24px; } /* 类选择器 —— 最常用,可复用 */ .card { background: #fff; padding: 16px; } .highlight { color: orange; } /* ID 选择器 —— 唯一的,一个页面同一个 ID 只用一次 */ #header { height: 60px; } /* 通配符选择器 —— 选中所有元素,常用于重置 */ * { margin: 0; padding: 0; box-sizing: border-box; /* 第 3 天会详细讲 */ } ``` - 优先使用类选择器,ID 选择器优先级太高不好覆盖,元素选择器范围太大容易误伤 - 组合选择器 - ```css /* 后代选择器(空格)—— 选中所有后代,不管嵌套多深 */ .article p { line-height: 1.8; } /* 子代选择器(>)—— 只选直接子元素 */ .nav > li { display: inline-block; } /* 相邻兄弟选择器(+)—— 紧挨着的下一个兄弟 */ h2 + p { font-size: 18px; /* 紧跟在 h2 后面的第一个 p */ } /* 通用兄弟选择器(~)—— 后面所有的兄弟 */ h2 ~ p { color: #666; /* h2 后面的所有 p */ } ``` - ```html <div class="nav"> <li>直接子元素 ✅ 被 .nav > li 选中</li> <li>直接子元素 ✅ <li>孙子元素 ❌ 不会被 .nav > li 选中</li> <li>孙子元素 ❌ 但会被 .nav li 选中</li> </li> </div> ``` - 多选择器组合技巧 - ```css /* 并集选择器(逗号)—— 同时给多个选择器设置相同样式 */ h1, h2, h3 { font-weight: bold; color: #222; } /* 交集选择器(紧挨着写)—— 同时满足多个条件 */ p.highlight { /* 既是 p 标签,又有 highlight 类 */ background: yellow; } /* 链式类选择器 —— 同时拥有多个类 */ .btn.primary.large { /* class="btn primary large" 的元素 */ padding: 12px 24px; } ``` - 伪类选择器 - 伪类用 : 开头,表示元素的某种状态或位置 - 交互状态伪类 ```css /* 鼠标悬停 */ .btn:hover { background: #2563eb; color: white; } /* 获得焦点(键盘 Tab 到或点击输入框时) */ input:focus { border-color: #2563eb; outline: none; } /* 点击瞬间 */ .btn:active { transform: scale(0.98); } /* 已访问的链接 */ a:visited { color: #666; } ``` - 结构位置伪类 ```css /* 第一个 / 最后一个子元素 */ li:first-child { font-weight: bold; } li:last-child { border-bottom: none; } /* 第 n 个子元素 —— 最灵活的选择器 */ tr:nth-child(2n) { background: #f9f9f9; /* 偶数行加背景,实现斑马纹 */ } tr:nth-child(odd) { background: #fff; /* odd = 奇数,even = 偶数 */ } /* 前 3 个 */ li:nth-child(-n+3) { color: red; } /* 每隔 3 个 */ li:nth-child(3n) { font-weight: bold; } /* 同类型的第一个(区分标签类型) */ p:first-of-type { font-size: 18px; } /* 唯一的子元素 */ p:only-child { text-align: center; } ``` - 其他实用伪类 ```css /* 否定伪类 —— 排除某些元素 */ li:not(:last-child) { border-bottom: 1px solid #eee; /* 除了最后一个都加下划线 */ } input:not([type="submit"]) { border: 1px solid #ddd; /* 除了提交按钮外的 input */ } /* 空元素 */ div:empty { display: none; /* 隐藏没有内容的 div */ } ``` - 伪元素选择器 - 用 :: 开头,会创建一个虚拟元素插入到页面中 - ```css /* ::before 在元素内容前面插入 */ .required::before { content: "* "; color: red; } /* ::after 在元素内容后面插入 */ a.external::after { content: " ↗"; font-size: 12px; } /* 经典用法:清除浮动(了解即可) */ .clearfix::after { content: ""; display: block; clear: both; } /* 装饰性下划线 */ .fancy-title::after { content: ""; display: block; width: 60px; height: 3px; background: #2563eb; margin-top: 8px; } /* 选中文本的样式 */ ::selection { background: #2563eb; color: white; } /* 输入框占位符 */ input::placeholder { color: #aaa; font-style: italic; } ``` - ::before 和 ::after 必须有 content 属性,哪怕是空字符串 "",否则不会显示。 - 属性选择器 - 根据 HTML 属性来选择元素,在表单样式中特别好用: - ```css /* 精确匹配 */ input[type="text"] { border: 1px solid #ddd; } input[type="password"] { letter-spacing: 4px; } /* 开头匹配 ^= */ a[href^="https"] { color: green; /* https 开头的链接标绿 */ } /* 结尾匹配 $= */ a[href$=".pdf"] { color: red; /* PDF 链接标红 */ } /* 包含匹配 *= */ img[src*="avatar"] { border-radius: 50%; /* src 中含 avatar 的图片变圆 */ } /* 有这个属性就选中 */ [disabled] { opacity: 0.5; cursor: not-allowed; } ``` - 优先级(权重)计算 - 当多条规则冲突时,浏览器按优先级决定谁生效 - ```css !important → 10000(尽量不用) 行内样式 style="" → 1000 ID 选择器 #id → 100 类/伪类/属性选择器 → 10 元素/伪元素选择器 → 1 通配符 * → 0 ``` - ```css /* 权重:0-0-1(一个元素) */ p { color: black; } /* 权重:0-1-0(一个类) */ .text { color: blue; } /* 权重:0-1-1(一个类 + 一个元素) */ p.text { color: green; } /* 权重:1-0-0(一个 ID)→ 这条赢 */ #intro { color: red; } /* 权重:0-2-1(两个类 + 一个元素) */ .card .content p { color: gray; } ``` - 盒模型深入 - 盒模型四层结构 -  - ```css .box { /* 内容区 */ width: 200px; height: 100px; /* 内边距 —— 内容到边框的呼吸空间 */ padding: 16px; /* 四边相同 */ padding: 16px 24px; /* 上下 16 左右 24 */ padding: 8px 16px 24px; /* 上 8 左右 16 下 24 */ padding: 8px 16px 24px 32px;/* 上 右 下 左(顺时针) */ /* 边框 */ border: 1px solid #ddd; border-radius: 8px; /* 圆角 */ /* 外边距 —— 盒子与其他元素的间距 */ margin: 20px; margin: 0 auto; /* 水平居中的经典写法 */ } ``` - padding 和 margin 的简写顺序是顺时针:上 → 右 → 下 → 左。两个值时是"上下"和"左右",三个值是"上""左右""下"。 - box-sizing:最重要的一个属性 - ```css /* 默认值:width 只算内容区 */ .content-box { box-sizing: content-box; width: 200px; padding: 20px; border: 2px solid #000; /* 实际占据宽度 = 200 + 20*2 + 2*2 = 244px 😰 */ } /* 推荐值:width 包含 padding 和 border */ .border-box { box-sizing: border-box; width: 200px; padding: 20px; border: 2px solid #000; /* 实际占据宽度 = 200px 🎉 内容区自动缩小为 156px */ } ``` - content-box 的计算方式违反直觉,设了 width: 200px 结果实际更宽。 - 所以现代开发第一行永远是: ```css /* 全局重置 —— 背下来,每个项目都加 */ *, *::before, *::after { box-sizing: border-box; } ``` - margin 合并 - 相邻兄弟的 margin 合并 ```css .box-a { margin-bottom: 30px; } .box-b { margin-top: 20px; } /* 两者之间的间距不是 50px,而是 30px(取较大值) */ ``` - 父子元素的 margin 穿透 ```html <div class="parent"> <div class="child">子元素</div> </div> <style> .parent { background: #f0f0f0; } .child { margin-top: 30px; } /* 这个 margin 会"穿透"到父元素外面!父元素整体下移 30px */ </style> ``` - 解决办法 ```css /* 方法 1:给父元素加 overflow */ .parent { overflow: hidden; } /* 方法 2:给父元素加 padding 代替子元素的 margin */ .parent { padding-top: 30px; } /* 方法 3:给父元素加 border(哪怕是透明的) */ .parent { border-top: 1px solid transparent; } /* 方法 4(最推荐):用 Flexbox,自动避免合并 */ .parent { display: flex; flex-direction: column; } ``` - 为了避免 margin 合并带来的混乱,养成一个好习惯:统一只用一个方向的 margin。推荐只用 margin-bottom,不要上下混用 ```css /* ✅ 好习惯:统一用 margin-bottom */ h2 { margin-bottom: 12px; } p { margin-bottom: 16px; } /* ❌ 坏习惯:上下混用 */ h2 { margin-bottom: 12px; } p { margin-top: 20px; } /* 和 h2 的 bottom 合并了,不直观 */ ``` - display 属性 ```css /* block —— 独占一行,可以设宽高 */ /* 默认就是 block 的标签:div, p, h1~h6, ul, li, section... */ .block-el { display: block; width: 300px; /* ✅ 生效 */ height: 100px; /* ✅ 生效 */ margin: 20px; /* ✅ 四个方向都生效 */ } /* inline —— 和文字一起排列,不能设宽高 */ /* 默认就是 inline 的标签:span, a, strong, em, img... */ .inline-el { display: inline; width: 300px; /* ❌ 无效 */ height: 100px; /* ❌ 无效 */ margin-top: 20px; /* ❌ 上下 margin 无效 */ padding: 10px; /* ⚠️ 左右有效,上下视觉上有但不影响布局 */ } /* inline-block —— 既能和文字一行排列,又能设宽高 */ /* 两全其美,适合按钮、标签等小组件 */ .inline-block-el { display: inline-block; width: 120px; /* ✅ 生效 */ height: 40px; /* ✅ 生效 */ margin: 10px; /* ✅ 四个方向都生效 */ } ``` - 常用技巧:把 <a> 或 <span> 改成 inline-block,就可以给它们设置宽高和完整的 padding/margin 了。 ```css /* 经典用例:inline-block 做按钮 */ .tag { display: inline-block; padding: 4px 12px; background: #e8f4fd; color: #1a73e8; border-radius: 4px; font-size: 13px; } ``` - border 的用法 - ```css /* 基础语法:宽度 样式 颜色 */ .box { border: 1px solid #ddd; } /* 单侧边框 */ .section { border-left: 3px solid #2563eb; padding-left: 12px; } /* 圆角 */ .card { border-radius: 8px; } /* 四角相同 */ .avatar { border-radius: 50%; } /* 正圆(需要宽高相等) */ .pill { border-radius: 9999px; } /* 胶囊形状 */ /* 只圆上面两个角 */ .tab { border-radius: 8px 8px 0 0; } /* 虚线 / 点线 */ .dashed { border: 2px dashed #aaa; } .dotted { border: 1px dotted #aaa; } /* outline —— 不占空间的"边框",常用于焦点指示 */ button:focus { outline: 2px solid #2563eb; outline-offset: 2px; } ``` - 文档流与定位 - 正常文档流 -  - 正常文档流中的元素互相"知道"彼此的存在,会自动避让 - 接下来要学的定位方式,有些会脱离文档流,让元素"飘起来",不再影响其他元素的排列 - position 五种定位方式 - static 默认值 - ```css .box { position: static; /* 默认就是这个,不用写 */ /* top/left/right/bottom 无效 */ /* z-index 无效 */ } ``` - relative 相对定位 - ```css .box { position: relative; top: 20px; /* 相对原位向下移 20px */ left: 30px; /* 相对原位向右移 30px */ } ``` -  - relative 单独用的场景不多,它最大的价值是给 absolute 子元素提供定位参考点。 - absolute 绝对定位(重点) - 脱离文档流,相对于最近的有定位属性的祖先元素来定位 - ```css /* 经典搭配:父 relative + 子 absolute */ .parent { position: relative; /* 成为定位参考点 */ width: 400px; height: 300px; } .child { position: absolute; top: 0; right: 0; /* 贴在父元素的右上角 */ /* 这个子元素已经脱离文档流,不占原来的空间 */ } ``` - 如果找不到有定位属性的祖先,就相对于整个页面(<html>)定位。 - 常见用法 ```css /* 右上角的徽标 / 角标 */ .avatar-wrap { position: relative; display: inline-block; } .badge { position: absolute; top: -4px; right: -4px; width: 18px; height: 18px; background: red; color: white; font-size: 12px; text-align: center; line-height: 18px; border-radius: 50%; } /* 绝对定位实现水平垂直居中 */ .centered { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } /* 铺满父元素(做遮罩层常用) */ .overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; /* 等价于简写:inset: 0; */ background: rgba(0, 0, 0, 0.5); } ``` - fixed - 脱离文档流,相对于浏览器视口定位,滚动页面也不动: - ```css /* 固定顶部导航 */ .navbar { position: fixed; top: 0; left: 0; right: 0; /* left + right = 0 让它撑满整个宽度 */ height: 60px; background: white; z-index: 100; /* 确保在最上层 */ } /* 别忘了给 body 加上 padding,防止内容被导航遮住 */ body { padding-top: 60px; } /* 右下角的回到顶部按钮 */ .back-to-top { position: fixed; bottom: 30px; right: 30px; width: 44px; height: 44px; } ``` - sticky 粘性定位(现代常用) - 结合了 relative 和 fixed 的效果——正常情况下跟着滚动,滚到指定位置后就"粘"住 - 父元素不能设 overflow: hidden 或 overflow: auto,否则 sticky 会失效。 - ```css /* 表格的表头粘在顶部 */ thead th { position: sticky; top: 0; background: white; } /* 侧边栏跟着滚到一定位置后固定 */ .sidebar { position: sticky; top: 80px; /* 距顶部 80px 时粘住 */ } /* 分组列表的标题吸顶 */ .group-title { position: sticky; top: 0; background: #f5f5f5; padding: 8px 16px; font-weight: bold; } ``` - z-index 层叠上下文 - 当元素重叠时,z-index 控制谁在上面 - ```css /* z-index 只对有定位属性的元素生效(非 static) */ .layer-back { position: relative; z-index: 1; } .layer-middle { position: relative; z-index: 10; } .layer-front { position: relative; z-index: 100; } ``` - 分层规范 ```css /* 建议给项目定一套层级规范,避免数字乱飞 */ :root { --z-dropdown: 100; /* 下拉菜单 */ --z-sticky: 200; /* 粘性元素 */ --z-navbar: 300; /* 导航栏 */ --z-overlay: 400; /* 遮罩层 */ --z-modal: 500; /* 弹窗 */ --z-toast: 600; /* 提示消息 */ } .modal { z-index: var(--z-modal); } .overlay { z-index: var(--z-overlay); } .navbar { z-index: var(--z-navbar); } ``` - z-index 不是全局比较的。每个设置了定位 + z-index 的元素会创建一个层叠上下文,子元素的 z-index 只在父元素内部比较: - 浮动 float (了解即可) - 浮动是 CSS2 时代的布局方式,现在已经被 Flexbox 和 Grid 替代 - ```css /* 文字环绕图片 —— float 唯一还在用的场景 */ .article img { float: left; margin-right: 16px; margin-bottom: 8px; } ``` - 如果遇到浮动导致父元素高度塌陷的问题,用学过的 ::after 清除浮动或者直接改用 Flexbox 就好 - 颜色与背景 - 颜色表示法 - HEX(十六进制) ```css /* 6 位写法:#RRGGBB */ .box { color: #2563eb; } /* 3 位简写:当每组两位相同时 */ .box { color: #fff; } /* 等价于 #ffffff */ .box { color: #333; } /* 等价于 #333333 */ /* 8 位写法:带透明度 #RRGGBBAA */ .box { background: #2563eb80; } /* 50% 透明度 */ ``` - HEX 是最常见的写法,设计稿给的颜色值通常就是 HEX。缺点是不直观,看不出颜色长什么样。 - RGB/RGBA - ```css /* rgb(红, 绿, 蓝) 每个值 0~255 */ .box { color: rgb(37, 99, 235); } /* rgba 加一个透明度参数 0~1 */ .box { background: rgba(37, 99, 235, 0.5); } /* 50% 透明 */ /* 现代写法:直接在 rgb 里加透明度,用 / 分隔 */ .box { background: rgb(37 99 235 / 0.5); } ``` - HSL / HSLA(推荐学会) - ```css /* hsl(色相, 饱和度, 亮度) */ /* 色相 H:0~360 的色环角度 0/360 = 红,120 = 绿,240 = 蓝 饱和度 S:0% = 灰色,100% = 纯色 亮度 L:0% = 黑色,50% = 正常,100% = 白色 */ .box { color: hsl(220, 85%, 53%); } /* 蓝色 */ .box { color: hsl(220, 85%, 53%, 0.5); } /* 半透明蓝 */ ``` - ```css /* 同一个蓝色,只改亮度就能得到一组深浅色 */ .blue-50 { background: hsl(220, 85%, 97%); } /* 极浅蓝背景 */ .blue-100 { background: hsl(220, 85%, 90%); } /* 浅蓝 */ .blue-500 { background: hsl(220, 85%, 53%); } /* 主色 */ .blue-700 { background: hsl(220, 85%, 35%); } /* 深蓝 */ .blue-900 { background: hsl(220, 85%, 20%); } /* 极深蓝文字 */ /* 只改色相就能切换颜色 */ .red { color: hsl(0, 85%, 53%); } .green { color: hsl(120, 85%, 35%); } .purple { color: hsl(270, 85%, 53%); } ``` - 命名颜色 - ```css .box { color: red; } .box { background: transparent; } /* 透明,这个经常用 */ .box { color: currentColor; } /* 继承当前元素的 color 值 */ ``` - opacity 与 RGBA 透明的区别 ```css /* opacity:整个元素(包括文字和子元素)都变透明 */ .card { background: #2563eb; color: white; opacity: 0.5; /* 背景、文字、子元素全部变成 50% 透明 😰 */ } /* rgba / hsla:只让颜色本身透明 */ .card { background: rgba(37, 99, 235, 0.5); color: white; /* 只有背景半透明,文字完全清晰 ✅ */ } ``` - 背景属性 - background-color ```css .box { background-color: #f5f5f5; } ``` - 渐变背景(重点) ```css /* 线性渐变 */ .banner { background: linear-gradient(to right, #667eea, #764ba2); } /* 指定角度 */ .banner { background: linear-gradient(135deg, #667eea, #764ba2); } /* 多色渐变 */ .rainbow { background: linear-gradient( 90deg, #ff6b6b, #feca57, #48dbfb, #ff9ff3 ); } /* 控制颜色位置 */ .sharp { background: linear-gradient( 90deg, #2563eb 0%, #2563eb 50%, /* 蓝色占前 50% */ #f59e0b 50%, /* 黄色从 50% 开始,形成硬分界 */ #f59e0b 100% ); } /* 径向渐变(从中心向外) */ .spotlight { background: radial-gradient(circle, #fff, #e2e8f0); } /* 椭圆渐变 + 偏移中心 */ .glow { background: radial-gradient( ellipse at top left, rgba(37, 99, 235, 0.2), transparent 60% ); } ``` - 背景图片 ```css .hero { /* 基础用法 */ background-image: url('hero.jpg'); /* 不重复 */ background-repeat: no-repeat; /* 覆盖整个容器(可能裁切) */ background-size: cover; /* 完全显示(可能留白) */ background-size: contain; /* 居中显示 */ background-position: center; /* 滚动时背景固定(视差效果) */ background-attachment: fixed; /* 简写(推荐记住这个万能组合) */ background: url('hero.jpg') no-repeat center / cover; } ``` - 多重背景叠加 ```css /* 渐变叠加在图片上 —— 做图文卡片的经典手法 */ .card-with-image { background: linear-gradient( to bottom, transparent 40%, rgba(0, 0, 0, 0.7) 100% ), url('photo.jpg') no-repeat center / cover; color: white; } /* 装饰性渐变叠加 */ .fancy-bg { background: radial-gradient(ellipse at top left, rgba(37, 99, 235, 0.15), transparent 50%), radial-gradient(ellipse at bottom right, rgba(245, 158, 11, 0.15), transparent 50%), #fafafa; } ``` - 配色技巧 ```css /* 一个页面通常只需要这几种颜色 */ :root { --color-primary: hsl(220, 85%, 53%); /* 主色:按钮、链接 */ --color-primary-light: hsl(220, 85%, 95%);/* 主色浅版:背景 */ --color-primary-dark: hsl(220, 85%, 40%); /* 主色深版:悬停 */ --color-gray-50: hsl(220, 10%, 98%); /* 页面背景 */ --color-gray-200: hsl(220, 10%, 88%); /* 边框、分隔线 */ --color-gray-500: hsl(220, 10%, 50%); /* 次要文字 */ --color-gray-900: hsl(220, 10%, 12%); /* 主文字 */ --color-success: hsl(145, 65%, 42%); /* 成功 */ --color-warning: hsl(40, 95%, 55%); /* 警告 */ --color-danger: hsl(0, 75%, 55%); /* 错误 */ } /* 使用变量 */ .btn-primary { background: var(--color-primary); color: white; } .btn-primary:hover { background: var(--color-primary-dark); } ``` - 字体与文字排版 - font-family 字体栈 - ```css /* 字体栈:从左到右依次尝试,找到第一个可用的就使用 */ body { font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; } ``` - ```css font-family: "PingFang SC", /* macOS 上的中文字体 */ "Microsoft YaHei", /* Windows 上的中文字体 */ "Helvetica Neue", /* macOS 上的英文字体 */ Arial, /* Windows 上的英文字体 */ sans-serif; /* 兜底:让系统选一个无衬线字体 */ ``` - 引入 google fonts - ```html <!-- 在 HTML 的 <head> 中引入 --> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet"> ``` - ```css body { font-family: "Noto Sans SC", sans-serif; } ``` -系统字体栈(零加载延迟) ```css /* 直接使用用户系统里的字体,不需要下载 */ body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans SC", sans-serif; } /* 代码用等宽字体 */ code, pre { font-family: "Fira Code", "JetBrains Mono", "Source Code Pro", Consolas, monospace; } ``` - font-size 与单位 - ```css /* px —— 绝对单位,固定大小 */ h1 { font-size: 32px; } /* em —— 相对于父元素的 font-size */ .parent { font-size: 16px; } .child { font-size: 1.5em; } /* = 16 × 1.5 = 24px */ /* em 的问题:层层嵌套时会复利式叠加 */ .child .grandchild { font-size: 1.5em; } /* = 24 × 1.5 = 36px 😰 */ /* rem —— 相对于根元素(html)的 font-size ⭐ 推荐 */ html { font-size: 16px; } /* 浏览器默认就是 16px */ h1 { font-size: 2rem; } /* = 16 × 2 = 32px */ h2 { font-size: 1.5rem; } /* = 16 × 1.5 = 24px */ p { font-size: 1rem; } /* = 16px */ ``` - ```css /* px —— 绝对单位,固定大小 */ h1 { font-size: 32px; } /* em —— 相对于父元素的 font-size */ .parent { font-size: 16px; } .child { font-size: 1.5em; } /* = 16 × 1.5 = 24px */ /* em 的问题:层层嵌套时会复利式叠加 */ .child .grandchild { font-size: 1.5em; } /* = 24 × 1.5 = 36px 😰 */ /* rem —— 相对于根元素(html)的 font-size ⭐ 推荐 */ html { font-size: 16px; } /* 浏览器默认就是 16px */ h1 { font-size: 2rem; } /* = 16 × 2 = 32px */ h2 { font-size: 1.5rem; } /* = 16 × 1.5 = 24px */ p { font-size: 1rem; } /* = 16px */ ``` - 记住一个原则:font-size 用 rem,padding/margin 等间距可以用 rem 或 px。 - rem 的好处是后面做响应式时,只需要改 html 的 font-size 就能整体缩放。 - font 相关属性大全 - ```css .text { /* 字号 */ font-size: 16px; /* 字重:100~900,或 normal(400) / bold(700) */ font-weight: 400; /* 正常 */ font-weight: 700; /* 加粗 */ /* 风格 */ font-style: normal; /* 正常 */ font-style: italic; /* 斜体 */ /* 简写(顺序固定:style weight size/line-height family) */ font: italic 700 18px/1.6 "Noto Sans SC", sans-serif; } ``` - 一般只用 400(正文)和 700(标题/强调)两种就够了,最多加一个 500(中等) - 行高与文本间距 - ```css .article { /* line-height:行高,控制行与行之间的距离 */ line-height: 1.8; /* 无单位写法(推荐)= font-size × 1.8 */ line-height: 28px; /* 固定值写法 */ /* letter-spacing:字间距 */ letter-spacing: 0.5px; /* 中文正文加一点字间距更透气 */ letter-spacing: 2px; /* 大标题可以拉大 */ letter-spacing: -0.5px; /* 紧凑排列 */ /* word-spacing:词间距(英文用) */ word-spacing: 2px; } ``` - ```css /* 正文:1.6 ~ 1.8,中文偏大更舒适 */ .body-text { line-height: 1.7; } /* 标题:1.2 ~ 1.4,紧凑一些更有力 */ h1, h2, h3 { line-height: 1.3; } /* 按钮/标签:等于 height 实现垂直居中 */ .btn { height: 40px; line-height: 40px; /* 文字垂直居中 */ } /* UI 组件:1.4 ~ 1.5 */ .card p { line-height: 1.5; } ``` - 文本对齐与装饰 - ```css /* 水平对齐 */ .left { text-align: left; } .center { text-align: center; } .right { text-align: right; } .justify { text-align: justify; } /* 两端对齐,中文排版常用 */ /* 文本装饰 */ a { text-decoration: none; } /* 去掉链接下划线 */ .del { text-decoration: line-through; } /* 删除线 */ .underline { text-decoration: underline; text-decoration-color: #2563eb; /* 下划线颜色 */ text-underline-offset: 4px; /* 下划线和文字的距离 */ text-decoration-thickness: 2px; /* 下划线粗细 */ } /* 文本转换 */ .upper { text-transform: uppercase; } /* 全大写 */ .lower { text-transform: lowercase; } /* 全小写 */ .cap { text-transform: capitalize; } /* 首字母大写 */ /* 文本缩进(中文段落首行缩进) */ .cn-paragraph { text-indent: 2em; } /* 两个字的宽度 */ ``` - 文本溢出管理 - ```css /* 单行文本溢出显示省略号 —— 三件套必须一起用 */ .ellipsis { white-space: nowrap; /* 不换行 */ overflow: hidden; /* 超出隐藏 */ text-overflow: ellipsis; /* 显示省略号 */ } /* 多行文本溢出(限制 3 行) */ .line-clamp { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; /* 最多显示 3 行 */ overflow: hidden; } /* white-space 的几个值 */ .nowrap { white-space: nowrap; } /* 强制不换行 */ .pre { white-space: pre; } /* 保留空格和换行(像 <pre>) */ .prewrap { white-space: pre-wrap; } /* 保留空格换行,但允许自动折行 */ /* overflow-wrap:长单词/URL 强制断行 */ .break-word { overflow-wrap: break-word; /* 旧写法:word-break: break-all; 也能用但更暴力 */ } ``` 布局体系 - 布局体系 - Flex 基础与主轴 - Flex 容器与项目 - ```html <!-- 容器(父元素):加 display: flex 的那个 --> <div class="container"> <!-- 项目(子元素):容器的直接子元素自动成为 flex 项目 --> <div class="item">A</div> <div class="item">B</div> <div class="item">C</div> </div> ``` - ```css .container { display: flex; /* 这一行就开启了 Flexbox */ } ``` -  - inline-flex,和 flex 唯一的区别是容器本身变成行内元素 - ```css /* flex:容器是块级元素,独占一行 */ .container { display: flex; } /* inline-flex:容器是行内元素,可以和其他内容并排 */ .badge-group { display: inline-flex; } ``` - 主轴与交叉轴 -  - justify-content 控制主轴,align-items 控制交叉轴。主轴方向由 flex-direction 决定。 - flex-direction 主轴方向 - ```css .container { display: flex; /* 四个方向 */ flex-direction: row; /* 默认:从左到右 → */ flex-direction: row-reverse; /* 从右到左 ← */ flex-direction: column; /* 从上到下 ↓ */ flex-direction: column-reverse; /* 从下到上 ↑ */ } ``` - ```css /* 水平导航 */ .nav { display: flex; flex-direction: row; } /* 垂直堆叠的表单 */ .form { display: flex; flex-direction: column; } /* 移动端常见:图标在上文字在下 */ .tab-item { display: flex; flex-direction: column; align-items: center; } ``` - justify-content 主轴对齐(重点) - ```css .container { display: flex; justify-content: flex-start; /* 默认:靠起始端 */ justify-content: flex-end; /* 靠末端 */ justify-content: center; /* 居中 */ justify-content: space-between; /* 两端对齐,中间等分 */ justify-content: space-around; /* 每个项目两侧等间距 */ justify-content: space-evenly; /* 所有间距完全相等 */ } ``` -  - 常用场景 ```css /* 导航栏:logo 在左,链接在右 */ .navbar { display: flex; justify-content: space-between; } /* 按钮组居中 */ .button-group { display: flex; justify-content: center; } /* 底部 Tab 栏等分 */ .tab-bar { display: flex; justify-content: space-evenly; } /* 表单按钮靠右 */ .form-actions { display: flex; justify-content: flex-end; } ``` - flex-wrap - 默认情况下所有项目挤在一行,即使溢出也不换行 - ```css .container { display: flex; flex-wrap: nowrap; /* 默认:不换行,项目可能被压缩 */ flex-wrap: wrap; /* 放不下就换行 */ flex-wrap: wrap-reverse; /* 换行但方向反转(新行在上面) */ } ``` - flex-wrap: wrap 配合固定宽度的项目可以实现简单的网格效果 ```css /* 简易卡片网格 */ .card-list { display: flex; flex-wrap: wrap; gap: 16px; } .card { width: 300px; /* 固定宽度,放不下自动换行 */ } ``` - flex-flow 简写 - ```css /* 简写:flex-flow: <direction> <wrap> */ .container { flex-flow: row wrap; /* 水平排列 + 允许换行 */ flex-flow: column nowrap; /* 垂直排列 + 不换行 */ } /* 等价于分开写 */ .container { flex-direction: row; flex-wrap: wrap; } ``` - gap 间距 - gap 是控制 Flex 项目之间间距的最佳方式,比给每个项目加 margin 干净得多 - ```css .container { display: flex; gap: 16px; /* 行列间距都是 16px */ gap: 16px 24px; /* 行间距 16px,列间距 24px */ row-gap: 16px; /* 只设行间距 */ column-gap: 24px; /* 只设列间距 */ } ``` - gap 相比 margin 的优势 ```css /* ❌ 用 margin 的痛苦:最后一个元素多出右边距 */ .item { margin-right: 16px; } .item:last-child { margin-right: 0; } /* 还得手动去掉 */ /* ✅ 用 gap:自动只在项目之间加间距,边缘没有 */ .container { display: flex; gap: 16px; } ``` - Flexbox 交叉轴与项目属性 - align-items 交叉轴对齐(重点) - justify-content 管主轴,align-items 管交叉轴。当主轴是水平方向时,交叉轴就是垂直方向 - ```css .container { display: flex; height: 200px; /* 容器要有高度才能看出交叉轴效果 */ align-items: stretch; /* 默认:项目拉伸撑满容器高度 */ align-items: flex-start; /* 顶部对齐 */ align-items: flex-end; /* 底部对齐 */ align-items: center; /* 垂直居中 */ align-items: baseline; /* 按文字基线对齐 */ } ``` -  - align-self 单个项目的对齐 - align-items 是容器属性,控制所有项目。align-self 写在项目上,让单个项目有不同的对齐方式 - ```css .container { display: flex; align-items: flex-start; /* 所有项目顶部对齐 */ height: 200px; } /* 只让第三个项目底部对齐 */ .item-c { align-self: flex-end; } /* 只让第二个项目居中 */ .item-b { align-self: center; } ``` - align-content 多行对齐 - align-content 只在 flex-wrap: wrap 产生多行时才有效果,它控制的是多行整体在交叉轴上的分布 - ```css .container { display: flex; flex-wrap: wrap; height: 400px; align-content: flex-start; /* 所有行挤在顶部 */ align-content: flex-end; /* 所有行挤在底部 */ align-content: center; /* 所有行居中 */ align-content: space-between; /* 首行贴顶末行贴底,中间等分 */ align-content: space-around; /* 每行上下等间距 */ align-content: stretch; /* 默认:行高拉伸填满容器 */ } ``` -  - Flex 项目属性(核心) - 写在子元素上的三个属性,控制项目如何分配空间 - flex-basis 基础尺寸 - ```css .item { /* flex-basis 定义项目在分配多余空间之前的初始大小 */ flex-basis: auto; /* 默认:使用 width 或内容宽度 */ flex-basis: 200px; /* 固定初始宽度 200px */ flex-basis: 30%; /* 初始占 30% */ flex-basis: 0; /* 初始宽度为 0,完全由 flex-grow 决定 */ } ``` - flex-basis 和 width 的关系:当两者同时存在时,flex-basis 优先级更高(主轴为水平时) - 用了 Flexbox 后建议统一用 flex-basis 代替 width。 - flex-grow 放大比例 - 当容器有剩余空间时,flex-grow 决定每个项目分到多少: - ```css /* 默认 flex-grow: 0,不放大 */ /* 三个项目各自宽 100px,容器宽 600px,剩余 300px */ .item-a { flex-grow: 1; } /* 分到 300 × 1/3 = 100px → 总宽 200px */ .item-b { flex-grow: 1; } /* 分到 300 × 1/3 = 100px → 总宽 200px */ .item-c { flex-grow: 1; } /* 分到 300 × 1/3 = 100px → 总宽 200px */ ``` - 常见用法 ```css /* 让某个项目占满剩余空间 */ .sidebar { width: 250px; } /* 固定宽度 */ .main { flex-grow: 1; } /* 占满剩余 */ /* 搜索框:按钮固定宽,输入框自适应 */ .search-bar { display: flex; } .search-input { flex-grow: 1; } /* 输入框占满剩余空间 */ .search-btn { width: 80px; } /* 按钮固定宽 */ ``` - flex-shrink 缩小比例 - 当容器空间不足时,flex-shrink 决定每个项目被压缩多少 - ```css /* 默认 flex-shrink: 1,所有项目等比缩小 */ /* 不想被压缩(比如侧边栏固定宽度) */ .sidebar { flex-shrink: 0; /* 我不缩! */ width: 250px; } .main { flex-shrink: 1; /* 空间不够时我来缩 */ } ``` - flex 简写 ⭐ 必须掌握 - ```css /* flex: grow shrink basis */ flex: 0 1 auto; /* 默认值:不放大,可缩小,尺寸由内容决定 */ flex: 1; /* 等价于 flex: 1 1 0% → 等分剩余空间 */ flex: auto; /* 等价于 flex: 1 1 auto → 基于内容的弹性 */ flex: none; /* 等价于 flex: 0 0 auto → 完全不伸缩 */ flex: 0 0 200px; /* 固定 200px,不伸不缩 */ ``` - flex: 1 vs flex: auto 的区别 - ```css /* flex: 1 → flex: 1 1 0% */ /* basis 为 0,忽略内容宽度,纯按 grow 比例分配 */ /* flex: auto → flex: 1 1 auto */ /* basis 为 auto,先考虑内容宽度,再按 grow 比例分配剩余空间 */ ``` - 大多数情况用 flex: 1 就对了,想要项目完全等宽就用它。 - 经典布局模式 - ```css /* 模式 1:水平垂直居中(最经典的 Flex 用法) */ .center-box { display: flex; justify-content: center; align-items: center; } /* 模式 2:左右两端对齐 + 垂直居中(导航栏) */ .navbar { display: flex; justify-content: space-between; align-items: center; } /* 模式 3:固定侧边栏 + 自适应主内容 */ .layout { display: flex; } .sidebar { flex: 0 0 250px; } /* 固定 250px */ .main { flex: 1; } /* 占满剩余 */ /* 模式 4:底部固定 Footer(页面不够高时 Footer 也贴底) */ .page { display: flex; flex-direction: column; min-height: 100vh; } .page-header { /* 自然高度 */ } .page-main { flex: 1; } /* 撑满中间空间 */ .page-footer { /* 自然高度 */ } /* 模式 5:左图右文(或左文右图) */ .media { display: flex; align-items: flex-start; /* 顶部对齐,不拉伸 */ gap: 16px; } .media-image { flex: 0 0 120px; } /* 图片固定宽 */ .media-body { flex: 1; } /* 文字占满 */ ``` - Grid 基础与轨道定义 - Grid 容器与基本概念 - ```css .container { display: grid; /* 开启 Grid 布局 */ } ``` -  - grid-template-columns 定义列 - ```css /* 固定宽度 */ .grid { display: grid; grid-template-columns: 200px 300px 200px; /* 三列:200 + 300 + 200 */ } /* 百分比 */ .grid { grid-template-columns: 25% 50% 25%; } /* fr 单位 —— 按比例分配剩余空间 ⭐ */ .grid { grid-template-columns: 1fr 2fr 1fr; /* 总共 4fr,第一列 1/4,第二列 2/4,第三列 1/4 */ } /* 混合使用 */ .grid { grid-template-columns: 250px 1fr 200px; /* 两侧固定,中间自适应 —— 比 Flex 更简洁! */ } ``` - `fr` 是 Grid 独有的单位,表示 fraction(份)。它会在固定尺寸分配完之后,按比例分配剩余空间 - grid-template-rows 定义行 - ```css .grid { display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 80px 1fr 60px; /* 三行:头 80px + 中间自适应 + 底 60px */ } /* 行通常不需要显式定义,让内容撑开即可 */ .grid { grid-template-columns: 1fr 1fr 1fr; /* 不写 grid-template-rows,行高由内容决定 */ } ``` - repeat() 简化重复定义 - ```css /* 写 12 列等宽 */ /* ❌ 繁琐写法 */ grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; /* ✅ repeat 写法 */ grid-template-columns: repeat(12, 1fr); /* 重复模式 */ grid-template-columns: repeat(3, 1fr 2fr); /* 等价于:1fr 2fr 1fr 2fr 1fr 2fr(6 列) */ /* 和固定列混合 */ grid-template-columns: 200px repeat(3, 1fr) 200px; /* 等价于:200px 1fr 1fr 1fr 200px(5 列) */ ``` - minmax() 弹性范围 - minmax(最小值, 最大值) 让列宽在一个范围内弹性变化: - ```css .grid { grid-template-columns: minmax(200px, 300px) 1fr 1fr; /* 第一列:最小 200px,最大 300px */ /* 后两列等分剩余空间 */ } /* 行高最小 100px,内容多时自动撑高 */ .grid { grid-template-rows: minmax(100px, auto); } ``` - gap 行列间距 - 和 Flexbox 的 gap 一样,但 Grid 里可以分别控制行间距和列间距 - ```css .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; /* 行列间距都是 20px */ gap: 16px 24px; /* 行间距 16px,列间距 24px */ row-gap: 16px; /* 只设行间距 */ column-gap: 24px; /* 只设列间距 */ } ``` - 隐式网格与 grid-auto-rows - 你定义了 2 行 3 列 = 6 个格子,但如果有 9 个项目,多出来的 3 个怎么办? - Grid 会自动创建新行来容纳它们,这就是隐式网格: - ```css .grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: 150px 150px; /* 只定义了 2 行 */ /* 隐式创建的行默认高度由内容决定 */ /* 用 grid-auto-rows 控制隐式行的高度 */ grid-auto-rows: 150px; /* 隐式行也是 150px */ grid-auto-rows: minmax(100px, auto); /* 最少 100px,内容多就撑高 */ } ``` - Grid 项目放置与区域 - grid-column / grid-row 放置项目 - 用网格线编号来精确控制项目的位置和大小 - ```css .item { /* 完整写法 */ grid-column-start: 1; grid-column-end: 3; /* 从列线 1 到列线 3 → 跨 2 列 */ grid-row-start: 1; grid-row-end: 2; /* 从行线 1 到行线 2 → 占 1 行 */ /* 简写(推荐) */ grid-column: 1 / 3; /* 起始线 / 结束线 */ grid-row: 1 / 2; } ``` -  - span 关键字 - 用 span 表示跨几格 - ```css /* 以下三种写法效果相同 */ .item { grid-column: 1 / 3; } /* 从线 1 到线 3 */ .item { grid-column: 1 / span 2; } /* 从线 1 起,跨 2 列 */ .item { grid-column: span 2; } /* 从自然位置起,跨 2 列 */ ``` - span 在实际开发中更常用,因为你不需要关心具体的线编号 - ```css /* 图片画廊:让某张特色图跨 2 列 2 行 */ .photo-featured { grid-column: span 2; grid-row: span 2; } /* 跨满整行 */ .full-width { grid-column: 1 / -1; /* 从第一条线到最后一条线 */ } ``` - 实现各种跨列跨行布局 - ```css .grid { display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-rows: 120px; gap: 12px; } /* 横幅:跨满一整行 */ .banner { grid-column: 1 / -1; /* 第 1 条线到最后一条线 */ } /* 大卡片:跨 2 列 2 行 */ .large-card { grid-column: span 2; grid-row: span 2; } /* 侧边栏:固定在右侧,跨 3 行 */ .sidebar { grid-column: 4; /* 只写一个值 = 起始线,占 1 列 */ grid-row: 1 / span 3; /* 从第 1 行起跨 3 行 */ } /* 自由放置到任意位置 */ .special { grid-column: 2 / 4; /* 第 2~3 列 */ grid-row: 2 / 4; /* 第 2~3 行 */ } /* ┌──────────────────────────────┐ │ banner (跨 4 列) │ ├───────────┬────────┬─────────┤ │ │ C │ │ │ large ├────────┤ sidebar │ │ (2×2) │ D │ (1×3) │ ├─────┬─────┼────────┤ │ │ E │ F │ G │ │ └─────┴─────┴────────┴─────────┘ */ ``` - grid-template-areas 命名区域 ⭐ 重点 - 这是 Grid 最直观的布局方式——用 ASCII 字符画的方式"画"出你的布局: - ```css .page { display: grid; grid-template-columns: 220px 1fr; grid-template-rows: 60px 1fr 50px; grid-template-areas: "header header" "sidebar main" "footer footer"; min-height: 100vh; } /* 每个子元素用 grid-area 对号入座 */ .page-header { grid-area: header; } .page-sidebar { grid-area: sidebar; } .page-main { grid-area: main; } .page-footer { grid-area: footer; } ``` - 规则 ```css grid-template-areas: "header header header" /* 同名区域必须是矩形 */ "sidebar main main" /* 每行的区域数 = 列数 */ "sidebar footer footer"; /* 用 . 表示空白格 */ /* ❌ 非矩形会报错 */ grid-template-areas: "header sidebar" "main main" "footer sidebar"; /* sidebar 呈 L 形,不合法! */ /* ✅ 用 . 留空 */ grid-template-areas: "header header header" "sidebar main ." /* 右下角留空 */ "footer footer footer"; ``` - Grid 中的对齐 - 容器级对齐(控制所有项目) - ```css .grid { display: grid; grid-template-columns: repeat(3, 100px); /* 网格总宽 300px,可能比容器窄 */ height: 400px; /* justify-items:项目在单元格内的水平对齐 */ justify-items: stretch; /* 默认:撑满单元格宽度 */ justify-items: start; /* 靠左 */ justify-items: end; /* 靠右 */ justify-items: center; /* 水平居中 */ /* align-items:项目在单元格内的垂直对齐 */ align-items: stretch; /* 默认:撑满单元格高度 */ align-items: start; /* 靠上 */ align-items: end; /* 靠下 */ align-items: center; /* 垂直居中 */ /* place-items:简写(垂直 水平) */ place-items: center; /* 水平垂直都居中 */ place-items: start end; /* 垂直靠上,水平靠右 */ } /* start + start center + center end + end ┌────────────┐ ┌────────────┐ ┌────────────┐ │[item] │ │ │ │ │ │ │ │ [item] │ │ │ │ │ │ │ │ [item]│ └────────────┘ └────────────┘ └────────────┘ */ ``` - 整个网格在容器内的对齐 - 当网格的总尺寸小于容器时,可以控制整个网格的位置: - ```css .grid { display: grid; grid-template-columns: repeat(3, 100px); /* 总宽 300px */ width: 600px; /* 容器宽 600px,有 300px 空余 */ /* justify-content:整个网格水平方向的对齐 */ justify-content: center; /* 网格整体水平居中 */ justify-content: space-between; /* 列之间等分空间 */ /* align-content:整个网格垂直方向的对齐 */ align-content: center; /* place-content 简写 */ place-content: center; } ``` - 单个项目对齐 - ```css /* 某个项目单独设置 */ .special-item { justify-self: center; /* 这个项目在格子里水平居中 */ align-self: end; /* 这个项目在格子里垂直靠底 */ /* 简写 */ place-self: end center; } ``` - Grid 自适应与 Flex vs Grid 选型 - auto-fill 与 auto-fit - auto-fill:能塞多少列就塞多少列 - ```css .grid { display: grid; grid-template-columns: repeat(auto-fill, 200px); gap: 16px; } ``` - auto-fit:同上,但空轨道会被折叠 - ```css .grid { display: grid; grid-template-columns: repeat(auto-fit, 200px); gap: 16px; } ``` - auto-fit + minmax 万能自适应 - ```css .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; } ``` - 这一行做了什么:每列最小 280px,有多余空间时等分放大到 1fr。容器变宽就自动多放一列,变窄就自动减少,每列永远在 280px ~ 等分之间弹性变化。 - auto-fill vs auto-fit + minmax 的区别 - 当项目数量少于能放下的列数时,差别才显现 - ```css /* 容器 1200px,3 个项目 */ /* auto-fill:空轨道保留,项目不会撑到 1fr */ repeat(auto-fill, minmax(280px, 1fr)) | [280px] [280px] [280px] [空280px] | 项目大小固定在 280px,因为还有空轨道分 fr /* auto-fit:空轨道折叠,项目撑满 */ repeat(auto-fit, minmax(280px, 1fr)) | [ 400px ] [ 400px ] [ 400px ] | 空轨道折叠了,3 个项目等分 1200px ``` 响应式与进阶 - 响应式与进阶 - 响应式基础与媒体查询 - viewport meta 标签 - ```html <meta name="viewport" content="width=device-width, initial-scale=1.0"> ``` - 手机浏览器会把页面当做 980px 宽的桌面页面来渲染,然后整体缩小 - 媒体查询 @media 基础用法 - ```css /* 基础语法 */ @media (条件) { /* 条件满足时才生效的样式 */ } /* 最大宽度:屏幕 ≤ 768px 时生效 */ @media (max-width: 768px) { .sidebar { display: none; } } /* 最小宽度:屏幕 ≥ 1024px 时生效 */ @media (min-width: 1024px) { .container { max-width: 1200px; } } /* 范围:屏幕在 768px ~ 1024px 之间 */ @media (min-width: 768px) and (max-width: 1024px) { .card-grid { grid-template-columns: repeat(2, 1fr); } } ``` - 现代范围语法 ```css /* 新语法:用比较运算符,更易读 */ @media (width <= 768px) { .sidebar { display: none; } } @media (width >= 1024px) { .container { max-width: 1200px; } } @media (768px <= width <= 1024px) { .card-grid { grid-template-columns: repeat(2, 1fr); } } ``` - 移动优先 vs 桌面优先 - 桌面优先 - ```css /* 默认样式 = 桌面 */ .grid { grid-template-columns: repeat(3, 1fr); } /* 平板:≤ 1024px */ @media (max-width: 1024px) { .grid { grid-template-columns: repeat(2, 1fr); } } /* 手机:≤ 768px */ @media (max-width: 768px) { .grid { grid-template-columns: 1fr; } } ``` - 移动优先 - ```css /* 默认样式 = 手机 */ .grid { grid-template-columns: 1fr; } /* 平板:≥ 768px */ @media (min-width: 768px) { .grid { grid-template-columns: repeat(2, 1fr); } } /* 桌面:≥ 1024px */ @media (min-width: 768px) { .grid { grid-template-columns: repeat(3, 1fr); } } ``` - 断点设计 - 断点就是布局发生变化的屏幕宽度 - 需要按布局需要记 - ```css /* 推荐的断点体系 */ /* 小手机 */ /* 默认样式覆盖,不需要媒体查询 */ /* 大手机 / 小平板 */ @media (min-width: 640px) { } /* 平板 */ @media (min-width: 768px) { } /* 小桌面 / 大平板横屏 */ @media (min-width: 1024px) { } /* 桌面 */ @media (min-width: 1280px) { } /* 大桌面 */ @media (min-width: 1536px) { } ``` - ```css /* 大多数项目只需要这三个 */ /* 手机:默认 */ /* 平板:768px */ /* 桌面:1024px */ ``` - 媒体查询的组织方式 - 集中在底部 ```css /* 所有默认样式 */ .header { ... } .hero { ... } .card { ... } /* 所有平板适配 */ @media (min-width: 768px) { .header { ... } .hero { ... } .card { ... } } /* 所有桌面适配 */ @media (min-width: 1024px) { .header { ... } .hero { ... } .card { ... } } ``` - 跟在组件后面(按组件分组) ```css /* 一个组件的所有状态都在一起,好维护。这也是大多数 CSS 框架和组件库的做法。*/ /* Header */ .header { ... } @media (min-width: 768px) { .header { ... } } @media (min-width: 1024px) { .header { ... } } /* Hero */ .hero { ... } @media (min-width: 768px) { .hero { ... } } @media (min-width: 1024px) { .hero { ... } } /* Card */ .card { ... } @media (min-width: 768px) { .card { ... } } ``` - 常见响应式模式 - 导航栏折叠 ```css /* 移动端:隐藏链接,显示菜单按钮 */ .nav { display: none; } .menu-toggle { display: block; } /* 桌面端:显示链接,隐藏按钮 */ @media (min-width: 768px) { .nav { display: flex; } .menu-toggle { display: none; } } ``` - 侧边栏变底部 ```css /* 移动端:单列,侧边栏在下面 */ .layout { display: grid; grid-template-columns: 1fr; } /* 桌面端:双列 */ @media (min-width: 1024px) { .layout { grid-template-columns: 1fr 280px; } } ``` - Grid areas 重排 ```css /* 移动端布局 */ .page { display: grid; grid-template-areas: "header" "main" "sidebar" "footer"; grid-template-columns: 1fr; } /* 桌面端布局 */ @media (min-width: 1024px) { .page { grid-template-areas: "header header" "sidebar main" "footer footer"; grid-template-columns: 240px 1fr; } } ``` - 表格在移动端变卡片 ```css /* 移动端:隐藏表头,每行变成卡片 */ @media (max-width: 768px) { table thead { display: none; } table tr { display: block; margin-bottom: 16px; border: 1px solid #eee; border-radius: 8px; padding: 12px; } table td { display: flex; justify-content: space-between; padding: 6px 0; } /* 用 data 属性模拟表头 */ table td::before { content: attr(data-label); font-weight: 700; color: #333; } } ``` - 间距随屏幕缩放 ```css /* 用 CSS 变量集中管理间距 */ :root { --page-padding: 16px; --section-gap: 24px; } @media (min-width: 768px) { :root { --page-padding: 32px; --section-gap: 40px; } } @media (min-width: 1024px) { :root { --page-padding: 48px; --section-gap: 56px; } } /* 全局使用变量,不用每个组件单独写媒体查询 */ .header { padding: 0 var(--page-padding); } .hero { padding: var(--section-gap) var(--page-padding); } .content { padding: var(--section-gap) var(--page-padding); } ``` - 响应式进阶内容 - 图片是响应式布局中最容易出问题的元素——要么溢出容器,要么被拉伸变形。 - max-width 防溢出 - ```css /* 所有项目都应该全局加上这条 */ img { max-width: 100%; /* 永远不超过容器宽度 */ height: auto; /* 保持原始比例 */ display: block; /* 去掉图片底部的空隙 */ } ``` - object-fit:控制图片填充方式 - 当图片容器有固定的宽高比时,object-fit 控制图片如何适应 - ```css /* 原图 800×400,容器 300×300 */ .img-cover { width: 300px; height: 300px; object-fit: cover; /* 填满容器,多余裁掉(保持比例)✅ 最常用 */ } .img-contain { width: 300px; height: 300px; object-fit: contain; /* 完整显示,可能留白(保持比例) */ } .img-fill { width: 300px; height: 300px; object-fit: fill; /* 拉伸填满(变形)❌ 默认值 */ } .img-none { width: 300px; height: 300px; object-fit: none; /* 不缩放,只显示原图中心部分 */ } ``` - 配合 object-position 控制裁切焦点: ```css .avatar { width: 100px; height: 100px; border-radius: 50%; object-fit: cover; object-position: center top; /* 头像聚焦在上半部分 */ } /* 常用值 */ object-position: center; /* 默认:居中裁切 */ object-position: top; /* 从顶部裁切 */ object-position: left top; /* 从左上角裁切 */ object-position: 20% 30%; /* 自定义焦点 */ ``` - aspect-ratio:固定宽高比 ```css /* 不用 padding hack 了,直接声明宽高比 */ .card-cover { width: 100%; aspect-ratio: 16 / 9; /* 16:9 视频比例 */ object-fit: cover; } .avatar { width: 80px; aspect-ratio: 1; /* 正方形 = 1/1 */ border-radius: 50%; object-fit: cover; } .photo-portrait { aspect-ratio: 3 / 4; /* 竖版照片 */ } /* 配合 Grid 让所有卡片封面等高 */ .article-grid .card-cover { aspect-ratio: 16 / 10; object-fit: cover; } ``` - 响应式字体 - vw 单位:随视口缩放 - ```css /* vw = viewport width 的百分比 */ /* 1vw = 视口宽度的 1% */ h1 { font-size: 5vw; /* 手机 375px → 18.75px */ /* 平板 768px → 38.4px */ /* 桌面 1440px → 72px */ } ``` - 问题很明显:手机上太小,桌面上太大,没有上下限。 - clamp() 有上下限的响应式值 - clamp(最小值, 首选值, 最大值) 完美解决了这个问题 - ```css h1 { font-size: clamp(1.75rem, 4vw, 3rem); /* 最小 1.75rem (28px) 首选 4vw(随视口变化) 最大 3rem (48px) 三者取中间值 */ } ``` - 常用 clamp 配方 ```css /* 标题 */ h1 { font-size: clamp(1.75rem, 4vw, 3rem); } h2 { font-size: clamp(1.25rem, 3vw, 2rem); } h3 { font-size: clamp(1.1rem, 2.5vw, 1.5rem); } /* 正文 */ body { font-size: clamp(0.875rem, 1.5vw, 1.125rem); } /* 间距也可以用 clamp */ .section { padding: clamp(24px, 5vw, 64px) clamp(16px, 4vw, 48px); } /* 容器最大宽度 */ .container { width: clamp(320px, 90vw, 1200px); margin: 0 auto; } ``` - 用了 clamp 后,很多字号和间距就不需要写媒体查询了——它自动在范围内平滑缩放。 - 其他响应式单位 - ```css /* vh = viewport height */ .hero { min-height: 100vh; } /* 占满整个视口高度 */ /* svh / dvh = 小/动态视口高度(解决手机地址栏问题) */ .hero { min-height: 100dvh; } /* 推荐用 dvh */ /* vmin / vmax = 视口较小/较大边 */ .square { width: 50vmin; height: 50vmin; } /* 始终是正方形 */ ``` - 手机浏览器的地址栏会收起/展开,导致 100vh 的高度有时候会偏大。dvh(dynamic viewport height)会自动适应地址栏的变化 - ```css .full-screen { min-height: 100vh; /* 兜底 */ min-height: 100dvh; /* 现代浏览器用这个 */ } ``` - 容器查询 - 媒体查询根据视口宽度变化,但一个组件可能在不同位置出现 - 主内容区很宽,侧边栏很窄 - 容器查询根据父容器的宽度变化,让组件真正做到"放在哪里就适配哪里" - ```css /* 第一步:声明容器 */ .card-wrapper { container-type: inline-size; /* 启用容器查询 */ container-name: card; /* 可选:给容器命名 */ } /* 第二步:根据容器宽度写样式 */ @container card (min-width: 400px) { .card { display: flex; /* 容器宽时横排 */ gap: 16px; } .card-cover { flex: 0 0 200px; } } @container card (max-width: 399px) { .card { display: block; /* 容器窄时竖排 */ } .card-cover { width: 100%; aspect-ratio: 16 / 9; } } ``` - 容器查询单位 ```css .card-wrapper { container-type: inline-size; } .card-title { font-size: clamp(14px, 3cqi, 20px); /* cqi = 容器 inline-size 的 1% */ /* 类似于 vw,但基于容器宽度而不是视口 */ } ``` - 常用容器单位:cqi(容器宽度%)、cqb(容器高度%)、cqmin(较小边%)。 - 不需要媒体查询的响应式技巧 - 很多响应式效果可以用纯 CSS 实现,不写一行 @media - ```css /* 技巧 1:Grid auto-fit + minmax(Day 13 学过) */ .grid { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); /* 自动调整列数,完全不需要媒体查询 */ } /* 技巧 2:clamp 响应式字号 */ h1 { font-size: clamp(1.5rem, 4vw, 3rem); } /* 技巧 3:clamp 响应式间距 */ .section { padding: clamp(24px, 5vw, 64px); } /* 技巧 4:flex-wrap 自动换行 */ .button-group { display: flex; flex-wrap: wrap; gap: 12px; } /* 技巧 5:min() / max() 控制宽度 */ .container { width: min(90vw, 1200px); /* 取小值:窄屏时 90vw,宽屏时 1200px */ margin: 0 auto; } .card { width: max(280px, 30%); /* 取大值:至少 280px */ } /* 技巧 6:auto-fit 让不够时换行 */ .stats { display: flex; flex-wrap: wrap; gap: 16px; } .stat-item { flex: 1 1 150px; /* 至少 150px,不够就换行 */ } ``` - translation 过渡动画 - translation 基础语法 - ```css /* 没有 transition:悬停时背景色瞬间变化 */ .btn { background: #2563eb; } .btn:hover { background: #1d4ed8; } /* 加上 transition:背景色用 0.3 秒平滑过渡 */ .btn { background: #2563eb; transition: background 0.3s; } .btn:hover { background: #1d4ed8; } ``` - 完整语法 ```css transition: 属性 时长 缓动函数 延迟; /* 示例 */ transition: background 0.3s ease 0s; transition: transform 0.2s ease-in-out; transition: opacity 0.5s linear 0.1s; ``` - 分开写的写法 ```css .box { transition-property: background; /* 哪个属性要过渡 */ transition-duration: 0.3s; /* 过渡多长时间 */ transition-timing-function: ease; /* 缓动曲线 */ transition-delay: 0s; /* 延迟多久开始 */ } ``` - transition-property 过渡哪些属性 - ```css /* 指定单个属性 */ transition: background 0.3s; /* 指定多个属性 */ transition: background 0.3s, color 0.3s, transform 0.2s; /* 所有可过渡属性一起 */ transition: all 0.3s; ``` - all 方便但有两个问题 ```css /* ❌ all 的隐患 */ .card { transition: all 0.3s; /* 问题 1:可能过渡你不想过渡的属性(比如 width、height 导致布局抖动)*/ /* 问题 2:性能比指定属性差 */ } /* ✅ 推荐:明确列出要过渡的属性 */ .card { transition: transform 0.3s, box-shadow 0.3s; } ``` - 哪些属性可以过渡 ```css /* 能过渡的属性必须有中间值(浏览器知道怎么从 A 插值到 B)*/ /* ✅ 可以过渡 */ color, background-color /* 颜色有中间值 */ opacity /* 0 到 1 之间连续 */ transform /* 位移、缩放、旋转都有中间值 */ width, height, padding, margin /* 数值类都可以 */ border-color, border-radius box-shadow font-size, letter-spacing, line-height /* ❌ 不能过渡 */ display /* none → block 没有中间状态 */ font-family /* 字体之间没法渐变 */ background-image /* 图片之间不能插值 */ position /* static → absolute 没有中间值 */ ``` - display: none 不能过渡是一个经典痛点。要做显隐动画,用 opacity + visibility 代替 ```css /* 用 opacity + visibility 替代 display 的显隐过渡 */ .dropdown { opacity: 0; visibility: hidden; transform: translateY(-8px); transition: opacity 0.2s, visibility 0.2s, transform 0.2s; } .trigger:hover .dropdown { opacity: 1; visibility: visible; transform: translateY(0); } ``` - transition-duration 时长 - ```css transition-duration: 0.3s; /* 300 毫秒 */ transition-duration: 300ms; /* 同上,用 ms 写法 */ transition-duration: 0.15s; /* 150 毫秒,很快 */ transition-duration: 0.5s; /* 500 毫秒,中速 */ transition-duration: 1s; /* 1 秒,比较慢 */ ``` - 不同场景的推荐时长 ```css /* 微交互(按钮、链接、图标)→ 快速 */ .btn { transition: background 0.15s, transform 0.15s; } /* 悬停效果(卡片、图片)→ 中速 */ .card { transition: transform 0.3s, box-shadow 0.3s; } /* 展开/收起(下拉菜单、折叠面板)→ 稍慢 */ .panel { transition: max-height 0.35s, opacity 0.35s; } /* 页面级动画(模态框、页面切换)→ 慢 */ .modal { transition: opacity 0.4s, transform 0.4s; } ``` - 越小的元素过渡越快,越大的元素过渡可以稍慢。超过 0.5s 用户会觉得卡顿。 - transition-timing-function 缓动函数(重点) - 缓动函数决定过渡的"节奏感"——匀速、先快后慢、先慢后快 - ```css /* 预设曲线 */ transition-timing-function: linear; /* 匀速 */ transition-timing-function: ease; /* 默认:慢→快→慢,最常用 */ transition-timing-function: ease-in; /* 慢→快(加速) */ transition-timing-function: ease-out; /* 快→慢(减速)*/ transition-timing-function: ease-in-out; /* 慢→快→慢,比 ease 对称 */ ``` - 适合的场景 ```css /* ease-out(减速)→ 元素进入画面 */ .card-enter { transition: transform 0.3s ease-out; } /* ease-in(加速)→ 元素离开画面 */ .card-leave { transition: transform 0.3s ease-in; } /* ease-in-out → 来回移动、展开收起 */ .accordion { transition: max-height 0.35s ease-in-out; } /* linear → 颜色渐变、进度条 */ .progress-bar { transition: width 0.5s linear; } ``` - cubic-bezier 自定义曲线 - 所有预设曲线都是 cubic-bezier 的简写 ```css /* 预设值的真身 */ ease: cubic-bezier(0.25, 0.1, 0.25, 1.0) ease-in: cubic-bezier(0.42, 0, 1.0, 1.0) ease-out: cubic-bezier(0, 0, 0.58, 1.0) ease-in-out: cubic-bezier(0.42, 0, 0.58, 1.0) /* 自定义:弹性回弹效果 */ .bounce { transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); /* y 值超过 1 就会产生"过冲"回弹效果 */ } ``` - 推荐网站 cubic-bezier.com 可以预览和对比各种曲线 - transition-delay 延迟 - ```css /* 鼠标悬停 0.1s 后才开始变化 */ .tooltip { transition: opacity 0.3s ease 0.1s; } /* 多个属性不同延迟 → 顺序动画 */ .card:hover .card-title { transform: translateY(0); transition: transform 0.3s ease 0s; /* 标题先动 */ } .card:hover .card-desc { transform: translateY(0); transition: transform 0.3s ease 0.1s; /* 描述延迟 0.1s */ } .card:hover .card-btn { transform: translateY(0); transition: transform 0.3s ease 0.2s; /* 按钮延迟 0.2s */ } ``` - transform 变化 - 2D 变化基础 - transform 不会影响文档流——元素在页面中的原始位置仍然保留,只是视觉上发生了变化 - translate 位移 - ```css /* 水平和垂直移动 */ transform: translateX(50px); /* 右移 50px */ transform: translateY(-20px); /* 上移 20px */ transform: translate(50px, -20px); /* 合写:右移 50 + 上移 20 */ /* 百分比相对于元素自身尺寸 */ transform: translateX(100%); /* 右移自身宽度的距离 */ transform: translateY(-50%); /* 上移自身高度的一半 */ ``` - translate 最经典的用途——绝对定位居中 ```css .centered { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); /* top/left 把左上角移到容器中心 */ /* translate 再把元素自身偏移回去 */ } ``` - scale 缩放 - ```css transform: scale(1.5); /* 等比放大 1.5 倍 */ transform: scale(0.8); /* 等比缩小到 80% */ transform: scale(1); /* 原始大小 */ transform: scale(0); /* 缩小到消失 */ transform: scaleX(1.5); /* 只水平拉宽 */ transform: scaleY(0.5); /* 只垂直压扁 */ transform: scale(1.5, 0.8); /* 水平 1.5 倍,垂直 0.8 倍 */ ``` - 常见用法 ```css /* 按钮点击缩小反馈 */ .btn:active { transform: scale(0.96); } /* 图片悬停放大 */ .img-wrapper { overflow: hidden; } .img-wrapper img { transition: transform 0.4s ease; } .img-wrapper:hover img { transform: scale(1.1); } /* 从 0 到 1 的弹出效果 */ .popup { transform: scale(0); transition: transform 0.3s ease; } .popup.active { transform: scale(1); } ``` - rotate 旋转 - ```css transform: rotate(45deg); /* 顺时针旋转 45 度 */ transform: rotate(-90deg); /* 逆时针旋转 90 度 */ transform: rotate(180deg); /* 翻转 */ transform: rotate(360deg); /* 转一圈(用于 loading 动画) */ transform: rotate(0.5turn); /* 半圈 = 180deg */ ``` - ```css transform: rotate(45deg); /* 顺时针旋转 45 度 */ transform: rotate(-90deg); /* 逆时针旋转 90 度 */ transform: rotate(180deg); /* 翻转 */ transform: rotate(360deg); /* 转一圈(用于 loading 动画) */ transform: rotate(0.5turn); /* 半圈 = 180deg */ ``` - ```css transform: rotate(45deg); /* 顺时针旋转 45 度 */ transform: rotate(-90deg); /* 逆时针旋转 90 度 */ transform: rotate(180deg); /* 翻转 */ transform: rotate(360deg); /* 转一圈(用于 loading 动画) */ transform: rotate(0.5turn); /* 半圈 = 180deg */ ``` - ```css /* 箭头图标旋转表示展开/收起 */ .arrow { display: inline-block; transition: transform 0.3s ease; } .expanded .arrow { transform: rotate(180deg); } /* 悬停微旋转 */ .icon:hover { transform: rotate(15deg); } ``` - skew 倾斜 - ```css transform: skewX(10deg); /* 水平方向倾斜 */ transform: skewY(5deg); /* 垂直方向倾斜 */ transform: skew(10deg, 5deg); /* 合写 */ ``` - ```css /* 平行四边形按钮 */ .skew-btn { transform: skewX(-10deg); } /* 文字反向 skew 保持正常 */ .skew-btn span { display: inline-block; transform: skewX(10deg); } /* 斜切背景装饰 */ .section-divider { height: 80px; background: hsl(220, 85%, 55%); transform: skewY(-3deg); } ``` - 组合变化 ```css /* 先旋转 45 度,再右移 100px */ transform: translateX(100px) rotate(45deg); /* 实际执行顺序:先 rotate → 再 translate */ /* 注意:旋转后坐标轴也跟着转了,所以 translate 方向会变 */ /* 先移动再旋转 vs 先旋转再移动,效果完全不同! */ ``` - ```css transform: translateX(100px) rotate(45deg); 原始 → 先旋转 45° → 再沿旋转后的 X 轴移动 100px 结果:元素在右上方 transform: rotate(45deg) translateX(100px); 原始 → 先沿原始 X 轴移动 100px → 再旋转 45° 结果:元素在正右方但旋转了 ``` - 先写 translate,再写其他变化 ```css /* ✅ 推荐顺序:translate → scale → rotate */ .card:hover { transform: translateY(-8px) scale(1.02) rotate(1deg); } ``` - translate-origin 变换原点 - 默认情况下所有变换都以元素中心为原点。transform-origin 可以改变这个原点: - ```css /* 默认:中心 */ transform-origin: center; /* 50% 50% */ /* 关键字 */ transform-origin: top left; /* 左上角 */ transform-origin: bottom right; /* 右下角 */ transform-origin: center top; /* 顶部中间 */ /* 百分比 */ transform-origin: 0% 0%; /* 左上角 */ transform-origin: 100% 100%; /* 右下角 */ /* 像素值 */ transform-origin: 20px 30px; ``` - 改变原点最直观的效果在旋转和缩放上: - ```css /* 旋转:绕中心转 vs 绕左上角转 */ .spin-center { transform-origin: center; transform: rotate(45deg); } .spin-corner { transform-origin: top left; transform: rotate(45deg); } /* spin-center: spin-corner: ╱╲ ┌╲ ╱ ╲ │ ╲ ╱ ◉ ╲ ←原点在中心 │ ╲ ← 原点在左上角 ╲ ╱ │ ╱ ╲╱ │╱ */ ``` - 实用场景 ```css /* 下拉菜单从顶部展开 */ .dropdown { transform-origin: top center; transform: scaleY(0); /* 从顶部收起 */ transition: transform 0.2s ease; } .trigger:hover .dropdown { transform: scaleY(1); /* 从顶部展开 */ } /* 导航下划线从左侧展开 */ .nav-link::after { transform-origin: left; transform: scaleX(0); transition: transform 0.3s ease; } .nav-link:hover::after { transform: scaleX(1); } /* 图片从左上角放大 */ .zoom-corner { transform-origin: top left; } .zoom-corner:hover { transform: scale(1.5); } ``` - 3D 变化 - ```css /* 给父元素设透视 */ .scene { perspective: 800px; /* 观察者距离屏幕 800px */ /* 值越小 → 透视感越强(像凑近看)*/ /* 值越大 → 透视感越弱(像远处看)*/ } ``` - @keyframes 关键帧动画 - 基础语法 - 动画分两步:先用 @keyframes 定义动画,再用 animation 应用到元素上 - ```css /* 第一步:定义关键帧 */ @keyframes fadeIn { from { opacity: 0; /* 起始状态 */ } to { opacity: 1; /* 结束状态 */ } } /* 第二步:应用动画 */ .element { animation: fadeIn 0.5s ease; } ``` - 用百分比定义关键帧 - ```css @keyframes bounce { 0% { transform: translateY(0); } 30% { transform: translateY(-30px); } 50% { transform: translateY(0); } 70% { transform: translateY(-15px); } 100% { transform: translateY(0); } } .ball { animation: bounce 1s ease; } ``` - animation 属性详解 - animation 的完整简写 - ```css animation: 名称 时长 缓动 延迟 次数 方向 填充 播放状态; /* 示例 */ animation: fadeIn 0.5s ease 0s 1 normal forwards running; ``` - animation-name 动画名称 - ```css animation-name: fadeIn; /* 对应 @keyframes fadeIn */ animation-name: none; /* 不应用任何动画 */ ``` - animation-duration 持续时长 - ```css animation-duration: 0.5s; animation-duration: 300ms; animation-duration: 2s; ``` - animation-timing-function 缓动 - ```css animation-timing-function: ease; animation-timing-function: linear; animation-timing-function: ease-in-out; animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); /* steps() —— 逐帧动画,不做插值 */ animation-timing-function: steps(4); /* 分 4 步跳变 */ animation-timing-function: steps(1, end); /* 打字机效果 */ ``` - ```css /* 精灵图逐帧动画 */ .sprite { width: 64px; height: 64px; background: url('sprite.png'); animation: walk 0.6s steps(8) infinite; } @keyframes walk { to { background-position: -512px 0; } /* 8 帧 × 64px */ } /* 打字机效果 */ .typing { width: 0; overflow: hidden; white-space: nowrap; border-right: 2px solid; animation: typing 2s steps(15) forwards, /* 15 个字逐个显示 */ blink 0.8s step-end infinite; /* 光标闪烁 */ } @keyframes typing { to { width: 15ch; } /* ch = 一个字符的宽度 */ } @keyframes blink { 50% { border-color: transparent; } } ``` - animation-delay 延迟 - ```css animation-delay: 0.3s; /* 等 0.3 秒后开始 */ animation-delay: -0.5s; /* 负值:动画从 0.5s 处开始播放(跳过开头) */ ``` - ```css /* loading 动画:3 个点交错起步 */ .dot:nth-child(1) { animation-delay: 0s; } .dot:nth-child(2) { animation-delay: 0.15s; } .dot:nth-child(3) { animation-delay: 0.3s; } ``` - animation-iteration-count 播放次数 - ```css animation-iteration-count: 1; /* 默认:播放 1 次 */ animation-iteration-count: 3; /* 播放 3 次 */ animation-iteration-count: infinite; /* 无限循环 */ ``` - animation-direction 方向 - ```css animation-direction: normal; /* 默认:0% → 100% */ animation-direction: reverse; /* 反向:100% → 0% */ animation-direction: alternate; /* 交替:去 → 回 → 去 → 回 */ animation-direction: alternate-reverse; /* 反向交替:回 → 去 → 回 → 去 */ ``` - animation-fill-mode 填充模式 (重要) - ```css animation-fill-mode: none; /* 默认:动画结束后回到初始状态 */ animation-fill-mode: forwards; /* 保持在最后一帧 */ animation-fill-mode: backwards; /* 延迟期间就应用第一帧 */ animation-fill-mode: both; /* forwards + backwards */ ``` - ```css @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } none: 延迟中透明度=1 → 动画0→1 → 结束后回到1(跳了一下再回来) forwards: 延迟中透明度=1 → 动画0→1 → 结束后保持1 ✅ backwards: 延迟中透明度=0 → 动画0→1 → 结束后回到1 both: 延迟中透明度=0 → 动画0→1 → 结束后保持1 ✅✅ 最完整 ``` - animation-play-state 播放状态 - ```css animation-play-state: running; /* 默认:播放中 */ animation-play-state: paused; /* 暂停 */ /* 悬停暂停动画 */ .marquee:hover { animation-play-state: paused; } ``` - animation 简写 - 记住简写的顺序规则:第一个时间值是 duration,第二个是 delay - ```css /* animation: name duration timing delay count direction fill state */ animation: fadeIn 0.5s ease 0s 1 normal forwards running; /* 常用的简写 */ animation: fadeIn 0.5s ease forwards; /* 渐入并保持 */ animation: spin 1s linear infinite; /* 无限匀速旋转 */ animation: bounce 0.6s ease-in-out 3; /* 弹跳 3 次 */ animation: float 3s ease-in-out infinite alternate; /* 无限来回浮动 */ ``` - 多动画同时应用 - ```css .element { animation: fadeIn 0.5s ease forwards, slideUp 0.5s ease forwards; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes slideUp { from { transform: translateY(30px); } to { transform: translateY(0); } } ``` 工程化与实战 - 标题
思维导图新功能测试
- 项目规划 - 前端 - 示例代码 ```js let a = 3 console.log(a) ``` - <h1 style="color: blue"> 你好 </h1> -  - java 代码 ```java public class TimestampWatermarkDemo { public static class Event { public String user; public long eventTime; public int value; public Event(){} public Event(String user, long eventTime, int value) { this.user = user; this.eventTime = eventTime; this.value = value; } public long getEventTime() { return eventTime; } @Override public String toString() { return "Event{" + "user='" + user + '\'' + ", eventTime=" + eventTime + ", value=" + value + '}'; } } public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); DataStream<Event> source = env.fromElements( new Event("alice", 1000L, 1), new Event("alice", 4000L, 1), new Event("alice", 3000L, 1), new Event("bob", 8000L, 1), new Event("alice", 12000L, 1) ); WatermarkStrategy<Event> wmStrategy = WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5)) .withTimestampAssigner((event, recordTimestamp) -> event.getEventTime()); DataStream<Event> withWaterMark = source.assignTimestampsAndWatermarks(wmStrategy); withWaterMark.print(); env.execute("timestamp-water-mark-demo"); } } ``` 1 let a = 3
不同寻常
贴上“我的“标签, 带着强烈的执着, 一切都会显得不同寻常。 ...
Flink 复习
Flink 整体认知 - Flink 整体认知 - 认识 Flink - Flink 是什么 - Flink 是一个对于有界和无界数据流进行有状态计算的框架和分布式处理引擎 - 框架:Flink 不是一个单独的软件命令,而是一套开发 + 运行的系统 - Flink 和 Hadoop/Spark/Storm 的区别 - 基本定义 - Hadoop 是什么 - 大数据基础设施体系 - HDFS:分布式存储 - MapReduce:离线批处理计算 - YARN:资源管理 - 早期大数据时代的“存储+资源管理+离线计算“基础平台 - Spark 是什么 - 通用的大数据计算引擎 - 流处理主要通过 Structured Streaming - 更偏通用分析平台,批处理很强,流处理也能做 - Storm 是什么 - 是一个分布式实时计算系统 - 较早一代的纯流式处理框架,比 Flink 早 - 比较维度 - 主要定位 - Hadoop - 海量数据存储 - 离线批处理 - 高吞吐,不强调低延迟 - Spark - 通用大数据引擎 - 强批处理 - 可扩展到 SQL、ML、Streaming - Flink - 有状态流处理 - 事件时间 - 流批统一 - 更强实时计算语义 - 处理模型 - Hadoop: Batch - 读一批数据->map->shuffle->reduce->输出结果 - Spark: Batch + Micro-batch - Flink: Native Streaming,批流统一 - 延迟特征 - Hadoop - 延迟高,通常分钟级甚至更长 - Spark - micor-batch 延迟比原生流高一点 - Flink - 低延迟实时计算 - 状态能力 - Hadoop - 不擅长生命周期有状态流计算 - Spark - 能做状态处理,但不是以 stateful streaming 为核心 - Flink - stateful computations over data streams - exactly-once state consistency - 什么是批处理、流处理、实时计算 - 批处理 - 定义:先把一段时间内的数据收集起来,等数据攒够一批后,再统一进行计算 - 特点:先到齐,再处理、延迟高、吞吐高、程序有结束点 - 流处理 - 定义:把数据看成一个持续到来的事件流,数据一到,系统就持续处理 - 特点:连续不断、持续进行、低延迟、要处理历史记忆 - 实时计算 - 定义:从数据产生,到计算结果可用,时间尽可能短 - 实时计算不是来了就必须零延迟,而是在业务可接受的时间范围内尽快给出结果 - Flink 的应用场景 - 事件驱动应用 - 系统不是定时查数据库,而是收到事件才触发计算、状态更新或外部动作 - 典型业务 - 欺诈检测 - 异常检测 - 规则告警 - 业务流程监控 - 数据分析应用 - 从原始数据中提取信息和洞察 - 典型业务 - 每分钟订单量 - 热门榜单 - 实时数仓 - 离线 + 实时一体分析 - 数据管道与 ETF - 从上游系统读数据,做清洗、转换、补全、标准化、过滤、路由,再写到下游系统 - 典型业务 - Kafka 到 Kafka - Kafka 到数据库/搜索引擎/湖仓 - CDC + 实时同步 - Flink 基本架构 - JobManager、TaskManager - 最小 Flink 集群 -  - 在本地或某台机器上提交作业 - JobManager 接收这个作业,分析要怎么跑 - 把真正要执行的子任务分发给多个 TaskManager - TaskManager 在各自机器上并行处理数据 - 什么是 JobManager - 是 Flink 集群的控制中心 - 负责接收/解析/调度/监控/作业、协调故障恢复、管理元信息 - 什么是 TaskManager - 是 Flink 集群里真正干活的 worker - 主要负责执行子任务/占用和提供计算资源/与其他 TaskManger 传输数据/向 JobManager 汇报状态 - Slot 是什么 - 定义 - Flink 用 Slot 告诉调度器:这台 TaskManager 最多能接多少份执行任务 - 为什么需要 Slot - 控制一个 TaskManager 能接多少任务 - 做资源切分 - 在隔离与利用率之间做平衡 - Slot 和 TaskManager 的关系 - 一个 TaskManager 可以有多个 Slot - Slot 是 TaskManager 提供给集群调度的执行容量 - Slot 是资源申请和任务放置的基本单位之一 - Slot 和并行度的关系 - 并行度指的是一个算子会拆成多少个并行子任务 - 执行这些子任务需要 Slot - 在默认 slot sharing 开启时,作业大致需要的 slot 数 = 该作业的最高并行度 - JobGraph、ExecutionGraph - 什么是 JobGraph - Flink 作业的逻辑图/逻辑执行计划 - JobGraph 里面的点,叫做 JobVertex,它是一个逻辑阶段/一个逻辑算子节点 - JobGraph 不强调每个并行子任务 - 什么是 ExecutionGraph - 是 JobGraph 经过并行展开后的实际执行拓扑 - ExecutionGraph 的核心点是 ExecutionVertex - 并行度是什么 - 本质 - Flink 算子可以想象成一个类似读 Kafka/做 map 转换/做窗口聚合的工作 - 并行度决定一个算子会被拆分成几个 subtask 一起做 - 为什么需要并行度 - 一份工作只让一个实例做,处理能力有限 - 提高并行度,本质就是把工作拆开,让多个 subtask 同时处理 - 一个 flink 作业如何运行起来 - 总流程 - 写 Flink 程序,调用 execute() - Client 负责提交作业 - 程序先形成逻辑数据流图 - JobManager 接收作业 - JobManager 把 JobGraph 转成 ExecutionGraph - 调度器根据并行度和可用 slot 做任务部署 - TaskManager 真正执行 subtask - 运行中持续监控、处理状态、必要时恢复故障 - 编写代码时,发生了什么 - 写的是逻辑处理链路,不是真正的分布式执行指令 - 调用 execute() 后,作业才真正进入提交流程 - Client 做了什么 - Client 是提交入口,负责将应用提交到集群 - Client 不是主要计算节点 - JobGraph 在什么时候出现 - 代码先被整理为数据流图,也就是 JobGraph - JobGraph 偏逻辑层,还没有真正展开成每个 subtask,不是最终的并发执行形态 - JobManager 接手后做什么 - 接收作业 - 管理作业生命周期 - 把逻辑图变成执行图 - 协调资源和任务部署 - Execution Graph 怎么来的 - ExecutionGraph 是 JobGraph 的并行化版本 - 并行度在这里真正展开 - 运行状态也在这里变得具体 - 调度阶段发生什么 - 要跑多少个 subtask - 这些 subtask 放到哪些地方跑 - 数据怎么在它们之间流动 - slot 发挥什么作用 - TaskManager 提供 slots - 调度器找空闲 slot - 默认 slot sharing 会影响所需 slot 数 - TaskManager 真正干活时发生了什么 - TaskManager 执行 subtask - 每个 subtask 处理自己那部分的输入数据 - 上下游 subtask 之间可能发生网络传输 - 状态、窗口、定时器等能力都在这里真正运作 - 作业运行中,JobManager 还在做什么 - 跟踪任务状态 - 监控资源与任务健康情况 - 失败时触发恢复 - 作业失败了会怎样 - JobManager 感知失败 - 调度器决定如何恢复 - TaskManager 上的任务可能被重新部署 - 搭建开发环境 - Maven 项目骨架 - POM 文件 -  - fink-streaming-java - 是 DataStream API 入门最核心的依赖之一 - 编写 StreamExecutionEnvironment、DataStream 这类代码会用到它 - flink-clients - 用于本地执行、提交作业相关能力 - DataStream API 的基本项目结构 - 最小程序骨架 -  -  - 代码详解 - StreamExecutionEnvironment - 这是 DataStream 程序的入口 - StreamExectionEnviroment 是所有 Flink 程序的基础,通过 getExecutionEnvironment() 获取 - 是整个 Flink 程序的运行上下文或总入口 - 负责创建 source、串起数据流、设置并行度/checkpoint 等运行参数、触发执行 - Source - Source 就是数据从哪里来 - DataStrema 最初是从各种 source 创建出来的 - Transformation - 就是对数据流做处理 - DataStream 程序是对数据流做 transformation 的程序 - 基础类别 - map:一条变一条 - filter:按条件过滤 - flatMap:一条变多条 - keyBy:按 key 分组 - sum/reduce:聚合 - Sink - Sink 就是结果往哪里去 - 结果通过 sink 返回,可以写入文件、标准输出等 - print() 可以直接在控制台打印输出 - sinkTo(xxx) 可以写入自定义的目的 - execute() - Flink Job 是在调用 execute() 时创建并提交的 - DataStream 程序最后一步就是触发程序执行 - 跑通 WordCount - 最小 WordCount 代码 -  -  - 代码解析 - 创建执行环境 - StreamExecutionEnvironment 是每个 DataStream 程序的起点,表示开始描述一条 Flink 数据处理流水线了 - 创建输入流 - flatMap 把每一行拆分成单词 - flatMap 可以对一个输入产生任意多输出 - Tuple2<String, Integer> - 是 Flink 常见的二元组类型 - keyBy(value -> value.f0):按单词分组 - keyBy 会把流按照 key 逻辑分区,所有相同 key 会进入同一个分区 - sum(1):对第二个字段求和 - DataStreamAPI 基础 - Source、Transformation、Sink - Source - Flink 程序接收数据的地方 - 可以来自内存集合、文件、消息队列、socket 等 - 集合 Source - DataStream<String> stream = env.fromElements("a","b","c"); - DataStream<Integer> nums = env.fromElements(1,2,3); - 真实生产的 Source - Kafka - 文件 - Socket - 自定义 source - 其他外部系统 connector - Transformation - Operators 会把一个或多个 DataStream 转换成新的 DataStream - 程序可以把这些变换组合成复杂的数据流拓扑 - map - 一条输入变成一条输出 - DataStream<Integer> result = nums.map(x -> x*2) - map 不会改变条数 - 适合做字段转换、格式转换、简单加工 - flatMap - 一条输入可以变为多条输出,也可以一条都不输出 -  - flat 适合切词、拆分、展开 - WordCount 通常用它来把一行拆分成多个词 - 比 map 更灵活 - filter - 按条件保留或丢弃数据 - DataStream<Integer> evenNums = nums.filter(x -> x % 2 == 0); - filter 不改数据结构,主要做筛选 - keyBy - 按 Key 对数据流做分组 - 所有具有相同 key 的记录会被分到同一个分区上,通常内部是哈希分区 -  - keyBy 不是聚合 - 只是按 key 把数据归组 - 后面的 sum、reduce、状态、窗口通常都要建立在 keyBy 后面 - reduce - 对同一个 key 下的数据做增量操作 -  - reduce 必须通常接在 keyBy 后面 - 是增量聚合 - 比一次性收集所有数据再算更高效 - Sink - result.print() - print() 是最小可用 Sink - 主要用于学习和调试 - 后续接 Kafka、文件、数据库,思路还是一样,只是 Sink 换了 - union/connect - union - 定义 - 会把两条或多条数据流合并为一条新流,新流包含所有输入流的元素 - 所有输入流必须是相同类型 - 示例 -  - 关键点 - 类型必须一直 - union 后得到的还是同一种类型的普通 DataStream - union 适合本来就是同类数据,只是来自不同来源 - connect - 连接两条流,但是保留各自的类型信息 - 示例 -  - connect 更像先把两条流并排放在一起,然后你自己决定怎么分别处理左边和右边 - connect 后怎么用 - connect 一般不会单独停在那,通常会继续接处理函数 -  - RichFunction - 为什么要有 RichFunction - map/filter/flatMap 拿不到 Flink 运行时信息,也不方便做初始化和清理 - 需求:程序开始时初始化资源/程序结束时做清理/知道当前子任务编号/知道并行度/拿到运行时上下文 - RichFunction 定义了生命周期方法,以及访问函数执行上下文方法 - RichFunction 是什么 - RichFunction 是一个基础接口,很多常见的 rich 版本函数都建立在它之上 - 平时真正会写的,不是 RichFunction 本身,而是具体子类 - RichMapFunction - RichFilterFunction - RichFlatMapFunction - 可以理解为普通 Function 的增强版 - 和普通函数的区别 - 除了处理逻辑,还多了 open()、close()、getRuntimeContext() - open 用于初始化,close 用于清理,getRuntimeContext 用于获取运行时上下文 - RichFunction 最核心的三个能力 - open(OpenContext openContext) - 这是初始化方法 - 会在真正工作方法例如 map、join 前调用 - 适合做一次性初始化工作 - 就像开工前准备 - close() - 这是清理方法 - 会在主工作方法最后一次调用之后执行 - 适合做清理工作 - 就像收工后打扫现场 - getRuntimeContext() - 拿运行时上下文的方法 - 包含函数运行并行度、当前 subtask 的索引、执行该 task 的名称 - 例子 -  - 基本调试方法 - 先用最小输入跑通 - 先用 fromElements() 构造很小的输入集 - 最基础的调试方式: print() - 中间的关键步骤也可以加上 print() -  - 按 source/transformation/sink 分段排查 - 先怀疑 source - source 有没有真的产生数据 - 输入是不是空的 - 输入格式是不是和我想的不一样 - 可以先 lines.print("source") 确认下 - 再怀疑 transformation - 每做一步变化,就临时加一个 print() - 最后怀疑 sink 时间与窗口 - 时间与窗口 - 时间语义 - processing time - 什么是 processing time - processing time 就是某个算子在处理这条数据时,所在机器的系统时间 - 不看事件真正什么时候发生,而是看 Flink 现在处理到它时是几点 - processing time 的本质 - 用机器时钟当做时间标准 - 不需要从数据取时间戳 - 不需要 watermark 来推动时间前进 - event time - 什么是 event time - 不看 flink 什么时候处理到这条数据,而是看这条数据业务上到底是什么时候发生的 - 这个时间通常在进入 flink 前就嵌入到记录了 - 为什么 event time 重要 - 如果想得到可复现、符合业务真实时间的分析结果,就应该用 event time - 把统计口径从系统什么时间看到它,切回什么时候真正发生 - 本质 - 用事件自带的时间戳,作为所有时间相关操作的基准 - 必须能从数据集提取时间戳 - 系统需要某种机制来判断某个时间点之前的数据大致到齐了 - ingestion time - 事件进入 Flink 时被赋予的时间 - WaterMark - WaterMark 是什么 - WaterMark 是 Flink 用于衡量 EventTime 进度的机制 - Watermark 在数据流中携带一个时间戳 t,WaterMark(t) 表示事件已经推进到 t - 为什么需要 WaterMark - 真实世界里的数据经常会乱序到达 - 问题:应该等到什么时候,才能认为某个时间点之前的数据差不多到齐了 - Watermark 表示什么 - 到目前为止,我认为时间戳小于等于 t 的事件应该都到了 - Watermark 和窗口的关系 - Watermark 最常见的作用,就是触发 EventTime 窗口计算 - Watermark 决定什么时间可以开始算 - Watermark 和迟到数据有什么关系 - 如果某条事件的时间戳小于等于当前已经到达的 watermark,但现在才到,这条数据就是迟到数据 - Watermark 怎么来的 - 使用 Event Time 时,Flink 需要知道每个元素的时间戳 - 时间戳可以通过 TimestampAssigner 提取 - 需要 WatermarkGenerator 生成 watermark,两者一起通过 WatermarkStrategy 配置 - Watermark 来自于定义的时间策略 - Watermark 理解 - Watermark 就像 Event Time 世界里的逻辑时钟 - 系统告诉下游:可以把时间推进到这里了 - 有序和乱序流的 Watermark 生成方式 - 为什么分有序流和乱序流 - Watermark 的生成方式,本质取决于你的数据时间戳分布特征 - 有序流的 Watermark 生成方式 - 核心假设 - 后面到来的事件,时间戳不会比前面的小 - 生成逻辑 - 在有序流里,当前看到的最大时间戳就代表时间已经推进到那里了 - 乱序流的 Watermark 生成方式 - 核心假设 - 虽然事件会乱序到达,但是可以给出一个最大乱序范围 B - 生成逻辑 - 当前 watermark = 已观察到的最大事件时间戳 - 最大乱序容忍时间 - 具体例子 -  - 事件对象里面要有事件时间字段 - Source 只是构造测试数据 - WatermarkStrategy 是核心 - forBoundedOutOfOrderness(Duration.ofSeconds(5)) - 允许这条流最多乱序 5s - Watermark 会根据当前看到的最大时间戳-5 秒来推进 - withTimestampAssigner()告诉 Flink 事件时间从那一列取 - assignTImestampsAnsWatermarks - 给流里的每个元素附上事件时间戳 - 同时让 Flink 根据策略找 Watermark - 窗口基础 - 为什么需要窗口 - 流数据是无界的,理论上不会自然结束 - 对这种数据不能简单说等全部到期再统计 - Flink 的窗口机制就是把连续到来的流按规则划分为一个个有序集合 - Flink 里窗口的总体分类 - Tumbling Window (滚动窗口) - 定义:是固定大小、连续、互不重叠的窗口 - 适合场景 - 每 5 分钟订单数 - 每小时 GMV - 每日 UV - 每分钟接口调用量 - Sliding Window (滑动窗口) - 定义:也是固定大小的窗口,会按照一个更小的步长持续滑动 - 示例:窗口大小 10 分钟,每 5 分钟滑动一次 - 适合场景 - 最近 10 分钟 PV,每分钟更新一次 - 最近 30 分钟订单量,每 5 分钟更新一次 - Session Window (会话窗口) - 定义 - Session Window 没有固定窗口,它的边界由活动间隔决定 - session window 的边界由 inactivity gap 定义 - 指定 gap 时间内没有新事件到来,当前 session 就关闭 - 如果中间一直没有很长时间断开,这些行为属于同一个会话 - 适合场景 - 用户会话分析 - App 连续使用时长 - 连续操作链路 - Count Window (计数窗口) - 定义 - Count Window 按元素个数切 - 每 10 条数据统计一次/每 100 条点击做一次聚合 - 适合常见 - 每 100 条日志做一次分析 - 每 50 条事件批量聚合 - 窗口最重要的两个维度 - 切分维度 - 按时间切 - 按数量切 - 是否重叠 - 不重叠 - 可能重叠 - 不固定边界 - 窗口对齐 - 时间窗口是对齐到 epoch 的 - 时间窗口默认是按时间对齐的,不是按程序启动时刻随意切 - 窗口聚合 - 窗口聚合解决什么问题 - 窗口把无界流切成有限片段后,通常还要对每个片段做计算 - 不同业务对怎么算有不同要求 - 有的业务只要一个简单结果,例如总数 - 有的要复杂中间结构,比如平均值要维护 sunm 和 count - 有的除了结果,还要输出窗口边界、key、触发时间等元信息 - 增量聚合 - 增量聚合,就是数据一到,就立即把它并入当前结果 - 不是把整个窗口所有元素都存起来,等窗口结束后再统一计算 - 全量窗口处理 - 先把窗口里所有元素都留着,等窗口触发时,再把完整集合交给函数处理 - reduce:最轻量的窗口聚合 - 本质 - ReduceFunction 的核心思想是:把两个同类型元素合并成一个同类型元素 - 为什么高效 - 窗口里不需要保留全部元素,只需要持续维护一个当前归并结果 - 最大的限制 - 输入类型和输出类型必须一致 - aggregate:更通用的增量聚合 - 本质 - 把聚合拆成了几个阶段 - 创建累加器 createAccumulator() - 把新元素加进累加器 add() - 从累加器得到最终输出 getResult() - 为什么比 reduce 更灵活 - 把三种类型分开 - 输入类型 IN - 累加器类型 ACC - 输出类型 OUT - ProcessWindowFunction:最灵活的窗口处理 - 本质 - 核心价值不是更快,而是信息更多、能力更强 - 适合什么场景 - 需要窗口开始时间/结束时间 - 需要访问 key - 需要输出带窗口元信息的结果 - 需要看完整窗口数据 - 需要复杂后处理逻辑 - 最大的优势 - 能够拿到窗口元信息 - 迟到数据处理 - 什么叫迟到数据 - 这条数据的事件时间本来属于某个旧窗口 - 但它到达系统时,这个窗口已经因为 watermark 推进而触发过了 - 它来晚了 - allow lateness - 是 Flink 为窗口保留的额外容忍时间 - allowedLateness(...) 指定窗口在 watermark 超过结束时间后还能保存多久,以接收迟到数据 - side output - side output 是把某些特殊数据从主流旁路输出的机制 - 把太晚了、已经不能再进主窗口的数据单独收集出来 - 结果修正 - 配置了 allow lateness 后,窗口第一次输出的结果可能不是最终结果 - 后面如果又来了还能接受的迟到数据,窗口结果会更新并再次触发输出 状态与容错 - 状态与容错 - State 基础 - 什么是状态 - 状态=程序对过去信息的记忆 - 没有状态:程序只看当前一条数据 - 有状态:程序处理当前数据时,还能参考历史数据 - 为什么流处理需要状态 - 要做累计 - 用户累计消费金额 - 某设备累计告警次数 - 某单词当前总出现次数 - 要做对比 - 当前温度和上一次温度相比是否突增 - 当前订单状态和之前状态是否矛盾 - 要做时序逻辑 - 10 s 内连续 3 次登录失败 - 下单后 15 分钟未支付 - 一段会话里的连续点击行为 - 要在故障后恢复业务上下文 - Key State - 定义 - Keyed State 是和 Key 绑定的状态 - Keyed State 只能用于 Keyed Stream 上 - 先对 DataStream 做 keyBy(...),然后才能使用 keyed state - 为什么重要 - 很多业务天然都是按照实体分开记忆的 - 底层直觉 - 一个分布式哈希表,按 key 切开,每个并行实例只负责自己那部分 key - Operate State - 定义 - Operator State 是和算子并行实例绑定到状态,而不是和业务 key 绑定 - 算子级记忆,而不是业务实体记忆 - 适合什么场景 - source/sink 级别的处理位点 - 算子实例自己的工作上下文 - Keyed State - ValueState:最常用的 Keyed State - 是什么 - ValueState<T> 表示:每个 key 只有一个值 - 典型场景 - 每个用户累计点击次数 - 每个订单当前状态 - 每个设备最近一次温度 - 每个用户最近一次登录时间 - 每个 key 当前告警开关状态 - ListState:每个 Key 存一串值 - 是什么 - ListState<T> 表示:每个 Key 下面维护一个列表 - 典型场景 - 缓存最近若干条事件 - 暂存等待拼接到多条消息 - 收集一个 key 下的一批原始记录 - 先攒起啦,后面统一处理 - 风险 - 容易无限增长 - MapState:每个 Key 下再套一个 map - 是什么 - MapState<UK,UV> 表示:每个 key 对应一个 map - 典型场景 - 每个用户下,不同商品类别的点击次数 - 每个设备下,不同传感器字段的最新值 - ReducingState:自动做同类型聚合 - 是什么 - ReducingState<T> 表示:每个 key 不直接存明细,而是存一个经过 reduce 的聚合结果 - AggregatingState:更灵活的聚合状态 - 是什么 - AggregatingState<IN, OUT> 和 Reducing State 很像,但更通用 - 包含输入类型 - ProcessFunction - 为什么需要 ProcessFunction - 传统的算子 - map/filter/flatMap/keyBy/window 聚合 - 更偏声明式处理,而不是自己掌控每条事件+时间回调+状态细节 - 无法解决的问题 - 订单创建后 15 分钟未支付就报警 - 用户 10s 内连续登录失败 3 次 - 某 key 1 分钟没有更新时输出当前统计值 - 无法解决问题共同点 - 要逐条处理事件 - 要记住状态 - 要在未来某个时间点触发逻辑 - ProcessFunction 是什么 - 当高级 API 不够表达你的业务规则时,就下到 ProcessFunction 这一层自己控制 - 更像一个加强版的 flatMap - 对每条输入都能处理 - 能产生 0、1 或多条输出 - 能访问上下文 - 能注册 timer - 在 timer 到点时再执行逻辑 - KeyedProcessFunction 的核心方法 - processElement(...) - 每来一条输入事件,Flink 就会调用它 - 事件到了,现在立即处理它 - onTimer(...) - 注册的 timer 到点时,Flink 会调用它 - 之前约好的时间到了,现在执行补充逻辑 - Context 提供什么 - 在 processElement(...),Flink 会提供一个 Context,这个 Context 能做几件事 - timestamp():拿当前元素时间戳 - timeService():访问定时器服务 - getCurrentKey():拿当前 Key - output(...):输出到 side output - 什么是 Timer - 在当前处理某条事件时,告诉 Flink:“到未来某个时间点,请再回头执行一次逻辑“ - Timer 为什么 和状态一起用 - Timer 只负责未来提醒你一次,但是提醒你时应该做什么,要靠状态来判断 - Checkpoint 与容错 - Checkpoint 是什么 - Checkpoint 是 Flink 对“作业状态+输入流位置“的一致性快照 - Checkpoint 至少包含两类东西 - 各个 Stateful operator 的状态快照 - 各个 source 当前读到哪儿的位点,例如 Kafka offset 这类输入位置 - 为什么流处理必须依赖 Checkpoint - 状态负责记忆,checkpoint 负责让记忆在失败后不丢 - Checkpoint 保存的是什么 - Operator/Keyed State - Source 位置 - Timers 连接外部系统 - 连接外部系统 - Flink 读写文件与接 - Kafka 集成 - Flink 与数据库 - 序列化与数据格式 Flink SQL 与 Table API - Flink SQL 与 Table API - 认识 Table API 与 Flink SQL - 为什么要有 Table API 与 SQL - DataStream 更像手写处理逻辑,Table API/SQL 更像声明我要算什么 - Table API 是什么 - Table API 是一种 language-integrated query API - 是 SQL 的超集,专为 Flink 设计 - Flink SQL 是什么 - 用 SQL 语法来描述 Flink 上的流批处理逻辑 - 可以理解为 - 输入表先注册好 - 直接写 SQL - Flink 帮你把 SQL 变为实际可执行程序 - Table API 和 SQL 的关系是什么 - 不是两套互斥系统,而是同一层抽象的两种表达方式 - SQL - 更适合分析型开发 - 数仓开发 - DDL/DML 风格任务 - 让更多懂 SQL 的人参与开发 - Table SQL - 想保留代码层面的类型和组合能力 - 需要和 Java/Scala/Python 代码更紧密集成 - 想在 API 层做更多程序化的表运算 - 什么是 Dynamic Table (动态表) - 动态表就是把“不断到来的流“逻辑上看成一张会持续变化的表 - 什么是 Continuous Query (连续查询) - Flink SQL 不是查一下就结束都传统 SQL,而是持续运行、持续更新结果的 SQL - append/retract/upsert 结果是什么意思 - 问题:结果表变化,怎么把这个变化告诉下游? - 动态表包含三种编码方式 - Append-only - 结果表只会新增,不会修改旧结果,只发追加记录就行 - Retract - 如果结果表中的旧值会被更新或删除,就不能只 append - retract stream 用 add/retract message 来表示 - 对于 update 会先发撤回旧值,再发一条新增新值 - Upsert - upsert stream 需要唯一键 - INSERT 和 UPDATE 都可以用 upsert message 表示 - DELETE 用 delete message 表示 - 相比 retract,upsert 只需一条消息 - Flink SQL 基础语法 - 整体心智模型 - 典型 Flink SQL 作业 - CREATE TABLE source ... - CREATE TABLE sink ... - INSERT INTO sink - SELECT ... FROM source WHERE ... GROUP BY ... - 四步流程 - 定义源表 - 定义结果表 - 写查询逻辑 - 把查询逻辑结果写到目标表 - CREATE TABLE 是什么 - 本质 - CREATE TABLE 是在当前 Catalog 里注册一个表定义,让这个表可以被 SQL 使用 - 告诉 Flink:有一张表,它的字段是什么,连接那个外部系统,怎么读,怎么写 - 最小骨架 -  - 列定义:字段名和类型 - WITH (...):连接器和外部系统参数 - WITH(...) - 通常是连接外部系统所需的 table options - connector - 指定背后连接那个系统(Kafka/filesystem/upsert-kafka/print) - format 相关参数 - 告诉 Flink 数据是 JSON、CSV、Avro 等什么格式 - 外部系统连接参数 - 例如 Kafka topic、broker 地址、分区键等 - Flink SQL 最基础的查询语法 - SELECT - SELECT user_id, amount FROM orders; - WHERE - SELECT user_id, amount FROM orders WHERE amount > 100; - GROUP BY - SELECT user_id, COUNT(*) FROM orders GROUP BY user_id; - INSERT TO - INSERT INTO result SELECT user_id, COUNT(*) FROM orders GROUP BY user_id; - sqlQuery() 和 executeSql() 怎么理解 - sqlQuery() 用于 SELECT/VALUES 这类查询,返回一个 Table - executeSql() 用于执行 DDL、DML 等语句,例如 CREATE TABLE、INSERT INTO - 时间属性与窗口 SQL - 什么是时间属性 - 在流式 SQL 中,窗口函数要求引用的是有效的时间属性,也就是 processing time attribuet 或 event time attribute - 被 Flink 运行时承认,并能参与时间推进和窗口计算的特殊时间列 - 时间属性分类 - 处理时间属性 (Processing Time) - 这条记录被 Flink 当前机器处理到的时间 - 事件时间属性 (Event Time/Rowtime) - 事件时间是业务事件真正发生的时间 - 在 SQL 中定义处理时间属性 - 在 CREATE TABLE 时,可以定义一个处理时间列 -  - 在 SQL 中定义事件时间属性 - 事件时间通常来自源数据中的某个时间段 - 仅仅有这个字段还不够,通常要搭配 WATERMARK -  - ts 是事件时间类 - WATERMARK FOR ts AS ... 告诉 Flink 如何基于 ts 推进事件时间 - 窗口 SQL 推荐怎么写 - Windowing TVF (窗口表值函数) - 窗口聚合支持 TUMBLE/HOP/CUMULATE/SESSION 这些都通过 Windowing TVF 表达的 - 什么是 TUMBLE 窗口 SQL - TUMBLE 对应滚动窗口 - 按固定窗口大小分配数据 - 窗口不重叠 - 在结果中增加 window_start、window_end、window_time 三列 -  - DESCRIPTOR(ts):告诉 Flink 用 ts 这列作为时间属性 - INTERVAL '5' MINUTES:窗口大小 5 分钟 - 什么是 HOP 窗口 SQL - HOP 对应滑动窗口 - HOP 有 slide 和 size - 它会创建可能重叠的窗口 - 同一条记录可能会进入多个窗口 -  - 每 1 分钟滑动一次 - 每个窗口覆盖近 10 分钟 - 结果表同样会带窗口边界列 - 什么是 CUMULATE 窗口 SQL - CUMULATE 是累积窗口 - 适合总窗口范围较大,但想看逐步累积结果 - 每小时一个大窗口,每 5 分钟输出一次当前已累计到哪里的聚合值 -  - step 是 5 分钟 - max size 是 1 小时 - 窗口会逐步扩张输出 - 什么是 SESSION 窗口 SQL - SESSION 对应会话窗口 -  - 30 分钟内持续有事件,属于同一会话 - 维表 JOIN 与实时数仓 - 什么是事实流,什么是维表 - 实时流 - 不断到来的业务事件流 - 通常来自于 Kafka、CDC、日志流等 source table - 维表 - 维表通常是描述型、补充型信息 - 为什么实时数仓一定遇到维表 JOIN - 事实流提供发生了什么,维表提供这件事属于谁、是什么、怎么解释 - 什么是实时宽表 - 把事实流和一个或多个维表关联后,得到的一张字段更完整、可直接分析或落库的结果表 - 什么是 Lookup Join - 以事实流为驱动,对外部维表做按键查询 - 什么是 Temporal Join/Versioned Table Join - 按某个时间点,去取维表在那个时刻的版本,再和事实表做关联 - 什么是 Versioned Table - 一张会变化、并且保留变化历史的表 原理深入 - 原理深入 - 背压与性能调优 - 什么是背压 - 上游发太快,下游吃不动
价值
有人随着年龄增值, 有人随着年龄贬值, 这就说明了追求生命内涵的重要性。 ...
HBase 复习
基础认知 - 基础认知 - HBase 和 MySQL、Redis、Elasticsearch、Hive 的区别 - HBase - 一个建立在 Hadoop/HDFS 上的、支持联机、实时读写的分布式数据库,适合承载超大表 - 面向宽表/稀疏表 - 以 RowKey 为核心访问路径 - 强项是海量数据下的主键访问和范围扫描 - MySQL - 关系型数据库 - 强 SQL 能力 - 强事务 - 适合 OLTP 业务系统 - Redis - 内存型数据存储 - 超低延迟 - 丰富数据结构 - 更像高性能数据结构引擎/缓存平台 - ElasticSerach - 分布式搜索引擎 - 文档模型 - 擅长全文检索、相关性排序、聚合分析 - 不是传统事务数据库 - Hive - 数据仓库 - 面向离线分析 - 适合大规模批处理、报表、数仓 - 不适合在线高并发事务场景 - 列式/列族模型是什么 - 列族模型 - 看起来像一张表 - 但是每一行并不是必须拥有同样的列 - HBase 把列分成若干个 Column Family (列族) - 同一个列族里面的数据会被一起管理和物理存储 - 每一条具体的数据单元是一个 Cell,由行键、列族、列限定符、事件戳和值共同确定 - HBase 是一个按 RowKey 组织、按列族管理、按单元格存值、支持多版本的稀疏宽表结构 - 模型示例 - 层次结构 - Table -> Row -> Column Family -> Column Qualifier -> Cell(Value + TimeStamp) - 一张用户表 user_profile,定义了两个列族 info + stat - 一行数据 - RowKey = user_1001 - info:name = "Alice" - info:city = "Tokyo" - stat:login_cnt = 25 - info、stat 是列族 - name、city、login_cnt 是列限定符 - 完整列名是 family:qualifier - 每个值都带 timestamp,可以保留多个版本 - 列族、列、Cell - RowKey - 是一行数据的主键,也是 HBase 最核心的访问入口 - 查 HBase,本质是在查某个 RowKey,或者扫描一段 RowKey 的范围 - 决定这一行是谁 - 决定数据在字典序上的排序方式 - 决定大部分查询的性能上限 - Column Family - 是 HBase 中必须定义的逻辑分组 - 列族在创建表时就确定好 - HBase shell 和 API 文档都以先创建表,定义 family,再在 family 下写具体列的方式使用 HBase - 列族是一组经常一起出现,一起管理,一起存储策略配置的数据 - Column Qualifier(列限定符) - 列限定符就是列族下面具体的列名 - 列限定符不需要像 MySQL 字段在建表时全部固定列出来 - 列族通常固定,但是 qualifier 可以按需动态增加。 - HBase 适合列很多,而且不同记录拥有的列不一样的场景 - Cell - Cell 是最小存储单元 - 一个 Cell 由这些部分唯一定位 - row - family - qualifier - timestamp - value - TimeStamp/Version - HBase 天然支持多版本 - 同一个 row + family + qualifier,可以在不同 timestamp 下保存多个值 - Get/Scan API 也支持按照版本范围取数据 - HBase 的默认思维是:一个单元格可以有历史版本 - 为什么要有列族 - HBase 不只是逻辑上分组,还会影响物理管理和存储策略 - 列族可以把访问模式相近的数据放在一起 - 列族可以让不同数据有不同的存储策略 - 列族可以让稀疏表更自然 - 稀疏宽表 - 稀疏 - 不是每一行都有同样的列 - 举例 - 商品 A:颜色、尺寸、品牌 - 商品 B:品牌、功率、电压 - 商品 C:材质、长度、颜色、重量 - 如果用 MySQL,固定列会导致很多字段为空 - HBase 只保存实际存在的 family:qualifier->value 即可 - HBase 在列族下使用动态 qualifier - 宽表 - 列非常多,甚至 qualifier 的数量可以远大于传统关系表的字段数 - HBase 为什么是 Bigtable 风格数据库 - 什么是 Bigtable - Bigtable 是 Google 提出的一个分布式存储系统,用来存放结构化数据,典型特点 - 面向超大规模数据 - 稀疏,可扩展的表 - 以 row key 为核心组织数据 - 支持列族 - 支持多版本 - 支持按键范围扫描 - 以 RowKey 为中心,而不是以 SQL 为中心 - Bigtable 风格数据库核心访问方式 - 按 RowKey 精准查 - 按 RowKey 前缀或范围扫描 - 设计 schema 时优先围绕查询路径设计 row key - 设计数据库时,优先设计 RowKey 和访问路径,而不是优先设计复杂 SQL 数据模型与表设计 - 数据模型与表设计 - RowKey 设计原则 - 总纲 - RowKey 不是随便选一个主键,而是 HBase 里面最重要的访问索引、排序依据和分布依据。 - 每行只有一个被索引的值,就是 row key - 本质 - 数据如何定位 - 数据怎么按字典序排序 - 数据怎么在集群中分布,会不会热点 - RowKey 为什么重要 - 最快、最强、最自然的访问路径就是按照 RowKey 去查和按范围扫 - RowKey 设计不好 - 查询路径不顺 - scan 范围过大 - 热点集中 - 读写吞吐不均衡 - 很难靠 SQL 或者二级索引补救 - 核心原则 - 先按查询方式设计,不要按照字段含义设计 - 最常见的查询是什么 - 查询是点查还是范围查 - 查询维度谁排第一 - 结果取最近一条、最近 N 条还是一个时间段 - 让最常用的过滤维度出现在 RowKey 前部 - Row Key 前缀直接决定 - 哪些数据会排在一起 - 哪些数据可以用 prefix/range 高效扫描 - Row Key 要支持高频查询的自然范围扫描 - HBase 擅长的不是随意条件查询,而是连续键空间扫描 - 建议围绕 row key prefix 设计 schema - 例如查询设备时序数据 - 按设备查最近数据 - 按设备查某段时间数据 - device_id#reverse_ts - 避免单 key 递增直接写入,防止热点 - 不断递增的 rowKey 最新写入通常会不断打到键空间尾部,把写压力集中到少数 region - 在可扫描性和均匀分布之间做平衡 - 分桶:bucket(user_id)#user_id#reverse_ts - 时间类场景通常需要显式设计时间顺序 - rowKey 要短、稳定、可解析 - 常见设计模式 - 单主键模型 - user_id - 一行就是一个主键 - 点查为主 - 不需要时间维度 - 主维度 + 时间 - user_id#ts - 主维度 + 倒序时间 - user_id#reverse_ts - 桶 + 主维度 + 时间 - bucket(user_id)%16#user_id#reverse_ts - 数据集前缀 + 业务键 - U#1001 - 列族应该怎么拆 - 为什么列族拆分重要 - 列族会把同一族的一组列物理共置 - 列族带有独立的存储属性 - 每一行都拥有相同的列族集合 - 未写入的单元格不占用实际空间 - 列族拆分总原则 - 同查:经常一起读的字段,放在一起 - 同配:需要相同存储策略的字段,适合放在一起 - 同寿命:生命周期相近的字段,适合放在一起 - 同频率:更新频率相近的字段,适合放在一起 - 拆分口诀 - 基础资料一族、频繁指标一族、短期日志一族、打对象单独一族 - 版本机制 - 什么是版本 - 同一个字段可能保存多份历史值 - 每一份历史值靠 timestamp 区分 - HBase 官方文档把 Cell 展示为包含 row、column、timestamp 和 value - timestamp 和 version 的关系 - timestamp 是版本标识 - 在 HBase 里,每写入一个 Cell,都要有对应的 timestamp - timestamp 由系统自动生成,也可以由自己指定 - version 是字段保留的历史份数 - 按列族配置,说明最多留多少份历史数据 - TTL、压缩、Bloom Filter - TTL 是什么 - Time To Live,生存时间 - 是 cell contents 的生存时间,单位是 s - 是列族级的数据过期策略 - TTL 解决的问题 - 控制历史数据无限膨胀 - 降低存储成本 - 让冷热数据边界更为清晰 - 压缩是什么 - 压缩就是把 HBase 存到 StoreFile/HFile 里面的数据,用某种压缩算法压小 - 可以在 ColumnFamily 上启用压缩,且不需要重建表 - 用 CPU 换磁盘空间、换 I/O 带宽 - Bloom Filter 是什么 - 是一种概率型存在性判断结构 - 帮助你快速判断这个 StoreFile 里面大概率没有你想要的数据,就不去做无效读取了 - 热点问题 - 什么是热点 - HBase/Bigtable 这类系统的数据是按照 rowKey 的字段序排列的 - 相邻的 Key 也会落在相邻的 key range 上,这些 key range 被分配给特定的 Region/RegionServer 来服务 - 如果请求集中在某一个 RowKey/某一小段连续 RowKey/某个不断增长的最新 key 区间,负载就不会均匀摊开,造成热点 架构原理 - 架构原理 - HMaster 做什么 - HMaster 的定位 - HMaster 负责协调和管理,真正承担读写请求的是 RegionServer - HMaster 类似调度中心和元数据/运维控制器 - HMaster 的核心职责 - 管理整个集群的 Region 分配 - HBase 的表会被切分成很多 Region - 集群启动时给 Region 找归属的 RegionServer - RegionServer 宕机后重新分配它原来负责 Region - 负载均衡主动迁移 Region - 监控 RegionServer 的存活状态 - 处理 RegionServer 故障后的恢复 - 做负载均衡 - 处理 DDL 和 schema 管理 - 参与主备切换和集群高可用 - RegionServer 做什么 - RegionServer 的定位 - 是数据面 - 真正保存并服务 Region - 真正执行 Put/Get/Scan/Delete - 真正把数据从内存刷到 File - 真正做 compaction 和 split - RegionServer 的核心职责 - 持有并管理 Region - HMaster 决定 Region 给谁 - RegionServer 真正把 Region 跑起来 - 处理客户端读请求 - 处理客户端写请求 - 管理每个 Region 内部的 Store/MemStore/StoreFile - 运行后台维护线程:flush、compaction、WAL 滚动、split - 管理 BlockCache - 执行部分 Region 级维护操作 - Put 流程 - 客户端根据元数据找到 Region 所在的 Region Server - 请求发到该 Region Server - RegionServer 找到这个 Region 对应 family 的 Store - 先写 WAL - 在把数据写到 Store 里面的 MemStore - 后台达到条件后,MemStoreFlusher 把数据 flush 到 StoreFile - 之后 CompactSplitThread/MajorCompactionChecker 再持续维护这些文件 - Get/Scan 流程 - 客户端根据元数据找到 Region 所在的 Region Server - RegionServer 在对应 Region 的 Store 中查找 - 优先利用 BlockCache - 必要时再读 StoreFile/HFile - 利用索引和 BloomFilter 缩小无效读取 - 返回结果给客户端 - Region 是什么 - Region 定义 - Region 是一段连续的、按 RowKey 排序的行范围 - 表的层级结构 Table->Region->Store->MemStore/StoreFile->Block - 为什么要有 Region - 做分布式切片 - 支持扩展 - 支持负载均衡 - Region 结构 - 一个 Region 内部,每个 ColumFile 对应一个 Store,每个 Store 有自己的 MemStore 和若干 StoreFile - Region 如何分配 - 同一时刻,一个 Region 只由一个 RegionServer 提供服务 - 一个 RegionServer 可以持有很多 Region - HMaster 负责 Region 给谁、是否迁移、是否均衡 - RegionServer 负责真正持有 Region 并处理这个 Region 的读写 - Region 为什么需要切分 - 单个 Region 太大,迁移和恢复成本高 - 该 Region 所在 RegionServer 负载可能过重 - 并行度不够,一大段数据只能由一个 Region 服务 - 热点更集中,难以分散 - split 的目的,是把一个过大的 key range 拆分成两个更小的 key range,来提升可扩展性和分布性 - 什么时候会 split - 由 RegionSplitPolicy 决定 - 当 Region 增长到某个阈值/满足当前 split policy 的条件 - WAL 是什么 - 为什么需要 WAL - HBase 不是收到请求就立即写成 HFile - 写请求先进入内存里面的 MemStore - MemStore 是内存结构,掉电或进程崩溃会丢 - WAL 为还没落成 HFile 的持久化阶段兜底 - 写入请求里的 cells 会一直保留,直到成功持久化到 WAL 和 MemStore - MemStore 负责写入速度,WAL 负责稳定性 - 为什么叫 Write-Ahead - 先把将要发生的修改记录到日志中,再依赖后续流程把它整理进正式数据文件 - 这条写操作在正式长期存储结构整理完成之前 - 已经被写入到一个可恢复的日志里 - WAL 处在的位置/Put 操作链路 - 客户端把 Put 请求发送到目标 RegionServer - RegionServer 把这次修改追加到 WAL - 同时把数据写入目标 Store 的 MemStore - 之后后台线程再把 MemStore Flush 成 StoreFile/HFile - 最后通过 compaction 整理文件 - WAL 记录的内容 - WAL 记录的是这次写操作的增量编辑记录 - WAL 写入的内容加做 WALEdit,有 WAL.Entry、WALKey 类型来表示一条日志条目及其键 - MemStore、StoreFile/HFile、BlockCache - 概述 - MemStore:写入时的内存缓冲区 - StoreFile/HFile:Flush 之后落盘的正式数据文件 - BlockCache:读路径上的数据块缓存 - 在一个 Region 里,每个列族对应一个 Store,每个 Store 有自己的 MemStore 和若干 StoreFile - MemStore 是什么 - MemStore 是 Store 在内存中的缓冲区 - 客户端写入数据, RegionServer 会先把修改写入 WAL,并把数据存放在对应的 Store 的 MemStore - 后续达到条件后,再 flush 到磁盘形成 StoreFile - 主要解决写的快的问题,如果每次 Put/Delete 都直接改磁盘主文件,开销会很大 - 先写入内存,可以把随机小写聚合起来,再批量 flush 到磁盘 - MemStore 只是内存态,不是最终查询文件,内容会在后续 flush 后变为 StoreFile/HFile - StoreFile 是什么 - 是 Store 在磁盘上的数据文件 - MemStore 不会只 flush 一次,每次 flush 会生成新的 StoreFile - 当 BlockCache miss 且 MemStore 没有目标数据,RegionStore 会去 HFile/StoreFile 中查 - HFile 是什么 - 是底层文件格式,StoreFile 是 HBase 在 RegionServer/Store 这一层使用的数据文件抽象 - 一个 StoreFile 对应一个 HFile - BlockCache 是什么 - 是 HBase 读路径上的数据库缓存 - 缓存的是从 HFile 里读出来的 block - 解决的是读的快的问题 - Flush、Compaction、Split 的流程 - Flush 是什么 - 定义 - 把某个 Store 的 MemStore 内容写到磁盘,生成新的 StoreFile - 为什么必须 Flush - MemStore 在内存中,不能无限增长 - flush 后发生什么 - flush 完成后,这批数据会变成新的 StoreFile,被纳入该 Store 的文件集合。 - Flush 触发条件 - 内存压力触发 - 时间触发 - 运维/内部流程触发 - Compaction 是什么 - Compaction 是把一个 Store 多个 StoreFile 文件合并整理成更少的新文件 - Minor Compaction 和 Major Compaction 的区别 - Minor Compaction - 把若干个较小的 StoreFile 合并成更少的较大文件,但通常不是把该 Store 的所有文件一次性全部重写 - Major Compaction - 更彻底地重写该 Store 的文件集合 - Split 是什么 - Split 是把一个 Region 沿 RowKey 边界切成两个子 Region - Zookeeper 的作用 - 核心作用 - 主节点选举:决定谁是 active HMaster - 服务发现:让客户端知道该连谁 - 节点状态感知:知道哪些服务还活着 - 协调关键元数据入口 - 为什么 HBase 需要 ZooKeeper - HBase 把协调问题外包给 ZooKeeper,把自己更多精力放在存储和读写路径上 - Zookeeper 不做什么 - 不存储业务明细数据 - 不承担高吞吐读写 - 不等于 HMaster 实际操作 - 实操 - 启动单机 HBase - 单机模式是什么 - 单机模式是 HBase 最基础的步数形态 - 在 standalone 模式下,所有 HBase 守护进程都运行在一个 JVM 里 - Docker 启动 -  - 进入容器进行最小验证命令 -  - 用 Shell 建表、删表、put/get/scan - 进入 shell - hbase shell - 命令基本结构 - 表名:test - row key:row1 - 列:cf:a(列族:列限定符) - create 建表 - create 'test','cf' - 创建表 test,在表中预定义列族 cf - put 写入数据 - put 'test', 'row1', 'cf:a', 'value1' - 'test':表名 - 'row1':row key - 'cf:a':列 - 'value1':值 - 本质:往某个表的某一行、不同列写值 - scan 扫描数据 - scan 'test' - 按 RowKey 顺序把表里面的一批数据扫出来 - get 读一行数据 - get 'test', 'row1' - 按 Rowkey 精确读取某一行 - get 是点查,scan 是范围扫 - disable 和 drop - disable 的作用:先把表停用,把表从可服务状态切到不可服务状态 - drop 的作用:删除表 - 创建 namespace - 什么是 namespace - 如果创建表时未指定 namespace,则表存放在 default namespace 下 - create 'test','cf' 本质等价于 'default:test' - 为什么要有 namespace - 表分组管理 - 避免表名冲突 - 便于做配额/约束 - namespace 是 HBase 里按业务域组织表的基本单位 - hbase 存在哪些 namespace - default - 用户表默认所在 namespace - hbase - 系统 namespace,保留给 hbase 内部表 - 创建 namespace - create_namespace 'demo' - 查看 namespace - list_namespace - 在 namespace 下建表 - create 'demo:test', 'cf' - 写入和读取 - put 'demo:test', 'row1', 'cf:a', 'value1' - get 'demo:test', 'row1' - scan 'demo:test' - 删除 namespace - drop_namespace 'demo' - 设置版本数、TTL、压缩 - 列族级配置 - family 是物理和策略边界 - VERSIONS、TTL、COMPRESSION 这类配置,默认思维都是这个 family 怎么存,不是这一列怎么存 - alter 是什么命令 - alter 用来修改已有表的 schema 或表/列族相关配置 - VERSIONS - 控制什么 - 同一个 row+family+qualifier 最多保存多少个历史版本 - 怎么改 - alter 'test', NAME => 'cf', VERSIONS => 5 - TTL - 控制什么 - 这个列族里面的数据默认能活多久 - 怎么改 - alter 'test', NAME => 'cf', TTL => 2592000 - COMPRESSION - 控制什么 - 这个列族的 StoreFile/HFile 落盘时用什么压缩算法 - 怎么改 - alter 'test', NAME => 'cf', COMPRESSION => 'SNAPPY' - 验证修改 - describe 'test' - 查看 Region 分布 - web ui - localhost:16010 -  - 简单 filter 查询 - 为什么要用 filter - 不想整表扫出来自己筛 - 只看某些 row key 前缀/只看某些列名前缀/只取前 N 行/只保留某个列值满足条件的行 - 让 RegionServer 在服务端先过滤(server-side filtering) - filter 的基本写法 - scan '表明', { FILTER => "过滤器表达式" } - 准备练习数据 -  - 5 类简单 filter - PrefixFilter:按 row key 前缀过滤 -  - PageFilter:限制返回多少行 - scan 'demo:filter_test', { FILTER => "PageFilter(2)" } -  - ColumnPrefixFilter:按列限定符前缀过滤 - scan 'demo:filter_test', { FILTER => "ColumnPrefixFilter('na')" } -  - SingleColumnValueFilter:按某一列的值筛行 - scan 'demo:filter_test', { FILTER => "SingleColumnValueFilter('cf', 'city', =, 'binary:Tokyo')" } -  - ValueFilter:按值过滤列 - 如果只是做简单的 family:qualifier:value 等值判断,推荐限制后再配 ValueFilter,这样能避免扫描无关的 family/column - 组合使用 filter - "PrefixFilter ('Row') AND PageFilter (1) AND FirstKeyOnlyFilter ()" Java 客户端开发 - Java 客户端开发 - Connection/Admin/Table - 职责边界 - Connection:集群级入口 - 封装了到实际服务器和 ZooKeeper 的连接 - Admin:管理面接口 - Table:单表数据面接口 - 代码关系 -  - 最小 Java 示例
OpenClaw 原理
1. 学习主题 我要学习的内容: OpenClaw 原理 我为什么要学它: (写清楚用途、场景、目标,比如面试、项目、论文、考试) ...