🐣 纯新手先看这一框(30 秒秒懂)
JS 适合做什么?网页交互(前端)、服务器(Node.js)、小程序、桌面(Electron)、移动端。一句话:哪里有运行时,哪里就能跑 JS。
为什么要学它?它是 Web 的母语,前端绕不开;Node.js 让它也能写后端。学一门通吃前后端,性价比极高。
怎么跑起来?本站「动手实验室」选 JavaScript 点运行即可(Node.js 环境);浏览器里按 F12 打开控制台直接敲也行;本地装 Node 后 node xxx.js 运行。
新手最常踩的坑?① == 会偷偷类型转换,用 ===;② var 会变量提升且函数作用域,用 let/const;③ 回调嵌套成"地狱";④ this 指向看"怎么调用"而非"在哪定义"。
课程说明从零掌握 JS 语法 + 异步/原型/性能要点
JavaScript 诞生于 1995 年(Brendan Eich 十天设计出来),最初只是给网页加点交互。如今它借着 Node.js(2009)和各大浏览器引擎(V8 等),已经成为前后端通吃的主流语言。它的特点是:动态弱类型、基于原型、单线程 + 事件循环、函数是一等公民。
本课程假设你有最基础编程概念。重点放在JS 区别于 Java/C++ 的关键差异:没有类型的编译期约束、this 的四套绑定规则、原型链继承、以及最让新手头大的异步与事件循环。进阶部分直击面试高频:闭包、Promise、事件循环、Proxy、V8 优化等。
document)在这儿跑不了,纯逻辑随便跑。
1. 运行方式与 Hello World浏览器控制台 / Node.js / 本站运行器
JS 不需要编译,直接由引擎解释执行。下面三种方式都能跑同一段代码。
// 方式一:Node.js 或浏览器控制台直接敲
console.log("你好,JavaScript!");
console.log("2 + 3 =", 2 + 3);
// 方式二:浏览器里操作页面
// document.getElementById("app").textContent = "Hello";
// 方式三:写进 html 的 <script> 标签由浏览器执行
// <script>alert("hi")</script>
console.log。浏览器按 F12 打开 DevTools 的 Console 面板即可交互执行。Node 下直接命令行 node 进入 REPL 也能一行行试。
document/window/alert。想验证 DOM 操作请去浏览器控制台。纯逻辑、计算、数组字符串处理在这儿随便跑。
2. 变量:let / const / var块级作用域、提升、暂时性死区
现代 JS 用 let(可变)和 const(常量,绑定不可改)声明变量,二者都是块级作用域。var 是老写法,函数作用域且会"提升",不推荐再用。
let count = 1;
count = 2; // OK,let 可改
const PI = 3.14;
// PI = 3; // 报错:const 不能重新赋值(但对象内部属性可改)
if (true) {
let x = 10;
var y = 20; // var 不受块限制
}
// console.log(x); // 报错:x 不在作用域
console.log(y); // 20(var 泄漏到外层,坑)
// 暂时性死区:声明前访问会报错
// console.log(z); // ReferenceError
let z = 5;
const 保证的是"变量到值的绑定不变",不是"值不可变"。const obj = {a:1}; obj.a = 2; 完全合法(改的是对象内部)。想真正冻结用 Object.freeze。
const 就写 const,需要重新赋值才用 let,永远别用 var。这是现代 JS 的共识,能避开一大类作用域 bug。
3. 数据类型与类型转换原始类型、typeof、隐式转换
JS 有 7 种原始类型 + 对象:number / string / boolean / undefined / null / symbol / bigint 和 object。typeof 可看类型,但有几个反直觉点。
console.log(typeof 42); // "number"
console.log(typeof "hi"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object"(历史 bug!null 不是对象)
console.log(typeof []); // "object"(数组也是对象)
console.log(typeof function(){});// "function"
console.log(typeof Symbol()); // "symbol"
// 数组/对象判断用 Array.isArray
console.log(Array.isArray([])); // true
"5" - 3 === 2(字符串转数字),但 "5" + 3 === "53"(数字转字符串拼接)。这种不一致极易出错,用 === 并主动转换:Number("5")、String(3)、Boolean(x)。
false:0 / -0 / "" / null / undefined / NaN / false。其余皆为 truthy(包括 "0"、[]、{})。写 if (arr) 时小心:空数组也是 truthy。
4. 运算符与相等判断== vs ===,别让类型转换坑你
JS 有两套相等:== 会做隐式类型转换再比,=== 严格比(类型不同直接 false)。永远用 ===。
console.log(0 == false); // true(都转成 0)← 反直觉
console.log(0 === false); // false(类型不同)← 用这个
console.log("" == 0); // true ← 坑
console.log("" === 0); // false
console.log(null == undefined); // true(特例)
console.log(null === undefined); // false
// 正确的比较姿势
console.log(Number("5") === 5); // true
console.log("5" === String(5)); // true
[] == false 是 true,[] == ![] 也是 true——因为都经历了对象→原始值→数字的转换。这些"特性"毫无意义,一律用 === 就能彻底避开。
[1,2] === [1,2] 为 false。比内容要逐元素比较,或用 JSON.stringify(顺序敏感)等。深层比较可用 lodash 的 isEqual。
5. 流程控制if / for / while / switch / for...of
分支循环和主流语言类似。JS 额外有 for...of(遍历可迭代对象的值)和 for...in(遍历对象可枚举键,别乱用)。
let n = 7;
if (n % 2 === 0) console.log("偶数");
else console.log("奇数");
// for 循环
let sum = 0;
for (let i = 1; i <= 100; i++) sum += i;
console.log("1..100 和 =", sum);
// for...of 遍历数组(推荐)
const arr = [3, 1, 4];
for (const x of arr) console.log(x);
// for...in 遍历对象键
const obj = { a: 1, b: 2 };
for (const k in obj) console.log(k, obj[k]);
// switch
const grade = "B";
switch (grade) {
case "A": console.log("优秀"); break;
case "B": console.log("良好"); break;
default: console.log("其他");
}
for...in 会遍历到原型链上可枚举的属性,且顺序不保证。遍历数组请用 for...of 或 forEach,for...in 只用于普通对象。
6. 函数与箭头函数声明/表达式、默认参数、剩余、箭头函数
函数是 JS 的一等公民,可以赋值、传参、返回。ES6 的箭头函数写法更短,且没有自己的 this(继承外层)。
// 函数声明(会提升)
function add(a, b) { return a + b; }
// 函数表达式
const mul = function (a, b) { return a * b; };
// 箭头函数
const square = x => x * x; // 单参数可省括号,单表达式省 return
const sum = (a, b) => a + b;
// 默认参数 + 剩余参数
function greet(name = "匿名", ...tags) {
console.log(name, tags);
}
greet("小明", "vip", "new"); // 小明 [ 'vip', 'new' ]
this,它用的是"定义时外层的作用域的 this"。这让它特别适合做回调(如数组方法里)。但若你想在对象方法里用 this 指向对象本身,别用箭头函数(详见进阶第 12 节)。
function 声明会被提升到作用域顶部,调用可写在定义前;const fn = ... 这种表达式不会提升(先声明后使用)。
7. 数组与高阶方法map / filter / reduce / forEach
数组是 JS 最常用的数据结构。高阶方法把"遍历+处理"声明式地表达,比手写 for 更清晰,也更不易错。
const nums = [1, 2, 3, 4, 5];
// map:每个元素变换,返回新数组(不改原数组)
const doubled = nums.map(x => x * 2); // [2,4,6,8,10]
// filter:按条件筛选
const evens = nums.filter(x => x % 2 === 0); // [2,4]
// reduce:累积成单个值
const total = nums.reduce((acc, x) => acc + x, 0); // 15
const max = nums.reduce((a, b) => a > b ? a : b);
// forEach:仅遍历副作用,无返回值
nums.forEach(x => console.log(x));
console.log(doubled, evens, total, max);
map/filter/slice 返回新数组,原数组不变;sort/reverse/splice/push 会原地修改原数组。[3,1,2].sort() 默认按字符串排("10"<"2"),数字排序要传比较器:arr.sort((a,b)=>a-b)。
nums.filter(x=>x%2).map(x=>x*x).reduce(...)。声明式、可读、不易错,是现代 JS 的标志写法。
8. 对象与解构字面量、属性简写、解构赋值
对象是键值对的集合。ES6 的解构赋值能一次性从对象/数组里"拆"出变量,写起来极爽。
// 属性简写 + 方法简写
const name = "小明", age = 18;
const user = { name, age, sayHi() { console.log(this.name); } };
// 解构对象
const { name: n, age: a } = user;
console.log(n, a); // 小明 18
// 解构数组
const [first, , third] = [10, 20, 30];
console.log(first, third); // 10 30(跳过第二个)
// 默认值 + 剩余
const { x = 0, ...rest } = { x: 1, y: 2, z: 3 };
console.log(x, rest); // 1 { y: 2, z: 3 }
user.sayHi();
obj[key] 取值。但键只能是字符串或 Symbol;需要任意类型做键用 Map。注意对象比较是引用比较,{a:1}==={a:1} 为 false。
function f({url, method='GET'}={}){},调用方传不传 method 都有默认值,代码干净。
9. 原型链与 class原型是 JS 继承的根,class 是语法糖
JS 没有"类"的传统继承,而是用原型(prototype):对象读属性时,自己没有就沿原型链往上找。ES6 的 class 只是原型继承的语法糖,底层仍是 prototype。
class Animal {
constructor(name) { this.name = name; }
eat() { console.log(this.name + " 吃"); }
}
class Dog extends Animal {
bark() { console.log(this.name + " 汪汪"); }
}
const d = new Dog("旺财");
d.eat(); // 旺财 吃(沿原型链找到 Animal 的方法)
d.bark(); // 旺财 汪汪
// 原型链真相:Dog.prototype 的 __proto__ 指向 Animal.prototype
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
class 没有"类提升"(前有暂时性死区),方法默认不可枚举,且本质是函数。别把它当 Java 那种传统类——底层永远是原型链。理解原型链,才能看懂继承、instanceof、原型方法污染等坑(详见干货第 4 条)。
a instanceof B 实质是:沿 a 的原型链找,看有没有 B.prototype。所以它能跨窗口/框架失效(不同全局的同一"类"不是同一个 prototype)。
10. 模块:ESM 与 CommonJSimport/export vs require/module.exports
现代前端/Node 用 ES Module(ESM):export 导出、import 导入。老 Node 用 CommonJS:module.exports / require。
// ===== ESM(推荐,浏览器/现代 Node 都支持)=====
// math.js
export const add = (a, b) => a + b;
export default function sub(a, b) { return a - b; }
// main.js
import sub, { add } from "./math.js";
console.log(add(1, 2), sub(5, 3));
// ===== CommonJS(老 Node)=====
// math.cjs
module.exports = { add: (a, b) => a + b };
// main.cjs
const { add } = require("./math.cjs");
import 必须在顶层、编译期确定),CommonJS 是动态的(require 可在任意位置)。浏览器原生支持 ESM(需 <script type="module">),但本站 Node 运行器里直接写 import 可能需 CommonJS 风格或顶层。示例逻辑通用,落地时按环境选。
export(命名),但只能一个 export default。团队约定:工具函数用命名导出(便于 tree-shaking),主类/主函数用默认导出。
11. 闭包函数"记住"了它诞生时的作用域
闭包 = 函数 + 它捕获的外部变量。即使外层函数已返回,内层函数仍能访问那些变量。它是 JS 实现私有状态、工厂、回调的基础。
function makeCounter() {
let count = 0; // 被闭包"记住"的私有变量
return function () {
count++;
return count;
};
}
const c = makeCounter();
console.log(c()); // 1
console.log(c()); // 2(count 被保留)
console.log(c()); // 3
12. this 的绑定规则默认 / 隐式 / 显式 / new,箭头函数例外
this 指向谁,取决于函数怎么被调用,而不是在哪定义。四套规则按优先级:new 绑定 > 显式(call/apply/bind)> 隐式(对象调用)> 默认(严格模式 undefined,否则全局)。
function show() { console.log(this.label); }
const obj = { label: "obj", show };
obj.show(); // "obj"(隐式:谁调用 this 指向谁)
show(); // undefined(默认:非严格下指向全局;这里 label 不存在)
const bound = show.bind({ label: "bound" });
bound(); // "bound"(显式绑定)
// 箭头函数:没有自己的 this,继承外层
const obj2 = {
label: "obj2",
show: () => console.log(this.label) // 此 this 指向外层(模块/全局),不是 obj2
};
setTimeout(obj.show, 100)),this 会变成默认绑定(undefined/全局),方法内访问 this.x 报错。解决:用箭头函数、或 bind、或把方法写成箭头函数属性。这是面试和实战都极高发的坑(详见干货第 3 条)。
13. Promise 与异步解决回调地狱,状态机不可逆
JS 单线程,耗时操作(网络、定时)不能阻塞。Promise 表示一个"将来完成或失败"的值,有三种状态:pending → fulfilled 或 rejected,且不可逆。
// 创建一个 Promise
function delay(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve("完成"), ms);
});
}
delay(500).then(result => console.log(result)); // 500ms 后打印"完成"
// 链式:then 返回新 Promise
delay(100)
.then(r => r + "!")
.then(r => console.log(r))
.catch(err => console.error(err)); // 任一环节失败都进 catch
// Promise.all:等全部完成
Promise.all([delay(100), delay(200)])
.then(results => console.log(results));
resolve/reject 无效;then 注册的回调也只触发一次。想"多次事件"用 EventEmitter 或 RxJS,别用 Promise 硬凑。
a(()=>b(()=>c(...))) 横向越套越深、错误处理分散。Promise 的链式 .then().then() 把它拉直,可读性大增。
14. async / await让异步代码"看起来像同步"
async 函数返回 Promise,await 暂停等待 Promise 完成。它只是 Promise 的语法糖,但写起来像同步代码,极易读。
function delay(ms) {
return new Promise(r => setTimeout(r, ms));
}
async function main() {
console.log("开始");
await delay(300);
console.log("300ms 后");
const r = await Promise.resolve(42);
console.log("拿到", r);
}
main();
// 错误处理:用 try/catch 包住 await
async function safe() {
try {
const v = await Promise.reject(new Error("炸了"));
} catch (e) {
console.error("捕获:", e.message);
}
}
safe();
// 并行:多个 await 会串行,要并行用 Promise.all
async function parallel() {
const [a, b] = await Promise.all([delay(100), delay(200)]);
}
try/catch 只捕获里面的 await;把多个不相关的 await 都塞进一个大 try 会掩盖各自错误,按需细分。② 连续 await a(); await b(); 是串行,互不依赖时应 await Promise.all([a(), b()]) 并行,否则白白变慢(详见干货第 6 条)。
async/await 不会把异步变同步、不阻塞线程,它只是把 Promise 链"拍平"成线性代码。遇到 await 函数仍会交还控制权给事件循环。
15. 事件循环宏任务 vs 微任务,Promise 优先于 setTimeout
JS 单线程靠事件循环处理异步:先跑完当前同步代码(一个宏任务),再清空微任务队列(Promise 回调、queueMicrotask),然后取下一个宏任务(setTimeout、I/O、UI 渲染等)。微任务优先级高于宏任务。
console.log("1 同步");
setTimeout(() => console.log("2 宏任务(setTimeout)"), 0);
Promise.resolve().then(() => console.log("3 微任务(Promise)"));
console.log("4 同步");
// 输出顺序:1 → 4 → 3 → 2
// 因为微任务(Promise)在当前宏任务结束后、下一个宏任务前清空
setTimeout(fn, 0) 把 fn 放进宏任务队列,要等当前同步代码和所有微任务跑完才轮到它。所以 Promise 的回调总是先于 setTimeout 执行。理解这点,才能预测异步输出的顺序(高频面试题,详见干货第 1 条)。
.then/.catch/.finally、queueMicrotask、MutationObserver。宏任务:setTimeout/setInterval、I/O、setImmediate、UI 事件、requestAnimationFrame(特殊,介于两者之间)。
16. ES6+ 新特性精选模板字符串、展开、Proxy、可选链
ES6(2015)以来 JS 每年加特性。下面几个日常最高频。
// 模板字符串
const name = "小明";
console.log(`你好,${name},明年 ${18 + 1} 岁`);
// 展开运算符
const a = [1, 2], b = [3, 4];
console.log([...a, ...b]); // [1,2,3,4](数组展开)
const o = { ...{ x: 1 }, y: 2 }; // 对象展开(浅拷贝)
// 可选链 ?. 与空值合并 ??
const user = { profile: null };
console.log(user?.profile?.age); // undefined(不会报错)
console.log(user.profile?.age ?? "未知"); // "未知"
const n = 0;
console.log(n ?? 10); // 0(?? 只在 null/undefined 时取默认)
console.log(n || 10); // 10(|| 把 0 当 falsy)
|| 遇任何 falsy(0/""/false)就取右边;?? 只在 null/undefined 时取右边。想给"没值"兜底且保留 0/空串,用 ??。两者不能混写,需括号:(a || b) ?? c。
Proxy 能拦截对象的读/写/删等操作,是 Vue3 响应式、immer 不可变更新的底层原理(详见干货第 8 条)。Reflect 提供对应的"默认行为"调用,和 Proxy 陷阱一一对应。
进阶训练场(含答案)
题 1:用 let/const 重写并解释 var 的坑
下面代码输出什么?改用 let 后会怎样?
// var 的坑:循环变量泄漏 + 闭包共享
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 输出 3 3 3(i 是同一个)
}
// 改用 let:每次迭代独立绑定
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0); // 输出 0 1 2
}
题 2:用 map/filter/reduce 统计偶数平方和
给定数组,求所有偶数平方之和。
const nums = [1, 2, 3, 4, 5, 6];
const r = nums.filter(x => x % 2 === 0)
.map(x => x * x)
.reduce((a, b) => a + b, 0);
console.log(r); // 4 + 16 + 36 = 56
题 3:实现一个闭包计数器
写一个 makeCounter,返回的函数每次调用自增并返回当前值。
function makeCounter() {
let n = 0;
return () => ++n;
}
const c = makeCounter();
console.log(c(), c(), c()); // 1 2 3
题 4:修正 this 丢失
下面代码报错,如何让它正确打印 "obj"?
const obj = {
label: "obj",
show() { console.log(this.label); }
};
// 错误:show 被单独传出去,this 丢失
// setTimeout(obj.show, 100);
// 正确写法之一:箭头函数包一层
setTimeout(() => obj.show(), 100);
// 或:obj.show.bind(obj)
题 5:用 Promise 改写回调嵌套
把 a(()=>b(()=>c())) 形式的嵌套改成链式。
function step(v) {
return new Promise(res => setTimeout(() => res(v + 1), 10));
}
step(0).then(r => step(r)).then(r => step(r))
.then(r => console.log(r)); // 3
题 6:async/await 并行请求
两个独立异步任务,用并行写法让总耗时取较长的那个而非相加。
const wait = ms => new Promise(r => setTimeout(r, ms));
async function run() {
const start = Date.now();
await Promise.all([wait(200), wait(200)]); // 并行,约 200ms
console.log(Date.now() - start, "ms");
}
run();
题 7:防抖函数 debounce
实现一个 debounce(fn, ms),连续触发只在停止 ms 后执行一次。
function debounce(fn, ms) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), ms);
};
}
const log = debounce(() => console.log("触发"), 300);
log(); log(); log(); // 只打印一次
题 8:可选链安全取值
从可能缺失的嵌套对象里安全取城市名,缺失返回"未知"。
const user = { address: null };
const city = user?.address?.city ?? "未知";
console.log(city); // 未知(不会因 address 为 null 报错)
🧪 动手实验室
不用从零敲代码:下面每个程序都已写好,点「▶ 运行」直接看结果;想改就在框里改,再点运行。手机、电脑都一样用。打开本页时会自动跑一次,方便你先看效果。注意:运行器是 Node.js,没有浏览器 DOM,纯逻辑随便跑。
① 基础输出与计算
② 数组与高阶方法(偶数平方求和)
③ 对象与解构
④ 闭包计数器
⑤ 异步:Promise 与事件循环顺序
document/window 等浏览器 API 用不了。实战干货(12 条工业级技巧)面试与上线都绕不开
queueMicrotask),然后取下一个宏任务(setTimeout、I/O、UI 事件)。微任务优先级高于宏任务,所以 Promise 回调总先于 setTimeout(fn,0) 执行。预测异步输出顺序是高频面试题。
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
// 输出:A D C B(微任务 C 先于宏任务 B)
clearTimeout。解决:回调只引用必要变量、不再需要时解绑事件/清除定时器。
// 危险:定时器一直持有 big 不释放
let big = new Array(1e6).fill(1);
const t = setInterval(() => console.log(big.length), 1000);
// 不用了要清:clearInterval(t); big = null;
this 指向全局(严格模式 undefined);隐式:obj.fn() 指向 obj;显式:call/apply/bind 强制指定;new:构造函数里 this 指向新对象。箭头函数没有 this,用外层作用域的。优先级:new > 显式 > 隐式 > 默认。
function f() { console.log(this.x); }
const o = { x: 1, f };
f(); // undefined(默认)
o.f(); // 1(隐式)
f.call({ x: 2 }); // 2(显式)
class 只是原型继承的语法糖,底层仍是 prototype。对象读属性时沿 __proto__ 向上找,直到 null。理解原型链才能看懂继承、instanceof(沿原型链找 prototype)、以及"在原型上挂方法被所有实例共享"的现象。别把 class 当传统面向对象语言的类。
function Person(name){ this.name = name; }
Person.prototype.hi = function(){ console.log(this.name); };
const p = new Person("小明");
p.hi(); // 小明(方法在原型上,所有实例共享)
console.log(p.__proto__ === Person.prototype); // true
pending / fulfilled / rejected 三态,一旦从 pending 变成 fulfilled 或 rejected 就不可逆,之后再加的 then 回调仍按最终状态触发(值会被记住)。then(onFulfilled, onRejected) 返回新 Promise,支持链式;错误沿链向下直到被 catch 捕获。
catch 会抛出 unhandledRejection。async 函数里忘了 try/catch 也可能让错误"消失"。养成"每个 Promise 链都有 catch"的习惯。try/catch 只覆盖其内部的 await,把多个独立任务塞进一个大 try 会模糊错误来源,按需细分。② 连续 await a(); await b(); 是串行的,互不依赖时应 await Promise.all([a(), b()]) 并行,否则耗时相加。③ 循环里 await 也会串行,要并发用 Promise.all(arr.map(...))。
// 串行(慢)
for (const url of urls) await fetch(url);
// 并行(快)
await Promise.all(urls.map(u => fetch(u)));
WeakMap 的键是弱引用:键对象没有其他引用时,会被 GC 回收,对应条目随之消失——适合做"对象 → 元数据"的私有缓存,不阻止原对象回收。WeakRef 可持有对象的弱引用(通过 .deref() 取,可能已 null)。普通 Map 的键是强引用,会阻止 GC。
const cache = new WeakMap();
let obj = { id: 1 };
cache.set(obj, "元数据");
obj = null; // obj 可被回收,cache 中对应条目随之清除
Proxy 拦截对象的 get/set/delete/has 等 13 种操作,Reflect 提供对应的默认行为调用,二者陷阱一一对应。它是 Vue3 响应式、immer 不可变更新、表单校验的底层原理。注意 Proxy 是"浅"的,嵌套对象需递归代理才能真正追踪。
const target = { count: 0 };
const p = new Proxy(target, {
set(obj, key, val) {
console.log(`设置 ${key} = ${val}`);
return Reflect.set(obj, key, val);
}
});
p.count = 5; // 打印:设置 count = 5
delete 删属性(用置 undefined 代替),并尽量用一致的属性初始化顺序。
// 好:属性顺序一致,共享隐藏类
function Point(x, y) { this.x = x; this.y = y; }
new Point(1, 2); new Point(3, 4);
// 差:delete 破坏隐藏类
const p = new Point(1, 2);
delete p.y; // 避免!改用 p.y = undefined;
lodash.debounce/throttle,它们处理了边界(leading/trailing、cancel)。
function throttle(fn, ms) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= ms) { last = now; fn(...args); }
};
}
import/export)是静态的、可在编译期分析、支持 tree-shaking,浏览器与现代化构建原生支持;CommonJS(require/module.exports)是动态的、Node 传统模块。新项目用 ESM;老 Node 库可能是 CJS,引入时用默认导入或 import ... from 'cjs' 的互操作。两者混用要注意循环依赖行为不同。
<script type="module">,且受同源/CORS 限制;CommonJS 在浏览器里不能直接用,要靠打包工具(Webpack/Vite)转译。"use strict" 下:① 普通函数调用 this 是 undefined(非严格指向全局,更易写出全局污染 bug);② 不允许删除变量/函数(delete x 抛错);③ 不允许重复参数名;④ 给未声明变量赋值会抛 ReferenceError(非严格会静默建全局变量)。现代构建(ESM)默认严格模式,老脚本建议显式开启。
"use strict";
function f() { console.log(this); }
f(); // undefined(严格),而非 window
// x = 1; // 严格下:ReferenceError(未声明)
// function g(a, a){} // 严格下:SyntaxError(重复参数)
自测题(点击展开答案)
- Q1:JS 里比较两个值,应该用?A:
===(严格相等,不偷偷类型转换)。==会做隐式转换,出现0==false为真这类反直觉结果。 - Q2:现代 JS 声明变量首选?A:默认
const,需重赋值用let,别用var(函数作用域+提升,易出作用域 bug)。 - Q3:下面代码输出顺序?
console.log(1); setTimeout(()=>console.log(2),0); Promise.resolve().then(()=>console.log(3)); console.log(4);A:1 → 4 → 3 → 2。微任务(Promise)在当前宏任务结束、下一宏任务(setTimeout)前清空。 - Q4:箭头函数和普通函数在
this上最大区别?A:箭头函数没有自己的 this,继承外层作用域的 this;普通函数 this 由调用方式决定。所以回调里常用箭头函数避免 this 丢失。 - Q5:连续写
await a(); await b();有什么问题?A:这是串行执行,若 a、b 互不依赖会白白变慢。应await Promise.all([a(), b()])并行。 - Q6:
const obj={a:1}; obj.a=2;合法吗?A:合法。const只锁定"变量到值的绑定",对象内部属性仍可改。要冻结构用Object.freeze(obj)。 - Q7:
class和原型链什么关系?A:class只是原型继承的语法糖,底层仍是prototype。对象读属性沿__proto__向上找。 - Q8:严格模式下普通函数调用的
this是?A:undefined(非严格模式指向全局对象)。这是严格模式防止全局污染的体现。 - Q9:
0.1 + 0.2 === 0.3的结果是?A:false。IEEE754 双精度浮点无法精确表示十进制小数,产生舍入误差。金额用整数分或Number((a+b).toFixed(2))比较。 - Q10:模块里
export default和多个export能否混用?A:能。一个模块可同时有多个命名导出和一个默认导出;导入时默认导出不用花括号,命名导出用{ }。库/工具函数用命名导出利于 tree-shaking。
附录一 · 速查表面试前过一遍
1. 七种原始类型
| 类型 | typeof 结果 | 说明 |
|---|---|---|
number | "number" | 含 NaN、Infinity |
string | "string" | 不可变 |
boolean | "boolean" | true / false |
undefined | "undefined" | 未赋值 |
null | "object"(bug) | 空值,用 === null 判断 |
symbol | "symbol" | 唯一不可变值,做键 |
bigint | "bigint" | 大整数,123n |
2. falsy 值清单
| 值 | 布尔上下文 |
|---|---|
0 / -0 / NaN | false |
""(空串) | false |
null / undefined | false |
false | false |
"0" / [] / {} | true(易错!) |
3. 数组方法分类
| 类别 | 方法 | 是否改原数组 |
|---|---|---|
| 遍历 | forEach / map / filter / reduce / some / every / find | 否(除 forEach 副作用) |
| 增删改 | push / pop / shift / unshift / splice | 是 |
| 排序 | sort / reverse | 是 |
| 返回新数组 | slice / concat / map / filter / flat | 否 |
| 查找 | indexOf / includes / find / findIndex | 否 |
4. this 绑定优先级
| 规则 | 示例 | this 指向 |
|---|---|---|
| new 绑定 | new F() | 新实例 |
| 显式 | f.call/apply/bind | 指定对象 |
| 隐式 | obj.f() | obj |
| 默认 | f() | 全局 / undefined(严格) |
| 箭头函数 | () => {} | 外层 this(无自身) |
5. 宏任务 vs 微任务
| 队列 | 包含 | 执行时机 |
|---|---|---|
| 微任务 | Promise 回调、queueMicrotask、MutationObserver | 当前宏任务后、下一宏任务前,清空全部 |
| 宏任务 | setTimeout、setInterval、I/O、UI 事件、setImmediate | 每轮事件循环取一个 |
附录二 · 新手常见 20 个坑每条都值得记下来
- 用
==比较:隐式转换出怪事,一律===。 - 用
var:函数作用域 + 提升,改用let/const。 - 认为 const 完全不可变:它只锁绑定,对象内部可改;要冻结用
Object.freeze。 typeof null是 "object":历史 bug,判断 null 用=== null。- 数组也是 object:
typeof []为 "object",判断数组用Array.isArray。 - 对象/数组比较是引用:
[1]===[1]为 false,比内容要逐元素。 - 字符串比较用
==:实际可用===,但排序要用localeCompare,别用>/<比汉字。 sort默认按字符串排:数字要arr.sort((a,b)=>a-b)。- forEach 里 return 退不出外层:要提前结束用
some/every或 for 循环。 - for...in 遍历数组:会带原型属性、顺序不定,数组用
for...of。 - 闭包循环变量:
var循环里闭包共享同一变量,用let或 IIFE 解决。 - this 丢失:把方法当回调传出去 this 变默认绑定,用箭头函数/bind。
- 回调地狱:嵌套回调难维护,用 Promise/async。
- 忘记 catch Promise 拒绝:未处理的拒绝会抛 unhandledRejection。
- 连续 await 串行:不相关的任务应 Promise.all 并行。
delete对象属性:破坏 V8 隐藏类、性能下降,用置 undefined 代替。- 随意改变属性顺序:对象的隐藏类依赖初始化顺序,保持一致更快。
||当默认值掩盖 0/空串:想保留假值用??。- 浮点精度:
0.1+0.2!==0.3,金额用整数分或 decimal 库。 - 全局变量污染:非严格下给未声明变量赋值会建全局变量,开严格模式。
综合小项目 · 待办清单(Todo)把前面学的串起来
用数组 + 高阶方法 + 闭包 + 纯函数实现一个内存版 Todo:增、删、改、查、过滤。这是面试常考的"小项目骨架"。
function createTodo() {
let items = [];
let nextId = 1;
return {
add(text) {
items.push({ id: nextId++, text, done: false });
return this;
},
remove(id) {
items = items.filter(it => it.id !== id); // 用 filter 返回新数组
return this;
},
toggle(id) {
items = items.map(it =>
it.id === id ? { ...it, done: !it.done } : it);
return this;
},
list(filterDone) {
return filterDone === undefined
? items
: items.filter(it => it.done === filterDone);
}
};
}
const todo = createTodo();
todo.add("学 JS").add("写项目").toggle(1);
console.log("全部:", todo.list());
console.log("已完成:", todo.list(true));
items/nextId)做私有状态,方法返回 this 支持链式调用,增删改用 filter/map 返回新数组而非改原数组(不可变风格,易推理、易测试)。这正是 Redux/Vuex 的思想雏形。items 存到 localStorage;想响应式就加订阅(观察者模式),状态变时自动重渲染——这正是前端框架干的事。面试高频 12 问边学边想
structuredClone(现代)或 JSON.parse(JSON.stringify())(丢函数/undefined/循环引用);复杂用 lodash.cloneDeep 或手写递归。undefined 或重新设计数据结构。代码风格与工程规范写好 JS 的软实力
| 规范点 | 推荐 | 反面 |
|---|---|---|
| 变量 | 默认 const,需改才 let | 用 var |
| 相等 | === / !== | == / != |
| 字符串 | 模板字符串 `...${x}` | 大量 + 拼接 |
| 函数 | 箭头函数做回调 | function 嵌套易丢 this |
| 默认值 | ?? 保留 0/空串 | || 掩盖 falsy |
| 数组处理 | map/filter/reduce | 手写 for + push |
| 异步 | async/await + try/catch | 嵌套回调 |
| 分号 | 统一风格(推荐省略,靠构建) | 时有时无 |
[ ( + - / 开头的行容易和上一行连起来。最稳的是让构建工具/Prettier 统一处理。学完之后怎么走路线图
- 夯实语法:10 节入门 + 6 节进阶练熟,每个例子自己敲改。
- DOM 与浏览器:
document操作、事件、BOM(本站运行器无 DOM,去浏览器练)。 - 异步深入:事件循环、fetch、AbortController、并发控制。
- 工程化:npm、打包(Vite/Webpack)、ESLint、单元测试(Vitest/Jest)。
- 框架:React 或 Vue(理解了原型/响应式/闭包,学框架很快)。
- Node 后端:Express/Fastify、数据库、鉴权,打通全栈。
- 性能与原理:V8 优化、内存分析、Web 性能,往资深走必学。
扩展专题一 · 深度事件循环与异步把最难的点吃透
事件循环是 JS 异步的心。一次循环(tick)的流程:
- 执行一个宏任务(如当前同步脚本)。
- 清空微任务队列(反复取,直到为空)。
- 必要时渲染(requestAnimationFrame 在渲染前)。
- 取下一个宏任务,回到第 1 步。
// 微任务里再塞微任务,会一直清空(不会被宏任务插队)
Promise.resolve().then(() => {
console.log("微1");
Promise.resolve().then(() => console.log("微2"));
});
setTimeout(() => console.log("宏"), 0);
// 输出:微1 微2 宏
await 之后的代码,等价于注册进一个微任务,所以在当前同步块结束后才执行,顺序遵循上面的规则。扩展专题二 · Proxy 与响应式原理Vue3 背后的魔法
响应式核心:用 Proxy 包裹数据对象,拦截 get(收集依赖)和 set(触发更新)。下面是个迷你响应式 demo。
function reactive(obj, onChange) {
return new Proxy(obj, {
get(t, k, r) {
// 这里可"收集依赖":记录谁在读这个属性
return Reflect.get(t, k, r);
},
set(t, k, v, r) {
const old = t[k];
const ok = Reflect.set(t, k, v, r);
if (old !== v) onChange(k, v, old); // 值变了就通知
return ok;
}
});
}
const state = reactive({ count: 0 }, (k, v) => {
console.log(`变化 ${k}: ${v}`);
});
state.count = 1; // 打印:变化 count: 1
{user:{name}})需要递归 proxy 才能真正追踪深层变化,这正是 Vue3 的 reactive 内部做的事。Reflect.get/set 调用"默认行为",能保证 this 正确、返回值语义一致,且配合 receiver 支持原型链上的读取。扩展专题三 · 函数式技巧把数组玩出花
const users = [
{ name: "小明", age: 18 },
{ name: "小红", age: 25 },
{ name: "小刚", age: 16 }
];
// 分组
const byAdult = users.reduce((m, u) => {
const key = u.age >= 18 ? "成人" : "未成年";
(m[key] ||= []).push(u.name);
return m;
}, {});
console.log(byAdult); // { 成人: [ '小明', '小红' ], 未成年: [ '小刚' ] }
// 去重(对象按 id)
const list = [{id:1},{id:2},{id:1}];
const uniq = [...new Map(list.map(x => [x.id, x])).values()];
console.log(uniq); // [{id:1},{id:2}]
// 扁平化
console.log([[1,2],[3,4]].flat()); // [1,2,3,4]
console.log([1,[2,[3]]].flat(Infinity)); // [1,2,3]
map/filter 返回新数组、用展开 {...obj, k:v} 返回新对象。好处是易测试、易做撤销/时间旅行(Redux 核心思想)。常见算法模板(JS 版)刷题常客
1. 数组去重
const uniq = arr => [...new Set(arr)];
console.log(uniq([1, 2, 2, 3])); // [1,2,3]2. 数组求和/最值
const sum = arr => arr.reduce((a, b) => a + b, 0);
const max = arr => arr.reduce((a, b) => a > b ? a : b);3. 防抖(完整版,带 cancel)
function debounce(fn, ms) {
let t;
const d = (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
d.cancel = () => clearTimeout(t);
return d;
}4. 深拷贝(structuredClone)
const deep = structuredClone(obj); // 现代浏览器/Node 内置
// 旧环境:JSON.parse(JSON.stringify(obj))(丢函数/undefined/循环引用)5. 节流(时间戳版)
function throttle(fn, ms) {
let last = 0, timer;
return (...args) => {
const now = Date.now();
const remain = ms - (now - last);
if (remain <= 0) { last = now; fn(...args); }
else { clearTimeout(timer); timer = setTimeout(() => { last = Date.now(); fn(...args); }, remain); }
};
}设计模式入门(JS 版)写出像样的代码
1. 模块模式(闭包私有)
const Counter = (() => {
let n = 0; // 私有
return { inc: () => ++n, get: () => n };
})();
console.log(Counter.inc(), Counter.get());2. 观察者(发布订阅)
function createEmitter() {
const map = {};
return {
on(ev, fn) { (map[ev] ||= []).push(fn); },
emit(ev, data) { (map[ev] || []).forEach(fn => fn(data)); }
};
}
const bus = createEmitter();
bus.on("msg", m => console.log("收到", m));
bus.emit("msg", "hi");3. 工厂
function createUser(type) {
if (type === "vip") return { type, discount: 0.8 };
return { type, discount: 1 };
}
console.log(createUser("vip"));单元测试入门(Vitest/Jest)让代码可信
JS 生态测试很成熟。下面以 Vitest 风格为例(Jest 几乎一样)。
// math.test.js
import { describe, it, expect } from "vitest";
import { add } from "./math";
describe("add", () => {
it("正数相加", () => {
expect(add(1, 2)).toBe(3);
});
it("支持负数", () => {
expect(add(-1, 1)).toBe(0);
});
});| 匹配器 | 含义 |
|---|---|
toBe(a) | 严格相等(引用/原始) |
toEqual(obj) | 结构相等(对象内容) |
toBeTruthy/toBeFalsy | 布尔语义 |
toThrow() | 期望抛异常 |
resolves/rejects | 异步结果 |
it 开始。JavaScript 与其他语言对比别盲目站队
| 维度 | JavaScript | TypeScript | Python | Java | Go |
|---|---|---|---|---|---|
| 类型 | 动态弱 | 静态(JS 超集) | 动态强 | 静态强 | 静态强 |
| 运行 | 引擎(V8) | 编译成 JS | 解释 | JVM | 原生机器码 |
| 并发 | 事件循环 | 事件循环 | GIL 受限 | 线程 | goroutine |
| 主战场 | 前端/全栈 | 大型前端/全栈 | 脚本/AI | 后端/安卓 | 云服务 |
| 上手 | 低 | 中 | 低 | 中 | 低 |
术语中英对照读文档不卡壳
| 中文 | 英文 | 一句话 |
|---|---|---|
| 原始类型 | primitive | number/string 等直接存值 |
| 作用域 | scope | 变量可见范围 |
| 提升 | hoisting | 声明被移到作用域顶部 |
| 暂时性死区 | TDZ | let/const 声明前不可访问 |
| 闭包 | closure | 函数+捕获的变量 |
| 一等公民 | first-class | 函数可赋值/传参/返回 |
| 原型 | prototype | 对象共享方法的载体 |
| 原型链 | prototype chain | 沿 __proto__ 查找 |
| 事件循环 | event loop | 异步调度机制 |
| 宏任务/微任务 | macrotask/microtask | 异步队列两档优先级 |
| 回调 | callback | 完成后调用的函数 |
| 承诺 | Promise | 未来完成/失败的值 |
| 异步 | async | 不阻塞当前执行 |
| 箭头函数 | arrow function | () => {},无自身 this |
| 解构 | destructuring | 从结构里拆变量 |
| 模板字符串 | template literal | `...${}` |
| 可选链 | optional chaining | ?. 安全取值 |
| nullish coalescing | ?? 只在 null/undefined 兜底 | |
| 元编程 | metaprogramming | Proxy/Reflect 拦截对象 |
| 严格模式 | strict mode | "use strict" |
| 模块 | module | ESM/CommonJS |
21 天练习清单照着练
| 天数 | 主题 | 今日任务 |
|---|---|---|
| 1-2 | 环境/语法 | 装 Node、跑 Hello;let/const、类型转换各写 10 行 |
| 3-4 | 流程/函数 | 写猜数字、箭头函数、默认/剩余参数 |
| 5-6 | 数组高阶 | map/filter/reduce 改写求和、去重、分组 |
| 7-8 | 对象/解构 | 对象字面量、解构、默认参数对象 |
| 9-10 | 原型/class | 继承树 + instanceof 验证原型链 |
| 11 | 闭包 | 计数器、私有状态、模块模式 |
| 12-13 | this | 四规则练习 + 修正回调 this 丢失 |
| 14-15 | Promise | 链式、all、错误捕获 |
| 16-17 | async/await | 串行 vs 并行、try/catch 边界 |
| 18 | 事件循环 | 预测多段异步输出顺序 |
| 19 | ES6+ | 模板串、展开、可选链、Proxy 小 demo |
| 20 | 综合项目 | 完成 Todo 项目专题 |
| 21 | 复盘 | 重做自测题 + 面试题,查漏补缺 |
常见问题 FAQ新手问得最多
node xxx.js;本站运行器(Node 环境)直接跑纯逻辑。写 JS 前的小自查清单交代码前扫一眼
- 比较是否用了
===而非==? - 是否优先
const、必要时let、没用var? - 数组排序是否传了比较器(数字)?
- 回调里
this是否会丢失(用箭头/bind)? - 连续 await 是否应改
Promise.all并行? - Promise 链是否都有
catch? - 是否用
delete删属性(应置 undefined)? - 默认值用
??还是||(是否要保留 0/空串)? - 对象/数组比较是否误用
===(应比内容)? - 是否开了严格模式 / 用了 ESLint?
延伸资源学完去哪深造
- MDN JavaScript · 最权威的 Web/JS 参考,查语法/API 首选。
- javascript.info · 现代 JS 教程,讲得透且带练习。
- ECMAScript 规范 · 想抠语言细节看这里。
- LeetCode · 用 JS 刷算法,巩固语法与数据结构。
- React 官网 / Vue 官网 · 下一步学框架。
- 本站练习场 · 随时把想法敲成代码跑一跑。
扩展专题四 · 浏览器 DOM 与事件前端的核心(Node 运行器里跑不了)
在浏览器里,JS 通过 DOM(文档对象模型)操作页面。下面的代码要在浏览器控制台或 <script> 里执行,本站 Node 运行器没有 document。
// 查找元素
const btn = document.getElementById("myBtn");
const items = document.querySelectorAll(".item");
// 读写内容
btn.textContent = "点我";
console.log(btn.textContent);
// 改样式/类
btn.classList.add("active");
btn.style.color = "red";
// 事件监听(推荐,不覆盖已有)
btn.addEventListener("click", (e) => {
console.log("点击了", e.target);
});
// 创建并插入元素
const li = document.createElement("li");
li.textContent = "新项";
document.querySelector("ul").appendChild(li);
e.target 判断来源(事件冒泡)。这样动态增删子元素也不用重新绑,性能与可维护性都好。addEventListener 默认在冒泡阶段;第三参传 true 改为捕获阶段。想阻止冒泡用 e.stopPropagation()。扩展专题五 · fetch 与异步请求现代网络请求方式
浏览器和 Node 18+ 都有全局 fetch(基于 Promise)。下面示例演示为主,需要联网环境才能真跑通。
// GET
async function getTodos() {
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
if (!res.ok) throw new Error("HTTP " + res.status); // 注意先判 ok
return res.json();
}
// POST
async function createTodo() {
const res = await fetch("https://jsonplaceholder.typicode.com/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "学 JS", completed: false })
});
return res.json();
}
// 超时控制(AbortController)
async function withTimeout(url, ms) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), ms);
try {
const res = await fetch(url, { signal: ctrl.signal });
return res.json();
} finally {
clearTimeout(t);
}
}
fetch 只在网络层失败时 reject,HTTP 404/500 仍算"成功响应",需手动 if (!res.ok) throw。这是新手最常漏的一步。res.json() 返回 Promise,要 await;发送时 body 必须是字符串,用 JSON.stringify。扩展专题六 · 正则表达式文本处理的瑞士军刀
正则用 /pattern/flags 或 new RegExp。常用在校验、提取、替换。
// 校验邮箱(简化)
const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
console.log(emailRe.test("a@b.com")); // true
// 提取所有数字
const str = "订单 12 元,优惠 3 元";
const nums = str.match(/\d+/g); // ["12", "3"]
console.log(nums);
// 捕获组
const m = "2026-08-12".match(/(\d{4})-(\d{2})-(\d{2})/);
console.log(m[1], m[2], m[3]); // 2026 08 12
// 替换(用函数)
const r = "hello world".replace(/(\w+)/g, (w) => w.toUpperCase());
console.log(r); // HELLO WORLD
| 符号 | 含义 |
|---|---|
\d \w \s | 数字 / 词字符 / 空白 |
+ * ? | 1+/0+/0或1 |
{n,m} | n 到 m 次 |
^ $ | 开头 / 结尾 |
( ) | 捕获组 |
g i m | 全局 / 忽略大小写 / 多行 |
调试与常见错误排查会写更要会救火
1. 常用 console 方法
console.log("普通");
console.warn("警告");
console.error("错误");
console.table([{a:1},{a:2}]); // 表格展示
console.time("t"); /* ... */ console.timeEnd("t"); // 计时
console.dir(obj); // 展开对象结构
2. 断点调试
浏览器 DevTools 的 Sources 面板可打断点、单步、看调用栈与变量;Node 用 node --inspect 或 IDE 调试。比满屏 console.log 高效十倍。
3. 常见错误速查
| 错误 | 常见原因 | 排查 |
|---|---|---|
TypeError: xxx is not a function | 调用了 undefined/类型不对的方法 | 检查变量是否未初始化/拼写 |
Cannot read properties of undefined | 读了 null/undefined 的属性 | 用可选链 ?. |
ReferenceError | 用了未声明变量(严格模式) | 检查拼写/作用域 |
SyntaxError | 语法错(少括号/引号) | 看报错行附近的括号引号 |
Uncaught (in promise) | Promise 拒绝没 catch | 加 .catch 或 try/catch |
扩展专题七 · 模块深入ESM 的细节与坑
命名导出 vs 默认导出
// 命名导出:可多个
export const a = 1;
export function f() {}
// 默认导出:至多一个
export default class Main {}
// 导入
import Main, { a, f } from "./mod.js";
import * as mod from "./mod.js"; // 命名空间导入
import { a as alias } from "./mod.js"; // 别名
动态导入
// 按需加载(返回 Promise,可 await)
button.onclick = async () => {
const mod = await import("./heavy.js");
mod.run();
};
扩展专题八 · 日期与时间Date 的坑与正确姿势
JS 内置 Date 对象,但月份从 0 开始、对象可变,是经典坑。新项目也可考虑 Intl 做格式化。
const d = new Date(2026, 7, 12); // 注意:月份 7 表示八月!
console.log(d.getMonth()); // 7(即八月)
console.log(d.getFullYear()); // 2026
// 时间戳(毫秒)
console.log(Date.now()); // 当前时间毫秒数
const t = new Date(0); // 1970-01-01(Unix 纪元)
// 格式化
console.log(d.toISOString()); // 2026-08-11T...Z(UTC)
console.log(new Intl.DateTimeFormat("zh-CN").format(d)); // 2026/8/12
// 解析字符串要小心:YYYY-MM-DD 被当 UTC,其它当本地
console.log(new Date("2026-08-12")); // UTC 零点
console.log(new Date("2026/08/12")); // 本地零点
getMonth/getDay/getHours 等返回本地时区,混用 UTC 方法(getUTCMonth)易错;③ 直接比较 new Date() == new Date() 永远 false(对象引用)。需要精确时区处理建议用 day.js / date-fns / Luxon 库。Date.now() 毫秒或秒)或 ISO 字符串最稳,展示时再按用户时区格式化。扩展专题九 · JSON 处理前后端数据交换的通用语言
JSON.stringify 把对象转字符串,JSON.parse 转回对象。注意它只认双引号、不支持函数/undefined/循环引用。
const obj = { name: "小明", age: 18, tags: ["a", "b"] };
const s = JSON.stringify(obj); // '{"name":"小明","age":18,...}'
console.log(s);
const back = JSON.parse(s);
console.log(back.name); // 小明
// 美化输出
console.log(JSON.stringify(obj, null, 2));
// replacer 过滤 / reviver 转换
const safe = JSON.stringify(obj, (k, v) => k === "age" ? undefined : v);
console.log(safe); // 不含 age 字段
eval 解析 JSON——eval 会执行任意代码,有严重注入风险,用 JSON.parse。② 循环引用对象 JSON.stringify 会抛错(用 replacer 或库处理)。③ undefined、函数、Symbol 在序列化时会被忽略(或变 null)。JSON.parse(JSON.stringify(obj)) 是快速深拷贝,但会丢函数、undefined、Date(变字符串)、循环引用。要求高用 structuredClone 或 lodash。扩展专题十 · 错误处理与自定义错误让程序优雅失败
throw 抛错、try/catch/finally 捕获。自定义错误类便于区分错误类型。
// 自定义错误
class ValidationError extends Error {
constructor(msg) {
super(msg);
this.name = "ValidationError";
}
}
function checkAge(age) {
if (age < 0) throw new ValidationError("年龄不能为负");
return age;
}
try {
checkAge(-1);
} catch (e) {
if (e instanceof ValidationError) {
console.log("校验错:", e.message);
} else {
throw e; // 不认识的错继续往上抛
}
} finally {
console.log("清理资源");
}
catch 里空着会吞掉 bug;② 异步函数里的错误要用 try/catch(async)或 .catch(Promise),否则变未处理拒绝;③ finally 里的 return 会覆盖 try/catch 的返回值,谨慎。TypeError / RangeError / SyntaxError / ReferenceError 等,按 instanceof 区分处理更精准。生产项目用日志框架收集错误栈。扩展专题十一 · 数字与精度浮点陷阱与 BigInt
JS 的数字是 IEEE754 双精度浮点,整数安全范围约 ±9×10¹⁵。超大整数用 BigInt。
// 浮点精度
console.log(0.1 + 0.2); // 0.30000000000000004(不是 0.3)
console.log(0.1 + 0.2 === 0.3); // false
// 比较用误差:Math.abs(a - b) < 1e-9
// 金额用整数分: (0.1*100 + 0.2*100)/100 === 0.3
// 格式化
console.log((0.1 + 0.2).toFixed(2)); // "0.30"(字符串)
console.log(Number((0.1+0.2).toFixed(2))); // 0.3
// NaN 判断:用 Number.isNaN(不会把 "abc" 当 NaN)
console.log(Number.isNaN("abc")); // false
console.log(Number.isNaN(NaN)); // true
// BigInt:大整数,后缀 n
console.log(9007199254740991n + 2n); // 9007199254740993n
// 注意:BigInt 不能和 Number 混运算:1n + 1 会报错
NaN === NaN 是 false(NaN 不等于任何值,包括自己)。判断 NaN 必须用 Number.isNaN(x),别用全局 isNaN(它会先把参数转数字,isNaN("abc") 竟然是 true)。parseInt("08") 在旧引擎会按八进制解析得到 0;永远传第二参:parseInt("08", 10)。或用 Number("08")。扩展专题十二 · 生成器与迭代器惰性序列与协程雏形
迭代器协议:对象有 next() 返回 {value, done},for...of 就是消费它的语法糖。生成器(function*)用 yield 产出一个个值,能写惰性序列。
// 生成器:每次 yield 暂停,下次 next 继续
function* countUp() {
let i = 0;
while (true) {
yield i++;
}
}
const gen = countUp();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
// 惰性斐波那契
function* fib() {
let a = 0, b = 1;
while (true) { yield a; [a, b] = [b, a + b]; }
}
const f = fib();
console.log([f.next().value, f.next().value, f.next().value, f.next().value]); // 0 1 1 2
// 自定义可迭代对象
const range = {
from: 1, to: 3,
[Symbol.iterator]() {
let cur = this.from;
return { next: () =>
cur <= this.to ? { value: cur++, done: false } : { done: true } };
}
};
for (const x of range) console.log(x); // 1 2 3
扩展专题十三 · 性能优化要点别让页面卡
- 批量 DOM 操作:多次读写 DOM 会触发重排重绘。改样式前先读、用
DocumentFragment或innerHTML一次性插入。 - 事件委托:大量子元素用父级一个监听(见专题四)。
- 防抖节流:输入、滚动、resize 必用(见干货 10)。
- requestAnimationFrame:动画/视觉更新放进 rAF,对齐浏览器刷新节奏,比 setTimeout 更顺。
- 避免长任务:单个同步任务别超过 ~50ms,否则阻塞渲染。重计算用 Web Worker 放到后台线程。
- 懒加载:图片
loading="lazy"、路由级代码分割(动态 import)。
// 用 rAF 做平滑动画,而不是 setTimeout
function animate(timestamp) {
// 更新位置...
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
postMessage 与主线程通信。适合加密、压缩、大量计算等纯逻辑,把它们移出主线程保流畅。扩展专题十四 · 闭包实战:记忆化(memoize)用闭包做缓存
闭包不仅能做计数器,还能缓存函数结果:同样的参数只算一次,之后直接返回。
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// 斐波那契:朴素递归指数级,记忆化后线性
const fib = memoize(function (n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
});
console.log(fib(40)); // 瞬间(否则极慢)
cache 被返回的包装函数"记住",每次调用共享同一个 Map——这正是闭包:函数 + 它捕获的词法环境。搞懂这个,前面所有闭包用法都通了。扩展专题十五 · 函数式进阶纯函数、柯里化、组合
纯函数
相同输入永远相同输出、且无副作用(不改外部变量/DOM)。易测试、易并行、易推理。
// 纯:只看输入
const add = (a, b) => a + b;
// 不纯:依赖/修改外部
let total = 0;
const addTo = (x) => (total += x); // 副作用,别这么写核心逻辑
柯里化(curry)
把多参数函数转成"每次吃一个参数、返回新函数"的形式,便于复用与偏应用。
const curry = (fn) => {
return function curried(...args) {
return args.length >= fn.length
? fn.apply(this, args)
: (...more) => curried.apply(this, args.concat(more));
};
};
const sum3 = curry((a, b, c) => a + b + c);
console.log(sum3(1)(2)(3)); // 6
console.log(sum3(1, 2)(3)); // 6
函数组合(compose)
const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x);
const trim = s => s.trim();
const upper = s => s.toUpperCase();
const process = compose(upper, trim);
console.log(process(" hi ")); // "HI"
扩展专题十六 · 手写基础数据结构栈 / 队列 / 链表
栈(LIFO)
class Stack {
constructor() { this.items = []; }
push(x) { this.items.push(x); }
pop() { return this.items.pop(); }
peek() { return this.items[this.items.length - 1]; }
get size() { return this.items.length; }
}
const s = new Stack();
s.push(1); s.push(2);
console.log(s.pop(), s.peek()); // 2 1
队列(FIFO)
class Queue {
constructor() { this.items = []; }
enqueue(x) { this.items.push(x); }
dequeue() { return this.items.shift(); } // 头部出队
get size() { return this.items.length; }
}
const q = new Queue();
q.enqueue("a"); q.enqueue("b");
console.log(q.dequeue()); // a
单链表节点
class Node {
constructor(val) { this.val = val; this.next = null; }
}
const head = new Node(1);
head.next = new Node(2);
head.next.next = new Node(3);
// 遍历
let cur = head;
while (cur) { console.log(cur.val); cur = cur.next; } // 1 2 3
扩展专题十七 · 浏览器存储localStorage / sessionStorage / cookie
前端持久化三件套,都在浏览器环境(Node 运行器没有,需去浏览器试)。
// localStorage:持久,同源共享,约 5MB,同步 API
localStorage.setItem("token", "abc");
console.log(localStorage.getItem("token")); // "abc"
localStorage.removeItem("token");
// 存对象要先 JSON 序列化
localStorage.setItem("user", JSON.stringify({ name: "小明" }));
const u = JSON.parse(localStorage.getItem("user"));
// sessionStorage:关闭标签页即清,用法同 localStorage
sessionStorage.setItem("temp", "1");
// cookie:小(~4KB)、随每个请求自动发给服务器
document.cookie = "theme=dark; max-age=3600; path=/";
| 方式 | 生命周期 | 容量 | 是否随请求发送 |
|---|---|---|---|
| localStorage | 持久(手动清) | ~5MB | 否 |
| sessionStorage | 标签页关闭即清 | ~5MB | 否 |
| cookie | 可设过期 | ~4KB | 是(每次请求) |
扩展专题十八 · 补充算法模板面试常客收尾
数组乱序(Fisher–Yates)
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
console.log(shuffle([1, 2, 3, 4]));
深拷贝对比
// 快速但有损
const a = JSON.parse(JSON.stringify(obj));
// 现代内置(支持更多类型,仍不支持函数/DOM)
const b = structuredClone(obj);
// 复杂场景用 lodash.cloneDeep
HTML 转义(防 XSS 基础)
function escapeHtml(s) {
return s.replace(/[&<>"']/g, c => ({
"&": "&", "<": "<", ">": ">",
'"': """, "'": "'"
}[c]));
}
// 把用户输入插 DOM 前先转义,避免 <script> 注入
innerHTML 是 XSS 重灾区。要么用 textContent,要么先转义,要么用可信框架的自动转义。永远不要 eval 用户数据。JS 为什么是这样:设计哲学小史理解"怪"背后的原因
很多"坑"不是设计失误,而是历史约束下的权衡。了解背景,你会更包容也更会避坑。
- 十天设计:1995 年 Brendan Eich 被要求十天搞出一个给网页加交互的脚本语言,借鉴了 Scheme(函数为一等公民)和 Self(原型继承),又套了点 Java 的语法外壳。
- 动态弱类型:为了"非专业程序员也能上手",牺牲了编译期类型安全。代价就是
==转换、typeof null等怪现象。 - 单线程 + 事件循环:避免多线程共享内存的复杂性(当年浏览器也跑不动),用异步回调处理并发,成就了 Node 的高吞吐。
- 原型而非类:Self 的影响,对象直接委托给另一个对象,比传统类更灵活也更难懂。ES6 的
class只是补一层糖。 - 每年一版:从 ES3(1999)到 ES5(2009)停滞十年,ES6(2015)大爆发,此后每年小步快跑,语言越来越好用。
let/const、===、箭头函数、Promise/async、可选链,就能避开绝大多数历史包袱。别拿十年前的黑历史否定今天的 JS。一行代码技巧(收藏夹)小而美,但先看懂
// 数组去重
const uniq = a => [...new Set(a)];
// 求最大值
const max = a => a.reduce((x, y) => x > y ? x : y);
// 展平一层
const flat1 = a => a.flat();
// 分组(按 key 函数)
const groupBy = (a, f) => a.reduce((m, x) => ((m[f(x)] ||= []).push(x), m), {});
// 对象按值排序成数组
const sortByVal = obj => Object.entries(obj).sort((a, b) => b[1] - a[1]);
// 生成区间数组
const range = (n) => [...Array(n).keys()];
// 截断字符串
const truncate = (s, n) => s.length > n ? s.slice(0, n) + "…" : s;
// 随机整数 [min, max]
const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
groupBy 里的逗号运算符和 ||=)。炫技的前提是真正理解。JS 版本速查每年一版,认得出特性属于哪年
| 版本 | 年份 | 代表特性 |
|---|---|---|
| ES3 | 1999 | 正则表达式、try/catch |
| ES5 | 2009 | strict、JSON、Array 方法(forEach/map…) |
| ES6 / ES2015 | 2015 | let/const、箭头函数、类、Promise、解构、模板串、模块 |
| ES2016 | 2016 | Array.includes、幂运算符 ** |
| ES2017 | 2017 | async/await、Object.values/entries |
| ES2018 | 2018 | rest/spread 对象、async 迭代、Promise.finally |
| ES2019 | 2019 | Array.flat、Object.fromEntries、可选 catch 绑定 |
| ES2020 | 2020 | 可选链 ?.、空值合并 ??、动态 import、BigInt |
| ES2021 | 2021 | 逻辑赋值(&&=、||=、??=)、replaceAll |
| ES2022 | 2022 | 顶层 await、Array.at、类字段、静态块 |