前端面试宝典:从"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 —— "从青铜到王者"*

前端面试宝典:从"hello world"到"hello 面试官"

posted @ 2026-08-15 21:39  木头人吃菜不吃饭  阅读(2)  评论(0)    收藏  举报