JavaScript
一、基础语法与数据类型
1. JavaScript 数据类型有哪些?
定义
JavaScript 数据类型分为两大类:基本数据类型(简单类型/原始类型)和引用数据类型(复杂类型/对象类型)。
原理
- 基本数据类型:存储在栈(Stack)中,按值访问,值不可变
- 引用数据类型:栈中存储引用地址,实际数据存储在堆(Heap)中,按引用访问
ES5 基本数据类型(5种)
| 类型 |
说明 |
示例 |
String |
字符串 |
"hello", 'world' |
Number |
数字(整数和浮点数) |
42, 3.14 |
Boolean |
布尔值 |
true, false |
null |
空值,表示"空对象指针" |
null |
undefined |
未定义,表示变量已声明但未赋值 |
undefined |
ES6 新增基本数据类型
| 类型 |
说明 |
示例 |
Symbol |
唯一标识符 |
Symbol('id') |
BigInt |
任意精度整数 |
9007199254740991n |
引用数据类型
| 类型 |
说明 |
Object |
普通对象 {} |
Array |
数组 [] |
Function |
函数 |
Date |
日期对象 |
RegExp |
正则表达式 |
Map / Set |
ES6 新增集合类型 |
WeakMap / WeakSet |
ES6 弱引用集合 |
示例代码
// 基本数据类型 - 存储在栈中
let name = "JavaScript"; // String
let age = 30; // Number
let isActive = true; // Boolean
let empty = null; // null
let notDefined; // undefined
// 基本类型按值传递,修改副本不影响原值
let a = 10;
let b = a;
b = 20;
console.log(a); // 10,不受影响
// 引用数据类型 - 栈中存地址,堆中存数据
let obj = { name: "JS", version: "ES6" };
let arr = [1, 2, 3];
let fn = function() { return "Hello"; };
// 引用类型传递的是地址,修改会影响原对象
let obj1 = { a: 1 };
let obj2 = obj1;
obj2.a = 2;
console.log(obj1.a); // 2,被修改了
存储差异对比
| 特性 |
基本类型 |
引用类型 |
| 存储位置 |
栈(Stack) |
栈(地址)+ 堆(数据) |
| 访问方式 |
按值访问 |
按引用访问 |
| 复制方式 |
复制值本身 |
复制引用地址 |
| 比较方式 |
值比较 |
引用地址比较 |
| 内存管理 |
自动回收 |
需要垃圾回收 |
| 可变性 |
不可变 |
可变 |
常见误区
typeof null 返回 "object" 是历史遗留 bug
- 基本类型不是对象,但 String、Number、Boolean 有包装对象
NaN 是 Number 类型,但 typeof NaN 返回 "number"
2. null 和 undefined 的区别
定义
- null:表示"空值"或"不存在的对象",需要显式赋值
- undefined:表示"未定义",变量已声明但未赋值时的默认值
对比表格
| 特性 |
null |
undefined |
| 类型 |
object(历史 bug) |
undefined |
| 含义 |
空对象指针 |
未定义 |
| 来源 |
需要手动赋值 |
系统自动赋值 |
| 转换为数字 |
0 |
NaN |
| 转换为布尔 |
false |
false |
| 转换为字符串 |
"null" |
"undefined" |
原理
null 是 JavaScript 关键字,表示有意的"空值"
undefined 是全局对象的属性,表示"缺少值"
代码示例
// undefined 的场景
let a; // 变量声明但未赋值
let obj = {}; obj.x; // 访问不存在的属性
function fn() {} fn(); // 函数无返回值
let arr = []; arr[5]; // 访问不存在的数组索引
// null 的场景
let element = null; // DOM 元素不存在
let user = null; // 主动清空引用
document.getElementById('nonexist'); // 返回 null
// 类型检测
console.log(typeof null); // "object" (历史 bug)
console.log(typeof undefined); // "undefined"
// 相等性比较
console.log(null == undefined); // true (隐式转换)
console.log(null === undefined); // false (类型不同)
使用场景
// 使用 null 的场景
// 1. 函数参数默认值
function createUser(name = null) {
return { name, age: 0 };
}
// 2. 释放对象引用(帮助垃圾回收)
let largeObject = { /* 大量数据 */ };
largeObject = null; // 解除引用
// 使用 undefined 的场景
// 1. 检查变量是否已赋值
if (value === undefined) { /* 未赋值处理 */ }
// 2. 函数参数可选
function greet(name) {
if (name === undefined) name = "Guest";
return `Hello, ${name}`;
}
常见误区
- 不要使用
== 判断 null 和 undefined,用 ===
null 是关键字,undefined 是全局变量(ES5 前可被重新赋值)
- JSON 中不支持
undefined,序列化时会被忽略
3. == 和 === 的区别
定义
==(相等运算符):先进行类型转换,再比较值
===(严格相等运算符):不转换类型,直接比较值和类型
对比表格
| 特性 |
==(相等) |
===(严格相等) |
| 类型转换 |
会进行隐式转换 |
不转换 |
| 比较规则 |
值相等即可 |
类型和值都必须相等 |
| 性能 |
较慢(需转换) |
较快 |
| 推荐度 |
不推荐 |
强烈推荐 |
== 隐式转换规则
// 数字与字符串:字符串转数字
console.log(1 == "1"); // true
// 布尔值与其他:布尔转数字(true->1, false->0)
console.log(true == 1); // true
console.log(false == 0); // true
console.log(true == "1"); // true
// null 和 undefined
console.log(null == undefined); // true
console.log(null == 0); // false
console.log(undefined == 0); // false
// 对象与原始值:对象转原始值
console.log([1] == 1); // true (数组toString后是"1")
console.log([] == 0); // true
console.log({} == "[object Object]"); // true
特殊情况(永远为 false)
NaN == NaN // false(NaN 不等于任何值,包括自己)
null == 0 // false
undefined == 0 // false
null == false // false
undefined == false // false
代码示例
// 推荐使用 ===
console.log(0 === false); // false
console.log("" === false); // false
console.log(null === undefined); // false
console.log(NaN === NaN); // false
// 判断 null 或 undefined 的简便写法
// 只有 null 和 undefined 满足 == null
if (value == null) {
// 相当于 value === null || value === undefined
}
选择策略
- 始终使用
===,除非你明确需要类型转换
- 判断
null 或 undefined 时可用 == null(最简洁)
- 使用
Object.is() 处理特殊场景(如 NaN、+0、-0)
// Object.is 的特殊之处
Object.is(NaN, NaN); // true(=== 为 false)
Object.is(+0, -0); // false(=== 为 true)
Object.is(0, -0); // false
4. NaN 特性及检测方法
定义
NaN(Not-a-Number) 是一个特殊的 Number 类型值,表示非数字。通常由无效的数学运算产生。
特性
typeof NaN 返回 "number"
NaN 不等于任何值,包括自己:NaN === NaN 为 false
- 任何涉及
NaN 的运算都返回 NaN
NaN 是唯一一个不自等的值
产生场景
// 无效的数学运算
Math.sqrt(-1); // NaN
Math.log(-1); // NaN
0 / 0; // NaN
Infinity - Infinity; // NaN
Infinity / Infinity; // NaN
// 类型转换失败
Number("abc"); // NaN
parseInt("abc"); // NaN
parseFloat("xyz"); // NaN
检测方法
// 方法1:全局 isNaN(有缺陷)
isNaN("abc"); // true(先转换为数字,失败则返回true)
isNaN("123"); // false
isNaN(true); // false(true转换为1)
// 方法2:Number.isNaN(推荐)
Number.isNaN(NaN); // true
Number.isNaN("abc"); // false(不转换类型)
Number.isNaN(123); // false
// 方法3:自不等特性
function isNaN2(val) {
return val !== val;
}
isNaN2(NaN); // true
// 方法4:ES6 之前兼容写法
function isNaN3(val) {
return typeof val === "number" && isNaN(val);
}
isNaN 与 Number.isNaN 对比
| 特性 |
isNaN() |
Number.isNaN() |
| 类型转换 |
会转换 |
不转换 |
| 引入版本 |
ES1 |
ES6 |
| 准确性 |
有缺陷 |
准确 |
| 推荐度 |
不推荐 |
推荐 |
// 对比示例
isNaN("NaN"); // true(字符串"NaN"转换为数字NaN)
Number.isNaN("NaN"); // false(类型不同)
isNaN(undefined); // true(undefined转换为NaN)
Number.isNaN(undefined); // false
5. 数据类型检测方法
四种检测方法对比
1. typeof 运算符
// 返回值
typeof "" // "string"
typeof 42 // "number"
typeof true // "boolean"
typeof undefined // "undefined"
typeof Symbol() // "symbol"
typeof 9007199254740991n // "bigint"
typeof function(){} // "function"
typeof {} // "object"
typeof [] // "object"
typeof null // "object"(历史 bug)
适用场景 :检测基本类型和函数 局限性 :无法区分对象、数组、null(都返回 "object")
2. instanceof 运算符
[] instanceof Array; // true
{} instanceof Object; // true
function(){} instanceof Function; // true
/new Date() instanceof Date; // true
适用场景 :检测引用类型 局限性:
- 不能检测基本类型
- 跨 iframe 时失效(不同全局对象)
- 原型链被修改时结果可能错误
3. Object.prototype.toString.call()
Object.prototype.toString.call(""); // "[object String]"
Object.prototype.toString.call(42); // "[object Number]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call(function(){}); // "[object Function]"
Object.prototype.toString.call(/regex/); // "[object RegExp]"
Object.prototype.toString.call(new Date()); // "[object Date]"
Object.prototype.toString.call(new Map()); // "[object Map]"
Object.prototype.toString.call(new Set()); // "[object Set]"
Object.prototype.toString.call(Symbol()); // "[object Symbol]"
Object.prototype.toString.call(BigInt(1)); // "[object BigInt]"
适用场景 :最准确的类型检测方法 原理 :每个对象内部都有 [[Class]] 属性,toString() 会读取该属性
4. 通用类型判断函数
function typeOf(val) {
const typeMap = {
'[object String]': 'string',
'[object Number]': 'number',
'[object Boolean]': 'boolean',
'[object Null]': 'null',
'[object Undefined]': 'undefined',
'[object Object]': 'object',
'[object Array]': 'array',
'[object Function]': 'function',
'[object RegExp]': 'regexp',
'[object Date]': 'date',
'[object Map]': 'map',
'[object Set]': 'set',
'[object Symbol]': 'symbol',
'[object BigInt]': 'bigint',
'[object Promise]': 'promise',
'[object Error]': 'error',
'[object Arguments]': 'arguments',
};
return typeMap[Object.prototype.toString.call(val)] || 'unknown';
}
检测方式对比表
| 方法 |
基本类型 |
引用类型 |
null |
跨iframe |
推荐度 |
| typeof |
✅ |
❌(只识别function) |
❌(返回object) |
✅ |
中 |
| instanceof |
❌ |
✅ |
❌ |
❌ |
低 |
| Object.prototype.toString |
✅ |
✅ |
✅ |
✅ |
高 |
| constructor |
✅ |
✅ |
❌ |
❌ |
中 |
6. typeof 和 instanceof 的区别
对比表格
| 特性 |
typeof |
instanceof |
| 返回值 |
字符串(类型名) |
布尔值 |
| 检测基本类型 |
✅ |
❌ |
| 检测引用类型 |
❌(只识别function) |
✅ |
| null 检测 |
❌(返回"object") |
❌(null 不是任何对象实例) |
| 数组检测 |
❌(返回"object") |
✅ |
| 跨 iframe |
✅ |
❌ |
| 原理 |
读取内部类型标签 |
检查原型链 |
使用场景
// typeof 适合检测基本类型
if (typeof value === "string") { /* ... */ }
if (typeof value === "number") { /* ... */ }
if (typeof value === "function") { /* ... */ }
// instanceof 适合检测引用类型
if (value instanceof Array) { /* ... */ }
if (value instanceof Date) { /* ... */ }
if (value instanceof RegExp) { /* ... */ }
// 最佳实践:组合使用
function isObject(value) {
return value !== null && typeof value === "object";
}
function isArray(value) {
return Array.isArray(value); // 或使用 instanceof
}
7. 数据类型转换
显式类型转换
// 转字符串
String(123); // "123"
(123).toString(); // "123"
(123).toString(2); // "1111011"(二进制)
(123).toString(16); // "7b"(十六进制)
// 转数字
Number("123"); // 123
Number("12.34"); // 12.34
Number(""); // 0
Number("abc"); // NaN
Number(true); // 1
Number(false); // 0
Number(null); // 0
Number(undefined); // NaN
// 整数解析
parseInt("123px"); // 123
parseInt("0x10"); // 16(十六进制)
parseInt("12.34"); // 12
parseInt("abc"); // NaN
parseInt("10", 2); // 2(二进制)
// 浮点数解析
parseFloat("12.34px"); // 12.34
parseFloat("12.00"); // 12
// 转布尔值
Boolean(1); // true
Boolean(0); // false
Boolean(""); // false
Boolean("abc"); // true
Boolean(null); // false
Boolean(undefined); // false
Boolean(NaN); // false
Boolean([]); // true
Boolean({}); // true
隐式类型转换规则
// 1. 字符串 + 任意类型 -> 字符串
"1" + 2; // "12"
"1" + true; // "1true"
"1" + null; // "1null"
"1" + undefined; // "1undefined"
// 2. 数字运算 -> 转数字
"5" - 2; // 3
"5" * 2; // 10
"5" / 2; // 2.5
"5" - "2"; // 3
+"123"; // 123(一元+运算符)
// 3. 比较运算
"5" > 3; // true(转数字)
"5" > "3"; // true(字符串比较,逐字符)
"10" > "3"; // false("1" < "3")
5 > null; // true(null转0)
5 > undefined; // false(undefined转NaN)
// 4. 布尔上下文
if ([]) { /* 执行,[]转布尔为true */ }
if ({}) { /* 执行,{}转布尔为true */ }
if ("") { /* 不执行,空字符串为false */ }
// 5. == 比较时的转换
0 == false; // true(false转0)
"" == 0; // true(空字符串转0)
"0" == false; // true
null == undefined; // true
// 6. 对象转原始值
// 先调用 valueOf(),再调用 toString()
[1, 2] == "1,2"; // true
{} + []; // 0({}是代码块,+[]转数字0)
常见转换陷阱
// 1. 空数组转数字
[] == 0; // true
[] == false; // true
Number([]); // 0
// 2. 多元素数组
[1] == 1; // true
[1, 2] == "1,2"; // true
[1, 2] == 12; // false
// 3. 对象比较
{} == "[object Object]"; // true
// 4. null 和 undefined
null == 0; // false
undefined == 0; // false
null >= 0; // true(比较时null转0)
undefined >= 0; // false
8. 基本类型与引用类型的区别
定义
- 基本类型:简单的数据段,存储在栈中
- 引用类型:由多个值构成的对象,引用在栈,数据在堆
对比表格
| 特性 |
基本类型 |
引用类型 |
| 存储位置 |
栈 |
栈(引用)+ 堆(数据) |
| 大小 |
固定 |
不固定 |
| 复制 |
值复制(独立副本) |
引用复制(共享数据) |
| 比较 |
值比较 |
引用地址比较 |
| 参数传递 |
值传递 |
引用传递 |
| 属性/方法 |
无(有包装对象) |
有 |
| 垃圾回收 |
作用域结束 |
引用计数/标记清除 |
示例代码
// 基本类型:值复制
let a = 10;
let b = a;
b = 20;
console.log(a); // 10(独立副本)
// 引用类型:引用复制
let obj1 = { name: "Alice" };
let obj2 = obj1;
obj2.name = "Bob";
console.log(obj1.name); // "Bob"(共享数据)
// 函数参数传递
function modifyValue(val) {
val = 100;
}
function modifyObj(obj) {
obj.name = "Modified";
}
let num = 5;
let person = { name: "Alice" };
modifyValue(num);
console.log(num); // 5(基本类型不受影响)
modifyObj(person);
console.log(person.name); // "Modified"(引用类型受影响)
包装对象
// 基本类型可以临时调用方法(自动装箱)
let str = "hello";
str.toUpperCase(); // "HELLO"(临时创建 String 对象)
str.length; // 5
// 等价于
let temp = new String("hello");
temp.toUpperCase();
temp = null; // 立即销毁
// 注意:基本类型不能添加属性
let name = "JS";
name.version = "ES6";
console.log(name.version); // undefined(包装对象已销毁)
二、函数与作用域
9. JavaScript 作用域类型
定义
作用域(Scope) 是变量和函数的可访问范围,控制着变量和函数的可见性与生命周期。
作用域类型
1. 全局作用域(Global Scope)
// 全局变量
var globalVar = "I'm global";
function fn() {
console.log(globalVar); // 可访问
}
// 不在任何函数/块内声明的变量都是全局的
window.browserGlobal = "browser global"; // 浏览器环境
global.nodeGlobal = "node global"; // Node.js 环境
2. 函数作用域(Function Scope)
// var 声明的变量只在函数内部可见
function myFunction() {
var localVar = "I'm local";
console.log(localVar); // "I'm local"
}
console.log(localVar); // ReferenceError
3. 块级作用域(Block Scope)- ES6
// let 和 const 声明的变量只在 {} 块内可见
{
let blockLet = "block let";
const blockConst = "block const";
console.log(blockLet); // "block let"
}
console.log(blockLet); // ReferenceError
// if 语句块
if (true) {
let ifVar = "inside if";
}
console.log(ifVar); // ReferenceError
// for 循环块
for (let i = 0; i < 3; i++) {
console.log(i); // 0, 1, 2
}
console.log(i); // ReferenceError
4. 词法作用域(Lexical Scope)
// 函数在定义时确定作用域(静态作用域)
let x = 1;
function outer() {
let x = 2;
function inner() {
console.log(x); // 2(读取定义时的作用域)
}
return inner;
}
let fn = outer();
fn(); // 2
5. 模块作用域(Module Scope)- ES6
// 每个 ES6 模块有独立作用域
// moduleA.js
let moduleVar = "I'm in module A";
export function getVar() { return moduleVar; }
// moduleB.js
let moduleVar = "I'm in module B"; // 不会与 A 冲突
import { getVar } from './moduleA.js';
console.log(getVar()); // "I'm in module A"
作用域对比
| 作用域类型 |
声明方式 |
边界 |
提升 |
| 全局 |
var/let/const |
整个程序 |
是 |
| 函数 |
var |
函数体 |
是 |
| 块级 |
let/const |
{} 块 |
否(暂时性死区) |
| 词法 |
- |
函数定义位置 |
- |
| 模块 |
import/export |
模块文件 |
否 |
10. 作用域与作用域链
定义
- 作用域:变量的可访问范围
- 作用域链:当访问变量时,从当前作用域开始,逐级向外查找的链条
原理
JavaScript 采用词法作用域(静态作用域),函数在定义时就确定了其作用域链。作用域链的查找规则:
- 从当前执行环境的作用域开始查找
- 如果未找到,沿着作用域链向上查找
- 直到全局作用域,如果仍未找到则抛出 ReferenceError
代码示例
let globalVar = "global";
function outer() {
let outerVar = "outer";
function inner() {
let innerVar = "inner";
console.log(innerVar); // "inner"(当前作用域)
console.log(outerVar); // "outer"(向上级作用域查找)
console.log(globalVar); // "global"(继续向上查找)
}
inner();
}
outer();
作用域链示意图
inner 作用域
│
├─ innerVar: "inner"
│
▼
outer 作用域
│
├─ outerVar: "outer"
│
▼
全局作用域
│
├─ globalVar: "global"
作用域链与性能
// 频繁访问外层变量性能较差
function slow() {
let count = 0;
return function() {
count++; // 需要沿作用域链查找
return count;
};
}
// 优化:缓存到局部变量
function fast() {
let count = 0;
return function(localCount) {
localCount++;
return localCount;
};
}
11. 变量提升(Hoisting)
定义
变量提升 是指 JavaScript 引擎在代码执行前,将变量和函数的声明提升到作用域顶部的行为。
原理
JavaScript 执行分为两个阶段:
- 编译阶段:解析代码,收集变量和函数声明
- 执行阶段:执行代码,进行赋值和操作
var 变量提升
// 实际代码
console.log(a); // undefined(不会报错)
var a = 10;
console.log(a); // 10
// 等价于
var a; // 声明被提升
console.log(a); // undefined
a = 10; // 赋值不提升
console.log(a); // 10
函数提升
// 函数声明提升
fn(); // "Called"(可以正常执行)
function fn() {
console.log("Called");
}
// 函数表达式不提升
fn2(); // TypeError: fn2 is not a function
var fn2 = function() {
console.log("Called");
};
// 等价于
var fn2;
fn2(); // undefined 不是函数
fn2 = function() { ... };
let/const 不提升
// 实际有提升,但存在"暂时性死区"
console.log(a); // ReferenceError
let a = 10;
// const 同样
console.log(b); // ReferenceError
const b = 20;
提升优先级
// 函数提升 > 变量提升
console.log(typeof foo); // "function"
function foo() {}
var foo = 123;
// 等价于
function foo() {} // 函数提升优先
var foo; // 变量提升被忽略
console.log(typeof foo);
foo = 123;
// 同名变量和函数
var a;
function a() {}
console.log(typeof a); // "number"(执行到赋值时覆盖)
var a = 1;
最佳实践
- 始终在作用域顶部声明变量
- 优先使用
let 和 const 避免提升问题
- 函数声明优先于函数表达式
12. 函数声明与函数表达式的区别
对比表格
| 特性 |
函数声明 |
函数表达式 |
| 语法 |
function name() {} |
var name = function() {} |
| 提升 |
完整提升(声明+定义) |
变量提升,函数不提升 |
| 名称 |
必须有名称 |
可匿名 |
| 调用时机 |
声明前可调用 |
声明后才能调用 |
| 条件创建 |
不能(部分环境支持但不推荐) |
可以 |
| name属性 |
函数名 |
变量名或匿名 |
代码示例
// 函数声明 - 可提前调用
console.log(add(2, 3)); // 5
function add(a, b) {
return a + b;
}
// 函数表达式 - 必须声明后调用
// console.log(sub(5, 2)); // TypeError
var sub = function(a, b) {
return a - b;
};
console.log(sub(5, 2)); // 3
// 命名函数表达式
var multiply = function mult(a, b) {
return a * b;
};
console.log(multiply.name); // "mult"
// 条件创建函数
if (true) {
var fn1 = function() { return 1; }; // ✅ 可以
}
// IIFE(立即执行函数表达式)
(function() {
console.log("IIFE");
})();
// 箭头函数表达式
const divide = (a, b) => a / b;
13. this 指向规则
定义
this 是 JavaScript 中的一个关键字,它在函数执行时自动创建,指向函数的执行上下文。
五种绑定规则
1. 默认绑定(全局/独立函数调用)
function fn() {
console.log(this);
}
fn(); // 浏览器:window,Node:global
// 严格模式下:undefined
var a = 1;
function test() {
console.log(this.a);
}
test(); // 1(非严格模式)
2. 隐式绑定(对象方法调用)
let obj = {
name: "Alice",
greet: function() {
console.log(this.name);
}
};
obj.greet(); // "Alice"(this 指向 obj)
// 隐式丢失
let fn = obj.greet;
fn(); // undefined(this 指向全局对象)
// 链式调用
let user = {
profile: {
name: "Bob",
getName: function() {
return this.name;
}
}
};
console.log(user.profile.getName()); // "Bob"(this 指向 profile)
3. 显式绑定(call/apply/bind)
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
let user = { name: "Alice" };
greet.call(user, "Hello", "!"); // "Hello, Alice!"
greet.apply(user, ["Hello", "!"]); // "Hello, Alice!"
let boundGreet = greet.bind(user);
boundGreet("Hello", "!"); // "Hello, Alice!"
4. new 绑定(构造函数调用)
function Person(name) {
this.name = name;
}
let p = new Person("Alice");
console.log(p.name); // "Alice"(this 指向新对象)
// new 的执行过程:
// 1. 创建空对象 {}
// 2. 将 this 指向该对象
// 3. 执行函数体
// 4. 返回该对象(如果函数无返回值)
5. 箭头函数绑定(词法 this)
// 箭头函数没有自己的 this,继承外层 this
let obj = {
name: "Alice",
greet: () => {
console.log(this); // 指向定义时的外层作用域
}
};
obj.greet(); // window/global(箭头函数在对象中指向全局)
// 正确用法:在回调中保持 this
let counter = {
count: 0,
start() {
setInterval(() => {
this.count++; // this 指向 counter
console.log(this.count);
}, 1000);
}
};
counter.start(); // 1, 2, 3...
this 优先级
new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
箭头函数与普通函数对比
| 特性 |
普通函数 |
箭头函数 |
| this 指向 |
调用时确定 |
定义时确定(继承外层) |
| arguments |
有 |
无(用 rest 参数) |
| new |
可以 |
不可以 |
| prototype |
有 |
无 |
| call/apply/bind |
可修改 this |
无效(this 固定) |
| 作为方法 |
适合 |
不适合 |
| 作为回调 |
需要 bind |
适合 |
14. call、apply、bind 的区别
定义
这三个方法都用于修改函数的 this 指向,但使用方式不同。
对比表格
| 特性 |
call |
apply |
bind |
| 参数形式 |
逐个传入 |
数组传入 |
逐个传入 |
| 执行时机 |
立即执行 |
立即执行 |
返回新函数(稍后执行) |
| 返回值 |
函数执行结果 |
函数执行结果 |
绑定 this 后的新函数 |
| 柯里化 |
不支持 |
不支持 |
支持 |
代码示例
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
return this.name;
}
let user = { name: "Alice" };
// call - 逐个参数
let result1 = greet.call(user, "Hello", "!");
// "Hello, Alice!"
// apply - 数组参数
let result2 = greet.apply(user, ["Hello", "!"]);
// "Hello, Alice!"
// bind - 返回新函数
let boundGreet = greet.bind(user, "Hello");
let result3 = boundGreet("!");
// "Hello, Alice!"
// 实际应用场景
// 1. 借用方法
let arrayLike = { 0: "a", 1: "b", length: 2 };
let arr = Array.prototype.slice.call(arrayLike);
// ["a", "b"]
// 2. Math.max 求数组最大值
let nums = [1, 5, 3, 9, 2];
Math.max.apply(null, nums); // 9
Math.max(...nums); // 9(ES6 展开运算符)
// 3. 柯里化
function add(a, b, c) {
return a + b + c;
}
let add5 = add.bind(null, 5);
add5(3, 2); // 10
// 4. 定时器中保持 this
let timer = {
count: 0,
start() {
setTimeout(function() {
this.count++; // this 指向 window
}.bind(this), 1000); // 绑定到 timer
}
};
手写实现
// 手写 call
Function.prototype.myCall = function(context, ...args) {
if (typeof this !== 'function') {
throw new TypeError('Not a function');
}
context = context || window;
context.fn = this;
const result = context.fn(...args);
delete context.fn;
return result;
};
// 手写 apply
Function.prototype.myApply = function(context, args = []) {
if (typeof this !== 'function') {
throw new TypeError('Not a function');
}
context = context || window;
context.fn = this;
const result = context.fn(...args);
delete context.fn;
return result;
};
// 手写 bind
Function.prototype.myBind = function(context, ...args1) {
if (typeof this !== 'function') {
throw new TypeError('Not a function');
}
const fn = this;
function BoundFn(...args2) {
// new 调用时 this 指向实例
return fn.apply(
this instanceof BoundFn ? this : context,
[...args1, ...args2]
);
}
BoundFn.prototype = Object.create(fn.prototype);
return BoundFn;
};
三、闭包与原型链
15. 闭包
定义
闭包(Closure) 是指有权访问另一个函数作用域中变量的函数。闭包由函数及其引用的外部词法环境组合而成。
原理
当函数被创建时,会保存其创建时的作用域链。即使外部函数已执行完毕,内部函数仍能访问外部函数的变量,这些变量不会被垃圾回收。
代码示例
// 基本闭包
function outer() {
let count = 0;
return function inner() {
count++;
return count;
};
}
const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count 变量被闭包保存,不会丢失
闭包的用途
1. 数据私有化
function createCounter() {
let count = 0; // 私有变量
return {
increment() { return ++count; },
decrement() { return --count; },
getCount() { return count; }
};
}
const counter = createCounter();
counter.increment(); // 1
counter.getCount(); // 1
// counter.count; // undefined(无法直接访问)
2. 函数工厂
function createMultiplier(multiplier) {
return function(x) {
return x * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
3. 延迟执行
function delay(fn, ms) {
return function(...args) {
setTimeout(() => fn(...args), ms);
};
}
const log = delay(console.log, 1000);
log("Delayed message"); // 1秒后输出
4. 事件处理器
function setupButton() {
let clickCount = 0;
document.getElementById('btn').addEventListener('click', function() {
clickCount++;
console.log(`Clicked ${clickCount} times`);
});
}
5. 模块模式
const myModule = (function() {
let privateVar = "secret";
function privateFn() {
console.log("Private function");
}
return {
publicVar: "public",
publicFn() {
console.log(privateVar);
privateFn();
}
};
})();
myModule.publicFn(); // 可访问私有变量和函数
闭包的优缺点
| 优点 |
缺点 |
| 数据私有化,封装性强 |
内存占用大(变量不被回收) |
| 避免全局变量污染 |
使用不当会导致内存泄漏 |
| 创建函数工厂 |
调试困难 |
| 实现柯里化 |
性能开销 |
内存泄漏问题
// 问题:闭包持有大对象引用
function createLeak() {
let largeData = new Array(1000000).fill("data");
return function() {
return "small"; // 只需要返回值,但持有 largeData
};
}
// 解决:手动释放
function createSafe() {
let largeData = new Array(1000000).fill("data");
return function() {
let result = "small";
largeData = null; // 手动释放
return result;
};
}
经典面试题
// 问题:输出什么?
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出 3, 3, 3
}, 100);
}
// 解决方案1:使用 let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 0, 1, 2
}
// 解决方案2:使用 IIFE 创建闭包
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// 解决方案3:setTimeout 第三个参数
for (var i = 0; i < 3; i++) {
setTimeout((j) => console.log(j), 100, i);
}
16. 原型与原型链
定义
- 原型(Prototype) :每个函数都有一个
prototype 属性,指向其原型对象
- 原型链(Prototype Chain) :对象通过
__proto__ 链接到其原型,形成查找链
核心概念
function Person(name) {
this.name = name;
}
// prototype 属性(函数独有)
Person.prototype; // 原型对象
Person.prototype.constructor === Person; // true
// 实例对象
let p = new Person("Alice");
// __proto__ 属性(所有对象都有)
p.__proto__ === Person.prototype; // true
// 原型链
p.__proto__.__proto__ === Object.prototype; // true
p.__proto__.__proto__.__proto__ === null; // true
原型链查找机制
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
let p = new Person("Alice");
// 属性查找顺序:
// 1. 先在对象自身查找
console.log(p.name); // "Alice"(自身属性)
// 2. 在原型上查找
p.sayHi(); // "Hi, I'm Alice"(原型方法)
// 3. 在原型链上查找
p.toString(); // "[object Object]"(Object.prototype)
// 4. 查找失败
p.nonExist; // undefined
原型链示意图
p (实例)
│ __proto__
▼
Person.prototype
│ sayHi()
│ __proto__
▼
Object.prototype
│ toString()
│ valueOf()
│ __proto__
▼
null
prototype vs proto vs constructor
| 属性 |
所属 |
说明 |
prototype |
函数 |
指向原型对象,用于实例共享属性 |
__proto__ |
对象 |
指向创建该对象的函数的原型 |
constructor |
原型对象 |
指回构造函数本身 |
function Person() {}
let p = new Person();
p.constructor === Person; // true
p.__proto__ === Person.prototype; // true
Person.prototype.constructor === Person; // true
17. JavaScript 继承的实现方式
1. 原型链继承
function Parent() {
this.name = "Parent";
this.colors = ["red", "blue"];
}
Parent.prototype.getName = function() {
return this.name;
};
function Child() {}
// 核心:将子类的原型指向父类的实例
Child.prototype = new Parent();
Child.prototype.constructor = Child;
let child = new Child();
console.log(child.getName()); // "Parent"
// 问题1:引用类型共享
child.colors.push("green");
let child2 = new Child();
console.log(child2.colors); // ["red", "blue", "green"]
// 问题2:无法传参
function Child2(age) {
this.age = age;
}
Child2.prototype = new Parent(); // 无法传递参数给 Parent
2. 构造函数继承(经典继承)
function Parent(name) {
this.name = name;
this.colors = ["red", "blue"];
}
function Child(name, age) {
// 核心:在子类构造函数中调用父类
Parent.call(this, name);
this.age = age;
}
let child = new Child("Alice", 10);
console.log(child.name); // "Alice"
console.log(child.age); // 10
// 优点:解决了引用类型共享和传参问题
child.colors.push("green");
let child2 = new Child("Bob", 8);
console.log(child2.colors); // ["red", "blue"]
// 缺点:无法继承原型上的方法
// child.getName(); // TypeError
3. 组合继承(最常用)
function Parent(name) {
this.name = name;
this.colors = ["red", "blue"];
}
Parent.prototype.getName = function() {
return this.name;
};
function Child(name, age) {
Parent.call(this, name); // 第二次调用 Parent
this.age = age;
}
Child.prototype = new Parent(); // 第一次调用 Parent
Child.prototype.constructor = Child;
Child.prototype.getAge = function() {
return this.age;
};
let child = new Child("Alice", 10);
console.log(child.getName()); // "Alice"
console.log(child.getAge()); // 10
// 优点:结合了两种继承方式的优点
// 缺点:调用了两次父类构造函数
4. 寄生组合继承(最优)
function inheritPrototype(Child, Parent) {
// 创建父类原型的副本
let prototype = Object.create(Parent.prototype);
prototype.constructor = Child;
Child.prototype = prototype;
}
function Parent(name) {
this.name = name;
}
Parent.prototype.getName = function() {
return this.name;
};
function Child(name, age) {
Parent.call(this, name); // 只调用一次
this.age = age;
}
// 核心:继承原型而不是父类实例
inheritPrototype(Child, Parent);
Child.prototype.getAge = function() {
return this.age;
};
5. ES6 Class 继承
class Parent {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
static sayHello() {
return "Hello";
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 必须先调用 super
this.age = age;
}
getAge() {
return this.age;
}
// 方法重写
getName() {
return `Child: ${super.getName()}`;
}
}
let child = new Child("Alice", 10);
console.log(child.getName()); // "Child: Alice"
console.log(Child.sayHello()); // "Hello"(继承静态方法)
// ES6 继承的本质
// class Child extends Parent
// 等价于
// Child.prototype = Object.create(Parent.prototype)
// Child.prototype.constructor = Child
继承方式对比
| 方式 |
继承实例属性 |
继承原型方法 |
支持传参 |
调用次数 |
推荐度 |
| 原型链 |
✅ |
✅ |
❌ |
1次 |
⭐ |
| 构造函数 |
✅ |
❌ |
✅ |
1次 |
⭐⭐ |
| 组合继承 |
✅ |
✅ |
✅ |
2次 |
⭐⭐⭐ |
| 寄生组合 |
✅ |
✅ |
✅ |
1次 |
⭐⭐⭐⭐⭐ |
| ES6 Class |
✅ |
✅ |
✅ |
1次 |
⭐⭐⭐⭐⭐ |
四、事件机制与事件委托
18. 事件冒泡与事件捕获
定义
- 事件冒泡(Event Bubbling):事件从目标元素向上传播到根元素
- 事件捕获(Event Capturing):事件从根元素向下传播到目标元素
DOM 事件流三阶段
1. 捕获阶段(Capture Phase)
document → html → body → parent → target
2. 目标阶段(Target Phase)
target 元素
3. 冒泡阶段(Bubbling Phase)
target → parent → body → html → document
代码示例
<div id="outer">
<div id="inner">
<button id="btn">Click</button>
</div>
</div>
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
const btn = document.getElementById('btn');
// 冒泡阶段(默认)
btn.addEventListener('click', () => console.log('btn bubble'));
inner.addEventListener('click', () => console.log('inner bubble'));
outer.addEventListener('click', () => console.log('outer bubble'));
// 点击 btn 输出:btn bubble → inner bubble → outer bubble
// 捕获阶段
outer.addEventListener('click', () => console.log('outer capture'), true);
inner.addEventListener('click', () => console.log('inner capture'), true);
btn.addEventListener('click', () => console.log('btn capture'), true);
// 点击 btn 输出:outer capture → inner capture → btn capture
// 混合使用
// 点击 btn 输出:outer capture → inner capture → btn capture → btn bubble → inner bubble → outer bubble
阻止事件传播
// 阻止冒泡
element.addEventListener('click', function(e) {
e.stopPropagation();
});
// 阻止捕获
element.addEventListener('click', function(e) {
e.stopImmediatePropagation(); // 阻止同一元素上其他监听器
}, true);
不冒泡的事件
// 不冒泡的事件
focus, blur
load, unload
mouseenter, mouseleave
resize, scroll
mouseenter vs mouseover
| 特性 |
mouseenter |
mouseover |
| 冒泡 |
不冒泡 |
冒泡 |
| 触发 |
只在进入元素时 |
进入元素和子元素时都触发 |
| 适用 |
简单悬停效果 |
需要捕获子元素事件 |
19. 事件委托
定义
事件委托(Event Delegation) 是利用事件冒泡原理,将子元素的事件监听器设置在父元素上,通过事件对象判断实际触发元素。
原理
- 在父元素上绑定一个事件监听器
- 利用事件冒泡,子元素事件会传播到父元素
- 通过
event.target 判断实际触发元素
- 根据判断结果执行相应处理
代码示例
// 未使用事件委托(性能差)
const items = document.querySelectorAll('.item');
items.forEach(item => {
item.addEventListener('click', function() {
console.log(this.textContent);
});
});
// 使用事件委托(推荐)
const list = document.getElementById('list');
list.addEventListener('click', function(e) {
if (e.target.classList.contains('item')) {
console.log(e.target.textContent);
}
});
优势
- 内存优化:减少事件监听器数量
- 动态元素:新添加的子元素自动具有事件处理
- 代码简洁:只需一个监听器
完整实现
// 通用事件委托函数
function delegate(element, eventType, selector, handler) {
element.addEventListener(eventType, function(e) {
const target = e.target;
if (target.matches(selector)) {
handler.call(target, e);
} else {
// 向上查找匹配元素
let parent = target.parentElement;
while (parent && parent !== element) {
if (parent.matches(selector)) {
handler.call(parent, e);
break;
}
parent = parent.parentElement;
}
}
});
}
// 使用
const container = document.getElementById('container');
delegate(container, 'click', '.btn', function(e) {
console.log('Button clicked:', this.textContent);
});
20. DOM 事件级别
DOM0 级事件
// 直接赋值,只能绑定一个处理函数
element.onclick = function() {
console.log('Clicked');
};
// 移除
element.onclick = null;
DOM2 级事件
// 可绑定多个处理函数
element.addEventListener('click', function() {
console.log('Handler 1');
});
element.addEventListener('click', function() {
console.log('Handler 2');
});
// 移除(必须使用相同函数引用)
function handler() { console.log('Clicked'); }
element.addEventListener('click', handler);
element.removeEventListener('click', handler);
// 第三个参数:capture(布尔值)或 options(对象)
element.addEventListener('click', handler, false); // 冒泡
element.addEventListener('click', handler, {
capture: false,
once: true, // 只触发一次
passive: true // 不会调用 preventDefault
});
DOM3 级事件
// 增加了更多事件类型
// UI事件、焦点事件、鼠标事件、滚轮事件、键盘事件、输入事件等
// 自定义事件
const event = new CustomEvent('myEvent', {
detail: { data: 'custom data' },
bubbles: true,
cancelable: true
});
element.dispatchEvent(event);
element.addEventListener('myEvent', function(e) {
console.log(e.detail.data);
});
addEventListener vs attachEvent
| 特性 |
addEventListener |
attachEvent |
| 浏览器 |
标准(现代浏览器) |
IE8及以下 |
| 事件类型 |
'click' |
'onclick' |
| this 指向 |
触发元素 |
window |
| 执行顺序 |
添加顺序 |
逆序 |
| 事件对象 |
第一个参数 |
window.event |
五、异步编程
21. 回调函数与回调地狱
回调函数
// 同步回调
[1, 2, 3].forEach(item => console.log(item));
// 异步回调
setTimeout(function() {
console.log('Delayed');
}, 1000);
// Node.js 风格回调
fs.readFile('file.txt', function(err, data) {
if (err) {
console.error(err);
return;
}
console.log(data);
});
回调地狱
// 问题:嵌套回调难以阅读和维护
getUser(function(user) {
getOrders(user.id, function(orders) {
getOrderDetails(orders[0].id, function(details) {
updateOrder(details, function(result) {
sendNotification(result, function() {
console.log('Done');
});
});
});
});
});
// 解决方案1:命名函数
function handleNotification() { console.log('Done'); }
function handleUpdate(result) { sendNotification(result, handleNotification); }
function handleDetails(details) { updateOrder(details, handleUpdate); }
function handleOrders(orders) { getOrderDetails(orders[0].id, handleDetails); }
function handleUser(user) { getOrders(user.id, handleOrders); }
getUser(handleUser);
// 解决方案2:Promise
getUser()
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => updateOrder(details))
.then(result => sendNotification(result))
.then(() => console.log('Done'))
.catch(err => console.error(err));
// 解决方案3:async/await
async function processOrder() {
try {
const user = await getUser();
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
const result = await updateOrder(details);
await sendNotification(result);
console.log('Done');
} catch (err) {
console.error(err);
}
}
22. Promise
定义
Promise 是 JavaScript 中处理异步操作的对象,表示一个异步操作的最终完成或失败。
三种状态
| 状态 |
说明 |
可转换 |
pending |
进行中 |
→ fulfilled 或 rejected |
fulfilled |
已成功 |
不可变 |
rejected |
已失败 |
不可变 |
基本用法
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
const success = true;
if (success) {
resolve('Success!');
} else {
reject(new Error('Failed!'));
}
}, 1000);
});
promise
.then(result => console.log(result))
.catch(error => console.error(error))
.finally(() => console.log('Done'));
Promise 链式调用
// then 返回 Promise,实现链式调用
fetchUser(1)
.then(user => fetchOrders(user.id))
.then(orders => fetchOrderDetails(orders[0].id))
.then(details => console.log(details))
.catch(err => console.error(err));
// then 的返回值
Promise.resolve(1)
.then(val => val + 1) // 返回普通值
.then(val => Promise.resolve(val * 2)) // 返回 Promise
.then(val => console.log(val)); // 4
Promise 静态方法
// Promise.resolve - 创建成功 Promise
Promise.resolve('value');
Promise.resolve(promise); // 返回原 Promise
// Promise.reject - 创建失败 Promise
Promise.reject(new Error('error'));
// Promise.all - 所有 Promise 都成功
Promise.all([p1, p2, p3])
.then(results => console.log(results)) // [r1, r2, r3]
.catch(err => console.error(err)); // 任一失败则失败
// Promise.race - 第一个完成的 Promise
Promise.race([p1, p2, p3])
.then(first => console.log(first))
.catch(err => console.error(err));
// Promise.allSettled - 等待所有完成(不论成功失败)
Promise.allSettled([p1, p2, p3])
.then(results => {
results.forEach(r => {
if (r.status === 'fulfilled') console.log(r.value);
else console.error(r.reason);
});
});
// Promise.any - 第一个成功的 Promise
Promise.any([p1, p2, p3])
.then(first => console.log(first))
.catch(err => console.error(err)); // 全部失败
Promise 错误处理
// 方法1:catch 捕获
promise
.then(result => process(result))
.catch(err => console.error(err));
// 方法2:then 第二个参数
promise.then(
result => process(result),
err => console.error(err)
);
// 方法3:async/await try-catch
async function handle() {
try {
const result = await promise;
process(result);
} catch (err) {
console.error(err);
}
}
// 注意:catch 之后的 then 仍会执行
promise
.then(() => { throw new Error('Oops'); })
.catch(err => console.log('Caught:', err.message))
.then(() => console.log('Still runs')); // 会执行
23. async/await
定义
async/await 是基于 Promise 的语法糖,使异步代码看起来像同步代码。
原理
async/await 基于 Generator 函数和自动执行器实现:
async 函数返回 Promise
await 暂停函数执行,等待 Promise 完成
- 使用 Generator 的
yield 实现暂停
- 使用自动执行器实现自动恢复
基本用法
// async 函数总是返回 Promise
async function fetchUser() {
return { name: 'Alice' };
// 等价于 return Promise.resolve({ name: 'Alice' });
}
fetchUser().then(user => console.log(user));
// await 必须在 async 函数内
async function getUser() {
const response = await fetch('/api/user');
const data = await response.json();
return data;
}
// 错误处理
async function safeFetch() {
try {
const response = await fetch('/api/user');
const data = await response.json();
return data;
} catch (err) {
console.error('Fetch failed:', err);
return null;
}
}
并发控制
// 串行(顺序执行)
async function serial() {
const user = await fetchUser();
const orders = await fetchOrders(user.id);
return orders;
}
// 并行(同时执行)
async function parallel() {
const [user, orders] = await Promise.all([
fetchUser(),
fetchOrders(1)
]);
return { user, orders };
}
// 限制并发数量
async function limitConcurrency(tasks, limit) {
const results = [];
const executing = [];
for (const task of tasks) {
const p = task().then(r => r);
results.push(p);
if (limit <= tasks.length) {
const e = p.then(() =>
executing.splice(executing.indexOf(e), 1)
);
executing.push(e);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
}
return Promise.all(results);
}
async/await vs Promise
| 特性 |
Promise |
async/await |
| 语法 |
链式调用 |
同步风格 |
| 错误处理 |
.catch() |
try-catch |
| 并发 |
Promise.all |
Promise.all + await |
| 调试 |
困难 |
容易(同步堆栈) |
| 兼容性 |
ES6 |
ES2017 |
| 返回值 |
Promise |
Promise |
24. 事件循环(Event Loop)
定义
事件循环(Event Loop) 是 JavaScript 实现异步的核心机制,负责协调调用栈、任务队列和回调的执行。
浏览器事件循环
调用栈(Call Stack)
│
▼
微任务队列(Microtask Queue)
├─ Promise.then/catch/finally
├─ MutationObserver
├─ queueMicrotask
│
▼
宏任务队列(Macrotask Queue)
├─ setTimeout
├─ setInterval
├─ I/O
├─ UI 渲染
├─ setImmediate (Node.js)
执行顺序
console.log('1'); // 同步代码
setTimeout(() => {
console.log('2'); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('3'); // 微任务
});
console.log('4'); // 同步代码
// 输出顺序:1 → 4 → 3 → 2
详细执行流程
// 复杂示例
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
async1();
new Promise(resolve => {
console.log('promise executor');
resolve();
}).then(() => console.log('promise then'));
console.log('script end');
// 输出顺序:
// script start
// async1 start
// async2
// promise executor
// script end
// async1 end
// promise then
// setTimeout
微任务与宏任务对比
| 特性 |
宏任务(Macrotask) |
微任务(Microtask) |
| 包含 |
setTimeout, setInterval, I/O, UI渲染 |
Promise, MutationObserver, queueMicrotask |
| 优先级 |
低 |
高 |
| 执行时机 |
每次事件循环执行一个 |
清空所有微任务 |
| 队列数量 |
多个 |
一个 |
执行规则
- 执行同步代码(调用栈)
- 清空所有微任务
- 执行一个宏任务
- 清空所有微任务
- 重复 3-4
// 验证规则
setTimeout(() => console.log('timeout1'), 0);
setTimeout(() => console.log('timeout2'), 0);
Promise.resolve()
.then(() => console.log('promise1'))
.then(() => console.log('promise2'));
// 输出:promise1 → promise2 → timeout1 → timeout2
// 微任务全部执行完才执行下一个宏任务
六、DOM 操作
25. DOM 操作方法
查找元素
// 旧方法
document.getElementById('id');
document.getElementsByClassName('class');
document.getElementsByTagName('tag');
document.getElementsByName('name');
// 新方法(推荐)
document.querySelector('.class');
document.querySelectorAll('.class');
// 元素内查找
element.querySelector('button');
element.querySelectorAll('li');
创建元素
// 创建元素
const div = document.createElement('div');
const text = document.createTextNode('Hello');
const fragment = document.createDocumentFragment();
// 设置属性
div.setAttribute('id', 'myDiv');
div.setAttribute('data-value', '123');
div.id = 'myDiv';
div.className = 'my-class';
div.classList.add('class1', 'class2');
div.classList.remove('class3');
div.classList.toggle('active');
// 设置内容
div.textContent = 'Text content';
div.innerHTML = '<span>HTML</span>';
div.innerText = 'Visible text';
添加/删除元素
// 添加
parent.appendChild(child);
parent.insertBefore(newChild, referenceChild);
parent.append(...nodes); // ES6,可添加多个
parent.prepend(...nodes); // ES6,插入到开头
element.before(...nodes); // ES6,插入到元素前
element.after(...nodes); // ES6,插入到元素后
element.replaceWith(...nodes); // ES6,替换元素
// 删除
parent.removeChild(child);
element.remove();
element.parentNode.removeChild(element);
// 克隆
let clone = element.cloneNode(false); // 浅克隆
let deepClone = element.cloneNode(true); // 深克隆
innerHTML vs outerHTML
| 特性 |
innerHTML |
outerHTML |
| 内容 |
元素内部 HTML |
元素自身+内部 HTML |
| 赋值影响 |
替换内部内容 |
替换整个元素 |
| 安全性 |
XSS 风险 |
XSS 风险 |
// innerHTML
<div id="box"><p>Original</p></div>
document.getElementById('box').innerHTML = '<span>New</span>';
// 结果:<div id="box"><span>New</span></div>
// outerHTML
<div id="box"><p>Original</p></div>
document.getElementById('box').outerHTML = '<div>New</div>';
// 结果:<div>New</div>(原元素被替换)
innerText vs textContent
| 特性 |
innerText |
textContent |
| 可见性 |
只获取可见文本 |
获取所有文本 |
| 性能 |
较慢(触发重排) |
较快 |
| 格式 |
保留换行和空格 |
不保留格式 |
| 隐藏元素 |
不包含隐藏文本 |
包含隐藏文本 |
七、ES5 核心特性
26. 严格模式
定义
严格模式(Strict Mode) 是一种限制性更强的 JavaScript 执行模式,用于消除不合理语法、提高安全性。
启用方式
// 全局启用
"use strict";
// 函数内启用
function fn() {
"use strict";
}
// 模块自动启用严格模式
严格模式的特点
// 1. 变量必须声明
"use strict";
x = 1; // ReferenceError
// 2. 禁止删除变量
"use strict";
var x = 1;
delete x; // SyntaxError
// 3. 对象属性不能重复
"use strict";
var obj = { a: 1, a: 2 }; // SyntaxError
// 4. 函数参数不能重名
"use strict";
function fn(a, a) {} // SyntaxError
// 5. this 指向 undefined
"use strict";
function fn() { console.log(this); }
fn(); // undefined(非严格模式是 window)
// 6. 禁止八进制字面量
"use strict";
var n = 010; // SyntaxError
// 7. eval 有独立作用域
"use strict";
eval("var x = 1");
console.log(x); // ReferenceError
// 8. 禁止 arguments.callee
"use strict";
function fn() { arguments.callee; } // TypeError
27. Object.defineProperty
定义
Object.defineProperty 用于精确控制对象属性的行为,可定义数据属性和访问器属性。
参数说明
Object.defineProperty(obj, prop, descriptor);
数据属性
let person = {};
Object.defineProperty(person, 'name', {
value: 'Alice', // 属性值
writable: true, // 是否可写
enumerable: true, // 是否可枚举
configurable: true // 是否可删除/修改描述符
});
console.log(person.name); // "Alice"
person.name = 'Bob';
console.log(person.name); // "Bob"
访问器属性(getter/setter)
let product = {
_price: 100
};
Object.defineProperty(product, 'price', {
get() {
console.log('Getting price');
return this._price;
},
set(value) {
if (value < 0) {
throw new Error('Price cannot be negative');
}
console.log('Setting price to', value);
this._price = value;
},
enumerable: true,
configurable: true
});
console.log(product.price); // Getting price → 100
product.price = 200; // Setting price to 200
数据属性与访问器属性互斥
// 错误:不能同时有 value 和 get
Object.defineProperty(obj, 'prop', {
value: 1,
get() { return 2; } // TypeError
});
// 正确:二选一
Object.defineProperty(obj, 'prop', {
value: 1 // 数据属性
});
Object.defineProperty(obj, 'prop', {
get() { return 1; }, // 访问器属性
set(val) { /* ... */ }
});
Vue 2 中的响应式实现
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(`Getting ${key}`);
return val;
},
set(newVal) {
if (newVal !== val) {
console.log(`Setting ${key} to ${newVal}`);
val = newVal;
// 通知更新
notifyUpdate();
}
}
});
}
局限性
- 无法检测对象新增属性
- 无法检测数组索引变化
- Vue 2 使用
Vue.set 解决此问题
- Vue 3 使用
Proxy 替代
28. Object.defineProperty vs Proxy
| 特性 |
Object.defineProperty |
Proxy |
| 监听范围 |
单个属性 |
整个对象 |
| 新增属性 |
无法监听 |
可以监听 |
| 数组索引 |
无法监听 |
可以监听 |
| 性能 |
较好 |
稍差 |
| 兼容性 |
IE9+ |
ES6+ |
| 拦截操作 |
get/set |
13种拦截 |
// Proxy 示例
let obj = {};
let proxy = new Proxy(obj, {
get(target, key) {
console.log(`Getting ${key}`);
return target[key];
},
set(target, key, value) {
console.log(`Setting ${key} to ${value}`);
target[key] = value;
return true;
},
deleteProperty(target, key) {
console.log(`Deleting ${key}`);
delete target[key];
return true;
},
has(target, key) {
console.log(`Checking ${key}`);
return key in target;
}
});
proxy.name = "Alice"; // Setting name to Alice
console.log(proxy.name); // Getting name → "Alice"
八、JavaScript 设计模式
29. 单例模式
定义
单例模式(Singleton) 确保一个类只有一个实例,并提供全局访问点。
实现方式
// 方式1:简单实现
const singleton = {
method1() { /* ... */ },
method2() { /* ... */ }
};
// 方式2:惰性单例
class Singleton {
constructor(name) {
if (Singleton.instance) {
return Singleton.instance;
}
this.name = name;
Singleton.instance = this;
}
}
const s1 = new Singleton('A');
const s2 = new Singleton('B');
console.log(s1 === s2); // true
// 方式3:闭包实现
const Singleton2 = (function() {
let instance;
function createInstance(name) {
return { name, getName() { return name; } };
}
return {
getInstance(name) {
if (!instance) {
instance = createInstance(name);
}
return instance;
}
};
})();
const a = Singleton2.getInstance('A');
const b = Singleton2.getInstance('B');
console.log(a === b); // true
30. 观察者模式 vs 发布订阅模式
观察者模式(Observer)
// 主题(被观察者)
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
this.observers = this.observers.filter(o => o !== observer);
}
notify(data) {
this.observers.forEach(o => o.update(data));
}
}
// 观察者
class Observer {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} received:`, data);
}
}
// 使用
const subject = new Subject();
const obs1 = new Observer('A');
const obs2 = new Observer('B');
subject.addObserver(obs1);
subject.addObserver(obs2);
subject.notify('Hello'); // 两者都收到通知
发布订阅模式(Pub/Sub)
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
}
emit(event, ...args) {
if (this.events[event]) {
this.events[event].forEach(cb => cb(...args));
}
}
once(event, callback) {
const wrapper = (...args) => {
callback(...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}
// 使用
const emitter = new EventEmitter();
emitter.on('data', (data) => console.log('Handler 1:', data));
emitter.on('data', (data) => console.log('Handler 2:', data));
emitter.emit('data', 'Hello');
对比
| 特性 |
观察者模式 |
发布订阅模式 |
| 耦合度 |
紧耦合 |
松耦合 |
| 调度中心 |
无(直接通知) |
有(事件中心) |
| 灵活性 |
低 |
高 |
| 适用场景 |
对象间一对多 |
组件间通信 |
| 示例 |
MVC 模型-视图 |
Node.js EventEmitter |
31. 工厂模式
// 简单工厂
class Car {
constructor(type) {
this.type = type;
}
}
class CarFactory {
static create(type) {
switch(type) {
case 'sedan': return new Car('sedan');
case 'suv': return new Car('suv');
case 'truck': return new Car('truck');
default: throw new Error('Unknown type');
}
}
}
const sedan = CarFactory.create('sedan');
// 工厂方法模式
class Vehicle {
drive() { console.log('Driving'); }
}
class Car2 extends Vehicle {
drive() { console.log('Driving car'); }
}
class Truck extends Vehicle {
drive() { console.log('Driving truck'); }
}
class VehicleFactory {
create(type) {
switch(type) {
case 'car': return new Car2();
case 'truck': return new Truck();
}
}
}
32. 模块模式
// IIFE 模块
const myModule = (function() {
// 私有成员
let privateVar = 0;
function privateMethod() { return privateVar; }
// 公共接口
return {
increment() { privateVar++; },
getValue() { return privateMethod(); }
};
})();
myModule.increment();
console.log(myModule.getValue()); // 1
// ES6 模块
// utils.js
export function helper() { /* ... */ }
export const constant = 42;
export default class MyClass { /* ... */ }
// main.js
import MyClass, { helper, constant } from './utils.js';
九、性能优化与防抖节流
33. 防抖(Debounce)
定义
防抖:在事件被触发后等待一段时间,如果这段时间内再次触发,则重新计时。只有最后一次触发才会执行。
实现
// 基础防抖
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 支持立即执行
function debounce(fn, delay, immediate = false) {
let timer = null;
let result;
return function(...args) {
clearTimeout(timer);
if (immediate) {
const callNow = !timer;
timer = setTimeout(() => { timer = null; }, delay);
if (callNow) result = fn.apply(this, args);
} else {
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
}
return result;
};
}
// 支持取消
function debounce(fn, delay) {
let timer = null;
function debounced(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
}
debounced.cancel = function() {
clearTimeout(timer);
timer = null;
};
return debounced;
}
应用场景
// 搜索框输入
const searchInput = document.getElementById('search');
const search = debounce(function(query) {
fetchResults(query);
}, 300);
searchInput.addEventListener('input', (e) => search(e.target.value));
// 窗口大小调整
window.addEventListener('resize', debounce(function() {
updateLayout();
}, 200));
// 滚动事件
window.addEventListener('scroll', debounce(function() {
checkVisibility();
}, 100));
34. 节流(Throttle)
定义
节流:在一段时间内只执行一次函数,控制函数执行的频率。
实现
// 时间戳版(立即执行)
function throttle(fn, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
};
}
// 定时器版(最后执行)
function throttle(fn, delay) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
}
// 完整版(支持前后执行)
function throttle(fn, delay, options = { leading: true, trailing: true }) {
let timer = null;
let lastTime = 0;
return function throttled(...args) {
const now = Date.now();
// 处理首次执行
if (!lastTime && options.leading === false) {
lastTime = now;
}
const remaining = delay - (now - lastTime);
if (remaining <= 0 || remaining > delay) {
if (timer) {
clearTimeout(timer);
timer = null;
}
lastTime = now;
fn.apply(this, args);
} else if (!timer && options.trailing !== false) {
timer = setTimeout(() => {
lastTime = options.leading === false ? 0 : Date.now();
timer = null;
fn.apply(this, args);
}, remaining);
}
};
throttled.cancel = function() {
clearTimeout(timer);
timer = null;
lastTime = 0;
};
}
应用场景
// 滚动加载
window.addEventListener('scroll', throttle(function() {
checkAndLoadMore();
}, 200));
// 按钮点击限制
const submitBtn = document.getElementById('submit');
submitBtn.addEventListener('click', throttle(function() {
submitForm();
}, 1000, { leading: true, trailing: false }));
// 鼠标移动追踪
document.addEventListener('mousemove', throttle(function(e) {
updatePosition(e.clientX, e.clientY);
}, 50));
防抖 vs 节流对比
| 特性 |
防抖 |
节流 |
| 执行时机 |
最后一次触发后 |
固定时间间隔 |
| 特点 |
合并多次为一次 |
控制执行频率 |
| 适用 |
搜索输入 |
滚动、点击限制 |
| 比喻 |
电梯关门(等人齐) |
红绿灯(固定间隔) |
35. 深拷贝与浅拷贝
浅拷贝
// 方法1:Object.assign
let obj = { a: 1, b: { c: 2 } };
let copy1 = Object.assign({}, obj);
copy1.b.c = 3;
console.log(obj.b.c); // 3(嵌套对象仍共享)
// 方法2:展开运算符
let copy2 = { ...obj };
// 方法3:Array.slice
let arr = [1, { a: 2 }];
let copy3 = arr.slice();
// 方法4:Array.from
let copy4 = Array.from(arr);
// 方法5:手写浅拷贝
function shallowCopy(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
const copy = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = obj[key];
}
}
return copy;
}
深拷贝
// 方法1:JSON(有缺陷)
let obj = { a: 1, b: new Date(), c: undefined };
let copy = JSON.parse(JSON.stringify(obj));
// Date 变字符串,undefined 丢失
// JSON 缺陷:
// 1. 函数丢失
// 2. undefined 丢失
// 3. Symbol 丢失
// 4. Date 变字符串
// 5. RegExp 变空对象
// 6. Map/Set 丢失
// 7. 循环引用报错
// 方法2:手写深拷贝
function deepClone(obj, map = new WeakMap()) {
// 处理基本类型
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// 处理循环引用
if (map.has(obj)) {
return map.get(obj);
}
// 处理特殊对象
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Map) {
const copy = new Map();
map.set(obj, copy);
obj.forEach((val, key) => copy.set(key, deepClone(val, map)));
return copy;
}
if (obj instanceof Set) {
const copy = new Set();
map.set(obj, copy);
obj.forEach(val => copy.add(deepClone(val, map)));
return copy;
}
// 处理对象和数组
const copy = Array.isArray(obj) ? [] : {};
map.set(obj, copy);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepClone(obj[key], map);
}
}
return copy;
}
// 测试
let original = {
a: 1,
b: { c: 2 },
d: [1, 2, 3],
e: new Date(),
f: /regex/g,
g: new Map([['key', 'value']]),
h: undefined,
i: Symbol('sym')
};
let cloned = deepClone(original);
cloned.b.c = 100;
console.log(original.b.c); // 2(不受影响)
36. 数组去重
// 方法1:Set(推荐)
function unique1(arr) {
return [...new Set(arr)];
}
// 方法2:filter + indexOf
function unique2(arr) {
return arr.filter((item, index) => arr.indexOf(item) === index);
}
// 方法3:reduce + includes
function unique3(arr) {
return arr.reduce((acc, cur) => {
return acc.includes(cur) ? acc : [...acc, cur];
}, []);
}
// 方法4:Map
function unique4(arr) {
const map = new Map();
return arr.filter(item => !map.has(item) && map.set(item, true));
}
// 方法5:对象键
function unique5(arr) {
const obj = {};
return arr.filter(item => {
const key = typeof item + item;
return obj.hasOwnProperty(key) ? false : (obj[key] = true);
});
}
// 包含对象的去重
function uniqueWithObjects(arr) {
const seen = new Map();
return arr.filter(item => {
const key = JSON.stringify(item);
return seen.has(key) ? false : seen.set(key, true);
});
}
37. 数组扁平化
// 方法1:flat(ES6)
[1, [2, [3, [4]]]].flat(Infinity); // [1, 2, 3, 4]
// 方法2:递归
function flatten(arr) {
return arr.reduce((acc, cur) => {
return acc.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, []);
}
// 方法3:迭代 + 栈
function flatten(arr) {
const result = [];
const stack = [...arr];
while (stack.length) {
const item = stack.pop();
if (Array.isArray(item)) {
stack.push(...item);
} else {
result.unshift(item);
}
}
return result;
}
// 方法4:toString + split(仅数字)
function flattenNums(arr) {
return arr.toString().split(',').map(Number);
}
// 方法5:Generator
function* flattenGenerator(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flattenGenerator(item);
} else {
yield item;
}
}
}
[...flattenGenerator([1, [2, [3]]])]; // [1, 2, 3]
十、核心操作符与特性
38. new 操作符
执行过程
- 创建一个新对象
- 将新对象的
__proto__ 指向构造函数的 prototype
- 将
this 绑定到新对象,执行构造函数
- 返回新对象(如果构造函数没有返回对象)
手写实现
function myNew(Constructor, ...args) {
// 1. 创建新对象
const obj = {};
// 2. 设置原型链
obj.__proto__ = Constructor.prototype;
// 3. 绑定 this 并执行
const result = Constructor.apply(obj, args);
// 4. 返回对象
return result instanceof Object ? result : obj;
}
// ES6 实现
function myNew2(Constructor, ...args) {
const obj = Object.create(Constructor.prototype);
const result = Constructor.apply(obj, args);
return result instanceof Object ? result : obj;
}
// 测试
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function() {
return `Hi, I'm ${this.name}`;
};
const p = myNew(Person, 'Alice', 25);
console.log(p.name); // "Alice"
console.log(p.sayHi()); // "Hi, I'm Alice"
39. instanceof 原理
原理
instanceof 用于检测构造函数的 prototype 属性是否出现在对象的原型链上。
手写实现
function myInstanceof(obj, Constructor) {
if (typeof Constructor !== 'function') {
throw new TypeError('Right-hand side is not a function');
}
let proto = Object.getPrototypeOf(obj);
const prototype = Constructor.prototype;
while (proto !== null) {
if (proto === prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
// 测试
function Person() {}
const p = new Person();
console.log(myInstanceof(p, Person)); // true
console.log(myInstanceof(p, Object)); // true
console.log(myInstanceof(p, Array)); // false
console.log(myInstanceof([], Array)); // true
console.log(myInstanceof({}, Object)); // true
console.log(myInstanceof(42, Number)); // false(基本类型)
40. 其他常用操作符
Object.create
// 创建以指定对象为原型的对象
let person = { name: 'Alice', sayHi() { return `Hi, ${this.name}`; } };
let student = Object.create(person);
student.age = 20;
console.log(student.name); // "Alice"(继承自 person)
console.log(student.sayHi()); // "Hi, Alice"
// 第二个参数定义属性
let obj = Object.create(null, {
name: { value: 'Alice', writable: true, enumerable: true }
});
Object.assign
// 浅拷贝/合并对象
let target = { a: 1 };
let source = { b: 2, c: 3 };
Object.assign(target, source);
console.log(target); // { a: 1, b: 2, c: 3 }
// 多个源对象
Object.assign({}, obj1, obj2, obj3);
// 数组合并
Object.assign([1, 2], [3, 4]); // [3, 4]
delete 操作符
let obj = { a: 1, b: 2 };
delete obj.a;
console.log(obj); // { b: 2 }
// 只能删除自身属性,不能删除原型链属性
// 不能删除 var 声明的变量
// 不能删除函数声明
in 操作符
let obj = { a: 1 };
console.log('a' in obj); // true
console.log('toString' in obj); // true(原型链)
console.log('b' in obj); // false
// 检查属性存在性(包括原型)
// hasOwnProperty 只检查自身
void 操作符
// void 表达式返回 undefined
void 0; // undefined
void(0); // undefined
void function() {}; // undefined
// 常见用法:避免返回值的链接
<a href="javascript:void(0)">Click</a>
十一、内存管理与垃圾回收
41. 垃圾回收机制
垃圾回收算法
1. 引用计数
// 原理:记录每个值被引用的次数
let a = { name: 'Alice' }; // 对象引用计数:1
let b = a; // 对象引用计数:2
b = null; // 对象引用计数:1
a = null; // 对象引用计数:0 → 垃圾回收
// 问题:循环引用
function problem() {
let a = {};
let b = {};
a.ref = b;
b.ref = a;
}
problem(); // a 和 b 永远不会被回收
2. 标记清除(Mark and Sweep)
// 原理:
// 1. 标记阶段:从根对象开始,标记所有可达对象
// 2. 清除阶段:清除未标记的对象
// 现代浏览器使用此算法
// 优点:解决循环引用问题
// 缺点:内存碎片化
3. 标记整理(Mark and Compact)
// 在标记清除基础上增加整理阶段
// 将存活对象移动到内存一端,解决碎片化
4. 分代收集(Generational Collection)
// V8 引擎使用
// 新生代(Young Generation):短命对象,频繁回收
// 老生代(Old Generation):长命对象,较少回收
V8 垃圾回收
// 新生代:Scavenge 算法
// - 使用复制算法
// - 分为 From 和 To 空间
// - 存活对象从 From 复制到 To
// 老生代:标记清除 + 标记整理
// - 标记阶段识别可达对象
// - 清除阶段回收不可达对象
// - 整理阶段整理内存碎片
42. 内存泄漏
常见内存泄漏场景
1. 全局变量
function fn() {
leakedVar = "I'm global"; // 忘记 var/let/const
}
// 解决:使用严格模式
2. 未清除的定时器
// 泄漏
function start() {
setInterval(function() {
const data = new Array(1000000).fill('data');
console.log(data);
}, 1000);
}
// 解决
let timer = setInterval(/* ... */);
clearInterval(timer); // 手动清除
3. 未移除的事件监听器
// 泄漏
function setup() {
document.getElementById('btn').addEventListener('click', function() {
const largeData = new Array(1000000).fill('data');
// ...
});
}
// 解决
const handler = function() { /* ... */ };
element.addEventListener('click', handler);
element.removeEventListener('click', handler);
4. 闭包持有
// 泄漏
function createLeak() {
const largeData = new Array(1000000).fill('data');
return function() {
console.log('small'); // 持有 largeData
};
}
// 解决:手动释放
function createSafe() {
let largeData = new Array(1000000).fill('data');
return function() {
largeData = null; // 释放引用
console.log('small');
};
}
5. DOM 泄漏
// 泄漏
function removeElement() {
const el = document.getElementById('myElement');
el.remove();
// 如果其他地方仍持有 el 的引用,不会被回收
}
// 解决:确保没有引用
function removeElementSafe() {
const el = document.getElementById('myElement');
el.remove();
el = null; // 明确释放
}
6. 控制台日志
// 大型对象被控制台引用
const largeObj = { /* ... */ };
console.log(largeObj); // 在开发者工具打开时不会被回收
检测方法
// Chrome DevTools Memory 面板
// 1. Heap Snapshot:堆快照
// 2. Allocation Timeline:分配时间线
// 3. Memory:内存使用监控
// performance.memory API(Chrome)
console.log(performance.memory.usedJSHeapSize);
console.log(performance.memory.totalJSHeapSize);
十二、模块化
43. CommonJS vs ES Modules
| 特性 |
CommonJS |
ES Modules |
| 语法 |
require() / module.exports |
import / export |
| 加载时机 |
运行时加载 |
编译时加载 |
| 输出 |
值的拷贝(缓存) |
值的引用(只读) |
| 动态加载 |
支持 |
import() 支持 |
| this 指向 |
module 对象 |
undefined |
| 环境 |
Node.js |
浏览器 / 现代 Node.js |
| 循环依赖 |
可能返回未完成 |
正确处理 |
CommonJS
// math.js
module.exports = {
add: (a, b) => a + b,
subtract: (a, b) => a - b
};
// app.js
const math = require('./math');
console.log(math.add(1, 2)); // 3
// 单个导出
module.exports = function() { /* ... */ };
exports.helper = function() { /* ... */ };
ES Modules
// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export default function multiply(a, b) { return a * b; }
// app.js
import multiply, { add, PI } from './math.js';
console.log(add(1, 2)); // 3
// 动态导入
const module = await import('./math.js');
// 重命名导入
import { add as sum } from './math.js';
// 命名空间导入
import * as MathUtils from './math.js';
UMD(通用模块定义)
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['dependency'], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS
module.exports = factory(require('dependency'));
} else {
// 全局变量
root.myModule = factory(root.dependency);
}
})(this, function(dependency) {
// 模块代码
return {};
});
十三、函数式编程
44. 函数柯里化
定义
柯里化(Currying) 是将接受多个参数的函数转换为接受单一参数的函数,并返回接受余下参数的新函数。
实现
// 基础柯里化
function add(a, b, c) {
return a + b + c;
}
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, [...args, ...moreArgs]);
};
};
}
const curriedAdd = curry(add);
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1)(2, 3); // 6
// 实用柯里化
function curry2(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return (...moreArgs) => curried(...args, ...moreArgs);
};
}
应用场景
// 1. 参数复用
function match(pattern, str) {
return str.match(new RegExp(pattern));
}
const matchEmail = curry(match)(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
matchEmail('test@example.com'); // 匹配
// 2. 延迟执行
function ajax(method, url, data) {
return fetch(url, { method, body: JSON.stringify(data) });
}
const post = curry(ajax)('POST');
const postUser = post('/api/users');
postUser({ name: 'Alice' });
// 3. 函数组合
const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));
const double = x => x * 2;
const increment = x => x + 1;
const transform = compose(double, increment);
transform(5); // 12
45. 纯函数
定义
纯函数(Pure Function) 是相同输入始终返回相同输出,且没有副作用的函数。
特性
- 确定性:相同输入 → 相同输出
- 无副作用:不修改外部状态
- 引用透明:可用返回值替换函数调用
示例
// 纯函数
function add(a, b) { return a + b; }
function square(x) { return x * x; }
function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); }
// 非纯函数
let count = 0;
function increment() { return ++count; } // 依赖外部状态
function random() { return Math.random(); } // 不确定输出
function saveToDB(data) { db.save(data); } // 副作用
副作用类型
// 1. 修改全局变量
// 2. I/O 操作(网络请求、文件读写)
// 3. DOM 操作
// 4. 修改传入参数
// 5. console.log
// 6. Math.random(), Date.now()
46. 高阶函数
定义
高阶函数(Higher-Order Function) 是接受函数作为参数或返回函数的函数。
示例
// 接受函数作为参数
function forEach(arr, fn) {
for (let i = 0; i < arr.length; i++) {
fn(arr[i], i, arr);
}
}
// 返回函数
function multiplier(factor) {
return function(x) { return x * factor; };
}
// 常见高阶函数
[1, 2, 3].map(x => x * 2); // [2, 4, 6]
[1, 2, 3].filter(x => x > 1); // [2, 3]
[1, 2, 3].reduce((acc, x) => acc + x, 0); // 6
[1, 2, 3].every(x => x > 0); // true
[1, 2, 3].some(x => x > 2); // true
十四、正则表达式
47. 正则表达式基础
创建方式
// 字面量(推荐)
const re1 = /pattern/flags;
// 构造函数
const re2 = new RegExp('pattern', 'flags');
修饰符
| 修饰符 |
说明 |
g |
全局匹配 |
i |
忽略大小写 |
m |
多行模式 |
s |
dotAll 模式(. 匹配换行) |
u |
Unicode 模式 |
y |
粘连模式 |
元字符
| 元字符 |
说明 |
. |
除换行外的任意字符 |
\d |
数字 [0-9] |
\D |
非数字 |
\w |
单词字符 [a-zA-Z0-9_] |
\W |
非单词字符 |
\s |
空白字符 |
\S |
非空白字符 |
^ |
开头 |
$ |
结尾 |
* |
0 或多次 |
+ |
1 或多次 |
? |
0 或 1 次 |
{n} |
恰好 n 次 |
{n,} |
至少 n 次 |
{n,m} |
n 到 m 次 |
分组与捕获
// 捕获分组
const re = /(\d{4})-(\d{2})-(\d{2})/;
const match = re.exec('2024-01-15');
console.log(match[1]); // "2024"(年)
console.log(match[2]); // "01"(月)
// 非捕获分组
const re2 = /(?:\d{3})-(\d{4})/;
// 命名捕获组(ES2018)
const re3 = /(?<year>\d{4})-(?<month>\d{2})/;
const match3 = re3.exec('2024-01');
console.log(match3.groups.year); // "2024"
// 前瞻/后瞻
const re4 = /(?<=\$)\d+/; // 后瞻:美元符号后的数字
const re5 = /\d+(?=%)/; // 前瞻:百分号前的数字
const re6 = /(?<!\$)\d+/; // 负向后瞻
const re7 = /\d+(?!%)/; // 负向前瞻
贪婪与非贪婪
// 贪婪(默认)
'aaa'.match(/a+/); // ["aaa"]
'<div>content</div>'.match(/<.*>/); // ["<div>content</div>"]
// 非贪婪
'aaa'.match(/a+?/); // ["a"]
'<div>content</div>'.match(/<.*?>/); // ["<div>"]
常用方法
const re = /abc/i;
// test - 返回布尔值
re.test('ABC'); // true
// exec - 返回匹配数组
re.exec('ABC'); // ["ABC", index: 0, input: "ABC"]
// 字符串方法
'abc'.match(/abc/); // ["abc"]
'abc'.search(/bc/); // 1(位置)
'abc'.replace(/b/, 'B'); // "aBc"
'abc'.split(/b/); // ["a", "c"]
十五、常用工具函数手写
48. URL 参数解析
// 方法1:简单实现
function parseQueryString(url) {
const params = {};
const queryString = url.split('?')[1] || url;
queryString.split('&').forEach(pair => {
const [key, value] = pair.split('=');
if (key) {
params[decodeURIComponent(key)] = decodeURIComponent(value || '');
}
});
return params;
}
// 方法2:完整实现(支持数组)
function parseQueryString2(url) {
const params = {};
const queryString = url.split('?')[1] || url;
queryString.split('&').forEach(pair => {
const [key, value = ''] = pair.split('=');
const decodedKey = decodeURIComponent(key);
const decodedValue = decodeURIComponent(value);
if (decodedKey.includes('[')) {
// 数组参数:key[]=1&key[]=2
const arrKey = decodedKey.match(/(\w+)\[\]/)[1];
if (!params[arrKey]) params[arrKey] = [];
params[arrKey].push(decodedValue);
} else {
params[decodedKey] = decodedValue;
}
});
return params;
}
// 测试
const url = 'https://example.com?name=John&age=30&city=NYC';
console.log(parseQueryString(url));
// { name: "John", age: "30", city: "NYC" }
49. 深比较
function deepEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (typeof a !== typeof b) return false;
// 对象比较
if (typeof a === 'object') {
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every(key =>
deepEqual(a[key], b[key]) && keysB.includes(key)
);
}
return false;
}
50. 函数 once
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
// 使用
const init = once(function() {
console.log('Initializing...');
return { status: 'ready' };
});
init(); // "Initializing..."
init(); // 无输出
init(); // 无输出
51. 函数记忆(Memoize)
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// 使用
const fibonacci = memoize(function(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
fibonacci(100); // 瞬间完成(缓存结果)
十六、常用内置方法
52. 常用数组方法
| 方法 |
说明 |
是否修改原数组 |
push() |
末尾添加 |
✅ |
pop() |
末尾删除 |
✅ |
shift() |
开头删除 |
✅ |
unshift() |
开头添加 |
✅ |
splice() |
增删改 |
✅ |
sort() |
排序 |
✅ |
reverse() |
反转 |
✅ |
fill() |
填充 |
✅ |
copyWithin() |
复制 |
✅ |
map() |
映射 |
❌ |
filter() |
过滤 |
❌ |
reduce() |
归并 |
❌ |
forEach() |
遍历 |
❌ |
find() |
查找 |
❌ |
findIndex() |
查找索引 |
❌ |
some() |
任一满足 |
❌ |
every() |
全部满足 |
❌ |
includes() |
包含 |
❌ |
indexOf() |
查找索引 |
❌ |
slice() |
截取 |
❌ |
concat() |
合并 |
❌ |
flat() |
扁平化 |
❌ |
flatMap() |
映射+扁平化 |
❌ |
join() |
连接字符串 |
❌ |
53. 常用字符串方法
| 方法 |
说明 |
charAt(index) |
获取指定位置字符 |
charCodeAt(index) |
获取字符 Unicode |
concat(...strs) |
连接字符串 |
includes(str) |
是否包含 |
startsWith(str) |
是否以...开头 |
endsWith(str) |
是否以...结尾 |
indexOf(str) |
查找位置 |
lastIndexOf(str) |
最后出现位置 |
match(regex) |
正则匹配 |
replace(regex, str) |
替换 |
search(regex) |
正则搜索 |
slice(start, end) |
截取 |
split(separator) |
分割为数组 |
substr(start, len) |
截取(已废弃) |
substring(start, end) |
截取 |
toLowerCase() |
转小写 |
toUpperCase() |
转大写 |
trim() |
去首尾空白 |
padStart(len, str) |
头部填充 |
padEnd(len, str) |
尾部填充 |
repeat(n) |
重复 |
54. 常用对象方法
| 方法 |
说明 |
Object.keys(obj) |
可枚举属性名数组 |
Object.values(obj) |
可枚举属性值数组 |
Object.entries(obj) |
键值对数组 |
Object.assign(target, ...sources) |
合并对象 |
Object.create(proto) |
创建对象(指定原型) |
Object.defineProperty() |
定义属性 |
Object.defineProperties() |
定义多个属性 |
Object.getOwnPropertyDescriptor() |
获取属性描述符 |
Object.getOwnPropertyNames() |
所有属性名(不含Symbol) |
Object.getOwnPropertySymbols() |
Symbol 属性名 |
Object.freeze(obj) |
冻结对象 |
Object.seal(obj) |
密封对象 |
Object.preventExtensions() |
禁止扩展 |
Object.isFrozen() |
是否冻结 |
Object.isSealed() |
是否密封 |
Object.is(a, b) |
严格相等 |
Object.hasOwn(obj, key) |
是否有自身属性(ES2022) |
十七、虚拟 DOM
55. 虚拟 DOM 的优缺点
定义
虚拟 DOM(Virtual DOM) 是用 JavaScript 对象表示的 DOM 树结构,是真实 DOM 的轻量级副本。
实现原理
// 虚拟节点
const vNode = {
tag: 'div',
attrs: { id: 'app' },
children: [
{ tag: 'p', attrs: {}, children: ['Hello'] },
{ tag: 'span', attrs: {}, children: ['World'] }
]
};
// 渲染为真实 DOM
function render(vNode) {
if (typeof vNode === 'string') {
return document.createTextNode(vNode);
}
const el = document.createElement(vNode.tag);
Object.keys(vNode.attrs || {}).forEach(key => {
el.setAttribute(key, vNode.attrs[key]);
});
(vNode.children || []).forEach(child => {
el.appendChild(render(child));
});
return el;
}
优点
- 性能优化:减少 DOM 操作(批量更新)
- 跨平台:可渲染到非浏览器环境(React Native)
- 函数式编程:UI = render(state)
- 简化开发:声明式编程,自动 Diff
缺点
- 首次渲染慢:需要构建虚拟 DOM 树
- 内存占用:维护两份 DOM 树
- 过度优化:简单场景可能不需要
- 内存对比计算:Diff 算法本身有开销
与真实 DOM 的区别
| 特性 |
虚拟 DOM |
真实 DOM |
| 类型 |
JavaScript 对象 |
浏览器 API 对象 |
| 操作速度 |
快(内存操作) |
慢(触发重排/重绘) |
| 更新方式 |
批量更新 |
立即更新 |
| 内存占用 |
轻量 |
重量 |
十八、其他核心面试题
56. let 与 const 作用域差异
| 特性 |
let |
const |
| 作用域 |
块级 |
块级 |
| 变量提升 |
有(暂时性死区) |
有(暂时性死区) |
| 重新赋值 |
✅ |
❌ |
| 必须初始化 |
❌ |
✅ |
| 重复声明 |
❌ |
❌ |
| 修改对象属性 |
✅ |
✅ |
// let 可重新赋值
let x = 1;
x = 2; // ✅
// const 不可重新赋值
const y = 1;
y = 2; // TypeError
// const 对象属性可修改
const obj = { a: 1 };
obj.a = 2; // ✅
obj = {}; // TypeError
// 暂时性死区
console.log(z); // ReferenceError
let z = 10;
57. 箭头函数与普通函数的区别
| 特性 |
普通函数 |
箭头函数 |
| this 指向 |
动态(调用时确定) |
静态(定义时确定) |
| arguments |
有 |
无(用 rest 参数) |
| new |
可以 |
不可以 |
| prototype |
有 |
无 |
| call/apply/bind |
this 可改变 |
this 不可改变 |
| 作为方法 |
适合 |
不适合 |
| 作为构造器 |
可以 |
不可以 |
58. callee 和 caller
// arguments.callee - 指向当前执行函数
function factorial(n) {
if (n <= 1) return 1;
return n * arguments.callee(n - 1);
}
// 严格模式下不可用
// 替代方案:命名函数表达式
const factorial2 = function fact(n) {
if (n <= 1) return 1;
return n * fact(n - 1);
};
// function.caller - 指向调用当前函数的函数
function outer() {
inner();
}
function inner() {
console.log(inner.caller); // 指向 outer
}
outer();
// 严格模式下不可用
59. cookie、sessionStorage、localStorage 区别
| 特性 |
cookie |
localStorage |
sessionStorage |
| 生命周期 |
可设置过期时间 |
永久 |
会话结束 |
| 存储大小 |
4KB |
5-10MB |
5-10MB |
| 通信 |
每次请求携带 |
不参与 |
不参与 |
| 作用域 |
同源窗口 |
同源窗口 |
同源同标签页 |
| API |
document.cookie |
localStorage API |
sessionStorage API |
// localStorage
localStorage.setItem('key', 'value');
localStorage.getItem('key');
localStorage.removeItem('key');
localStorage.clear();
// sessionStorage
sessionStorage.setItem('key', 'value');
sessionStorage.getItem('key');