前端面试宝典:从"hello world"到"hello 面试官"
# 🚀 前端面试宝典:从"hello world"到"hello 面试官"
> **警告**:本文档含有以下成分——大量段子、过量代码、以及一个程序员对面试的复杂情感。请在一个没有 HR 的安静环境中阅读。
---
## 📋 阅读指南
| 难度标识 | 含义 | 适合人群 |
| -------- | ---------------------------- | ----------------- |
| 🟢 青铜 | 基础概念,背就完事了 | 实习生 / 校招小白 |
| 🟡 黄金 | 需要理解原理,能讲清楚为什么 | 1-3 年经验 |
| 🔴 王者 | 深挖底层,能手写实现 | 3-5 年 / 面大厂 |
| ⚫ 地狱 | 源码级理解,能教面试官做事 | 大佬请随意 |
---
# 第一章:数字的"精分"现场 —— 0.1 + 0.2 !== 0.3
## 🟢 原题回顾
> **问**:`0.1 + 0.2 === 0.3` 吗?为什么?
## 🟡 通俗讲解
**答**:不等于。这就好比你跟老板说"我加班了 0.1 天和 0.2 天",老板一算"哦,那给你算 0.30000000000000004 天的调休吧"。你:???
JavaScript 的 `Number` 类型遵循 **IEEE 754 双精度浮点数标准**,用 64 位表示一个数字:
```
┌──┬───────────┬────────────────────────────────────────────────────┐
│ │ 11 位 │ 52 位 │
│符号│ 指数位 │ 尾数位 │
└──┴───────────┴────────────────────────────────────────────────────┘
```
- **1 位符号位**:0 是正数,1 是负数("你看这像不像你的银行卡余额符号?")
- **11 位指数位**:表示小数点往哪飘
- **52 位尾数位**:有效数字,也就是精度所在
**最大安全整数**:`Number.MAX_SAFE_INTEGER = 2^53 - 1 = 9007199254740991`
为什么是 53 位而不是 52 位?因为 IEEE 754 规定尾数前面隐含了一个 1(规格化数),所以实际有效位是 53 位。这就像你买奶茶,菜单上写 52 种配料,但老板说"首位顾客默认加珍珠",所以实际是 53 种。
## 🔴 深挖原理
### 第一步:进制转换的"无限循环"
十进制的 0.1 转换成二进制:
```
0.1(十进制) = 0.0001100110011001100110011001100110011001100110011...(二进制)
```
你会发现 `0011` 这四个数字像复读机一样无限循环。这就像你试图用有限的人生去理解无限的工作量——**精度注定丢失**。
### 第二步:对阶运算的"截断惨案"
```javascript
// 0.1 的二进制近似值
0.1 -> 0x3fb999999999999a // 实际存储的值略大于 0.1
// 0.2 的二进制近似值
0.2 -> 0x3fc999999999999a // 实际存储的值略大于 0.2
// 相加后
0.1 + 0.2 -> 0.30000000000000004
```
**精度丢失的两个阶段**:
1. **进制转换阶段**:十进制小数转二进制时,无限循环被截断
2. **对阶运算阶段**:两个数的指数不同时,需要对齐小数点,低位被截断
## ⚫ 地狱模式:手写一个安全的浮点数比较函数
```javascript
/**
* 浮点数安全比较
* @param {number} a
* @param {number} b
* @param {number} epsilon 精度阈值,默认 Number.EPSILON
*/
function safeEqual(a, b, epsilon = Number.EPSILON) {
return Math.abs(a - b) < epsilon;
}
// 使用
console.log(safeEqual(0.1 + 0.2, 0.3)); // true
// 更严谨的版本(处理极大数和极小数)
function ultraSafeEqual(a, b, epsilon = Number.EPSILON) {
if (a === b) return true;
const diff = Math.abs(a - b);
const sum = Math.abs(a) + Math.abs(b);
return diff < Math.max(epsilon * sum, Number.MIN_VALUE);
}
```
## 🎯 发散衍生
### 🟡 衍生 1:为什么 `0.1 + 0.2 !== 0.3`,但 `0.1 + 0.2 === 0.30000000000000004`?
因为 JavaScript 在显示数字时会做**最短近似表示**,但底层存储的就是那个无限接近 0.3 但不等于 0.3 的二进制浮点数。
```javascript
console.log(0.1 + 0.2); // 0.30000000000000004
console.log((0.1 + 0.2).toFixed(20)); // "0.30000000000000004441"
console.log((0.1 + 0.2).toPrecision(16)); // "0.3000000000000000"
```
### 🔴 衍生 2:哪些语言也有这个问题?
| 语言 | 是否有此问题 | 解决方案 |
| ---------- | ------------ | ------------------------ |
| JavaScript | ✅ | 使用整数运算或第三方库 |
| Java | ✅ | `BigDecimal` |
| Python | ✅(默认) | `decimal` 模块 |
| C/C++ | ✅ | 没有银弹,自求多福 |
| PHP | ✅ | `bcadd()` 等 BCMath 函数 |
### ⚫ 衍生 3:电商系统中价格计算怎么做?
```javascript
// ❌ 错误示范:直接用浮点数算钱
const price = 0.1;
const quantity = 3;
console.log(price * quantity); // 0.30000000000000004
// 老板:"你多收了我 4e-17 块钱?"
// ✅ 正确姿势:全部转成整数(分)运算
const priceInCents = 10; // 0.1 元 = 10 分
const totalInCents = priceInCents * quantity; // 30
const total = totalInCents / 100; // 0.3
// ✅ 或者使用专门的库
// npm install decimal.js
const Decimal = require('decimal.js');
const a = new Decimal(0.1);
const b = new Decimal(0.2);
console.log(a.plus(b).toNumber()); // 0.3
```
### 🔴 衍生 4:`Number.EPSILON` 是什么?
```javascript
console.log(Number.EPSILON); // 2.220446049250313e-16
```
这是 1 与大于 1 的最小浮点数之间的差值。可以理解为"浮点数世界的量子尺度"——比这个更小的差异,在浮点数宇宙里被视为"同一个点"。
---
# 第二章:JS 数据类型 —— "七种武器"与"万物皆对象"
## 🟢 原题回顾
> **问**:JS 有哪些数据类型?Number 的存储空间是多大?如果后台发送了一个超过最大字节的数字怎么办?
## 🟡 通俗讲解
JavaScript 的数据类型就像火锅店的菜品分类:
**基本类型(Primitive)**—— 单点菜品,各吃各的:
- `Number`:数字(包括整数和浮点数,以及三个"怪胎":`NaN`、`Infinity`、`-Infinity`)
- `String`:字符串
- `Boolean`:`true` / `false`
- `null`:空值("这里曾经有个对象,现在它出家了")
- `undefined`:未定义("这里本来就没打算放东西")
- `Symbol`(ES6):独一无二的标识符
- `BigInt`(ES2020):超大整数("Number 装不下的梦想,我来装")
**引用类型(Reference)**—— 火锅锅底,大家共享:
- `Object`:万物之母
- `Array`:有序列表
- `Function`:可调用对象
- `Date`、`RegExp`、`Map`、`Set` 等
**Number 的存储空间**:64 位(8 字节),最大安全整数是 `2^53 - 1`。
如果后台发送了超过最大安全整数的数字,会发生**精度截断**:
```javascript
const huge = 9007199254740993; // 超过 MAX_SAFE_INTEGER
console.log(huge); // 9007199254740992(精度丢失!)
// 解决方案:后台传字符串,前端用 BigInt
const safeHuge = BigInt("9007199254740993");
console.log(safeHuge); // 9007199254740993n
```
## 🔴 深挖:`typeof null === 'object'` 这个千古奇冤
```javascript
console.log(typeof null); // "object"
```
**历史原因**:在 JavaScript 第一个版本中,值以 32 位存储,类型标签存储在低位的 1-3 位:
- `000`:对象
- `1`:整数
- `010`:双精度浮点数
- `100`:字符串
- `110`:布尔值
而 `null` 被表示为全 0(`0x00`),机器码解析时把前 3 位 `000` 当成了对象类型。这就好比把"空座位"误认为是"有人但隐身了"——**这是一个设计上的 bug,但因为兼容性太好,修不了了**。
```javascript
// 如何正确判断 null?
function isNull(value) {
return value === null;
}
// 或者更通用的类型判断
function getType(value) {
if (value === null) return 'null';
if (typeof value === 'undefined') return 'undefined';
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}
getType([]); // "array"
getType({}); // "object"
getType(null); // "null"
getType(undefined); // "undefined"
getType(() => {}); // "function"
```
## 🎯 发散衍生
### 🟡 衍生 1:`undefined` vs `undeclared` vs `null`
```javascript
let a;
console.log(a); // undefined(已声明,未赋值)
console.log(typeof a); // "undefined"
console.log(b); // ReferenceError: b is not defined(未声明)
console.log(typeof b); // "undefined"(typeof 的安全机制)
let c = null;
console.log(c); // null(已声明,赋值为空)
console.log(typeof c); // "object"(千古奇冤)
```
| 状态 | 含义 | typeof |
| ------------ | ---------------- | ------------------------------ |
| `undefined` | 声明了但没给值 | `"undefined"` |
| `null` | 声明了,主动给空 | `"object"` |
| `undeclared` | 连声明都没有 | `"undefined"`(typeof 的宽容) |
### 🔴 衍生 2:`BigInt` 和 `Number` 能混用吗?
```javascript
const a = 123n; // BigInt
const b = 456; // Number
console.log(a + b); // TypeError: Cannot mix BigInt and other types
// 必须显式转换
console.log(a + BigInt(b)); // 579n
console.log(Number(a) + b); // 579(但可能丢失精度!)
```
### ⚫ 衍生 3:手写一个完整的类型判断函数
```javascript
/**
* 终极类型判断器
* 能区分:null、undefined、object、array、function、date、regexp、
* map、set、weakmap、weakset、promise、error、symbol、bigint
*/
function typeOf(obj) {
const toString = Object.prototype.toString;
const map = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regExp',
'[object Undefined]': 'undefined',
'[object Null]': 'null',
'[object Object]': 'object',
'[object Symbol]': 'symbol',
'[object BigInt]': 'bigint',
'[object Map]': 'map',
'[object Set]': 'set',
'[object WeakMap]': 'weakMap',
'[object WeakSet]': 'weakSet',
'[object Promise]': 'promise',
'[object Error]': 'error',
'[object Arguments]': 'arguments',
};
return map[toString.call(obj)];
}
// 测试
console.log(typeOf(new Map())); // "map"
console.log(typeOf(Promise.resolve())); // "promise"
console.log(typeOf(new Error())); // "error"
```
---
# 第三章:深克隆与浅克隆 —— "复制粘贴"的艺术
## 🟢 原题回顾
> **问**:实现一个深克隆函数。
## 🟡 通俗讲解
**浅克隆**:就像你给朋友发了一份文件的快捷方式。你改文件,朋友的也变了——**因为你们指向的是同一个东西**。
**深克隆**:就像你给朋友发了一份文件的复印件。你往原件上画小王八,朋友的复印件上不会有——**因为那是两个独立的东西**。
```javascript
// 浅克隆 —— 只复制第一层
const shallowClone = (obj) => ({ ...obj });
const original = { name: '张三', hobby: ['吃饭', '睡觉'] };
const cloned = shallowClone(original);
cloned.name = '李四'; // ✅ 互不影响
cloned.hobby.push('打豆豆'); // ❌ original.hobby 也变了!
```
## 🔴 基础深克隆实现
```javascript
function deepClone(obj, hash = new WeakMap()) {
// 处理 null 或基本类型
if (obj === null || typeof obj !== 'object') return obj;
// 处理日期
if (obj instanceof Date) return new Date(obj);
// 处理正则
if (obj instanceof RegExp) return new RegExp(obj);
// 处理函数(通常深克隆不复制函数,直接返回引用)
if (typeof obj === 'function') return obj;
// 处理循环引用
if (hash.has(obj)) return hash.get(obj);
// 处理数组
if (Array.isArray(obj)) {
const cloneArr = [];
hash.set(obj, cloneArr);
obj.forEach((item, index) => {
cloneArr[index] = deepClone(item, hash);
});
return cloneArr;
}
// 处理对象
const cloneObj = Object.create(Object.getPrototypeOf(obj));
hash.set(obj, cloneObj);
// 复制 Symbol 类型的 key
const symKeys = Object.getOwnPropertySymbols(obj);
symKeys.forEach(symKey => {
cloneObj[symKey] = deepClone(obj[symKey], hash);
});
// 复制普通 key
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
```
## 🎯 发散衍生
### 🟡 衍生 1:`JSON.parse(JSON.stringify(obj))` 的"七宗罪"
```javascript
const obj = {
name: 'test',
date: new Date(), // ❌ 变成字符串
reg: /abc/g, // ❌ 变成空对象 {}
fn: function() {}, // ❌ 直接丢失
sym: Symbol('key'), // ❌ 直接丢失
undefinedKey: undefined, // ❌ 直接丢失
map: new Map([['a', 1]]), // ❌ 变成空对象 {}
set: new Set([1, 2, 3]), // ❌ 变成空对象 {}
[Symbol('foo')]: 'bar', // ❌ 直接丢失
circle: null // 循环引用会导致报错!
};
obj.circle = obj; // 制造循环引用
// JSON.stringify(obj); // TypeError: Converting circular structure to JSON
```
### 🔴 衍生 2:如何处理 `Map`、`Set`、`WeakMap`、`WeakSet`?
```javascript
function deepClonePro(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (typeof obj === 'function') return obj;
if (hash.has(obj)) return hash.get(obj);
// 处理 Map
if (obj instanceof Map) {
const cloneMap = new Map();
hash.set(obj, cloneMap);
obj.forEach((value, key) => {
cloneMap.set(deepClonePro(key, hash), deepClonePro(value, hash));
});
return cloneMap;
}
// 处理 Set
if (obj instanceof Set) {
const cloneSet = new Set();
hash.set(obj, cloneSet);
obj.forEach(value => {
cloneSet.add(deepClonePro(value, hash));
});
return cloneSet;
}
// ... 其余代码同上
}
```
### ⚫ 衍生 3:使用 `MessageChannel` 实现异步深克隆(浏览器环境)
```javascript
function deepCloneAsync(obj) {
return new Promise((resolve) => {
const { port1, port2 } = new MessageChannel();
port2.onmessage = (ev) => resolve(ev.data);
port1.postMessage(obj);
});
}
// 使用
(async () => {
const obj = { a: 1, b: { c: 2 } };
const cloned = await deepCloneAsync(obj);
console.log(cloned); // { a: 1, b: { c: 2 } }
})();
```
**原理**:`postMessage` 发送数据时会自动进行**结构化克隆算法**(Structured Clone),天然支持循环引用、`Map`、`Set`、`ArrayBuffer` 等。
### 🔴 衍生 4:`structuredClone()` —— 浏览器原生深克隆(现代浏览器)
```javascript
const obj = {
date: new Date(),
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3])
};
const cloned = structuredClone(obj); // ✅ 原生支持!
```
**注意**:不支持函数和 DOM 节点克隆。
---
# 第四章:闭包 —— JS 的"记忆胶囊"
## 🟢 原题回顾
> **问**:什么是闭包?
## 🟡 通俗讲解
闭包就像你高中暗恋同桌时偷偷记下的日记本——**函数执行完了(毕业了),但里面的变量(回忆)还在**。
官方定义:
> 闭包是指有权访问另外一个函数作用域中的变量的函数。
说人话:**一个函数记住了它被创建时的环境,即使那个环境已经"结束"了**。
```javascript
function createCounter() {
let count = 0; // 这个变量本该在 createCounter 执行完后销毁
return function() {
count++; // 但它被内部函数"记住"了!
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count 还活着,而且只有 counter 能访问它
```
## 🔴 闭包产生的本质
**当前环境中存在指向父级作用域的引用**。
用 Chrome 开发者工具看,闭包长这样:
```javascript
function outer() {
const secret = '我是秘密';
return function inner() {
console.log(secret);
};
}
const fn = outer();
// 在 Chrome 控制台输入 fn,展开 [[Scopes]]
// 你会看到 Closure (outer) { secret: "我是秘密" }
```
## 🎯 发散衍生
### 🟡 衍生 1:经典的循环闭包陷阱
```javascript
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 输出什么?
}, 100);
}
// 答案:3, 3, 3(不是 0, 1, 2!)
```
**原因**:`var` 没有块级作用域,三个定时器共享同一个 `i`。当定时器执行时,循环已经结束,`i === 3`。
**三种解法**:
```javascript
// 解法 1:IIFE(立即执行函数)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// 解法 2:使用 let(推荐)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 0, 1, 2
}
// let 有块级作用域,每次循环都创建一个新的 i
// 解法 3:使用 setTimeout 的第三个参数
for (var i = 0; i < 3; i++) {
setTimeout((j) => console.log(j), 100, i);
}
```
### 🔴 衍生 2:闭包与内存泄漏
```javascript
function heavyClosure() {
const hugeData = new Array(1000000).fill('x'); // 1MB 数据
return function() {
console.log('我只需要一个 tiny 变量');
// 但 hugeData 被闭包引用,无法被 GC 回收!
};
}
const leak = heavyClosure();
// 即使 hugeData 没有被直接使用,它也会一直占用内存
// 解决方案:不再使用时手动解除引用
leak = null; // hugeData 现在可以被回收了
```
### ⚫ 衍生 3:用闭包实现真正的私有变量
```javascript
const createPerson = (name) => {
let _age = 0; // 真正的私有变量,外部无法直接访问
return {
getName() {
return name;
},
getAge() {
return _age;
},
growUp() {
_age++;
}
};
};
const person = createPerson('张三');
console.log(person._age); // undefined(访问不到)
console.log(person.getAge()); // 0
person.growUp();
console.log(person.getAge()); // 1
```
### 🔴 衍生 4:闭包 vs 类 —— 模块模式
```javascript
// 使用闭包实现模块模式(ES6 之前的做法)
const Calculator = (function() {
// 私有变量
let history = [];
// 私有方法
function log(operation, result) {
history.push({ operation, result, time: new Date() });
}
// 公开 API
return {
add(a, b) {
const result = a + b;
log('add', result);
return result;
},
getHistory() {
return [...history]; // 返回副本,保护私有数据
}
};
})();
Calculator.add(1, 2);
console.log(Calculator.getHistory());
```
---
# 第五章:this —— JS 的"薛定谔的猫"
## 🟢 原题回顾
> **问**:了解 this 吗?bind、call、apply 具体指什么?手写 bind、apply、call。
## 🟡 通俗讲解
`this` 是 JavaScript 中最"玄学"的概念。它不像其他语言的 `this` 那样稳定可靠,而是**看脸(调用方式)行事**。
你可以把 `this` 想象成一个职场新人:
- **默认绑定**:没领导管,自己瞎混(指向全局对象 / undefined)
- **隐式绑定**:跟着某个对象混(指向调用它的对象)
- **显示绑定**:被领导点名安排(call / apply / bind 指定)
- **new 绑定**:被提拔为新项目负责人(指向新创建的实例)
**优先级**:`new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定`
```javascript
// 默认绑定
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2(非严格模式)/ undefined(严格模式)
// 隐式绑定
const obj = { a: 3, foo };
obj.foo(); // 3
// 显示绑定
const obj2 = { a: 4 };
foo.call(obj2); // 4
// new 绑定
const instance = new foo(); // this 指向新对象
```
## 🔴 手写 call、apply、bind
```javascript
// ==================== call ====================
Function.prototype.myCall = function(context, ...args) {
context = context || globalThis; // 兼容浏览器和 Node
const fnSymbol = Symbol('fn'); // 防止属性名冲突
context[fnSymbol] = this; // this 就是要调用的函数
const result = context[fnSymbol](...args);
delete context[fnSymbol];
return result;
};
// ==================== apply ====================
Function.prototype.myApply = function(context, argsArr) {
context = context || globalThis;
const fnSymbol = Symbol('fn');
context[fnSymbol] = this;
const result = context[fnSymbol](...argsArr);
delete context[fnSymbol];
return result;
};
// ==================== bind ====================
Function.prototype.myBind = function(context, ...bindArgs) {
const fn = this;
return function bound(...callArgs) {
// 如果被 new 调用,this 应该是新实例
if (this instanceof bound) {
return new fn(...bindArgs, ...callArgs);
}
return fn.apply(context, [...bindArgs, ...callArgs]);
};
};
```
## 🎯 发散衍生
### 🟡 衍生 1:箭头函数的 this —— "认死理"
```javascript
const obj = {
name: '对象',
regularFunc: function() {
console.log(this.name); // "对象"
},
arrowFunc: () => {
console.log(this.name); // undefined(指向定义时的外层 this)
}
};
// 箭头函数没有自己的 this,它"借用"外层作用域的 this
// 就像你问箭头函数:"你的领导是谁?"
// 箭头函数说:"我没有领导,我跟我爸混。"
```
### 🔴 衍生 2:`bind` 的柯里化应用
```javascript
function multiply(a, b, c) {
return a * b * c;
}
// 柯里化:先固定第一个参数
const double = multiply.bind(null, 2);
console.log(double(3, 4)); // 24
const doubleAndTriple = multiply.bind(null, 2, 3);
console.log(doubleAndTriple(4)); // 24
// 手写一个通用的柯里化函数
function curry(fn, ...args) {
return fn.length <= args.length
? fn(...args)
: (...more) => curry(fn, ...args, ...more);
}
const curriedMultiply = curry(multiply);
console.log(curriedMultiply(2)(3)(4)); // 24
```
### ⚫ 衍生 3:`call` 的骚操作 —— 借用方法
```javascript
// 1. 类数组转数组
function toArray() {
return Array.prototype.slice.call(arguments);
}
// 2. 判断数据类型
const toString = Object.prototype.toString;
toString.call([]); // "[object Array]"
toString.call({}); // "[object Object]"
toString.call(null); // "[object Null]"
// 3. 求数组最大值(不用 Math.max(...arr) 防止栈溢出)
const nums = [1, 5, 3, 9, 2];
Math.max.apply(null, nums); // 9
// 4. 用 push 合并两个数组
const arr1 = [1, 2];
const arr2 = [3, 4];
Array.prototype.push.apply(arr1, arr2);
console.log(arr1); // [1, 2, 3, 4]
```
### 🔴 衍生 4:如果构造函数返回一个对象会发生什么?
```javascript
function Person(name) {
this.name = name;
return { type: '我不是 Person' }; // 返回一个对象
}
const p = new Person('张三');
console.log(p); // { type: "我不是 Person" }
console.log(p.name); // undefined
// 规则:如果构造函数显式返回一个对象,则返回该对象
// 如果返回基本类型或没写 return,则返回新创建的实例
```
### ⚫ 衍生 5:bind + new 的优先级问题
```javascript
function foo(a) {
this.a = a;
}
const bar = foo.bind({ a: 1 });
const obj = new bar(2);
console.log(obj.a); // 2
// 结论:new 绑定的优先级高于 bind 显示绑定
// new 会创建一个新对象,替代 bind 绑定的对象作为 this
```
---
# 第六章:原型链 —— JS 的"族谱"
## 🟢 原题回顾
> **问**:说一下原型链和原型链的继承吧。
## 🟡 通俗讲解
JavaScript 的继承不像 Java 那样有严格的"类-子类"关系,而是更像**家族族谱**:
- 每个函数(比如 `Person`)都有一个 `prototype` 属性,指向它的"家族秘方"(原型对象)
- 每个对象都有一个 `__proto__`(或 `[[Prototype]]`),指向它的"老爸"
- 当你查找一个属性时,先在自己身上找,找不到就问老爸,老爸找不到就问爷爷……直到问到 `Object.prototype` 这位"老祖宗"
- `Object.prototype.__proto__ === null`,"老祖宗上面没人了"
```javascript
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
const alice = new Person('Alice');
// alice 的查找路径:
// alice -> alice.__proto__ (Person.prototype)
// -> Person.prototype.__proto__ (Object.prototype)
// -> Object.prototype.__proto__ (null)
```
## 🔴 完整的原型链图示
```
alice (实例对象)
├── name: "Alice"
└── __proto__ ────────────┐
▼
Person.prototype (原型对象)
├── constructor: Person
├── sayHi: function()
└── __proto__ ────────────┐
▼
Object.prototype (老祖宗)
├── toString: function()
├── hasOwnProperty: function()
└── __proto__: null
```
## 🎯 发散衍生
### 🟡 衍生 1:`instanceof` 的原理
```javascript
function myInstanceof(obj, Constructor) {
let proto = Object.getPrototypeOf(obj);
while (proto) {
if (proto === Constructor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
// 测试
myInstanceof([], Array); // true
myInstanceof([], Object); // true
myInstanceof({}, Function); // false
```
**注意**:`instanceof` 只能判断对象类型,对基本类型会翻车:
```javascript
'hello' instanceof String; // false
new String('hello') instanceof String; // true
```
### 🔴 衍生 2:ES6 之前如何实现继承?
```javascript
function Animal(name) {
this.name = name;
this.colors = ['black', 'white'];
}
Animal.prototype.getName = function() {
return this.name;
};
// ========== 继承属性:借用构造函数 ==========
function Dog(name, breed) {
Animal.call(this, name); // 继承父类的实例属性
this.breed = breed;
}
// ========== 继承方法:原型链继承 ==========
// ❌ 错误做法:Dog.prototype = Animal.prototype
// 这样修改 Dog.prototype 会影响 Animal.prototype
// ✅ 正确做法:创建一个中间对象
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修正 constructor 指向
Dog.prototype.getBreed = function() {
return this.breed;
};
const dog = new Dog('旺财', '柯基');
console.log(dog.getName()); // "旺财"(继承自 Animal)
console.log(dog.getBreed()); // "柯基"(自己的方法)
```
### ⚫ 衍生 3:`class` 语法糖的本质
```javascript
// ES6 class
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
}
// 本质上等价于上面的 ES5 写法
// class 只是语法糖,底层还是原型链
console.log(typeof Dog); // "function"
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
```
### 🔴 衍生 4:`Object.create(null)` 是什么?
```javascript
const dict = Object.create(null);
console.log(dict.toString); // undefined
// 与 {} 的区别
const obj = {};
console.log(obj.toString); // [Function: toString]
// 应用场景:作为纯字典/哈希表使用
// 避免原型链上的属性干扰(比如有人给 Object.prototype 加了属性)
```
---
# 第七章:Event Loop —— JS 的"时间管理大师"
## 🟢 原题回顾
> **问**:`setTimeout(fn, 0)` 多久才执行?Event Loop 是什么?手写 Promise 原理。
## 🟡 通俗讲解
JavaScript 是**单线程**的,就像一个只有一个窗口的银行。如果前面的人办业务太慢,后面的人只能等着——这就是同步阻塞。
但现实中银行有**叫号机**:你先取号(把任务放进队列),等窗口空了再叫号(执行回调)。Event Loop 就是 JS 的"叫号系统"。
```
┌─────────────────────────────────────────────┐
│ 调用栈 (Call Stack) │
│ 正在执行的代码,后进先出 │
└─────────────────────────────────────────────┘
↑
│ 执行完毕
↓
┌─────────────────────────────────────────────┐
│ 事件循环 (Event Loop) │
│ "看看宏任务队列有没有活?再看看微任务?" │
└─────────────────────────────────────────────┘
↑
┌─────────────┴─────────────┐
↓ ↓
┌───────────────┐ ┌───────────────────┐
│ 宏任务队列 │ │ 微任务队列 │
│ - script │ │ - Promise.then │
│ - setTimeout │ │ - MutationObserver│
│ - setInterval │ │ - queueMicrotask │
│ - I/O │ │ - process.nextTick│
│ - UI render │ │ │
└───────────────┘ └───────────────────┘
```
**执行顺序**:
1. 执行当前宏任务(最开始是 script 整体代码)
2. 执行过程中遇到微任务,放入微任务队列
3. 当前宏任务执行完毕后,**清空所有微任务**
4. 执行渲染(如果需要)
5. 开始下一个宏任务
## 🔴 经典面试题解析
```javascript
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => {
console.log('5');
}, 0);
});
console.log('6');
// 输出顺序:1 -> 6 -> 4 -> 2 -> 3 -> 5
```
**解析**:
1. `console.log('1')` —— 同步,直接输出
2. `setTimeout(...)` —— 宏任务,放入宏任务队列
3. `Promise.then(...)` —— 微任务,放入微任务队列
4. `console.log('6')` —— 同步,直接输出
5. 同步代码执行完毕,检查微任务队列 → 执行 `Promise.then` → 输出 `4`,遇到 `setTimeout` 放入宏任务队列
6. 微任务队列清空,开始下一个宏任务 → 执行第一个 `setTimeout` → 输出 `2`,遇到 `Promise.then` 放入微任务队列
7. 当前宏任务执行完毕,检查微任务队列 → 输出 `3`
8. 微任务队列清空,执行下一个宏任务 → 输出 `5`
## 🎯 发散衍生
### 🟡 衍生 1:`setTimeout(fn, 0)` 真的是 0 毫秒吗?
```javascript
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('sync');
// 输出:sync -> promise -> timeout
```
**答案**:不是!HTML5 标准规定 `setTimeout` 的最小延迟是 **4ms**(嵌套层级超过 5 层时)。即使设为 0,它也必须等当前执行栈清空、微任务队列清空后才能执行。
### 🔴 衍生 2:`async/await` 的本质
```javascript
async function foo() {
console.log('A');
await Promise.resolve();
console.log('B');
}
foo();
console.log('C');
// 输出:A -> C -> B
```
**原理**:`await` 后面的代码会被放入微任务队列。上面的代码等价于:
```javascript
function foo() {
console.log('A');
return Promise.resolve().then(() => {
console.log('B');
});
}
```
### ⚫ 衍生 3:Node.js 的 Event Loop(libuv)
Node.js 的 Event Loop 比浏览器更复杂,有 6 个阶段:
```
┌───────────────────────┐
│ timers │ 执行 setTimeout/setInterval 回调
│ (setTimeout...) │
└──────────┬────────────┘
↓
┌───────────────────────┐
│ pending callbacks │ 执行系统操作的回调(如 TCP 错误)
└──────────┬────────────┘
↓
┌───────────────────────┐
│ idle, prepare │ 内部使用
└──────────┬────────────┘
↓
┌───────────────────────┐
│ poll │ 获取新的 I/O 事件,执行 I/O 回调
└──────────┬────────────┘
↓
┌───────────────────────┐
│ check │ 执行 setImmediate 回调
└──────────┬────────────┘
↓
┌───────────────────────┐
│ close callbacks │ 执行 close 事件回调
└───────────────────────┘
```
**关键区别**:
- 浏览器:微任务在每个宏任务后执行
- Node.js:微任务在**每个阶段之间**执行
- `process.nextTick` 优先级高于所有微任务,在**当前操作完成后、进入下一阶段前**执行
```javascript
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 输出不确定!取决于当前事件循环所处的阶段
// 如果在 I/O 循环内,setImmediate 先执行
// 否则 setTimeout 可能先执行
```
### 🔴 衍生 4:手写一个 Promise
```javascript
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onResolvedCallbacks.forEach(fn => fn());
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
const promise2 = new MyPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
if (this.state === 'rejected') {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
if (this.state === 'pending') {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
});
return promise2;
}
}
// 处理 then 返回值的解析
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
let called = false;
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
const then = x.then;
if (typeof then === 'function') {
then.call(x, y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, r => {
if (called) return;
called = true;
reject(r);
});
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
```
---
# 第八章:事件流 —— 冒泡与捕获的"传话游戏"
## 🟢 原题回顾
> **问**:事件流是什么?事件是如何实现的?怎么加事件监听?
## 🟡 通俗讲解
事件流就像公司里的一个通知传递流程:
- **捕获阶段(Capture)**:董事长 → 总经理 → 部门经理 → 你(从上到下)
- **目标阶段(Target)**:通知到你本人了
- **冒泡阶段(Bubble)**:你 → 部门经理 → 总经理 → 董事长(从下到上)
```
┌──────────────┐
│ Document │ ← 捕获开始
└──────┬───────┘
│
┌──────┴───────┐
│ HTML │
└──────┬───────┘
│
┌──────┴───────┐
│ BODY │
└──────┬───────┘
│
┌──────┴───────┐
│ DIV │
└──────┬───────┘
│
┌──────┴───────┐
│ P │
└──────┬───────┘
│
┌──────┴───────┐
│ SPAN │ ← 目标阶段
└──────────────┘
│
(然后反向冒泡回去)
```
```javascript
// addEventListener 的第三个参数
element.addEventListener('click', handler, false); // false = 冒泡阶段(默认)
element.addEventListener('click', handler, true); // true = 捕获阶段
```
## 🔴 经典执行顺序题
```html
<div id="parent">
<span id="child">点击我</span>
</div>
```
```javascript
const parent = document.getElementById('parent');
const child = document.getElementById('child');
parent.addEventListener('click', () => console.log('parent 冒泡'), false);
parent.addEventListener('click', () => console.log('parent 捕获'), true);
child.addEventListener('click', () => console.log('child 冒泡'), false);
child.addEventListener('click', () => console.log('child 捕获'), true);
// 点击 child 的输出顺序:
// parent 捕获 → child 冒泡 → child 捕获 → parent 冒泡
```
**注意**:在目标阶段(`child` 元素上),事件处理程序的执行顺序**取决于注册顺序**,而不是 `useCapture` 参数。
## 🎯 发散衍生
### 🟡 衍生 1:事件委托 —— 利用冒泡的"偷懒技巧"
```javascript
// ❌ 不好的做法:给每个按钮都绑定事件
const buttons = document.querySelectorAll('button');
buttons.forEach(btn => {
btn.addEventListener('click', handleClick);
});
// ✅ 好的做法:委托给父元素
const container = document.getElementById('container');
container.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON') {
handleClick(e);
}
});
// 优点:
// 1. 减少内存占用(只需要一个监听器)
// 2. 动态添加的元素也能响应事件
// 3. 代码更简洁
```
### 🔴 衍生 2:`stopPropagation` vs `stopImmediatePropagation`
```javascript
element.addEventListener('click', (e) => {
console.log('第一个监听器');
e.stopPropagation(); // 阻止事件继续冒泡/捕获
});
element.addEventListener('click', (e) => {
console.log('第二个监听器'); // 仍然会执行!
});
// ========== 分割线 ==========
element.addEventListener('click', (e) => {
console.log('第一个监听器');
e.stopImmediatePropagation(); // 阻止事件传播 + 阻止同元素其他监听器
});
element.addEventListener('click', (e) => {
console.log('第二个监听器'); // 不会执行!
});
```
### ⚫ 衍生 3:自定义事件
```javascript
// 创建自定义事件
const myEvent = new CustomEvent('dogBark', {
detail: { name: '旺财', volume: 100 },
bubbles: true,
cancelable: true
});
// 监听自定义事件
document.addEventListener('dogBark', (e) => {
console.log(`${e.detail.name} 叫了,音量 ${e.detail.volume}`);
});
// 触发自定义事件
document.dispatchEvent(myEvent); // "旺财 叫了,音量 100"
```
### 🔴 衍生 4:发布订阅模式 —— 事件的底层原理
```javascript
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
return this;
}
off(event, callback) {
if (!this.events[event]) return this;
this.events[event] = this.events[event].filter(cb => cb !== callback);
return this;
}
once(event, callback) {
const wrapper = (...args) => {
callback(...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
return this;
}
emit(event, ...args) {
if (!this.events[event]) return this;
this.events[event].forEach(callback => callback(...args));
return this;
}
}
// 使用
const bus = new EventEmitter();
bus.on('login', (user) => console.log(`${user} 登录了`));
bus.emit('login', '张三'); // "张三 登录了"
```
---
# 第九章:隐式转换 —— JS 的"魔幻现实主义"
## 🟢 原题回顾
> **问**:JS 隐式转换、显示转换了解吗?`[] + {}` 和 `{} + []` 结果是多少?
## 🟡 通俗讲解
JavaScript 的类型转换就像你让一个不懂中文的外国朋友猜成语——**规则是有的,但结果经常出人意料**。
### 经典翻车现场
```javascript
[] + {} // "[object Object]"
{} + [] // 0(在某些浏览器中)
[] + [] // ""
{} + {} // "[object Object][object Object]"
true + true // 2
true + false // 1
'5' - 3 // 2
'5' + 3 // "53"
null == undefined // true
null == 0 // false
undefined == 0 // false
[] == ![] // true(经典面试题!)
```
## 🔴 转换规则速查表
### ToPrimitive(对象转原始值)
对象转原始值时,先调用 `valueOf()`,如果返回的不是原始值,再调用 `toString()`。
```javascript
const obj = {
valueOf() { return 42; },
toString() { return 'hello'; }
};
console.log(obj + 1); // 43(用了 valueOf)
console.log(`${obj} world`); // "hello world"(用了 toString)
```
### 抽象相等比较(==)
```javascript
// 规则 1:null 和 undefined 只等于对方
null == undefined; // true
null == 0; // false
// 规则 2:数字和字符串比较,字符串转数字
'5' == 5; // true
// 规则 3:布尔值和其他类型比较,布尔值转数字
true == 1; // true
false == 0; // true
// 规则 4:对象和原始类型比较,对象转原始值
[1, 2] == '1,2'; // true(数组转字符串)
```
### `[] == ![]` 为什么是 true?
```javascript
[] == ![]
// 第一步:![] 先算,[] 是 truthy,所以 ![] === false
[] == false
// 第二步:false 转数字 0
[] == 0
// 第三步:[] 转原始值,调用 toString() 得到 ""
"" == 0
// 第四步:"" 转数字 0
0 == 0
// 结果:true
```
## 🎯 发散衍生
### 🟡 衍生 1:假值列表(Falsy Values)
```javascript
// 以下值在布尔上下文中会被转为 false:
false
0 / -0 / 0n
"" / '' / ``
null
undefined
NaN
document.all // 历史遗留,为了兼容
// 其余全是 truthy,包括:
'0' // true(字符串!)
'false' // true(字符串!)
[] // true(空数组!)
{} // true(空对象!)
function(){} // true
```
### 🔴 衍生 2:`typeof` 的"迷惑行为"
```javascript
typeof NaN; // "number"(NaN 也是 number 类型)
typeof null; // "object"(千古奇冤)
typeof []; // "object"(数组也是对象)
typeof (() => {}); // "function"(函数特殊对待)
typeof undefined; // "undefined"
typeof typeof 1; // "string"(typeof 总是返回字符串)
```
### ⚫ 衍生 3:如何安全地进行类型转换?
```javascript
// 转字符串
String(123); // "123"
(123).toString(); // "123"
`${123}`; // "123"
// 转数字
Number('123'); // 123
Number(''); // 0(注意!)
Number(' '); // 0(注意!)
Number(null); // 0
Number(undefined); // NaN
parseInt('123abc'); // 123(更宽松)
parseFloat('123.45'); // 123.45
+'123'; // 123(一元加号)
// 转布尔值
Boolean(1); // true
!!1; // true(双重否定)
// 安全转换(避免意外)
function toNumber(val) {
const num = Number(val);
return Number.isNaN(num) ? 0 : num;
}
```
---
# 第十章:ES6+ 新特性 —— 现代 JS 的"装备升级"
## 🟢 原题回顾
> **问**:知道 ES6 的 Class 吗?Static 关键字有了解吗?箭头函数和普通函数有啥区别?Symbol 有什么用处?
## 🟡 通俗讲解
ES6 的 `class` 就像给 JavaScript 穿上了一件西装——**看起来像个"类",骨子里还是原型链**。
```javascript
class Animal {
// 静态属性/方法:属于类本身,不属于实例
static species = '动物';
static isAnimal(obj) {
return obj instanceof Animal;
}
constructor(name) {
this.name = name;
}
// 实例方法:自动放在 prototype 上
speak() {
console.log(`${this.name} makes a noise.`);
}
// getter/setter
get label() {
return `${this.species}: ${this.name}`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数
this.breed = breed;
}
speak() {
super.speak(); // 调用父类方法
console.log(`${this.name} barks.`);
}
}
console.log(Animal.species); // "动物"(静态属性)
const dog = new Dog('旺财', '柯基');
dog.speak();
```
## 🎯 发散衍生
### 🟡 衍生 1:`Symbol` 的妙用
```javascript
// 1. 创建唯一标识符
const id = Symbol('id');
const id2 = Symbol('id');
console.log(id === id2); // false
// 2. 作为对象的私有属性(伪私有)
const _password = Symbol('password');
class User {
constructor(name, password) {
this.name = name;
this[_password] = password; // 外部难以直接访问
}
}
const user = new User('张三', '123456');
console.log(Object.keys(user)); // ["name"](Symbol 属性不可枚举)
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(password)]
// 3. 实现迭代器
const obj = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
return {
next: () => ({
value: this.data[index++],
done: index > this.data.length
})
};
}
};
console.log([...obj]); // [1, 2, 3]
// 4. Symbol.for() —— 全局注册表
const globalSym = Symbol.for('app.config');
const sameSym = Symbol.for('app.config');
console.log(globalSym === sameSym); // true
```
### 🔴 衍生 2:箭头函数 vs 普通函数 —— 一张表搞定
| 特性 | 普通函数 | 箭头函数 |
| ----------- | ---------- | ------------------------- |
| `this` | 运行时绑定 | 词法作用域绑定 |
| `arguments` | 有 | 没有(用 `...args`) |
| `new` | 可以 | 不可以 |
| `prototype` | 有 | 没有 |
| `super` | 有 | 没有 |
| 作为方法 | ✅ 可以 | ❌ 慎用(this 会指向外层) |
| 简洁语法 | ❌ 不行 | ✅ `x => x * 2` |
```javascript
// 箭头函数不适合作为对象方法
const obj = {
name: '对象',
arrowMethod: () => {
console.log(this.name); // undefined!指向全局或 undefined
},
normalMethod() {
console.log(this.name); // "对象"
}
};
```
### ⚫ 衍生 3:`let` 的临时性死区(TDZ)
```javascript
console.log(a); // undefined(var 的变量提升)
var a = 1;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 2;
// TDZ:从块级作用域开始到 let 声明之间的区域
{
// TDZ 开始
console.log(c); // ReferenceError!
// TDZ 结束
let c = 3;
}
```
### 🔴 衍生 4:变量提升 vs 函数提升
```javascript
// 函数提升优先级高于变量提升
console.log(foo); // [Function: foo]
var foo = 1;
function foo() {}
console.log(foo); // 1(变量赋值覆盖了函数)
// 实际执行顺序等价于:
function foo() {} // 函数提升
var foo; // 变量提升(被忽略,因为已声明)
console.log(foo); // function
foo = 1; // 赋值
console.log(foo); // 1
```
---
# 第十一章:数组与函数 —— JS 的"瑞士军刀"
## 🟢 原题回顾
> **问**:数组能够调用的函数有哪些?函数中的 arguments 是数组吗?类数组转数组的方法?数组扁平化?数组去重?柯里化?
## 🟡 数组方法大全
```javascript
// ========== 改变原数组(破坏性方法)==========
arr.push(...items); // 末尾添加,返回新长度
arr.pop(); // 末尾删除,返回被删元素
arr.unshift(...items); // 开头添加,返回新长度
arr.shift(); // 开头删除,返回被删元素
arr.splice(start, deleteCount, ...items); // 万能手术刀
arr.sort(compareFn); // 排序
arr.reverse(); // 反转
arr.fill(value, start, end); // 填充
// ========== 不改变原数组(非破坏性方法)==========
arr.concat(...arrays); // 合并数组
arr.slice(start, end); // 切片
arr.join(separator); // 转字符串
arr.indexOf(item); // 查找索引
arr.includes(item); // 是否包含
arr.find(fn); // 查找第一个符合条件的元素
arr.findIndex(fn); // 查找第一个符合条件的索引
arr.filter(fn); // 过滤
arr.map(fn); // 映射
arr.reduce(fn, initial); // 归约
arr.every(fn); // 是否全部符合
arr.some(fn); // 是否有符合的
arr.flat(depth); // 扁平化(ES2019)
arr.flatMap(fn); // map + flat(ES2019)
// ========== 遍历方法 ==========
arr.forEach(fn); // 遍历(无返回值)
arr.entries(); // 返回键值对迭代器
arr.keys(); // 返回键迭代器
arr.values(); // 返回值迭代器
```
## 🔴 `arguments` —— "长得像数组的冒牌货"
```javascript
function foo() {
console.log(Array.isArray(arguments)); // false
console.log(arguments.length); // 可以像数组一样用 length
console.log(arguments[0]); // 可以像数组一样索引访问
// 但数组的方法它一个都没有!
// arguments.forEach(() => {}); // TypeError!
// 类数组转数组的 N 种方法:
const arr1 = Array.prototype.slice.call(arguments);
const arr2 = Array.from(arguments);
const arr3 = [...arguments]; // ES6 展开运算符
}
foo(1, 2, 3);
```
## 🎯 发散衍生
### 🟡 衍生 1:数组扁平化 —— 从递归到 Infinity
```javascript
// 方法 1:递归(面试常考)
function flatten(arr) {
return arr.reduce((acc, val) =>
acc.concat(Array.isArray(val) ? flatten(val) : val), []
);
}
// 方法 2:ES2019 原生支持
[1, [2, [3, [4]]]].flat(Infinity); // [1, 2, 3, 4]
// 方法 3:JSON 黑科技(仅限纯数字/字符串数组)
JSON.parse('[' + JSON.stringify(arr).replace(/\[|\]/g, '') + ']');
// 方法 4:toString(仅限纯数字数组)
[1, [2, [3]]].toString().split(',').map(Number); // [1, 2, 3]
```
### 🔴 衍生 2:数组去重 —— 从双重循环到 Set
```javascript
// 方法 1:Set(最简洁)
[...new Set([1, 2, 2, 3])]; // [1, 2, 3]
// 方法 2:filter + indexOf
arr.filter((item, index) => arr.indexOf(item) === index);
// 方法 3:reduce
arr.reduce((acc, cur) => acc.includes(cur) ? acc : [...acc, cur], []);
// 方法 4:对象键值对(兼容旧浏览器)
function unique(arr) {
const obj = {};
return arr.filter(item => {
const key = typeof item + JSON.stringify(item);
return obj.hasOwnProperty(key) ? false : (obj[key] = true);
});
}
// 去重对象数组(根据某个 key)
const users = [{id: 1, name: 'A'}, {id: 2, name: 'B'}, {id: 1, name: 'C'}];
const uniqueUsers = [...new Map(users.map(u => [u.id, u])).values()];
// [{id: 1, name: 'A'}, {id: 2, name: 'B'}]
```
### ⚫ 衍生 3:柯里化 —— 函数的"乐高积木"
```javascript
// 柯里化:把多参数函数变成一系列单参数函数
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
};
}
// 使用
const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
// 实际应用:创建配置化的函数
const replace = curry((regex, replacement, str) => str.replace(regex, replacement));
const replaceSpaces = replace(/\s+/g, '_');
const replaceVowels = replace(/[aeiou]/g, '*');
console.log(replaceSpaces('hello world')); // "hello_world"
console.log(replaceVowels('hello')); // "h*ll*"
```
### 🔴 衍生 4:`reduce` 的骚操作
```javascript
// 1. 数组转对象
const people = [{id: 1, name: 'A'}, {id: 2, name: 'B'}];
const peopleMap = people.reduce((acc, p) => ({...acc, [p.id]: p}), {});
// { 1: {id: 1, name: 'A'}, 2: {id: 2, name: 'B'} }
// 2. 统计频次
const chars = ['a', 'b', 'a', 'c', 'a'];
const count = chars.reduce((acc, c) => {
acc[c] = (acc[c] || 0) + 1;
return acc;
}, {});
// { a: 3, b: 1, c: 1 }
// 3. 管道函数
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
const add5 = x => x + 5;
const multiply2 = x => x * 2;
const addThenMultiply = pipe(add5, multiply2);
console.log(addThenMultiply(10)); // 30
// 4. 实现 map 和 filter
const map = (arr, fn) => arr.reduce((acc, item) => [...acc, fn(item)], []);
const filter = (arr, fn) => arr.reduce((acc, item) => fn(item) ? [...acc, item] : acc, []);
```
---
# 第十二章:NaN、new 与空对象 —— JS 的"边缘地带"
## 🟢 原题回顾
> **问**:NaN 是什么,用 typeof 会输出什么?new 一个函数发生了什么?new 一个构造函数,如果函数返回 return {}、return null、return 1、return true 会发生什么情况?如何判断一个对象是不是空对象?
## 🟡 NaN —— "不是数字的数字"
```javascript
console.log(typeof NaN); // "number"
// NaN 的特性
NaN === NaN; // false(自己不等于自己!)
Number.isNaN(NaN); // true
isNaN('abc'); // true(会尝试转换)
Number.isNaN('abc'); // false(严格判断,不转换)
// 产生 NaN 的方式
Number('abc'); // NaN
parseInt('abc'); // NaN
0 / 0; // NaN
Math.sqrt(-1); // NaN
```
## 🔴 new 操作符的"魔法"
```javascript
// new 一个函数时发生了什么?
function Person(name) {
this.name = name;
}
const p = new Person('张三');
// 等价于以下步骤:
// 1. 创建一个全新的空对象
// const obj = {};
// 2. 将对象的 [[Prototype]] 链接到构造函数的 prototype
// obj.__proto__ = Person.prototype;
// 3. 将构造函数中的 this 绑定到这个新对象
// Person.call(obj, '张三');
// 4. 如果构造函数没有返回对象,则返回这个新对象
// return obj;
```
### 构造函数返回值的规则
```javascript
// 情况 1:返回对象 → 返回该对象
function ReturnObj() {
this.name = '内部';
return { name: '外部对象' };
}
console.log(new ReturnObj()); // { name: "外部对象" }
// 情况 2:返回 null → null 也是对象!
function ReturnNull() {
this.name = '内部';
return null;
}
console.log(new ReturnNull()); // { name: "内部" }
// 等等,为什么不是 null?
// 因为规范规定:只有返回"对象"时才替换,null 虽然 typeof 是 object,
// 但规范里把它当特殊情况处理了,仍然返回新实例
// 情况 3:返回基本类型 → 忽略,返回新实例
function ReturnNum() {
this.name = '内部';
return 42;
}
console.log(new ReturnNum()); // { name: "内部" }
// 情况 4:返回 true → 基本类型,忽略
function ReturnBool() {
this.name = '内部';
return true;
}
console.log(new ReturnBool()); // { name: "内部" }
```
## 🎯 发散衍生
### 🟡 衍生 1:如何判断空对象?
```javascript
// 方法 1:Object.keys(最常用)
function isEmptyObject(obj) {
return Object.keys(obj).length === 0;
}
// 方法 2:for...in
function isEmptyObject2(obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) return false;
}
return true;
}
// 方法 3:JSON.stringify
function isEmptyObject3(obj) {
return JSON.stringify(obj) === '{}';
}
// 注意:以上方法对 Symbol 属性可能失效
const symObj = { [Symbol('key')]: 'value' };
console.log(Object.keys(symObj).length); // 0(Symbol 不可枚举!)
// 终极版本(包含 Symbol)
function isReallyEmpty(obj) {
return Object.keys(obj).length === 0
&& Object.getOwnPropertySymbols(obj).length === 0;
}
```
### 🔴 衍生 2:手写 new 操作符
```javascript
function myNew(Constructor, ...args) {
// 1. 创建空对象,继承 Constructor.prototype
const obj = Object.create(Constructor.prototype);
// 2. 执行构造函数,绑定 this
const result = Constructor.apply(obj, args);
// 3. 如果构造函数返回对象,则返回该对象;否则返回新对象
return (result !== null && (typeof result === 'object' || typeof result === 'function'))
? result
: obj;
}
// 测试
function Person(name) {
this.name = name;
}
const p = myNew(Person, '张三');
console.log(p.name); // "张三"
console.log(p instanceof Person); // true
```
### ⚫ 衍生 3:`Object.create()` 的 polyfill
```javascript
if (!Object.create) {
Object.create = function(proto) {
function F() {}
F.prototype = proto;
return new F();
};
}
```
---
# 第十三章:脚本加载与浏览器原理
## 🟢 原题回顾
> **问**:`<script src='xxx' />` 外部 js 文件先加载还是 onload 先执行?async、defer 问题?
## 🟡 通俗讲解
浏览器解析 HTML 就像厨师做菜,遇到 `<script>` 就像厨师突然接到电话——**必须停下来听完才能继续做菜**(同步阻塞)。
```html
<!-- 普通脚本:阻塞解析 -->
<script src="app.js"></script>
<!-- async:异步加载,加载完立即执行(可能阻塞解析) -->
<script src="analytics.js" async></script>
<!-- defer:异步加载,等 HTML 解析完再按顺序执行 -->
<script src="app.js" defer></script>
```
```
普通 script: [====下载====][====执行====] (阻塞!)
async: [====下载====] [====执行====] (下载不阻塞,执行可能阻塞)
defer: [====下载====] [====执行====] (完全不阻塞,DOMContentLoaded 前执行)
|<---------- HTML 解析 ---------->|
```
**执行顺序**:
- 普通脚本:按出现顺序执行,阻塞 HTML 解析
- `async`:下载不阻塞,但**加载完就执行**,执行顺序不确定
- `defer`:下载不阻塞,**等 HTML 解析完再按顺序执行**
**`onload` 事件**:等所有资源(图片、CSS、JS)加载完成后才触发。所以外部 JS 肯定在 `onload` 之前加载并执行完毕。
## 🎯 发散衍生
### 🟡 衍生 1:什么时候用 async,什么时候用 defer?
| 场景 | 推荐 | 原因 |
| ---------------------- | -------- | -------------------- |
| 独立脚本(如统计代码) | `async` | 不依赖其他脚本和 DOM |
| 依赖 DOM 的脚本 | `defer` | 确保 DOM 已构建 |
| 有依赖关系的脚本 | `defer` | 保证执行顺序 |
| 内联脚本 | 不放头部 | 避免阻塞 |
```html
<!-- 推荐组合 -->
<script src="jquery.js" defer></script>
<script src="app.js" defer></script>
<!-- app.js 肯定在 jquery.js 之后执行 -->
<script src="ga.js" async></script>
<!-- 统计代码,越早加载越好,不依赖其他东西 -->
```
### 🔴 衍生 2:动态加载脚本
```javascript
function loadScript(src, async = true) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.async = async;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// 使用
loadScript('https://cdn.example.com/lib.js')
.then(() => console.log('加载成功'));
```
### ⚫ 衍生 3:`DOMContentLoaded` vs `load`
```javascript
// DOMContentLoaded:HTML 解析完成,DOM 树构建完毕
// 不等待图片、CSS、iframe 等资源
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM 准备好了!');
});
// load:所有资源(图片、CSS、JS)都加载完成
window.addEventListener('load', () => {
console.log('所有资源都加载完了!');
});
// 如果脚本放在 <head> 中且没有 defer/async:
// 脚本执行时 DOM 还没构建完,此时 document.body 可能是 null
```
---
# 第十四章:实用工具函数 —— 面试中的"送分题"
## 🟢 原题回顾
> **问**:手写 getUrlParams(url, key);如何判断数组类型?TypeScript 的作用是什么?
## 🟡 URL 参数解析
```javascript
function getUrlParams(url, key) {
const search = url.includes('?') ? url.split('?')[1] : url;
const params = new URLSearchParams(search);
if (key === undefined) {
// 返回所有参数的对象
const result = {};
for (const [k, v] of params) {
// 处理重复 key
if (result[k]) {
result[k] = [].concat(result[k], v);
} else {
result[k] = v;
}
}
return result;
}
const values = params.getAll(key);
if (values.length === 0) return undefined;
if (values.length === 1) return values[0];
return values; // 多个值返回数组
}
// 测试
getUrlParams('https://a.com?name=张三&age=18');
// { name: "张三", age: "18" }
getUrlParams('https://a.com?tag=js&tag=ts', 'tag');
// ["js", "ts"]
getUrlParams('https://a.com?name=张三', 'name');
// "张三"
```
## 🔴 判断数组类型
```javascript
// 方法 1:Array.isArray(最可靠)
Array.isArray([]); // true
Array.isArray({}); // false
Array.isArray(null); // false
// 方法 2:instanceof(跨 iframe 可能失效)
[] instanceof Array; // true
// 方法 3:Object.prototype.toString
Object.prototype.toString.call([]); // "[object Array]"
// 方法 4:constructor(可被修改,不可靠)
[].constructor === Array; // true
// 方法 5:Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf([]); // true
```
## 🎯 发散衍生
### 🟡 衍生 1:TypeScript 的作用
```typescript
// TypeScript = JavaScript + 类型系统 + ESNext 语法
// 1. 静态类型检查(在编码时发现错误)
function add(a: number, b: number): number {
return a + b;
}
add(1, '2'); // 编译错误!
// 2. 更好的 IDE 支持(自动补全、重构、跳转定义)
// 3. 接口和类型别名,利于团队协作
interface User {
id: number;
name: string;
email?: string; // 可选属性
}
// 4. 泛型
function identity<T>(arg: T): T {
return arg;
}
identity<string>('hello');
identity<number>(42);
// 5. 枚举、装饰器、命名空间等高级特性
enum Color { Red, Green, Blue }
```
### 🔴 衍生 2:URL 参数的进阶处理
```javascript
// 处理 hash 路由
function parseHashUrl(url) {
const [path, hash] = url.split('#');
const [base, search] = path.split('?');
return { base, search, hash };
}
// 构建 URL
function buildUrl(base, params) {
const url = new URL(base);
Object.entries(params).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => url.searchParams.append(key, v));
} else {
url.searchParams.set(key, value);
}
});
return url.toString();
}
buildUrl('https://a.com', { name: '张三', tags: ['js', 'ts'] });
// "https://a.com?name=%E5%BC%A0%E4%B8%89&tags=js&tags=ts"
```
### ⚫ 衍生 3:解析 URL 的完整信息
```javascript
function parseUrl(url) {
const a = document.createElement('a');
a.href = url;
return {
protocol: a.protocol, // "https:"
hostname: a.hostname, // "www.example.com"
port: a.port, // "8080"
pathname: a.pathname, // "/path/to/page"
search: a.search, // "?name=value"
hash: a.hash, // "#section"
host: a.host, // "www.example.com:8080"
origin: a.origin // "https://www.example.com:8080"
};
}
// 现代浏览器也可以用 URL API
const url = new URL('https://user:pass@example.com:8080/path?query=1#hash');
console.log(url.username); // "user"
console.log(url.password); // "pass"
```
---
# 第十五章:PWA 与 Service Worker —— 离线也能浪
## 🟢 原题回顾
> **问**:PWA 使用过吗?serviceWorker 的使用原理是啥?
## 🟡 通俗讲解
**PWA(Progressive Web App)** 就像给你的网页穿上了"原生 App 的马甲":
- **可以离线访问**:没网也能看(Service Worker 缓存)
- **可以添加到主屏幕**:像原生 App 一样有个图标
- **可以推送通知**:即使网页关了也能收到消息
- **可以后台同步**:网络恢复后自动同步数据
**Service Worker** 是浏览器在后台运行的一个"特工":
- 独立于网页主线程,不会阻塞页面
- 可以拦截网络请求,决定用缓存还是发请求
- 生命周期与页面无关,页面关了它还在
```javascript
// 注册 Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW 注册成功', reg))
.catch(err => console.log('SW 注册失败', err));
}
```
```javascript
// sw.js —— Service Worker 脚本
const CACHE_NAME = 'v1';
const urlsToCache = ['/', '/styles.css', '/app.js', '/icon.png'];
// 安装阶段:缓存核心资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
// 激活阶段:清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
});
// 拦截请求:缓存优先策略
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 缓存命中,直接返回
if (response) return response;
// 缓存未命中,发网络请求
return fetch(event.request)
.then(response => {
// 缓存新请求的结果
const clone = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, clone);
});
return response;
});
})
);
});
```
## 🎯 发散衍生
### 🟡 衍生 1:Service Worker 的缓存策略
| 策略 | 适用场景 | 代码思路 |
| -------------------------- | -------- | ------------------------ |
| **Cache First** | 静态资源 | 先查缓存,没有再请求 |
| **Network First** | 实时数据 | 先请求网络,失败用缓存 |
| **Stale While Revalidate** | 新闻列表 | 先返回缓存,同时后台更新 |
| **Network Only** | 支付接口 | 只用网络,不缓存 |
| **Cache Only** | 离线应用 | 只用缓存 |
```javascript
// Stale While Revalidate 策略
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then(cache => {
return cache.match(event.request).then(response => {
const fetchPromise = fetch(event.request).then(networkResponse => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
// 返回缓存(可能旧),同时后台更新
return response || fetchPromise;
});
})
);
});
```
### 🔴 衍生 2:PWA 的 manifest 文件
```json
{
"name": "我的超级应用",
"short_name": "超级应用",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192" },
{ "src": "/icon-512.png", "sizes": "512x512" }
]
}
```
### ⚫ 衍生 3:Service Worker 的注意事项
```javascript
// 1. 必须在 HTTPS 下运行(localhost 除外)
// 2. 作用域限制:只能拦截同域下的请求
// 3. 不能直接操作 DOM
// 4. 生命周期独立,更新需要用户刷新或 skipWaiting
// 强制更新
self.addEventListener('install', (event) => {
self.skipWaiting(); // 立即激活,不等旧 SW 退出
});
```
---
# 第十六章:变量提升与作用域 —— JS 的"时空错乱"
## 🟢 原题回顾
> **问**:变量提升是什么?let 闭包?let 会产生临时性死区?
## 🟡 通俗讲解
JavaScript 的代码执行分为两个阶段:
1. **编译阶段**:引擎扫描代码,确定作用域,进行变量/函数提升
2. **执行阶段**:按顺序执行代码
```javascript
// 你以为的代码:
console.log(a); // ???
var a = 1;
// 引擎看到的代码:
var a; // 提升声明
console.log(a); // undefined
a = 1; // 赋值留在原地
```
**变量提升的规则**:
- `var`:声明提升,赋值不提升,初始值为 `undefined`
- `let`/`const`:声明提升,但处于**临时性死区(TDZ)**,访问会报错
- `function`:整个函数体提升
## 🔴 作用域链
```javascript
function outer() {
const a = 1;
function middle() {
const b = 2;
function inner() {
const c = 3;
console.log(a, b, c); // 都能访问到!
}
inner();
}
middle();
}
// inner 的作用域链:
// inner 作用域 -> middle 作用域 -> outer 作用域 -> 全局作用域
```
## 🎯 发散衍生
### 🟡 衍生 1:`var` vs `let` vs `const`
| 特性 | var | let | const |
| ---------------- | ---------- | ----------- | ----------- |
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | ✅ | ✅(但 TDZ) | ✅(但 TDZ) |
| 重复声明 | ✅ | ❌ | ❌ |
| 重新赋值 | ✅ | ✅ | ❌ |
| 声明时必须初始化 | ❌ | ❌ | ✅ |
```javascript
// TDZ 示例
{
console.log(a); // undefined(var)
var a = 1;
console.log(b); // ReferenceError(let 的 TDZ)
let b = 2;
}
// const 的"不可重新赋值"不等于"不可变"
const obj = { name: '张三' };
obj.name = '李四'; // ✅ 可以!修改属性没问题
obj = { name: '王五' }; // ❌ 报错!不能重新赋值引用
// 真正不可变
const frozen = Object.freeze({ name: '张三' });
frozen.name = '李四'; // 静默失败(严格模式报错)
```
### 🔴 衍生 2:作用域链与词法作用域
```javascript
const value = '全局';
function foo() {
console.log(value); // "全局"
}
function bar() {
const value = '局部';
foo(); // 输出"全局",不是"局部"!
}
bar();
```
**原因**:JavaScript 采用**词法作用域(静态作用域)**,作用域在代码写好的时候就已经确定了,与调用位置无关。`foo` 定义在全局,所以它的作用域链中 `value` 指向全局的 `value`。
### ⚫ 衍生 3:块级作用域的妙用
```javascript
// 使用 IIFE 模拟块级作用域(ES6 之前)
var callbacks = [];
for (var i = 0; i < 3; i++) {
callbacks.push((function(index) {
return function() {
console.log(index);
};
})(i));
}
callbacks.forEach(cb => cb()); // 0, 1, 2
// ES6 之后,let 天然解决
const callbacks2 = [];
for (let i = 0; i < 3; i++) {
callbacks2.push(() => console.log(i));
}
callbacks2.forEach(cb => cb()); // 0, 1, 2
```
---
# 第十七章:JS 全景图 —— "你对 JS 了解多少?"
## 🟢 原题回顾
> **问**:说一下对 JS 的了解吧。如何看待 PWA App、原生 App 以及 Flutter 和 React Native?
## 🟡 JS 全景图
JavaScript 严格来说分为两部分:
### 语言标准(ECMAScript)
```
ES3 (1999) → ES5 (2009) → ES6/ES2015 → ES2016 → ES2017 → ... → 每年一更
│ │ │
│ │ ├── let/const
│ │ ├── 箭头函数
│ │ ├── class
│ │ ├── Promise
│ │ ├── 模块化 import/export
│ │ ├── 解构、展开运算符
│ │ ├── 模板字符串
│ │ └── ...
│ │
│ └── "use strict"
│ ├── JSON 支持
│ ├── Array 方法扩展
│ └── Object 方法扩展
│
└── 基础语法、原型链、闭包、this、作用域...
```
### 宿主环境
| 环境 | 提供的 API | 典型应用 |
| ----------- | --------------------------------------- | ------------------------ |
| **浏览器** | DOM、BOM、Fetch、Storage、Canvas、WebGL | 网页、H5 游戏 |
| **Node.js** | fs、http、path、buffer、stream、crypto | 后端服务、CLI 工具 |
| **小程序** | 各平台自定义 API | 微信小程序、支付宝小程序 |
## 🔴 跨平台方案对比
| 方案 | 技术栈 | 性能 | 体验 | 生态 | 适用场景 |
| ---------------- | ---------------- | ---- | ---- | ---- | ------------------------ |
| **原生 App** | Swift/Kotlin | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | 追求极致体验 |
| **React Native** | React + 原生组件 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | 需要原生能力的业务 App |
| **Flutter** | Dart + Skia 自绘 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | 追求一致性的跨平台 App |
| **PWA** | Web 技术 | ⭐⭐ | ⭐⭐ | ⭐⭐ | 轻量级、不需要上架的应用 |
| **小程序** | 各平台框架 | ⭐⭐ | ⭐⭐ | ⭐⭐⭐ | 国内生态、轻量服务 |
| **WebView** | Web 技术 | ⭐⭐ | ⭐⭐ | ⭐⭐⭐ | 快速迭代的内容型应用 |
### 选择建议
- **追求极致性能和体验** → 原生开发
- **已有 Web 团队,想快速开发 App** → React Native / Flutter
- **不需要应用商店,轻量场景** → PWA
- **国内用户,依赖微信生态** → 小程序
- **快速验证 MVP** → PWA / WebView 套壳
---
# 🎓 附录:面试心法
## 回答问题的"金字塔结构"
```
┌─────────────┐
│ 结论先行 │ "0.1 + 0.2 不等于 0.3"
└──────┬──────┘
│
┌──────┴──────┐
│ 原因解释 │ "因为 JS 使用 IEEE 754 双精度浮点数..."
└──────┬──────┘
│
┌──────┴──────┐
│ 代码示例 │ 展示精度问题和解决方案
└──────┬──────┘
│
┌──────┴──────┐
│ 发散延伸 │ "在实际项目中,电商系统通常用整数(分)来计算价格..."
└─────────────┘
```
## 遇到不会的问题怎么办?
1. **诚实承认**:"这个知识点我了解得不够深入,但我可以尝试分析一下..."
2. **关联已知**:"这让我想到 XXX,它们之间可能有相似的地方..."
3. **展示思路**:"如果让我设计,我可能会这样考虑..."
4. **虚心请教**:"您能否指点一下这个问题的关键点?"
## 手写代码的 checklist
- [ ] 边界条件处理(null、undefined、空数组、空对象)
- [ ] 类型检查(typeof、instanceof、Array.isArray)
- [ ] 循环引用处理(深克隆时用 WeakMap)
- [ ] 异常处理(try/catch)
- [ ] 性能考虑(时间复杂度、空间复杂度)
---
> **最后的忠告**:面试不是背诵比赛,而是思维碰撞。与其死记硬背答案,不如理解原理、多写代码、多思考"为什么"。
>
> 祝你在面试中:**代码无 bug,算法全 AC,HR 给高薪,老板不画饼!** 🎉
---
*文档整理时间:2026-08-15*
*版本:v1.0 —— "从青铜到王者"*

浙公网安备 33010602011771号