🧭 编程学习站

JavaScript 入门 + 进阶训练

公网版 · 手机/电脑自适应

🐣 纯新手先看这一框(30 秒秒懂)

1

JS 适合做什么?网页交互(前端)、服务器(Node.js)、小程序、桌面(Electron)、移动端。一句话:哪里有运行时,哪里就能跑 JS

2

为什么要学它?它是 Web 的母语,前端绕不开;Node.js 让它也能写后端。学一门通吃前后端,性价比极高。

3

怎么跑起来?本站「动手实验室」选 JavaScript 点运行即可(Node.js 环境);浏览器里按 F12 打开控制台直接敲也行;本地装 Node 后 node xxx.js 运行。

4

新手最常踩的坑?== 会偷偷类型转换,用 ===;② var 会变量提升且函数作用域,用 let/const;③ 回调嵌套成"地狱";④ this 指向看"怎么调用"而非"在哪定义"。

课程说明从零掌握 JS 语法 + 异步/原型/性能要点

JavaScript 诞生于 1995 年(Brendan Eich 十天设计出来),最初只是给网页加点交互。如今它借着 Node.js(2009)和各大浏览器引擎(V8 等),已经成为前后端通吃的主流语言。它的特点是:动态弱类型、基于原型、单线程 + 事件循环、函数是一等公民

本课程假设你有最基础编程概念。重点放在JS 区别于 Java/C++ 的关键差异:没有类型的编译期约束、this 的四套绑定规则、原型链继承、以及最让新手头大的异步与事件循环。进阶部分直击面试高频:闭包、Promise、事件循环、Proxy、V8 优化等。

运行环境 浏览器里 JS 能操作 DOM、发请求;Node.js 里能读写文件、起服务器,但没有 DOM。本站运行器用的是 Node.js 22,所以 DOM 相关代码(如 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 是你的朋友 JS 没有"打印到屏幕"的标准说法,调试靠 console.log。浏览器按 F12 打开 DevTools 的 Console 面板即可交互执行。Node 下直接命令行 node 进入 REPL 也能一行行试。
本站运行器限制 运行器是 Node.js 环境,没有 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 保证的是"变量到值的绑定不变",不是"值不可变"。const obj = {a:1}; obj.a = 2; 完全合法(改的是对象内部)。想真正冻结用 Object.freeze
默认用 const 能写 const 就写 const,需要重新赋值才用 let永远别用 var。这是现代 JS 的共识,能避开一大类作用域 bug。

3. 数据类型与类型转换原始类型、typeof、隐式转换

JS 有 7 种原始类型 + 对象:number / string / boolean / undefined / null / symbol / bigintobject。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
隐式转换的坑 JS 会在运算时偷偷转换类型:"5" - 3 === 2(字符串转数字),但 "5" + 3 === "53"(数字转字符串拼接)。这种不一致极易出错,=== 并主动转换Number("5")String(3)Boolean(x)
falsy 值 以下在布尔上下文为 false0 / -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
== 的奇怪结果 [] == falsetrue[] == ![] 也是 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...in 会遍历到原型链上可枚举的属性,且顺序不保证。遍历数组请用 for...offorEachfor...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"。这让它特别适合做回调(如数组方法里)。但若你想在对象方法里用 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();
对象即字典 JS 里对象常当 Map 用: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 不是真正的类 class 没有"类提升"(前有暂时性死区),方法默认不可枚举,且本质是函数。别把它当 Java 那种传统类——底层永远是原型链。理解原型链,才能看懂继承、instanceof、原型方法污染等坑(详见干货第 4 条)。
instanceof 原理 a instanceof B 实质是:沿 a 的原型链找,看有没有 B.prototype。所以它能跨窗口/框架失效(不同全局的同一"类"不是同一个 prototype)。

10. 模块:ESM 与 CommonJSimport/export vs require/module.exports

现代前端/Node 用 ES Module(ESM)export 导出、import 导入。老 Node 用 CommonJSmodule.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");
两套别混 ESM 是静态的(import 必须在顶层、编译期确定),CommonJS 是动态的(require 可在任意位置)。浏览器原生支持 ESM(需 <script type="module">),但本站 Node 运行器里直接写 import 可能需 CommonJS 风格或顶层。示例逻辑通用,落地时按环境选。
命名导出 vs 默认导出 一个模块可多个 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
闭包内存泄漏 闭包会让外部变量一直活着,无法被 GC。常见泄漏:在循环里给 DOM 绑定事件,回调捕获了整个大对象或外部 DOM 引用,导致这些对象用不着了也回收不掉。解决:回调里只引用真正需要的变量,不用了及时解绑事件(详见干货第 2 条)。
闭包的正面价值 模块模式、防抖节流、React hooks 的useState 背后都是闭包。理解它,你才看得懂大半 JS 高级写法。

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
};
回调里 this 丢失 把对象方法当回调传出去(如 setTimeout(obj.show, 100)),this 会变成默认绑定(undefined/全局),方法内访问 this.x 报错。解决:用箭头函数、或 bind、或把方法写成箭头函数属性。这是面试和实战都极高发的坑(详见干货第 3 条)。
记忆口诀 "谁调用,this 就是谁;new/call/apply/bind 能强制指定;箭头函数没有 this,看外层。"

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));
不可逆 & 只决一次 Promise 一旦 settled(成功或失败)就定死,之后再 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 范围 & 并行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 0 不是立即 setTimeout(fn, 0) 把 fn 放进宏任务队列,要等当前同步代码和所有微任务跑完才轮到它。所以 Promise 的回调总是先于 setTimeout 执行。理解这点,才能预测异步输出的顺序(高频面试题,详见干货第 1 条)。
微任务清单 微任务:Promise 的 .then/.catch/.finallyqueueMicrotaskMutationObserver。宏任务: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/Reflect 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 与事件循环顺序

每个框都能用下拉切换语言、「↺ 重置」回到初始代码。想写完整程序或体验其他语言,去顶部「练习场」(支持 Python/Java/Go/JS/TS/Rust/C#/Ruby 等)。注意:运行器是 Node.js,document/window 等浏览器 API 用不了。

实战干货(12 条工业级技巧)面试与上线都绕不开

干货 1 · 事件循环(宏任务 vs 微任务) JS 单线程,异步靠事件循环:先执行完当前同步代码,再清空所有微任务(Promise 回调、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)
干货 2 · 闭包内存泄漏 闭包会让外部变量常驻内存。典型泄漏:循环里给大量 DOM 元素绑定事件,回调捕获了整个大组件或外部 DOM 引用,导致这些对象用不着了也回收不掉;或定时器回调持有大对象忘了 clearTimeout。解决:回调只引用必要变量、不再需要时解绑事件/清除定时器。
// 危险:定时器一直持有 big 不释放
let big = new Array(1e6).fill(1);
const t = setInterval(() => console.log(big.length), 1000);
// 不用了要清:clearInterval(t); big = null;
干货 3 · this 四规则 默认绑定:普通调用 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(显式)
干货 4 · 原型链 vs class 语法糖 ES6 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
干货 5 · Promise/A+ 状态机 Promise 有 pending / fulfilled / rejected 三态,一旦从 pending 变成 fulfilled 或 rejected 就不可逆,之后再加的 then 回调仍按最终状态触发(值会被记住)。then(onFulfilled, onRejected) 返回新 Promise,支持链式;错误沿链向下直到被 catch 捕获。
附:不要吞拒绝Promise 失败没人 catch 会抛出 unhandledRejection。async 函数里忘了 try/catch 也可能让错误"消失"。养成"每个 Promise 链都有 catch"的习惯。
干货 6 · async/await 错误处理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)));
干货 7 · WeakMap / WeakRef 弱引用 WeakMap 的键是弱引用:键对象没有其他引用时,会被 GC 回收,对应条目随之消失——适合做"对象 → 元数据"的私有缓存,不阻止原对象回收。WeakRef 可持有对象的弱引用(通过 .deref() 取,可能已 null)。普通 Map 的键是强引用,会阻止 GC。
const cache = new WeakMap();
let obj = { id: 1 };
cache.set(obj, "元数据");
obj = null;   // obj 可被回收,cache 中对应条目随之清除
干货 8 · Proxy / Reflect 元编程 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
干货 9 · V8 隐藏类与内联缓存 V8 给对象建"隐藏类(Shape/Map)"来加速属性访问。若运行时随意增删属性或改变属性添加顺序,对象的隐藏类会变,内联缓存(IC)失效,性能下降。别用 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;
干货 10 · 防抖与节流 防抖(debounce):连续触发只在停止后执行一次(搜索框输入)。节流(throttle):固定时间间隔内最多执行一次(滚动/resize 监听)。手写不难,但生产推荐用 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); }
  };
}
干货 11 · ESM vs CommonJS ESMimport/export)是静态的、可在编译期分析、支持 tree-shaking,浏览器与现代化构建原生支持;CommonJSrequire/module.exports)是动态的、Node 传统模块。新项目用 ESM;老 Node 库可能是 CJS,引入时用默认导入或 import ... from 'cjs' 的互操作。两者混用要注意循环依赖行为不同。
附:浏览器条件浏览器里 ESM 需 <script type="module">,且受同源/CORS 限制;CommonJS 在浏览器里不能直接用,要靠打包工具(Webpack/Vite)转译。
干货 12 · 严格模式陷阱 "use strict" 下:① 普通函数调用 thisundefined(非严格指向全局,更易写出全局污染 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"NaNInfinity
string"string"不可变
boolean"boolean"true / false
undefined"undefined"未赋值
null"object"(bug)空值,用 === null 判断
symbol"symbol"唯一不可变值,做键
bigint"bigint"大整数,123n

2. falsy 值清单

布尔上下文
0 / -0 / NaNfalse
""(空串)false
null / undefinedfalse
falsefalse
"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 个坑每条都值得记下来

  1. == 比较:隐式转换出怪事,一律 ===
  2. var:函数作用域 + 提升,改用 let/const
  3. 认为 const 完全不可变:它只锁绑定,对象内部可改;要冻结用 Object.freeze
  4. typeof null 是 "object":历史 bug,判断 null 用 === null
  5. 数组也是 objecttypeof [] 为 "object",判断数组用 Array.isArray
  6. 对象/数组比较是引用[1]===[1] 为 false,比内容要逐元素。
  7. 字符串比较用 ==:实际可用 ===,但排序要用 localeCompare,别用 >/< 比汉字。
  8. sort 默认按字符串排:数字要 arr.sort((a,b)=>a-b)
  9. forEach 里 return 退不出外层:要提前结束用 some/every 或 for 循环。
  10. for...in 遍历数组:会带原型属性、顺序不定,数组用 for...of
  11. 闭包循环变量var 循环里闭包共享同一变量,用 let 或 IIFE 解决。
  12. this 丢失:把方法当回调传出去 this 变默认绑定,用箭头函数/bind。
  13. 回调地狱:嵌套回调难维护,用 Promise/async。
  14. 忘记 catch Promise 拒绝:未处理的拒绝会抛 unhandledRejection。
  15. 连续 await 串行:不相关的任务应 Promise.all 并行。
  16. delete 对象属性:破坏 V8 隐藏类、性能下降,用置 undefined 代替。
  17. 随意改变属性顺序:对象的隐藏类依赖初始化顺序,保持一致更快。
  18. || 当默认值掩盖 0/空串:想保留假值用 ??
  19. 浮点精度0.1+0.2!==0.3,金额用整数分或 decimal 库。
  20. 全局变量污染:非严格下给未声明变量赋值会建全局变量,开严格模式。
一句话JS 的坑大多来自"动态 + 弱类型 + 异步 + 原型"。把本课程的干货和这份清单记熟,能避开 90% 的初级 bug。

综合小项目 · 待办清单(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 问边学边想

Q1 · var、let、const 区别?作用域:var 函数级、let/const 块级;提升:var 提升且可先用(undefined),let/const 有暂时性死区;const 绑定不可改(对象内部可改)。现代一律 let/const。
Q2 · == 和 === 区别?== 先隐式类型转换再比较,结果反直觉;=== 严格比类型和值。永远用 ===。
Q3 · 闭包是什么、有什么用?函数捕获其词法作用域的变量,即使外层已返回仍可访问。用于私有状态、工厂、回调、防抖节流、模块模式。注意它会导致变量常驻内存(潜在泄漏)。
Q4 · this 指向怎么判断?看调用方式:new > call/apply/bind > 对象调用 > 默认(严格 undefined)。箭头函数没有 this,取外层。
Q5 · 事件循环讲一下?单线程,先跑同步,再清空微任务(Promise 等),再取一个宏任务(setTimeout 等)。微任务优先。由此决定异步输出顺序。
Q6 · Promise 状态?pending → fulfilled/rejected,不可逆。then/catch 返回新 Promise 支持链式,错误向下传递直到被 catch。
Q7 · async/await 本质?Promise 的语法糖,把异步写得像同步。不阻塞线程,遇到 await 交还控制权。错误处理用 try/catch,并行用 Promise.all。
Q8 · 原型和原型链?对象通过 __proto__ 链接到其构造函数的 prototype,读属性沿链向上找。class 是语法糖,底层仍是原型。instanceof 沿原型链判断。
Q9 · 防抖和节流?防抖:停止触发后执行一次(输入搜索);节流:固定间隔最多一次(滚动)。场景不同,别混。
Q10 · 深拷贝怎么实现?简单对象用 structuredClone(现代)或 JSON.parse(JSON.stringify())(丢函数/undefined/循环引用);复杂用 lodash.cloneDeep 或手写递归。
Q11 · 0.1 + 0.2 为什么不等于 0.3?JS 用 IEEE754 双精度浮点,十进制小数无法精确表示,产生舍入误差。金额用整数分或 decimal 库比较。
Q12 · 为什么要避免 delete 属性?V8 用隐藏类加速属性访问,delete 会改隐藏类使内联缓存失效、性能下降。替代:置 undefined 或重新设计数据结构。

代码风格与工程规范写好 JS 的软实力

规范点推荐反面
变量默认 const,需改才 let用 var
相等=== / !==== / !=
字符串模板字符串 `...${x}`大量 + 拼接
函数箭头函数做回调function 嵌套易丢 this
默认值?? 保留 0/空串|| 掩盖 falsy
数组处理map/filter/reduce手写 for + push
异步async/await + try/catch嵌套回调
分号统一风格(推荐省略,靠构建)时有时无
用工具兜底ESLint 统一规则、Prettier 统一格式,交给 CI。团队项目别靠人肉 review 抠空格和引号。
分号自动插入(ASI)陷阱JS 可省略分号,但某些换行会被错误插入分号:以 [ ( + - / 开头的行容易和上一行连起来。最稳的是让构建工具/Prettier 统一处理。

学完之后怎么走路线图

  1. 夯实语法:10 节入门 + 6 节进阶练熟,每个例子自己敲改。
  2. DOM 与浏览器document 操作、事件、BOM(本站运行器无 DOM,去浏览器练)。
  3. 异步深入:事件循环、fetch、AbortController、并发控制。
  4. 工程化:npm、打包(Vite/Webpack)、ESLint、单元测试(Vitest/Jest)。
  5. 框架:React 或 Vue(理解了原型/响应式/闭包,学框架很快)。
  6. Node 后端:Express/Fastify、数据库、鉴权,打通全栈。
  7. 性能与原理:V8 优化、内存分析、Web 性能,往资深走必学。
别踩的弯路别只背 API 不动手;别一上来就啃框架却不懂 JS 本身(闭包/this/异步是框架地基);别忽视严格模式与类型(想更稳可学 TypeScript,本站也有 TS 课程)。
给纯新手的最后一句话JS 是"边做边懂"的语言。每天写 30 行、改 30 行,配合本站「练习场」和「动手实验室」,一个月后你能写出真正能跑的小应用。

扩展专题一 · 深度事件循环与异步把最难的点吃透

事件循环是 JS 异步的心。一次循环(tick)的流程:

  1. 执行一个宏任务(如当前同步脚本)。
  2. 清空微任务队列(反复取,直到为空)。
  3. 必要时渲染(requestAnimationFrame 在渲染前)。
  4. 取下一个宏任务,回到第 1 步。
// 微任务里再塞微任务,会一直清空(不会被宏任务插队)
Promise.resolve().then(() => {
  console.log("微1");
  Promise.resolve().then(() => console.log("微2"));
});
setTimeout(() => console.log("宏"), 0);
// 输出:微1 微2 宏
微任务饿死宏任务若在微任务里无限塞微任务,当前宏任务之后的宏任务永远没机会执行(页面卡死)。真实代码别这么干。
async 函数里的顺序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
浅代理上面的 Proxy 只代理第一层。嵌套对象(如 {user:{name}})需要递归 proxy 才能真正追踪深层变化,这正是 Vue3 的 reactive 内部做的事。
Reflect 的意义在 Proxy 陷阱里用 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); }
  };
}
造轮子 vs 用库防抖节流生产用 lodash;上面实现帮你理解原理,面试常考手写。

设计模式入门(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"));
模式是工具小项目直接写清楚;当"这里总在加 if 分支/总在改"时再引入对应模式。观察者模式是前端框架事件总线的雏形。

单元测试入门(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 与其他语言对比别盲目站队

维度JavaScriptTypeScriptPythonJavaGo
类型动态弱静态(JS 超集)动态强静态强静态强
运行引擎(V8)编译成 JS解释JVM原生机器码
并发事件循环事件循环GIL 受限线程goroutine
主战场前端/全栈大型前端/全栈脚本/AI后端/安卓云服务
上手
怎么选做网页/全栈选 JS/TS;想更稳、大型项目用 TypeScript(本站有 TS 课);后端/大数据用 Java;云原生用 Go;数据/AI 用 Python。
类型不是壁垒JS 学的闭包、异步、原型思想,迁移到 TS、Python、Java 都通用。本站还有 C/C++/Java/Python/Go/TS/Rust/C#/Ruby 课程,学有余力横向对比理解更深。

术语中英对照读文档不卡壳

<="td">空值合并
中文英文一句话
原始类型primitivenumber/string 等直接存值
作用域scope变量可见范围
提升hoisting声明被移到作用域顶部
暂时性死区TDZlet/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 兜底
元编程metaprogrammingProxy/Reflect 拦截对象
严格模式strict mode"use strict"
模块moduleESM/CommonJS
读法建议这些词在报错栈、MDN、Stack Overflow 里高频出现。混个脸熟,读英文资料会顺很多。

21 天练习清单照着练

天数主题今日任务
1-2环境/语法装 Node、跑 Hello;let/const、类型转换各写 10 行
3-4流程/函数写猜数字、箭头函数、默认/剩余参数
5-6数组高阶map/filter/reduce 改写求和、去重、分组
7-8对象/解构对象字面量、解构、默认参数对象
9-10原型/class继承树 + instanceof 验证原型链
11闭包计数器、私有状态、模块模式
12-13this四规则练习 + 修正回调 this 丢失
14-15Promise链式、all、错误捕获
16-17async/await串行 vs 并行、try/catch 边界
18事件循环预测多段异步输出顺序
19ES6+模板串、展开、可选链、Proxy 小 demo
20综合项目完成 Todo 项目专题
21复盘重做自测题 + 面试题,查漏补缺
关键心法每天写代码时间 > 看教程时间。卡住就回到对应章节改一改、跑一跑。本站「动手实验室」「练习场」随时可用。

常见问题 FAQ新手问得最多

Q:JS 和 Java 什么关系?A:基本没关系,只是名字营销撞车。一个动态弱类型跑在引擎、一个静态强类型跑在 JVM。本站另有 Java 课程。
Q:在哪写 JS?A:浏览器按 F12 控制台;本地装 Node 后 node xxx.js;本站运行器(Node 环境)直接跑纯逻辑。
Q:为什么本站运行器里 document 报错?A:运行器是 Node.js,没有浏览器 DOM。DOM 操作请去浏览器控制台验证。
Q:学 JS 要先学 HTML/CSS 吗?A:想做网页交互最好懂点 HTML/CSS,但 JS 语言本身可独立学(如 Node 写脚本)。本站聚焦 JS 语言。
Q:要不要直接学 TypeScript?A:TS 是 JS 的超集,先懂 JS(尤其类型相关的坑)再学 TS 事半功倍。本站有 TS 课程可衔接。
最后提醒报错时把错误信息完整读一遍,答案常在那几行里。善用 console.log 和浏览器断点。

写 JS 前的小自查清单交代码前扫一眼

  • 比较是否用了 === 而非 ==
  • 是否优先 const、必要时 let、没用 var
  • 数组排序是否传了比较器(数字)?
  • 回调里 this 是否会丢失(用箭头/bind)?
  • 连续 await 是否应改 Promise.all 并行?
  • Promise 链是否都有 catch
  • 是否用 delete 删属性(应置 undefined)?
  • 默认值用 ?? 还是 ||(是否要保留 0/空串)?
  • 对象/数组比较是否误用 ===(应比内容)?
  • 是否开了严格模式 / 用了 ESLint?
习惯成自然这 10 条覆盖 90% 新手错误。每次写完对照一遍,一个月后基本不会犯了。

延伸资源学完去哪深造

学习节奏每天 30 分钟 + 写代码,比周末突击 5 小时效果好。把"看懂→改写→独立写"循环起来,JS 很快上手。

扩展专题四 · 浏览器 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 不自动报错fetch 只在网络层失败时 reject,HTTP 404/500 仍算"成功响应",需手动 if (!res.ok) throw。这是新手最常漏的一步。
JSON 便捷res.json() 返回 Promise,要 await;发送时 body 必须是字符串,用 JSON.stringify

扩展专题六 · 正则表达式文本处理的瑞士军刀

正则用 /pattern/flagsnew 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全局 / 忽略大小写 / 多行
别过度用正则复杂文本解析(如 HTML)用正则易出错,优先用专门的解析器/库。正则适合"格式校验"和"简单提取"。

调试与常见错误排查会写更要会救火

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();
};
循环依赖ESM 能处理循环依赖(用"活绑定"),但逻辑上应尽量避免:A 导入 B、B 又导入 A 往往意味着设计问题,拆出公共模块更清晰。CommonJS 的循环依赖行为不同且更易踩坑。
tree-shakingESM 是静态的,打包工具能分析出"哪些导出没被用到"并摇树删除,减小产物体积。所以新项目、库都优先 ESM。

扩展专题八 · 日期与时间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"));     // 本地零点
Date 的坑① 月份 0-based(8 月要写 7);② 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)) 是快速深拷贝,但会丢函数、undefinedDate(变字符串)、循环引用。要求高用 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 的返回值,谨慎。
错误类型JS 内置 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 会报错
别用 == NaNNaN === NaN 是 false(NaN 不等于任何值,包括自己)。判断 NaN 必须用 Number.isNaN(x),别用全局 isNaN(它会先把参数转数字,isNaN("abc") 竟然是 true)。
parseInt 要传进制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
为什么有用生成器适合"无限/超长序列"按需产出,不占内存;也是理解异步(早期 co 库用生成器做协程)和历史演进的钥匙。现代异步直接用 async/await,但生成器在处理流式数据、自定义遍历时仍好用。

扩展专题十三 · 性能优化要点别让页面卡

  • 批量 DOM 操作:多次读写 DOM 会触发重排重绘。改样式前先读、用 DocumentFragmentinnerHTML 一次性插入。
  • 事件委托:大量子元素用父级一个监听(见专题四)。
  • 防抖节流:输入、滚动、resize 必用(见干货 10)。
  • requestAnimationFrame:动画/视觉更新放进 rAF,对齐浏览器刷新节奏,比 setTimeout 更顺。
  • 避免长任务:单个同步任务别超过 ~50ms,否则阻塞渲染。重计算用 Web Worker 放到后台线程。
  • 懒加载:图片 loading="lazy"、路由级代码分割(动态 import)。
// 用 rAF 做平滑动画,而不是 setTimeout
function animate(timestamp) {
  // 更新位置...
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Web Worker 限制Worker 不能直接操作 DOM,通过 postMessage 与主线程通信。适合加密、压缩、大量计算等纯逻辑,把它们移出主线程保流畅。
测量再优化用浏览器 Performance 面板/DevTools 找到真正瓶颈,别凭感觉微优化。多数卡顿来自"不合理的长同步任务"和"频繁 DOM 读写"。

扩展专题十四 · 闭包实战:记忆化(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));  // 瞬间(否则极慢)
权衡记忆化用空间换时间,适合"纯函数 + 重复调用 + 计算贵"的场景。缓存键用 JSON 有局限(函数/循环引用),生产用更稳健的键策略或库(如 lodash.memoize 支持 resolver)。
闭包本质回顾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"
为什么学这些不一定要手写 curry/compose(库里有),但理解后能写出"数据从右流向左、每步纯变换"的清晰管道,是 Redux、RxJS、函数式框架的思维基础。

扩展专题十六 · 手写基础数据结构栈 / 队列 / 链表

栈(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
面试价值栈/队列/链表是算法题高频结构。手写一遍能加深"指针/引用"理解,比只调数组 API 收获大。生产里优先用内置数组/Map,自写结构多为面试与特定优化场景。
复杂度直觉数组尾插 O(1)、头插/头删 O(n)(要搬移);链表头插/头删 O(1)、随机访问 O(n)。按访问模式选型。

扩展专题十七 · 浏览器存储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是(每次请求)
注意① localStorage 只能存字符串,对象要 JSON;② 它是同步的,存大对象会阻塞;③ 任何能访问页面的 JS 都能读它,别存密码/令牌等敏感信息;④ 用户可清缓存,不能当数据库。敏感状态放 httpOnly cookie(JS 读不到)更安全。

扩展专题十八 · 补充算法模板面试常客收尾

数组乱序(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 => ({
    "&": "&amp;", "<": "&lt;", ">": "&gt;",
    '"': "&quot;", "'": "&#39;"
  }[c]));
}
// 把用户输入插 DOM 前先转义,避免 <script> 注入
安全红线把用户输入直接拼进 HTML 或 innerHTML 是 XSS 重灾区。要么用 textContent,要么先转义,要么用可信框架的自动转义。永远不要 eval 用户数据。
算法小结上面这些模板(乱序、深拷贝、去重、防抖、闭包、递归)覆盖了 80% 入门算法题。动手敲熟,别只看。

JS 为什么是这样:设计哲学小史理解"怪"背后的原因

很多"坑"不是设计失误,而是历史约束下的权衡。了解背景,你会更包容也更会避坑。

  • 十天设计:1995 年 Brendan Eich 被要求十天搞出一个给网页加交互的脚本语言,借鉴了 Scheme(函数为一等公民)和 Self(原型继承),又套了点 Java 的语法外壳。
  • 动态弱类型:为了"非专业程序员也能上手",牺牲了编译期类型安全。代价就是 == 转换、typeof null 等怪现象。
  • 单线程 + 事件循环:避免多线程共享内存的复杂性(当年浏览器也跑不动),用异步回调处理并发,成就了 Node 的高吞吐。
  • 原型而非类:Self 的影响,对象直接委托给另一个对象,比传统类更灵活也更难懂。ES6 的 class 只是补一层糖。
  • 每年一版:从 ES3(1999)到 ES5(2009)停滞十年,ES6(2015)大爆发,此后每年小步快跑,语言越来越好用。
启示语言的"怪"往往有历史原因,但现代 JS(ES2022+)已经很好用:用 let/const===、箭头函数、Promise/async、可选链,就能避开绝大多数历史包袱。别拿十年前的黑历史否定今天的 JS。
学语言的态度理解"它为什么这样",比死记"它是什么"更有用。遇到反直觉的行为,多问一句"这背后解决了什么问题",你会成为真正懂语言的人,而不只是会用 API 的人。

一行代码技巧(收藏夹)小而美,但先看懂

// 数组去重
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 课程从语法、异步、原型到性能、安全,主干都过了一遍。下一步:去浏览器写个小项目,把"动手实验室"和"练习场"用透。写得多,才记得牢。

JS 版本速查每年一版,认得出特性属于哪年

版本年份代表特性
ES31999正则表达式、try/catch
ES52009strict、JSON、Array 方法(forEach/map…)
ES6 / ES20152015let/const、箭头函数、类、Promise、解构、模板串、模块
ES20162016Array.includes、幂运算符 **
ES20172017async/await、Object.values/entries
ES20182018rest/spread 对象、async 迭代、Promise.finally
ES20192019Array.flat、Object.fromEntries、可选 catch 绑定
ES20202020可选链 ?.、空值合并 ??、动态 import、BigInt
ES20212021逻辑赋值(&&=、||=、??=)、replaceAll
ES20222022顶层 await、Array.at、类字段、静态块
认特性不认版本日常不用背年份,知道"这个语法大概哪年起能用、目标浏览器/Node 版本支不支持"即可。用 Babel/构建工具可把新语法转译到旧环境。
本 JavaScript 课程遵循现代 ECMAScript(ES2022+)常用子集,手机与电脑端自适应,配套本站运行器(Node.js 22)练习效果最佳。 公网参考:MDN · javascript.info · ECMAScript 规范 (TC39)。
🛠 在线练习