JS入门

前言

  • 这一篇讲 JavaScript 的语言本身:变量与作用域、类型与转换、对象与数组、函数与 this、流程控制、异常、异步(事件循环 / Promise / async-await)、常用内置对象、模块化。浏览器里的 document、window 那些属于下一篇《DOM 和 BOM》。
  • 本文所有示例都在本机实跑过,环境是 Node v26.13.0(V8 引擎,与 Chrome 同源),输出原样贴在每个代码块下面的 text 块里。
    • 一个格式差别:Node 的控制台打印数组会写成 [ 1, 2, 3 ](元素两侧有空格),浏览器控制台是 [1, 2, 3]。值是一样的,只是显示风格。
    • 另一个语义差别:Node 的普通 .js 文件是 CommonJS 模块、默认宽松模式,和浏览器里不带 type="module" 的 <script> 行为一致;一旦写 'use strict' 或用模块,就是严格模式(下面有专门一段对比)。

JS 是怎么跑起来的

四种写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang="zh-cn">

<head>
<meta charset="utf-8">
<title>JS 引入方式</title>

<!-- 1. 内部脚本:写在页面里 -->
<script>
console.log('我是页面里的脚本');
</script>

<!-- 3. defer:下载不打断解析,等文档解析完再按顺序执行(推荐放在 head) -->
<script src="app.js" defer></script>

<!-- 4. module:ES 模块,天生 defer + 严格模式 + 独立作用域 -->
<script type="module" src="main.js"></script>
</head>

<body>
<!-- 2. 事件属性:内联 JS,能不用就不用(难维护、有 CSP 风险) -->
<button onclick="alert('你点击了按钮!')">点击我</button>

<!-- 5. async:下载完立刻执行,多个 async 脚本之间顺序不保证 -->
<script src="analytics.js" async></script>
</body>

</html>
写法 执行时机 说明
不带属性 解析到就停下来执行 阻塞 HTML 解析,所以老习惯是「脚本放 </body> 前」
defer 文档解析完、DOMContentLoaded 之前 保持书写顺序执行,适合依赖 DOM 的业务脚本
async 下载完就执行 谁先下载完谁先跑,适合埋点这类互不依赖的脚本
type="module" 默认等同 defer 严格模式、顶层 this 是 undefined、变量不泄漏到全局、可用 import/export
  • 调试用浏览器控制台(F12 → Console),console.log 只是最基础的一个:console.table(看数组/对象)、console.group、console.count、console.time/timeEnd 都更好用。
  • 控制台里可以直接写表达式求值;$0 代表当前在 Elements 里选中的元素。

alert / prompt / confirm

这三个弹窗会阻塞整个页面,只适合演示。真正的产品里用自定义弹窗组件。

1
2
3
4
5
6
// 浏览器里才有;Node 环境没有 window.alert
if (typeof window !== 'undefined') {
window.alert('提示'); // 只有「知道了」一个按钮
window.confirm('确定删除?'); // 返回 true / false
window.prompt('请输入名字'); // 返回字符串或 null
}

变量:var、let、const

1
2
3
4
5
6
7
8
9
10
// 三种声明关键字:var(函数作用域)、let(块级)、const(块级 + 不可重新赋值)
var a = 'var 声明';
let b = 'let 声明';
const c = 'const 声明';
console.log(a, b, c);

a = 'var 可以改';
b = 'let 也可以改';
console.log(a, b);
// c = 'const 改不了'; // 打开注释会抛 TypeError: Assignment to constant variable.
1
2
var 声明 let 声明 const 声明
var 可以改 let 也可以改
  • 新代码的默认选择:const 优先,需要重新赋值才用 let,var 只在读旧代码时遇到。
  • let/const 是块级作用域({} 之内),var 是函数作用域并且会「漏」出代码块:
1
2
3
4
5
6
7
8
9
10
11
12
13
// 块级作用域:{} 里用 let/const 声明的变量,出了这块就没了
if (true) {
let blockLet = '块内可见';
const blockConst = '块内也可见';
var leaked = 'var 会漏到块外';
console.log(blockLet, '/', blockConst);
}
console.log(leaked); // var 漏出来了
try {
console.log(blockLet); // let 的变量在块外不存在
} catch (e) {
console.log('访问块外 let ->', e.constructor.name + ': ' + e.message);
}
1
2
3
块内可见 / 块内也可见
var 会漏到块外
访问块外 let -> ReferenceError: blockLet is not defined
  • 提升(Hoisting)到底提升什么:
    • 函数声明整体提升,连函数体一起,所以声明之前就能调用。
    • var 只提升声明、不提升赋值,所以提前读到的是 undefined。
    • let / const 提升但不初始化,提前访问会进「暂时性死区」直接报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 提升:函数声明整个被提前;var 只提前「声明」,不提前「赋值」
console.log('调用提升的函数 ->', sayHi('Bob'));
// var 在声明前读:拿到 undefined(不报错,这是坑)
console.log('声明前读 var ->', typeof notYet);
var notYet = 1;
// let / const 在声明前读:直接 ReferenceError(暂时性死区 TDZ)
try {
console.log(tdz);
} catch (e) {
console.log('声明前读 let ->', e.constructor.name);
}
// 函数表达式(含箭头函数)不提升
try {
notDefined();
} catch (e) {
console.log('提前调用箭头函数 ->', e.constructor.name);
}
function sayHi(name) {
return 'Hi, ' + name;
}
const notDefined = () => 1;
1
2
3
4
调用提升的函数 -> Hi, Bob
声明前读 var -> undefined
声明前读 let -> ReferenceError
提前调用箭头函数 -> ReferenceError
  • 面试经典:for (var) 里存的闭包全部拿到同一个变量。
1
2
3
4
5
6
7
8
9
10
11
12
// 经典面试题:var 共享同一个变量,let 每轮循环新建一个绑定
const withVar = [];
for (var i = 0; i < 3; i++) {
withVar.push(() => i);
}
const withLet = [];
for (let j = 0; j < 3; j++) {
withLet.push(() => j);
}
console.log('for(var) 里的闭包:', withVar.map(f => f()));
console.log('for(let) 里的闭包:', withLet.map(f => f()));
console.log('循环结束后 var 还在:', typeof i, '/', 'let 已经没了:', typeof j);
1
2
3
for(var) 里的闭包: [ 3, 3, 3 ]
for(let) 里的闭包: [ 0, 1, 2 ]
循环结束后 var 还在: number / let 已经没了: undefined
  • 另外两个常见细节:
    • 不写关键字直接赋值:宽松模式下会悄悄创建一个全局变量(严格模式抛 ReferenceError),这是最阴的 bug 来源之一。
    • const 只锁「绑定」,不锁「内容」:const arr = [1]; arr.push(2); 完全合法,arr = [] 才报错。

数据类型

一眼看清:8 种类型

类别 类型 举例 typeof 结果
原始类型 Number 42、3.14、NaN、Infinity number
原始类型 String 'a'、`模板${字符串}` string
原始类型 Boolean true / false boolean
原始类型 Undefined 未赋值的变量 undefined
原始类型 Null 显式的「空」 object(历史 bug)
原始类型 Symbol Symbol('id') symbol
原始类型 BigInt 10n bigint
引用类型 Object {}、[]、函数、Date、RegExp、Map… object(函数是 function)
1
2
3
4
5
6
7
8
9
// typeof 返回字符串;注意两个历史遗留坑:typeof null === 'object',typeof [] === 'object'
console.log(typeof 42, typeof 'a', typeof true, typeof undefined);
console.log('typeof null ->', typeof null, '(历史 bug,规范已定为 object)');
console.log('typeof {} ->', typeof {});
console.log('typeof [] ->', typeof [], ' 判数组要用 Array.isArray:', Array.isArray([]));
console.log('typeof fn ->', typeof function () {});
console.log('typeof Symbol() ->', typeof Symbol());
console.log('typeof 10n ->', typeof 10n, '(BigInt 是独立类型)');
console.log('数组判断三连:', [Array.isArray([]), Array.isArray({}), typeof [] === 'object']);
1
2
3
4
5
6
7
8
number string boolean undefined
typeof null -> object (历史 bug,规范已定为 object)
typeof {} -> object
typeof [] -> object 判数组要用 Array.isArray: true
typeof fn -> function
typeof Symbol() -> symbol
typeof 10n -> bigint (BigInt 是独立类型)
数组判断三连: [ true, false, true ]

Number:只有一种数字类型,全是浮点

1
2
3
4
5
6
7
8
9
10
// JS 只有一个数字类型 Number,全是 IEEE 754 双精度浮点
console.log('整数与小数同类型:', typeof 100, typeof 10.5, 1 / 3);
console.log('0.1 + 0.2 =', 0.1 + 0.2, '| 等于 0.3 吗:', 0.1 + 0.2 === 0.3);
console.log('整数上限 MAX_SAFE_INTEGER =', Number.MAX_SAFE_INTEGER);
console.log('超上限后不再精确:', Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2);
console.log('除以零得 Infinity,不是报错:', 1 / 0, '(', 1 / 0 === Infinity, ')');
console.log('0/0 是 NaN:', 0 / 0, '| NaN === NaN ->', NaN === NaN);
console.log('字符串参与运算的 NaN:', 'a' / 1);
console.log('(1.005).toFixed(2) =', (1.005).toFixed(2), '(这个字面量其实是 1.004999...)');
console.log('比较浮点要留误差:', Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON);
1
2
3
4
5
6
7
8
9
整数与小数同类型: number number 0.3333333333333333
0.1 + 0.2 = 0.30000000000000004 | 等于 0.3 吗: false
整数上限 MAX_SAFE_INTEGER = 9007199254740991
超上限后不再精确: true
除以零得 Infinity,不是报错: Infinity ( true )
0/0 是 NaN: NaN | NaN === NaN -> false
字符串参与运算的 NaN: NaN
(1.005).toFixed(2) = 1.00 (这个字面量其实是 1.004999...)
比较浮点要留误差: true
  • 需要精确小数(金额)时,用「分」为单位的整数存,或引第三方十进制库,别拿浮点直接加减比较。
  • Infinity / -Infinity / NaN 都是合法的 number;NaN 不等于任何东西,判断要用 Number.isNaN(x)(注意老的全局 isNaN('abc') 会先把字符串转成数字,返回 true,是个陷阱)。

BigInt:超大整数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// BigInt:超出 Number 精度的整数,ES2020 引入,字面量结尾加 n
const huge = 1234567890123456789012345678901234567890n;
console.log('字面量:', huge);
console.log('用普通数字构造会先丢精度:', BigInt(12345678901234567890));
console.log('字符串构造是安全的:', BigInt('12345678901234567890'));
console.log('除法向零取整:', 7n / 2n);
console.log('幂运算:', 2n ** 64n);
try {
console.log(1n + 1); // BigInt 与 Number 不能混算
} catch (e) {
console.log('1n + 1 ->', e.constructor.name);
}
try {
JSON.stringify({ v: 1n });
} catch (e) {
console.log('JSON 不认 BigInt ->', e.constructor.name + ': ' + e.message);
}
console.log('自己转字符串就能序列化:', JSON.stringify({ v: String(huge) }));
1
2
3
4
5
6
7
8
字面量: 1234567890123456789012345678901234567890n
用普通数字构造会先丢精度: 12345678901234567168n
字符串构造是安全的: 12345678901234567890n
除法向零取整: 3n
幂运算: 18446744073709551616n
1n + 1 -> TypeError
JSON 不认 BigInt -> TypeError: Do not know how to serialize a BigInt
自己转字符串就能序列化: {"v":"1234567890123456789012345678901234567890"}

真值与假值:只有 8 个假值

1
2
3
4
5
6
7
// 只有 8 个假值,其余全部为真——空字符串、空数组、'0' 都是真值
const falsy = ['', 0, -0, 0n, NaN, null, undefined, false];
console.log('8 个假值(值为类型):', falsy.map(v => (v === '' ? "''" : String(v)) + '(' + typeof v + ')').join(', '));
console.log('取反后全为 true:', falsy.map(v => !v).join(','));
const truthy = ['0', 'false', ' ', [], [0], {}, function () {}, new Map(), Symbol()];
console.log('这些全为真:', truthy.map(v => Boolean(v)).join(','));
console.log('空数组也是真值:', [] === false, '| 但 == false 成立:', [] == false);
1
2
3
4
8 个假值(值为类型): ''(string), 0(number), 0(number), 0(bigint), NaN(number), null(object), undefined(undefined), false(boolean)
取反后全为 true: true,true,true,true,true,true,true,true
这些全为真: true,true,true,true,true,true,true,true,true
空数组也是真值: false | 但 == false 成立: true
  • 空数组、空对象、字符串 '0'、'false' 全都是真值——if (arr) 判断不了空数组,要看 arr.length。

对象是引用,赋值只是「多一个名字」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 对象赋值只是「多一个名字」;展开运算符是浅拷贝,只复制第一层
const a = { n: 1 };
const b = a; // b 和 a 指向同一个对象
b.n = 2;
console.log('改 b 会影响 a:', a.n, '| a === b:', a === b);

const c = { ...a }; // 浅拷贝:新建对象,复制第一层属性
c.n = 9;
console.log('浅拷贝后互不影响:', a.n, c.n, '| a === c:', a === c);

const deepSrc = { inner: { x: 1 } };
const shallow = { ...deepSrc };
shallow.inner.x = 50; // 第二层还是同一个对象
console.log('嵌套层仍然共享:', deepSrc.inner.x, '| deepSrc.inner === shallow.inner:', deepSrc.inner === shallow.inner);

const cloned = structuredClone(deepSrc); // 真正的深拷贝
cloned.inner.x = 500;
console.log('structuredClone 之后互不影响:', deepSrc.inner.x, cloned.inner.x, '| 是不同对象:', deepSrc.inner !== cloned.inner);
1
2
3
4
改 b 会影响 a: 2 | a === b: true
浅拷贝后互不影响: 2 9 | a === c: false
嵌套层仍然共享: 50 | deepSrc.inner === shallow.inner: true
structuredClone 之后互不影响: 50 500 | 是不同对象: true
  • 真正深拷贝:structuredClone(obj)(现代浏览器和 Node 17+ 都支持,Map/Set/Date 都能保留);JSON.parse(JSON.stringify(x)) 会丢函数和 undefined、把 Date 变成字符串、遇到循环引用直接报错。

类型转换与比较

隐式转换的规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 隐式转换:+ 只要有一边是字符串就变拼接,其它算术运算先把操作符转成数字
console.log("'1' + 2 =", '1' + 2);
console.log("'1' + [2] =", '1' + [2]);
console.log('[1] + [2] =', [1] + [2]);
console.log("'6' - '2' =", '6' - '2');
console.log("'6' * '2' =", '6' * '2');
console.log("+true =", +true);
console.log("+[] =", +[], '| +[1] =', +[1], '| +[1,2] =', +[1, 2]);
console.log("Number('') =", Number(''), '| Number(\" \") =', Number(' '));
console.log("Number('1e3') =", Number('1e3'), '| Number(\"0x10\") =', Number('0x10'));
console.log("'5' + null =", '5' + null, "| '5' + undefined =", '5' + undefined);
console.log('{} + {} 字符串化:', String({}) + String({}));
console.log("[1,2,3] + '' =", [1, 2, 3] + '');
console.log("'10' > '9' =", '10' > '9', '(两边都是字符串,按字典序比!)');
console.log('10 > "9" =', 10 > '9', '(一边是数字,字符串被转成数字)');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
'1' + 2   = 12
'1' + [2] = 12
[1] + [2] = 12
'6' - '2' = 4
'6' * '2' = 12
+true = 1
+[] = 0 | +[1] = 1 | +[1,2] = NaN
Number('') = 0 | Number(" ") = 0
Number('1e3') = 1000 | Number("0x10") = 16
'5' + null = 5null | '5' + undefined = 5undefined
{} + {} 字符串化: [object Object][object Object]
[1,2,3] + '' = 1,2,3
'10' > '9' = false (两边都是字符串,按字典序比!)
10 > "9" = true (一边是数字,字符串被转成数字)
  • 一句话记法:+ 只要有一边是字符串就拼接;其它算术运算符(- * / %)先把两边转成数字。
  • 关系运算符两侧都是字符串时按字典序比,所以 '10' > '9' 是 false;有一边是数字就转数字。

== 的坑清单

1
2
3
4
5
6
7
8
9
10
11
// == 会做类型转换,=== 不会。记住:除了 null/undefined 互相等,其它一律用 ===
console.log("0 == '0' ->", 0 == '0', '/ === ->', 0 === '0');
console.log("1 == true ->", 1 == true, '/ === ->', 1 === true);
console.log("0 == false ->", 0 == false, '/ === ->', 0 === false);
console.log("'' == false ->", '' == false, '/ === ->', '' === false);
console.log("null == undefined ->", null == undefined, '/ === ->', null === undefined);
console.log("undefined == 0 ->", undefined == 0, '(和 0 不等,别拿 ==0 判空)');
console.log("[] == '' ->", [] == '', '/ [] == 0 ->', [] == 0, '/ [1] == 1 ->', [1] == 1);
console.log("{} == [] ->", {} == [], '(两边都是对象时比引用,永远 false)');
console.log("NaN == NaN ->", NaN == NaN, '(判 NaN 用 Number.isNaN)');
console.log('null > 0 / null < 0 / null >= 0 ->', [null > 0, null < 0, null >= 0]);
1
2
3
4
5
6
7
8
9
10
0 == '0'      -> true / === -> false
1 == true -> true / === -> false
0 == false -> true / === -> false
'' == false -> true / === -> false
null == undefined -> true / === -> false
undefined == 0 -> false (和 0 不等,别拿 ==0 判空)
[] == '' -> true / [] == 0 -> true / [1] == 1 -> true
{} == [] -> false (两边都是对象时比引用,永远 false)
NaN == NaN -> false (判 NaN 用 Number.isNaN)
null > 0 / null < 0 / null >= 0 -> [ false, false, true ]
  • 结论:除了判「null 或 undefined」这一种情况,一律用 ===。
  • 判空值的三种写法对比:
1
2
3
4
5
6
7
// 实际写法建议:判空用显式比较,判假值用 Boolean 语义
const inputs = [0, '', null, undefined, false, NaN, [], '0'];
const row = f => inputs.map(f).join(', ');
console.log('输入顺序 :', row(x => typeof x === 'object' && x === null ? 'null' : typeof x === 'undefined' ? 'undef' : String(x)));
console.log('x == null 命中:', row(x => x == null), ' <- 只有 null / undefined');
console.log('x === 0 命中:', row(x => x === 0));
console.log('if (x) 为真 :', row(x => Boolean(x)));
1
2
3
4
输入顺序      : 0, , null, undef, false, NaN, , 0
x == null 命中: false, false, true, true, false, false, false, false <- 只有 null / undefined
x === 0 命中: true, false, false, false, false, false, false, false
if (x) 为真 : false, false, false, false, false, false, true, true
  • 想避免 == 又想让 NaN 自反、+0 与 -0 有别,用 Object.is(NaN, NaN) // true、Object.is(0, -0) // false。

可选链 ?. 与空值合并 ??

1
2
3
4
5
6
7
8
9
10
// 三目、短路、空值合并
const score = 85;
console.log('三目:', score >= 90 ? '优秀' : score >= 70 ? '良好' : '需要努力');
console.log('&& 短路: 返回第一个假值,否则返回最后一个:', 1 && 'a' && 0, '|', 1 && 'a');
console.log('|| 短路: 返回第一个真值:', 0 || '' || 'D');
console.log('?? 只挡 null/undefined:', [null ?? 'D', undefined ?? 'D', 0 ?? 'D', '' ?? 'D', false ?? 'D']);
console.log('|| 会把 0/\'\'/false 也替换掉:', [0 || 'D', '' || 'D', false || 'D']);
const cfg = { db: { port: 5432 } };
console.log('可选链 ?.:', cfg.db?.port, cfg.db?.host?.name, cfg?.missing?.deep);
console.log('?. 也能用于数组下标/深层缺失:', cfg.db?.['port'], cfg.missing?.deep?.deeper);
1
2
3
4
5
6
7
三目: 良好
&& 短路: 返回第一个假值,否则返回最后一个: 0 | a
|| 短路: 返回第一个真值: D
?? 只挡 null/undefined: [ 'D', 'D', 0, '', false ]
|| 会把 0/''/false 也替换掉: [ 'D', 'D', 'D' ]
可选链 ?.: 5432 undefined undefined
?. 也能用于数组下标/深层缺失: 5432 undefined
  • ?? 只把 null / undefined 当「空」,|| 把所有假值都当「空」——给默认值时想用 ??,否则 0、'' 会被吃掉。

对象与数组

对象的键与遍历

1
2
3
4
5
6
7
8
9
// 对象的键只能是字符串或 Symbol;数字字面量键会被转成字符串并排在最前
const key = 'dyn';
const o = { 1000: '数字键', [key]: '计算属性', short: 2, '带空格': 3 };
console.log('键的顺序:', Object.keys(o));
console.log('Object.values:', Object.values(o));
console.log('Object.entries:', Object.entries(o));
console.log("o['1000'] 与 o[1000] 是同一个:", o['1000'] === o[1000]);
console.log('Symbol 键不会被 Object.keys 看到:', Object.keys({ [Symbol('s')]: 1 }).length);
console.log('但能被这个 API 看到:', Object.getOwnPropertySymbols({ [Symbol('s')]: 1 }).length);
1
2
3
4
5
6
键的顺序: [ '1000', 'dyn', 'short', '带空格' ]
Object.values: [ '数字键', '计算属性', 2, 3 ]
Object.entries: [ [ '1000', '数字键' ], [ 'dyn', '计算属性' ], [ 'short', 2 ], [ '带空格', 3 ] ]
o['1000'] 与 o[1000] 是同一个: true
Symbol 键不会被 Object.keys 看到: 0
但能被这个 API 看到: 1
  • 键的顺序规则:整数键升序在前 → 其余字符串键按插入顺序 → Symbol 键按插入顺序。

解构与展开

1
2
3
4
5
6
7
8
9
10
11
12
13
// 解构与展开(...):从数组/对象里按位置或名字取值
const [first, second, ...restArr] = [1, 2, 3, 4];
const { short, missing = '默认值', ...restObj } = { short: 2, other: 3, more: 4 };
console.log('数组解构:', first, second, restArr);
console.log('对象解构:', short, missing, restObj);
const src = { a: 1, nested: { b: 2 } };
const copy = { ...src, a: 99 }; // 展开是浅拷贝
copy.nested.b = 999;
console.log('改副本的嵌套属性会影响原对象:', src.nested.b);
console.log('浅拷贝只复制一层:', copy.a, src.a);
const arr = [1, 2];
const arrCopy = [...arr, 3];
console.log('数组展开:', arr, arrCopy);
1
2
3
4
5
数组解构: 1 2 [ 3, 4 ]
对象解构: 2 默认值 { other: 3, more: 4 }
改副本的嵌套属性会影响原对象: 999
浅拷贝只复制一层: 99 1
数组展开: [ 1, 2 ] [ 1, 2, 3 ]

数组增删

1
2
3
4
5
6
7
8
9
10
11
// 增删:push/pop 在尾部,unshift/shift 在头部,splice 按索引增删改
const a = [1];
console.log('push 返回新长度:', a.push(2, 3), a);
console.log('pop 返回被删元素:', a.pop(), a);
console.log('unshift 返回新长度:', a.unshift(0, -1), a);
console.log('shift 返回被删元素:', a.shift(), a);
const b = [1, 2, 3, 4];
console.log('splice(1, 2, "a") 返回被删掉的:', b.splice(1, 2, 'a'), '=> 原数组', b);
console.log('slice(1, 3) 不改原数组,返回副本:', b.slice(1, 3), '=> 原数组', b);
console.log('slice 负索引:', b.slice(-2));
console.log('concat / join:', [1].concat([2, 3]), [1, 2, 3].join('-'), '| split 反过来:', '1-2-3'.split('-'));
1
2
3
4
5
6
7
8
push 返回新长度: 3 [ 1, 2, 3 ]
pop 返回被删元素: 3 [ 1, 2 ]
unshift 返回新长度: 4 [ 0, -1, 1, 2 ]
shift 返回被删元素: 0 [ -1, 1, 2 ]
splice(1, 2, "a") 返回被删掉的: [ 2, 3 ] => 原数组 [ 1, 'a', 4 ]
slice(1, 3) 不改原数组,返回副本: [ 'a', 4 ] => 原数组 [ 1, 'a', 4 ]
slice 负索引: [ 'a', 4 ]
concat / join: [ 1, 2, 3 ] 1-2-3 | split 反过来: [ '1', '2', '3' ]

数组遍历与计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 遍历类方法:map/filter/reduce 生成新值,forEach/find/some/every 只做判断
const nums = [1, 2, 3, 4];
console.log('map :', nums.map(x => x * 2));
console.log('filter:', nums.filter(x => x % 2 === 0));
console.log('reduce:', nums.reduce((sum, x) => sum + x, 0));
console.log('forEach 返回 undefined:', nums.forEach(x => x * 2));
console.log('find / findIndex:', nums.find(x => x > 2), nums.findIndex(x => x > 2));
console.log('some / every :', nums.some(x => x > 3), nums.every(x => x > 0));
console.log('includes / indexOf:', nums.includes(3), nums.indexOf(99), '| at(-1):', nums.at(-1));
try {
[].reduce((s, x) => s + x); // 空数组没有初始值会炸
} catch (e) {
console.log('空数组 reduce 不给初始值 ->', e.constructor.name);
}
console.log('reduce 拼对象:', nums.reduce((acc, x) => ({ ...acc, ['k' + x]: x }), {}));
1
2
3
4
5
6
7
8
9
map   : [ 2, 4, 6, 8 ]
filter: [ 2, 4 ]
reduce: 10
forEach 返回 undefined: undefined
find / findIndex: 3 2
some / every : true true
includes / indexOf: true -1 | at(-1): 4
空数组 reduce 不给初始值 -> TypeError
reduce 拼对象: { k1: 1, k2: 2, k3: 3, k4: 4 }

排序与其它工具

1
2
3
4
5
6
7
8
9
10
11
// sort 默认按字符串排序,必须传比较函数;还有扁平化、构造、排序稳定性
console.log('不传比较函数:', [10, 9, 1].sort());
console.log('传比较函数 :', [10, 9, 1].sort((x, y) => x - y));
console.log('降序 :', [10, 9, 1].sort((x, y) => y - x));
console.log('按对象属性 :', [{ n: 'b' }, { n: 'a' }].sort((x, y) => x.n.localeCompare(y.n)).map(o => o.n));
console.log('sort 会改原数组:', (() => { const t = [2, 1]; t.sort(); return [t, [...t].sort().length]; })());
console.log('flat(Infinity):', [1, [2, [3, [4]]]].flat(Infinity));
console.log('Array.from :', Array.from('abc'), Array.from({ length: 3 }, (_, i) => i * i));
console.log('new Array(3) :', new Array(3), new Array(3).fill(0));
console.log('for..of 的 keys/entries:', [...[9, 8].keys()], [...[9, 8].entries()]);
console.log('数组的 Object.keys 是下标字符串:', Object.keys(['x', 'y']));
1
2
3
4
5
6
7
8
9
10
不传比较函数: [ 1, 10, 9 ]
传比较函数 : [ 1, 9, 10 ]
降序 : [ 10, 9, 1 ]
按对象属性 : [ 'a', 'b' ]
sort 会改原数组: [ [ 1, 2 ], 2 ]
flat(Infinity): [ 1, 2, 3, 4 ]
Array.from : [ 'a', 'b', 'c' ] [ 0, 1, 4 ]
new Array(3) : [ <3 empty items> ] [ 0, 0, 0 ]
for..of 的 keys/entries: [ 0, 1 ] [ [ 0, 9 ], [ 1, 8 ] ]
数组的 Object.keys 是下标字符串: [ '0', '1' ]
  • [10, 9, 1].sort() 得到 [1, 10, 9] 是新手最常踩的一个坑:不传比较函数就按字符串排序。

Object 上的实用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Object 上的常用静态方法
const base = { a: 1, b: 2 };
console.log('assign(浅合并):', Object.assign({}, base, { b: 20, c: 3 }));
console.log('entries -> 对象:', Object.fromEntries([['k1', 1], ['k2', 2]]));
console.log('hasOwn 与 in :', [Object.hasOwn(base, 'a'), 'toString' in base, Object.hasOwn(base, 'toString')]);
const frozen = Object.freeze({ x: 1 });
frozen.x = 2; // 普通脚本(宽松模式)里,写入静默失败
console.log('宽松模式写冻结对象: 不报错,但值没变 ->', frozen.x, '| isFrozen:', Object.isFrozen(frozen));
(function () {
'use strict'; // 严格模式(模块、class 内部默认就是严格)会直接抛错
try {
frozen.x = 3;
} catch (e) {
console.log('严格模式写冻结对象 ->', e.constructor.name);
}
})();
const sealed = Object.seal({ y: 1 });
sealed.y = 2;
sealed.z = 3;
console.log('seal 能改属性但不能加新属性:', sealed.y, 'z' in sealed);
console.log('keys 数量 / 判空:', Object.keys({}).length, JSON.stringify({}) === '{}');
1
2
3
4
5
6
7
assign(浅合并): { a: 1, b: 20, c: 3 }
entries -> 对象: { k1: 1, k2: 2 }
hasOwn 与 in : [ true, true, false ]
宽松模式写冻结对象: 不报错,但值没变 -> 1 | isFrozen: true
严格模式写冻结对象 -> TypeError
seal 能改属性但不能加新属性: 2 false
keys 数量 / 判空: 0 true

for..in 与 for..of:一个拿键,一个拿值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// for..in 遍历「键」(含原型链上的可枚举键),for..of 遍历「值」(要求可迭代)
const arr = ['x', 'y'];
const keys = [];
for (const k in arr) {
keys.push(k + ':' + typeof k);
}
console.log('for..in 数组拿到的是字符串下标:', keys);
const values = [];
for (const v of arr) {
values.push(v);
}
console.log('for..of 数组拿到值 :', values);
console.log('for..of 字符串(按码点):', [...('中a')].length, (() => { const c = []; for (const ch of '中a') c.push(ch); return c; })());
console.log('for..of 普通对象 ->', (() => { try { for (const v of { a: 1 }) { /* 普通对象不可迭代 */ } return 'no error'; } catch (e) { return e.constructor.name + ': ' + e.message; } })());
const mapOut = [];
for (const kv of new Map([[1, 'a'], [2, 'b']])) {
mapOut.push(kv.join('='));
}
console.log('Map/Set/字符串/数组都可迭代:', mapOut, [...new Set([1, 2, 2, 3])]);
1
2
3
4
5
for..in 数组拿到的是字符串下标: [ '0:string', '1:string' ]
for..of 数组拿到值 : [ 'x', 'y' ]
for..of 字符串(按码点): 2 [ '中', 'a' ]
for..of 普通对象 -> TypeError: {(intermediate value)} is not iterable
Map/Set/字符串/数组都可迭代: [ '1=a', '2=b' ] [ 1, 2, 3 ]
  • 遍历对象用 Object.keys(obj) / Object.entries(obj) 配 for..of,别用 for..in(它会连原型链上的可枚举属性一起遍历)。
  • 需要「可中断 + 惰性」时,数组的 for..of 支持 break,而 forEach 不行。

Map / Set / Symbol

1
2
3
4
5
6
7
8
9
10
11
12
13
// Map:任何类型都能当键;Set:自动去重。普通对象只能拿字符串/Symbol 当键
const m = new Map();
m.set(1, '数字键 1').set('1', '字符串键 1').set({}, '对象键');
console.log('size / 取值:', m.size, m.get(1), m.get('1'));
console.log('has / delete:', m.has(1), m.delete(1), m.size);
console.log('遍历:', [...m.keys()], [...m.values()], [...m]);
console.log('Map -> 对象:', Object.fromEntries(m));
const s = new Set([1, 2, 2, 3, '3']);
console.log('Set 去重(含不同类型):', [...s], s.size, s.has(3), s.has('3'));
console.log('用 Set 数组去重:', [...new Set([1, 1, 2, 3, 3])]);
const sym = Symbol('id');
console.log('Symbol 每次都是新值:', Symbol('a') === Symbol('a'), '| 当键可与同名字符串键共存:',
(() => { const o = { [sym]: 'symbol 值', id: '字符串值' }; return [o[sym], o.id]; })());
1
2
3
4
5
6
7
size / 取值: 3 数字键 1 字符串键 1
has / delete: true true 2
遍历: [ '1', {} ] [ '字符串键 1', '对象键' ] [ [ '1', '字符串键 1' ], [ {}, '对象键' ] ]
Map -> 对象: { '1': '字符串键 1', '[object Object]': '对象键' }
Set 去重(含不同类型): [ 1, 2, 3, '3' ] 4 true true
用 Set 数组去重: [ 1, 2, 3 ]
Symbol 每次都是新值: false | 当键可与同名字符串键共存: [ 'symbol 值', '字符串值' ]
  • 什么时候不用普通对象而用 Map:键需要是对象/数字等非字符串、需要频繁增删与遍历、需要可靠的 size。键的这一点差别最实用:
1
2
3
4
5
6
7
8
9
10
11
// 普通对象的键会被字符串化,Map 的键不会——这是两者最实用的区别
const objKey = {};
const plain = {};
plain[1] = '数字键';
plain[objKey] = '对象键';
console.log('普通对象里 1 和 "1" 是同一个键:', Object.keys(plain));
const m = new Map();
m.set(1, '数字 1').set('1', '字符串 1').set(objKey, '对象引用');
console.log('Map 里三种键互不相干:', [...m.keys()].map(k => typeof k + ':' + String(k).slice(0, 12)));
console.log('取值分别对应:', m.get(1), m.get('1'), m.get(objKey));
console.log('用对象当键做计数(WeakMap 还能自动回收):', (() => { const wm = new WeakMap(); const o = {}; wm.set(o, 1); return wm.get(o); })());
1
2
3
4
普通对象里 1 和 "1" 是同一个键: [ '1', '[object Object]' ]
Map 里三种键互不相干: [ 'number:1', 'string:1', 'object:[object Obje' ]
取值分别对应: 数字 1 字符串 1 对象引用
用对象当键做计数(WeakMap 还能自动回收): 1

函数

四种写法与参数语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 声明方式:函数声明、函数表达式、箭头函数、立即执行
function decl(x) { return x + 1; }
const expr = function (x) { return x + 2; };
const arrow = (x) => x + 3;
const implicit = x => x * x;
console.log('四种写法:', decl(1), expr(1), arrow(1), implicit(4));
console.log('箭头函数简洁返回对象要加括号:', (x => ({ id: x }))(7));
console.log('IIFE:', (() => '立即执行了')());
console.log('函数是一等公民,能当参数传:', [1, 2, 3].map(decl), typeof decl);
function withDefaults(name = 'World', greet = 'Hello') {
return greet + ', ' + name + '!';
}
console.log('默认参数:', withDefaults(), '|', withDefaults('Dave'), '|', withDefaults(undefined, 'Hi'), '| null 不走默认值:', withDefaults(null));
function sumAll(...nums) {
return nums.reduce((s, x) => s + x, 0);
}
console.log('rest 参数:', sumAll(1, 2, 3, 4), '| 展开传参:', sumAll(...[5, 6, 7]));
console.log('函数的 length 只数到第一个默认值前:', (function (a, b, c = 1) {}).length);
1
2
3
4
5
6
7
四种写法: 2 3 4 16
箭头函数简洁返回对象要加括号: { id: 7 }
IIFE: 立即执行了
函数是一等公民,能当参数传: [ 2, 3, 4 ] function
默认参数: Hello, World! | Hello, Dave! | Hi, World! | null 不走默认值: Hello, null!
rest 参数: 10 | 展开传参: 18
函数的 length 只数到第一个默认值前: 2

this 由「怎么调用」决定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// this 由「怎么调用」决定,与定义在哪无关
function who() { return this === globalThis ? '全局对象' : (this && this.name) || String(this); }
const user = { name: 'Alice', who: who };
console.log('1. 普通调用 :', who());
console.log('2. 作为方法调用 :', user.who());
const detached = user.who;
console.log('3. 摘下来直接调用 :', detached());
console.log('4. call 指定 :', who.call({ name: 'by-call' }));
console.log('5. apply 指定 :', who.apply({ name: 'by-apply' }));
const bound = who.bind({ name: 'by-bind' });
console.log('6. bind 固定 :', bound(), '| 再 call 也改不动:', bound.call({ name: 'x' }));
function Ctor(name) { this.name = name; }
console.log('7. new 构造 :', new Ctor('by-new').name);
const cb = { name: 'outer', run: (f) => f() };
console.log('8. 回调里丢 this :', cb.run(who), '| 包一层箭头/绑定后:', cb.run(() => who.call(cb)));
1
2
3
4
5
6
7
8
1. 普通调用        : 全局对象
2. 作为方法调用 : Alice
3. 摘下来直接调用 : 全局对象
4. call 指定 : by-call
5. apply 指定 : by-apply
6. bind 固定 : by-bind | 再 call 也改不动: by-bind
7. new 构造 : by-new
8. 回调里丢 this : 全局对象 | 包一层箭头/绑定后: outer
  • this 的速记:
    • 点号左边是谁,this 就是谁。
    • 没有点号(普通调用):宽松模式下是全局对象,严格模式下是 undefined。
    • new 出来的是新实例。
    • call / apply / bind 由你手动指定。
  • 箭头函数没有自己的 this,它取定义时外层作用域的 this——这正是它在回调里受欢迎的原因(回调函数 = 你把它交给别人,由别人在合适的时机反过来调用你这个函数):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 箭头函数没有自己的 this / arguments / prototype,这是它与普通函数的核心差别
const timer = {
label: 'timer',
normal() {
return function () { return this === undefined ? 'undefined(严格)' : (this && this.label) || 'this 丢了'; };
},
arrow() {
return () => this.label;
}
};
console.log('普通函数被当回调丢出去:', timer.normal()());
console.log('箭头函数沿用定义处的 this:', timer.arrow()());
try {
(function () { })();
const arrowNoArgs = () => arguments.length;
console.log(arrowNoArgs());
} catch (e) {
console.log('箭头函数里没有 arguments ->', e.constructor.name);
}
console.log('箭头函数不能 new:', (() => { try { new (() => { })(); return 'ok'; } catch (e) { return e.constructor.name; } })());
1
2
3
4
普通函数被当回调丢出去: this 丢了
箭头函数沿用定义处的 this: timer
5
箭头函数不能 new: TypeError

闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 闭包:内层函数记住了外层作用域里的变量,外层函数返回后变量依然活着
function counter() {
let n = 0; // 外部拿不到 n,只能通过返回的函数操作
return {
inc: () => ++n,
get: () => n
};
}
const c1 = counter();
const c2 = counter();
c1.inc();
c1.inc();
c2.inc();
console.log('两个实例各自独立:', c1.get(), c2.get());
console.log('n 本身不在返回对象上:', Object.keys(c1));
// 闭包常用于「记住状态」:这里做一个带缓存的函数
function memoize(fn) {
const cache = new Map(); // 外层变量被内层函数长期持有
function wrapped(x) {
if (cache.has(x)) {
return cache.get(x) + ' (命中缓存)';
}
const v = fn(x);
cache.set(x, v);
return v;
}
wrapped.size = () => cache.size;
return wrapped;
}
const slowSquare = memoize(x => x * x);
console.log('第一次计算:', slowSquare(9), '| 第二次:', slowSquare(9), '| 缓存条目:', slowSquare.size());
1
2
3
两个实例各自独立: 2 1
n 本身不在返回对象上: [ 'inc', 'get' ]
第一次计算: 81 | 第二次: 81 (命中缓存) | 缓存条目: 1
  • 闭包 = 函数 + 它出生时所在的作用域。用途:私有状态、函数工厂、缓存(memoize)、防抖节流。
  • 副作用是要当心内存占用:被闭包引用的大对象不会被回收。

生成器:能暂停的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 生成器:function* + yield,函数可以「暂停」并分次产出值(异步语法的基础)
function* countdown(from) {
while (from > 0) {
yield from--;
}
return 'go';
}
const it = countdown(3);
console.log('手动 next:', JSON.stringify(it.next()), JSON.stringify(it.next()));
console.log('展开会自动跑到 done:', [...countdown(3)]);
for (const v of countdown(2)) {
console.log('for..of 消费:', v);
}
function* mapped() {
const a = yield 1; // next(x) 传进来的值成为 yield 表达式的结果
yield a * 2;
}
const m = mapped();
m.next();
console.log('next 的入参送进生成器:', JSON.stringify(m.next(10)));
1
2
3
4
5
手动 next: {"value":3,"done":false} {"value":2,"done":false}
展开会自动跑到 done: [ 3, 2, 1 ]
for..of 消费: 2
for..of 消费: 1
next 的入参送进生成器: {"value":20,"done":false}
  • function* + yield produce 一个迭代器;async/await 在机制上就是生成器 + Promise 的封装。日常业务代码很少直接写生成器,但读库源码时会遇到。

类与继承(class 是语法糖)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// class 是构造函数 + 原型链的语法糖;字段、方法、继承
class Animal {
#secret = '私有字段'; // # 开头是真私有,外部访问直接语法错误
constructor(name) {
this.name = name;
}
speak() {
return this.name + ' 发出声音';
}
static create(name) {
return new Animal(name);
}
get label() {
return '[动物] ' + this.name;
}
}
class Dog extends Animal {
speak() {
return super.speak() + '(汪汪)';
}
}
const dog = new Dog('旺财');
console.log(dog.speak(), '|', dog.label, '|', Animal.create('匿名').speak());
console.log('继承关系:', dog instanceof Dog, dog instanceof Animal, Object.getPrototypeOf(dog) === Dog.prototype);
console.log('方法挂在原型上,不在实例上:', Object.keys(dog), typeof dog.speak);
console.log('私有字段 # 开头,外部访问在「解析阶段」就报语法错误:', (() => { try { new Function('return dog.#secret'); return 'no'; } catch (e) { return e.constructor.name + ': ' + e.message; } })());
1
2
3
4
旺财 发出声音(汪汪) | [动物] 旺财 | 匿名 发出声音
继承关系: true true true
方法挂在原型上,不在实例上: [ 'name' ] function
私有字段 # 开头,外部访问在「解析阶段」就报语法错误: SyntaxError: Private field '#secret' must be declared in an enclosing class
  • class X extends Y 里用 super() 调父类构造、super.method() 调父类方法;子类构造器里必须在使用 this 之前调用 super()。
  • #name 是真私有(编译期检查),_name 只是命名约定,谁都拦不住。
  • 静态方法属于类本身(Animal.create),get/set 定义访问器属性。
  • 与 Python 对照:JS 的 this 必须显式写在参数里(Python 的 self 是隐式的),这也是 this 会「丢」的根源。

按位运算(偶尔会用到)

1
2
3
4
5
6
7
8
9
// 按位运算:先把数字转成 32 位整数再操作,日常只在掩码/取整/权限位里遇到
console.log('5 & 1 =', 5 & 1, '| 9 & 5 =', 9 & 5);
console.log('5 | 2 =', 5 | 2, '| 5 ^ 1 =', 5 ^ 1);
console.log('~5 =', ~5, '(等于 -5-1,取反再取补)');
console.log('5 << 1 =', 5 << 1, '| 5 >> 1 =', 5 >> 1, '| -5 >> 1 =', -5 >> 1, '(算术右移,保留符号)');
console.log('-5 >>> 1 =', -5 >>> 1, '(无符号右移,把 -5 当 32 位无符号数)');
console.log('>>>0 可以把数字强转成 32 位整数:', (-1 >>> 0), (2 ** 32 + 7 >>> 0));
console.log('按位与取偶偶判断:', [0, 1, 2, 3].map(x => x & 1));
console.log('移位等价于乘除 2 的幂:', 3 << 4, 3 * 2 ** 4, '| 100 >> 2', Math.floor(100 / 4));
1
2
3
4
5
6
7
8
5 & 1 = 1 | 9 & 5 = 1
5 | 2 = 7 | 5 ^ 1 = 4
~5 = -6 (等于 -5-1,取反再取补)
5 << 1 = 10 | 5 >> 1 = 2 | -5 >> 1 = -3 (算术右移,保留符号)
-5 >>> 1 = 2147483645 (无符号右移,把 -5 当 32 位无符号数)
>>>0 可以把数字强转成 32 位整数: 4294967295 7
按位与取偶偶判断: [ 0, 1, 0, 1 ]
移位等价于乘除 2 的幂: 48 48 | 100 >> 2 25

严格模式:让错误尽早暴露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 严格模式:'use strict'(模块、class 内部默认开启),把一批静默错误变成显式报错
console.log('宽松模式下未声明就赋值 -> 变成全局变量:', (() => { sloppyGlobal = 1; return typeof sloppyGlobal; })());
delete globalThis.sloppyGlobal;
console.log('严格模式下同样写法:', (() => {
'use strict';
try {
strictGlobal = 1;
return 'no error';
} catch (e) {
return e.constructor.name + ': ' + e.message;
}
})());
console.log('八进制字面量 010 —— 宽松:', new Function('return 010')(), '| 严格:', (() => { try { new Function('"use strict"; return 010')(); return 'no error'; } catch (e) { return e.constructor.name; } })());
console.log('重复形参 (a, a) —— 宽松:', new Function('return (function (a, a) { return a; })(1, 2)')(), '| 严格:', (() => { try { new Function('"use strict"; return 1')(); new Function('"use strict"; function f(a, a) { }'); return 'no error'; } catch (e) { return e.constructor.name; } })());
console.log('with 语句在严格模式:', (() => { try { new Function('"use strict"; with ({}) { }')(); return 'no error'; } catch (e) { return e.constructor.name; } })());
console.log('普通函数里的 this —— 宽松:', new Function('return (function () { return this === globalThis ? "全局对象" : typeof this; })()')(), '| 严格:', (function () { 'use strict'; return this === undefined; })());
1
2
3
4
5
6
宽松模式下未声明就赋值 -> 变成全局变量: number
严格模式下同样写法: ReferenceError: strictGlobal is not defined
八进制字面量 010 —— 宽松: 8 | 严格: SyntaxError
重复形参 (a, a) —— 宽松: 2 | 严格: SyntaxError
with 语句在严格模式: SyntaxError
普通函数里的 this —— 宽松: 全局对象 | 严格: true
  • <script> 里第一行写 'use strict';,或者干脆用 type="module"(默认严格)。
  • ES 模块、class 的类体内部天然是严格模式。

流程控制

条件与分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 自动分号插入 + 缩进错觉:第二行并不属于 if
const score = 85;
if (score >= 90) {
console.log('优秀');
} else if (score >= 70) {
console.log('良好');
} else {
console.log('需要努力');
}

function check(x) {
if (x > 0)
console.log(' 正数');
console.log(' 这行总会执行(它不在 if 里)');
}
check(1);
check(-1);
1
2
3
4
良好
正数
这行总会执行(它不在 if 里)
这行总会执行(它不在 if 里)
  • JS 有「自动分号插入」(ASI),靠换行推断语句边界会出错,所以下一行的开头若是 (、[、` 时,上一行必须显式写分号。
  • 多行语句一律写大括号,别学「单行 if 省略 {}」。

switch:注意穿透

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// switch 用 === 比较;不写 break 会「穿透」到下一个 case 继续执行
function describe(n) {
let out = '';
switch (n) {
case 1:
out += '一'; // 故意不写 break,演示穿透
case 2:
out += '二';
break;
default:
out = '其它';
}
return out;
}
console.log('穿透结果 describe(1):', describe(1), '| describe(2):', describe(2), '| describe(9):', describe(9));
console.log('switch 是严格比较,"1" 不会匹配 1:', (() => { switch ('1') { case 1: return 'num'; default: return '不匹配'; } })());
1
2
穿透结果 describe(1): 一二 | describe(2): 二 | describe(9): 其它
switch 是严格比较,"1" 不会匹配 1: 不匹配
  • 每个 case 结尾要么 break/return/throw,要么明确写注释说明「故意穿透」。
  • switch 内部是 === 比较,不能用它做范围判断。

循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// while / do...while / for / continue / break / 标签
let i = 0;
while (i < 3) {
i++;
}
console.log('while 结束 i =', i);

let j = 0;
do {
j++;
} while (j < 3);
console.log('do...while 结束 j =', j);

for (let k = 0; k < 4; k++) {
if (k === 1) {
continue; // 跳过这一轮
}
if (k === 3) {
break; // 整个退出
}
console.log('for 里 k =', k);
}

outer: for (const row of [[1, 2], [3, 4]]) {
for (const v of row) {
if (v === 3) {
break outer; // 连外层一起跳出
}
}
}
console.log('标签跳出后到达这里');
1
2
3
4
5
while 结束 i = 3
do...while 结束 j = 3
for 里 k = 0
for 里 k = 2
标签跳出后到达这里
  • forEach 里不能用 break/continue;要中断请用 for..of 或普通 for,判断存在性用 some(返回 true 就停)。
  • 遍历数组下标时优先 for (let i = 0; ...)。
  • 只关心元素就用 for..of。
  • 需要索引和元素用 entries()。

三元与短路

上面「可选链 ?. 与空值合并 ??」一节里的实测输出就是三目与短路运算符的结果:a ? b : c 只是一个「有返回值的 if」,别把复杂逻辑塞进去写成一行天书。嵌套超过两层就改回 if/else。

异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// try/catch/finally 与 throw;catch 的参数可以省略
function safeDiv(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('参数必须是数字'); // 抛「错误对象」比抛字符串好:有 name/message/stack
}
if (b === 0) {
throw new RangeError('除数不能为 0');
}
return a / b;
}
const cases = [[10, 2], [10, 0], ['10', 2]];
for (const [a, b] of cases) {
try {
console.log('结果:', safeDiv(a, b));
} catch (e) {
console.log('捕获:', e.constructor.name + ' -> ' + e.message, '| instanceof Error:', e instanceof Error);
} finally {
console.log('finally 总会执行');
}
}
try {
let x = 2;
let y = x + z; // z 未声明
} catch (e) {
console.log('运行时错误:', e.constructor.name + ': ' + e.message);
}
try {
throw '直接抛字符串也是合法的,但不推荐';
} catch (e) {
console.log('抛字符串:', e, '| typeof =', typeof e);
}
try {
null.x;
} catch {
console.log('catch 可以省略参数(ES2019)');
}
1
2
3
4
5
6
7
8
9
结果: 5
finally 总会执行
捕获: RangeError -> 除数不能为 0 | instanceof Error: true
finally 总会执行
捕获: TypeError -> 参数必须是数字 | instanceof Error: true
finally 总会执行
运行时错误: ReferenceError: z is not defined
抛字符串: 直接抛字符串也是合法的,但不推荐 | typeof = string
catch 可以省略参数(ES2019)
  • 抛 Error 的子类,不要抛字符串或数字:Error 有 name/message/stack,能被 instanceof Error 判断,也能被框架统一捕获。
  • 内置错误类型(够用就好):Error、TypeError(类型/调用方式不对)、ReferenceError(变量不存在)、SyntaxError(解析阶段就挂)、RangeError(值超出范围)、EvalError、URIError、AggregateError(多个错误打包,配合 Promise.any)。
  • 只读属性赋值、null.x、把非函数当函数调用 → TypeError;访问未声明的变量 → ReferenceError。
  • finally 里如果 return 或 throw,会吞掉 try/catch 里的返回值/异常,别这么写。
  • 全局兜底(浏览器):window.addEventListener('error', ...) 抓同步报错,unhandledrejection 抓没接住的 Promise 失败。Node 里是 process.on('uncaughtException') / 'unhandledRejection'。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 自定义错误:继承 Error,附带一个可供上层判断的 code
class AppError extends Error {
constructor(message, code) {
super(message);
this.name = 'AppError';
this.code = code;
}
}
function withdraw(balance, amount) {
if (amount > balance) {
throw new AppError('余额不足', 'NOT_ENOUGH');
}
return balance - amount;
}
try {
withdraw(100, 200);
} catch (e) {
if (e instanceof AppError) {
console.log('业务错误:', e.name, '| code =', e.code, '|', e.message);
} else {
console.log('意外错误:', e);
}
}
console.log('错误对象的特征: 有 stack 吗 ->', typeof new AppError('x', 'Y').stack, '| instanceof Error ->', new AppError('x', 'Y') instanceof Error);
1
2
业务错误: AppError | code = NOT_ENOUGH | 余额不足
错误对象的特征: 有 stack 吗 -> string | instanceof Error -> true

异步:事件循环、Promise、async/await

先看清执行顺序

1
2
3
4
5
6
// 事件循环:同步代码全部跑完,再跑微任务(Promise/queueMicrotask),最后才轮到定时器
console.log('同步开始');
setTimeout(() => console.log('宏任务 setTimeout 0'), 0);
queueMicrotask(() => console.log('微任务 queueMicrotask'));
Promise.resolve().then(() => console.log('微任务 promise.then'));
console.log('同步结束');
1
2
3
4
5
同步开始
同步结束
微任务 queueMicrotask
微任务 promise.then
宏任务 setTimeout 0
  • 先认识两个词:任务队列是浏览器把「等着执行的回调」排成的队;微任务(Promise.then 这类)优先级高,宏任务(setTimeout、事件回调这类)每取一个跑完都要回去清一遍微任务。模型:同步代码一口气跑完 → 清空微任务队列(Promise.then、queueMicrotask、MutationObserver)→ 取一个宏任务(setTimeout、事件回调、IO)执行 → 再清微任务 → 循环。
  • 所以「先注册的 setTimeout(0)」反而排在 Promise.then 后面。

定时器要能取消

1
2
3
4
5
6
7
8
9
10
11
// 定时器的返回值要存下来才能取消
let hits = 0;
const id = setTimeout(() => { hits++; console.log('不该被执行'); }, 10);
clearTimeout(id);
const iv = setInterval(() => {
hits++;
if (hits === 3) {
clearInterval(iv);
}
}, 5);
console.log('取消 setTimeout 后 hits =', hits);
1
取消 setTimeout 后 hits = 0
  • setInterval 不会等上一次回调跑完,任务重时容易堆积;轮询场景更稳的写法是「在回调里再 setTimeout 一次」。
  • 浏览器里 setTimeout(fn) 的返回值是数字 id;Node 里返回对象——跨环境代码请用 clearTimeout 配对,别对返回值做算术。

Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Promise:把「传回调」变成「返回一个代表未来结果的对象」,可以链式接下去
console.log('1. 同步开始');
const p = new Promise((resolve) => {
setTimeout(() => resolve('异步算好了'), 20);
});
console.log('2. 立即拿到的是对象,不是值:', p instanceof Promise);
p.then(v => {
console.log('4. then 收到:', v);
return v + ' -> 返回值传给下一个 then';
}).then(v => {
console.log('5. 链式第二环:', v);
}).finally(() => {
console.log('6. finally 总会执行');
});
Promise.reject(new Error('一进来就失败')).catch(e => {
console.log('7. catch 捕获:', e.message);
});
console.log('3. 同步结束(上面的 4~7 都要等在这里之后)');
1
2
3
4
5
6
7
1. 同步开始
2. 立即拿到的是对象,不是值: true
3. 同步结束(上面的 4~7 都要等在这里之后)
7. catch 捕获: 一进来就失败
4. then 收到: 异步算好了
5. 链式第二环: 异步算好了 -> 返回值传给下一个 then
6. finally 总会执行
  • 三条纪律:
    1. then 的返回值会传给下一个 then(返回 Promise 就会被等待),return 是最容易漏的关键字;
    2. 错误沿链向后冒泡,最后必须有一个 catch,否则变成 unhandled rejection;
    3. new Promise 的执行体是同步的,resolve 之后的代码还会继续跑,必要时 return。

四个组合器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 四个组合器:all / allSettled / race / any
const delay = (ms, v) => new Promise(r => setTimeout(() => r(v), ms));
(async () => {
console.log('all 结果按「传入顺序」而不是完成顺序:', await Promise.all([delay(30, 'a'), delay(10, 'b')]));
const settled = await Promise.allSettled([delay(5, 'ok'), Promise.reject(new Error('bad'))]);
console.log('allSettled 不会被单个失败打断:', settled.map(s => s.status).join(', '));
console.log('race 取第一个 settles 的:', await Promise.race([delay(40, '慢'), delay(5, '快')]));
console.log('any 取第一个成功的 :', await Promise.any([Promise.reject(new Error('x')), delay(5, 'y')]));
try {
await Promise.all([Promise.reject(new Error('第 1 个失败')), new Promise(() => { })]);
} catch (e) {
console.log('all 有一个 reject 就整体 reject:', e.message);
}
})();
1
2
3
4
5
all        结果按「传入顺序」而不是完成顺序: [ 'a', 'b' ]
allSettled 不会被单个失败打断: fulfilled, rejected
race 取第一个 settles 的: 快
any 取第一个成功的 : y
all 有一个 reject 就整体 reject: 第 1 个失败
方法 等全部 有一个失败就 取最快 典型用途
Promise.all ✅ 立刻 reject — 并行请求,要么全成功
Promise.allSettled ✅ 不 reject,逐个报状态 — 批量任务,需要知道每个结果
Promise.race — 跟随最快的那个 ✅ 超时控制
Promise.any — 全失败才 reject(AggregateError) ✅ 第一个成功 多镜像/多源取其一

async/await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// async/await:用「写法像同步」的方式使用 Promise;await 只能在 async 函数里用
function getUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => (id === 1 ? resolve({ id: 1, name: 'Alice' }) : reject(new Error('没有这个用户'))), 10);
});
}
async function loadName(id) {
const user = await getUser(id); // 这里会「等」,但只挂起当前 async 函数
return user.name.toUpperCase();
}
(async () => {
console.log('成功路径:', await loadName(1));
try {
await loadName(2);
} catch (e) {
console.log('失败路径被 try/catch 接住:', e.message);
}
console.log('async 函数返回值永远是 Promise:', loadName(1) instanceof Promise);
console.log('未 catch 的 reject 会变成 [unhandledRejection] 警告,务必接住');
const all = await Promise.all([loadName(1), loadName(1), loadName(1)]);
console.log('并发等待比串行快:', all.join(','));
})();
1
2
3
4
5
成功路径: ALICE
失败路径被 try/catch 接住: 没有这个用户
async 函数返回值永远是 Promise: true
未 catch 的 reject 会变成 [unhandledRejection] 警告,务必接住
并发等待比串行快: ALICE,ALICE,ALICE
  • async 函数永远返回 Promise;await 只是「挂起这个函数」,不会阻塞其它代码,更不会卡住页面。
  • await 只能用在 async 函数内部(模块顶层的 await 是特例,浏览器模块与 Node ESM 都支持)。
  • 并行要用 Promise.all([...]) 再 await,不要在循环里逐个 await(那样就变成了串行)。

常用内置对象速查

String:不可变,所有方法返回新串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 字符串是不可变的:所有方法都返回新字符串,原串永远不变
const s = ' Hello, World ';
console.log('length:', s.length, '| trim 后:', JSON.stringify(s.trim()), '| 原串未变:', s.length === 16);
console.log("charAt(2):", JSON.stringify(s.charAt(2)), '| at(-3):', JSON.stringify(s.at(-3)), '| [2]:', JSON.stringify(s[2]));
console.log("indexOf('World'):", s.indexOf('World'), '| 找不到返回 -1:', s.indexOf('zzz'), '| lastIndexOf("l"):', s.lastIndexOf('l'));
console.log("includes(区分大小写):", s.includes('world'), '| 配合 toLowerCase:', s.toLowerCase().includes('world'));
console.log("slice(7,12):", JSON.stringify(s.slice(7, 12)), '| 负索引 slice(-5,-1):', JSON.stringify(s.slice(-5, -1)));
console.log('substring 会把负数当 0:', JSON.stringify('abc'.substring(-2, 1)), '| substr 已废弃:', JSON.stringify('abc'.substr(1, 2)));
console.log("split(','):", 'a,b,,c'.split(','), '| 限制次数:', 'a,b,c'.split(',', 2), '| split() 不传参:', 'abc'.split());
console.log("split(''):", 'abc'.split(''), '| [...str] 更推荐:', [...'abc']);
console.log("replace 只换第一个:", 'aaa'.replace('a', 'X'), '| replaceAll 全换:', 'aaa'.replaceAll('a', 'X'));
console.log('replace 配正则 + 回调:', 'a1b2'.replace(/\d/g, d => '<' + d + '>'));
console.log('padStart/padEnd/repeat:', '7'.padStart(3, '0'), '7'.padEnd(3, '-'), 'ab'.repeat(2));
console.log('startsWith/endsWith:', 'x.png'.startsWith('x'), 'x.png'.endsWith('.png'));
console.log('concat 与模板串:', 'a'.concat('b', 'c'), `${'n'}|${1 + 2}|${[1, 2].join()}`);
console.log('大小写:', JSON.stringify('abc'.toUpperCase()), JSON.stringify('Á'.toLowerCase()));
console.log('matchAll:', Array.from('a1b2'.matchAll(/[a-z]\d/g), m => m[0]));
console.log('normalize 全角转半角:', 'A'.normalize('NFKC'));
console.log('长度陷阱(代理对):', '中文a'.length, [...'中文a'].length, '😀'.length, [...'😀'].length);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
length: 16 | trim 后: "Hello, World" | 原串未变: true
charAt(2): "H" | at(-3): "d" | [2]: "H"
indexOf('World'): 9 | 找不到返回 -1: -1 | lastIndexOf("l"): 12
includes(区分大小写): false | 配合 toLowerCase: true
slice(7,12): ", Wor" | 负索引 slice(-5,-1): "rld "
substring 会把负数当 0: "a" | substr 已废弃: "bc"
split(','): [ 'a', 'b', '', 'c' ] | 限制次数: [ 'a', 'b' ] | split() 不传参: [ 'abc' ]
split(''): [ 'a', 'b', 'c' ] | [...str] 更推荐: [ 'a', 'b', 'c' ]
replace 只换第一个: Xaa | replaceAll 全换: XXX
replace 配正则 + 回调: a<1>b<2>
padStart/padEnd/repeat: 007 7-- abab
startsWith/endsWith: true true
concat 与模板串: abc n|3|1,2
大小写: "ABC" "á"
matchAll: [ 'a1', 'b2' ]
normalize 全角转半角: A
长度陷阱(代理对): 3 3 2 1
  • '😀'.length 是 2、[...str].length 是 1:length 数的是 UTF-16 码元。要正确处理 emoji / 生僻字就用展开或 Intl.Segmenter。
  • 拼接优先用模板字符串;substring / substr 是老 API,新代码统一用 slice。

数字转换:parseInt / parseFloat / Number / +

1
2
3
4
5
6
7
8
9
// 三个「字符串转数字」的工具,行为差别很大
console.log('parseInt 从头读,遇到非法字符就停:', [parseInt('42'), parseInt('42.9'), parseInt(' 42abc')]);
console.log('parseInt 认 0x 前缀,第二个参数是进制:', [parseInt('0x10'), parseInt('10', 2), parseInt('11', 8), parseInt('10', 36)]);
console.log('parseInt 读不出数字给 NaN:', [Number.isNaN(parseInt('abc')), Number.isNaN(parseInt(''))]);
console.log('parseFloat 只认一个小数点:', [parseFloat('3.14xyz'), parseFloat('0.1e2'), parseFloat('1.2.3')]);
console.log('Number 要么整个转成功要么 NaN:', [Number('42'), Number('42.5'), Number(' 12 '), Number(''), Number('1e3'), Number('abc')]);
console.log('一元 + 与 Number 等价(但别对 null 用):', [+'42', +'abc', +null]);
console.log('Number -> 字符串:', String(42), (42).toString(), (42).toString(16), (255).toString(2), (0.1).toPrecision(1), (1234.5678).toFixed(2));
console.log('toFixed 是四舍五入的字符串,不能再算:', typeof (1.5).toFixed(0), ((1.5).toFixed(0) * 2));
1
2
3
4
5
6
7
8
parseInt  从头读,遇到非法字符就停: [ 42, 42, 42 ]
parseInt 认 0x 前缀,第二个参数是进制: [ 16, 2, 9, 36 ]
parseInt 读不出数字给 NaN: [ true, true ]
parseFloat 只认一个小数点: [ 3.14, 10, 1.2 ]
Number 要么整个转成功要么 NaN: [ 42, 42.5, 12, 0, 1000, NaN ]
一元 + 与 Number 等价(但别对 null 用): [ 42, NaN, 0 ]
Number -> 字符串: 42 42 2a 11111111 0.1 1234.57
toFixed 是四舍五入的字符串,不能再算: string 4
  • parseInt 允许尾部有非法字符(parseInt('42abc') 得 42),Number 要求整串合法(否则 NaN)。读用户输入用 Number,或老老实实 parseInt(s, 10) 指明进制。
  • 字符串转数字的其它路子:Math.trunc(s)、s | 0(会截成 32 位)、BigInt(s)、Number(s).toFixed(n)。

Math

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Math 是对象,不是函数;所有取整函数的负数行为不一样
console.log('random 区间 [0,1):', Math.random() >= 0 && Math.random() < 1, '| 取 1~6 的整数:', (() => { const v = Math.floor(Math.random() * 6) + 1; return v >= 1 && v <= 6; })());
console.log('round 0.5->1, -0.5->0(向 +∞ 取整):', Math.round(0.5), Math.round(-0.5), Math.round(1.5), Math.round(-1.5));
console.log('floor 向下(更负):', Math.floor(1.7), Math.floor(-1.7));
console.log('ceil 向上(更正) :', Math.ceil(1.2), Math.ceil(-1.2));
console.log('trunc 截掉小数 :', Math.trunc(1.7), Math.trunc(-1.7));
console.log('abs/sign:', Math.abs(-3), Math.sign(-5), Math.sign(0));
console.log('max/min 要求展开或 apply:', Math.max(1, 9, 3), Math.max(...[1, 9, 3]), Math.min(1, 9, 3));
console.log('max 对空数组是 -Infinity:', Math.max([]), Math.max(...[]));
console.log('pow/.sqrt/cbrt/hypot:', 2 ** 10, Math.pow(2, 10), Math.sqrt(16), Math.cbrt(27), Math.hypot(3, 4));
console.log('常量:', Math.PI.toFixed(4), Math.E.toFixed(4), Number.MAX_VALUE.toExponential(2));
const picks = [];
for (let k = 0; k < 200; k++) {
picks.push([10, 20, 30][Math.floor(Math.random() * 3)]);
}
console.log('随机取一个元素(跑 200 次的取值集合):', [...new Set(picks)].sort((x, y) => x - y));
1
2
3
4
5
6
7
8
9
10
11
random 区间 [0,1): true | 取 1~6 的整数: true
round 0.5->1, -0.5->0(向 +∞ 取整): 1 -0 2 -1
floor 向下(更负): 1 -2
ceil 向上(更正) : 2 -1
trunc 截掉小数 : 1 -1
abs/sign: 3 -1 0
max/min 要求展开或 apply: 9 9 1
max 对空数组是 -Infinity: 0 -Infinity
pow/.sqrt/cbrt/hypot: 1024 1024 4 3 5
常量: 3.1416 2.7183 1.80e+308
随机取一个元素(跑 200 次的取值集合): [ 10, 20, 30 ]
  • Math.round(-0.5) 得 -0(向 +∞ 方向取整);要「四舍六入五成双」或精确十进制取整得自己实现。
  • 生成随机整数用 Math.floor(Math.random() * (max - min + 1)) + min;要密码级随机用 crypto.getRandomValues()。

JSON

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// JSON.stringify 会「丢掉」它无法表示的值
const o = { s: 'x', n: 1, b: true, nul: null, u: undefined, f() { }, n2: NaN, inf: Infinity, d: new Date(0) };
console.log('undefined/函数被删掉,NaN/Infinity 变 null,Date 变 ISO 字符串:', JSON.stringify(o));
console.log('数组里的 undefined 也要占位成 null:', JSON.stringify([1, undefined, 2]));
console.log('缩进两格:', JSON.stringify({ a: 1 }, null, 2).replace(/\n/g, '\\n'));
console.log('第二个参数可以是白名单数组:', JSON.stringify({ a: 1, b: 2, c: 3 }, ['a', 'c']));
console.log('也可以传函数改写:', JSON.stringify({ a: 1, b: 2 }, (k, v) => k === 'a' ? undefined : v));
console.log('对象有 toJSON 就优先用它:', JSON.stringify({ a: 1, toJSON() { return 'replaced'; } }));
console.log('中文默认不转义:', JSON.stringify({ 中: '文' }));
console.log('parse 的返回值可以是任意 JSON 类型:', [typeof JSON.parse('1'), typeof JSON.parse('"a"'), typeof JSON.parse('true'), typeof JSON.parse('null'), typeof JSON.parse('[1]')]);
try {
JSON.parse('{name:x}'); // 键必须加引号,也不许有注释
} catch (e) {
console.log('不是合法 JSON ->', e.constructor.name);
}
console.log('第三个参数 reviver 可以把字符串还原成 Date:', JSON.parse('{"d":"1970-01-01T00:00:00.000Z"}', (k, v) => k === 'd' ? new Date(v).getUTCFullYear() : v).d);
console.log('深拷贝的两种做法:', (() => { const src = { a: { b: 1 } }; const j = JSON.parse(JSON.stringify(src)); j.a.b = 9; return [src.a.b, j.a.b, typeof structuredClone]; })());
1
2
3
4
5
6
7
8
9
10
11
undefined/函数被删掉,NaN/Infinity 变 null,Date 变 ISO 字符串: {"s":"x","n":1,"b":true,"nul":null,"n2":null,"inf":null,"d":"1970-01-01T00:00:00.000Z"}
数组里的 undefined 也要占位成 null: [1,null,2]
缩进两格: {\n "a": 1\n}
第二个参数可以是白名单数组: {"a":1,"c":3}
也可以传函数改写: {"b":2}
对象有 toJSON 就优先用它: "replaced"
中文默认不转义: {"中":"文"}
parse 的返回值可以是任意 JSON 类型: [ 'number', 'string', 'boolean', 'object', 'object' ]
不是合法 JSON -> SyntaxError
第三个参数 reviver 可以把字符串还原成 Date: 1970
深拷贝的两种做法: [ 1, 9, 'function' ]

Date

1
2
3
4
5
6
7
8
9
10
11
12
13
// Date:内部存「本地时区无关」的毫秒时间戳,月份从 0 开始
const d = new Date('2026-11-02T12:34:56Z');
console.log('Date.now() 是数字时间戳:', typeof Date.now(), Date.now() > 1e12);
console.log('ISO 串与毫秒:', d.toISOString(), d.getTime());
console.log('月份从 0 开始(11 月是 10):', d.getUTCMonth(), '| 年/日/时:', d.getUTCFullYear(), d.getUTCDate(), d.getUTCHours());
console.log('new Date(2026, 10, 2) 按本地时区解释:', new Date(2026, 10, 2).getFullYear(), new Date(2026, 10, 2).getMonth());
console.log('ISO 风格 ' + "'2026-11-02'" + ' 按 UTC,斜杠风格按本地:', new Date('2026-11-02').toISOString());
console.log('两个日期相减得毫秒,除 864e5 换成天:', Math.round((new Date('2026-11-05') - new Date('2026-11-01')) / 864e5));
const t = new Date(2026, 0, 1);
t.setDate(t.getDate() + 40); // 超出月份会自动进位
console.log('1 月 1 日 + 40 天 ->', t.toISOString().slice(0, 10));
console.log('epoch:', new Date(0).toISOString(), '| 无效日期:', new Date('not-a-date').toString(), isNaN(new Date('x').getTime()));
console.log('本地化展示(跟系统区域走):', new Date(2026, 0, 31).toLocaleDateString('zh-CN'), new Date('2026-11-02T12:34:56Z').toLocaleTimeString('zh-CN', { timeZone: 'UTC' }));
1
2
3
4
5
6
7
8
9
Date.now() 是数字时间戳: number true
ISO 串与毫秒: 2026-11-02T12:34:56.000Z 1793622896000
月份从 0 开始(11 月是 10): 10 | 年/日/时: 2026 2 12
new Date(2026, 10, 2) 按本地时区解释: 2026 10
ISO 风格 '2026-11-02' 按 UTC,斜杠风格按本地: 2026-11-02T00:00:00.000Z
两个日期相减得毫秒,除 864e5 换成天: 4
1 月 1 日 + 40 天 -> 2026-02-09
epoch: 1970-01-01T00:00:00.000Z | 无效日期: Invalid Date true
本地化展示(跟系统区域走): 2026/1/31 12:34:56
  • 两个最容易翻车的点:月份从 0 开始;'2026-11-02' 这种 ISO 串按 UTC 解析、'2026/11/2' 按本地时区解析。跨时区业务建议全链路传 ISO 字符串(带 Z 或偏移),只在展示层转本地。
  • 日期格式化想稳定可控用 Intl.DateTimeFormat,或第三方 dayjs(Date 原生 API 设计得很差)。

定时器之外的浏览器 API(下一篇详述)

requestAnimationFrame(逐帧动画)、requestIdleCallback、IntersectionObserver(懒加载/曝光埋点)、MutationObserver(DOM 变化监听)、fetch(网络)、localStorage / sessionStorage / indexedDB(存储)、URL / URLSearchParams(解析地址)。

模块化

1
2
3
4
5
6
7
8
9
10
11
12
// 模块化:import/export。这里用动态 import 现场建一个模块再引入
const fs = require('fs');
const path = require('path');
fs.writeFileSync(path.join(__dirname, '_math_mod.mjs'), 'export function add(x, y) {\n return x + y;\n}\nexport default { name: "math" };\n');
(async () => {
const mod = await import('./_math_mod.mjs');
console.log('命名导出:', mod.add(2, 3));
console.log('默认导出:', mod.default.name);
console.log('整个模块对象:', Object.keys(mod));
fs.unlinkSync(path.join(__dirname, '_math_mod.mjs'));
console.log('严格性对比 —— 模块内 this 是 undefined:', (() => { 'use strict'; return typeof (function () { return this; }).call(undefined); })());
})();
1
2
3
4
命名导出: 5
默认导出: math
整个模块对象: [ 'add', 'default' ]
严格性对比 —— 模块内 this 是 undefined: undefined

浏览器里怎么写:

1
2
3
4
5
6
7
8
9
10
<!-- math.js -->
<!-- export function add(x, y) { return x + y; } -->
<!-- export default { name: 'math' }; -->

<script type="module">
import add from './math.js'; // 默认导入,名字随意
import { add as plus } from './math.js'; // 命名导入,必须对得上
import * as math from './math.js'; // 整个模块对象
console.log(math.add(2, 3), plus(2, 3));
</script>
  • type="module" 的脚本自动具备:严格模式、独立作用域(不污染 window)、defer 语义、CORS 要求(file:// 直接打开会失败,必须走 http)。
  • CommonJS(require/module.exports)是 Node 的历史格式,读写老项目会遇到;新项目里浏览器端一律 ES 模块,Node 端也推荐 .mjs / "type": "module"。

常见坑速查

  • 忘记写 let/const → 变量泄漏成全局,或被后面的同名声明覆盖。
  • var + 闭包 → 循环里的回调都拿到最后一个值(见上面实测的 [3,3,3])。
  • this 丢了 → 把方法赋给变量再调用、或当回调传出去。解法:bind、包一层箭头函数、或改用 class 字段。
  • 0.1 + 0.2 !== 0.3 → 浮点固有误差,比较要留容差或改用整数/十进制库。
  • [] == false 为真但 [] === false 为假 → 别用 == 判空数组,用 arr.length。
  • [10, 9].sort() 排错 → 必须传 (a, b) => a - b。
  • 数组 forEach 里 return 想中断循环 → 做不到,换 for..of 或 some。
  • 用对象当普通对象的键 → 键会被 String() 成 '[object Object]',两个不同对象互相覆盖;要按引用记东西就用 Map / WeakMap。
  • 箭头函数返回对象字面量 → x => {a: 1} 被解析成函数体(而且没有 return),要写成 x => ({a: 1})。
  • 深拷贝用 JSON.parse(JSON.stringify(x)) → 丢函数、undefined、Date 变字符串、循环引用直接报错,改用 structuredClone。
  • parseInt 不写进制 → 老浏览器把 '010' 当八进制;统一写 parseInt(s, 10)。
  • await 写在循环里 → 串行化,批量请求变慢,改 Promise.all。
  • Promise 没接 catch → 报错无声消失(控制台只有一条 unhandled rejection 警告)。
  • setInterval 忘了 clearInterval → 组件卸载后仍在跑,页面越用越卡。
  • 在 for (const x in obj) 里处理原型属性 → 加 Object.hasOwn(obj, x) 判断,或直接用 Object.entries。
  • 模块用 file:// 打不开 → 起个本地静态服务器(npx serve、python -m http.server)。

练习

  • 用 map/filter/reduce 三连完成:给定 [{name, score}],筛出及格的人、按分数降序、拼成一句 '张三(95)、李四(88)'。
  • 亲手复现 for (var) 与 for (let) 的闭包差异,再用「IIFE 捕获」和「bind」两种老办法把 var 版本修正。
  • 写一个 debounce(fn, wait):连续调用只在最后一次之后执行。用 console.log 打印调用次数与真正执行的次数来验证(提示:需要 setTimeout + clearTimeout + 闭包)。
  • 把一段回调风格的代码(setTimeout 嵌套三层)改写成 Promise 链,再改写成 async/await,观察可读性差异。
  • 用 structuredClone 与 JSON 往返分别拷贝一个含 Date 和函数的对象,对比结果差异。

参考资料