JavaScript 核心概念
JavaScript 语言中必须理解的核心机制,包括数据类型、执行上下文、闭包、原型链、this 指向、事件循环、Promise、模块化、严格模式、错误处理、迭代器、for...in/for...of、对象不可变性、Symbol、Proxy。
数据类型
JavaScript 有 7 种基本类型和引用类型,以及丰富的类型检测、类型转换机制和各类型原型链上的方法。详细内容请阅读 JavaScript 数据类型与内置方法。
速查
| 基本类型 | 示例 | typeof |
|---|---|---|
| string | 'hello' | 'string' |
| number | 42, NaN, Infinity | 'number' |
| boolean | true, false | 'boolean' |
| undefined | undefined | 'undefined' |
| null | null | 'object'(bug,见下方说明) |
| symbol | Symbol('id') | 'symbol' |
| bigint | 42n | 'bigint' |
| 引用类型 | 示例 | typeof |
|---|---|---|
| Object | {} | 'object' |
| Array | [] | 'object' |
| Function | function(){} | 'function' |
| Date | new Date() | 'object' |
| RegExp | /\d+/ | 'object' |
| Map / Set | new Map() | 'object' |
// 假值(Falsy):false, 0, -0, 0n, '', null, undefined, NaN
// 除此之外都是真值(Truthy),包括 {} 和 []
// 类型检测推荐
// typeof:检测基本类型最方便,返回类型字符串
typeof 42 // 'number'(基本类型)
// Array.isArray:专用于判断是否为数组,跨 iframe 也安全
Array.isArray([]) // true(数组专用)
// Object.prototype.toString.call:最准确的类型检测,能区分所有内置类型
Object.prototype.toString.call(null) // '[object Null]'(最准确)
// Object.hasOwn:ES2022 新增,替代 obj.hasOwnProperty(),更安全(不受原型链覆盖影响)
Object.hasOwn(obj, 'key') // true(ES2022,替代 hasOwnProperty)
// NaN 的特殊性
NaN === NaN // false(NaN 不等于自身!)
Number.isNaN(NaN) // true(推荐,ES2015,不会做类型转换)
isNaN('abc') // true(会先 Number('abc') 得到 NaN,有坑!)
Number.isNaN('abc') // false(不会类型转换,更准确)
// typeof 对未声明变量的安全检测
typeof undeclaredVar // 'undefined'(不会报错)
// undeclaredVar // ❌ ReferenceError
// 为什么 typeof null === 'object'?
// 这是 JavaScript 诞生时的 bug。最初 JS 用 32 位存储值,低 3 位表示类型:
// 000 = object, 001 = int, 010 = double, 100 = string, 110 = boolean
// null 的机器码全零(空指针),低 3 位恰好是 000,所以被误判为 object
// 修复会破坏大量现有代码,所以一直保留到现在执行上下文与作用域
执行上下文
JavaScript 代码执行时,引擎会创建执行上下文(Execution Context)。
全局执行上下文
↓ 函数调用
函数执行上下文
↓ 函数调用
函数执行上下文
↓ 执行完毕出栈
...每个执行上下文包含:
- 变量环境:
var声明的变量、函数声明 - 词法环境:
let/const声明的变量 - this 绑定
作用域链
const global = '全局'
function outer() {
const outerVar = '外层'
function inner() {
const innerVar = '内层'
// 作用域链:inner → outer → global
console.log(innerVar) // ✅ 内层
console.log(outerVar) // ✅ 外层(向上查找)
console.log(global) // ✅ 全局(向上查找)
}
inner()
// console.log(innerVar) // ❌ ReferenceError
}var / let / const 的区别
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数级 | 块级 | 块级 |
| 变量提升 | ✅ 初始化为 undefined | ✅ 存在但不初始化(暂时性死区) | ✅ 存在但不初始化 |
| 重复声明 | ✅ 允许 | ❌ 报错 | ❌ 报错 |
| 重新赋值 | ✅ | ✅ | ❌ |
| 全局属性 | 挂载到 window | 不挂载 | 不挂载 |
// 暂时性死区(TDZ)
console.log(a) // undefined(var 提升)
// console.log(b) // ❌ ReferenceError(let 暂时性死区)
let b = 1// 块级作用域
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
// 输出:3, 3, 3(var 没有块级作用域)
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0)
}
// 输出:0, 1, 2(let 有块级作用域,每次循环创建新变量)闭包
什么是闭包
大白话解释: 闭包就像一个函数"记住了"它出生时的环境。就像你小时候在家学会了一个秘密配方,长大后离家了,但你仍然记得那个配方,可以用它。
为什么要用闭包?
- 数据私有化:想隐藏变量,不让外部直接访问,只能通过特定方法操作
- 保持状态:函数执行完毕后,某些变量还想保留下来继续用
- 函数工厂:根据参数生成不同的函数
闭包 = 函数 + 其引用的外部变量。函数能够"记住"它创建时的作用域。
function createCounter() {
let count = 0 // 外部变量
return function () {
count++ // 引用外部变量
return count
}
}
const counter = createCounter()
console.log(counter()) // 1
console.log(counter()) // 2
console.log(counter()) // 3
// count 变量被闭包"记住",不会被回收闭包的本质
function createMultiplier(factor) {
// factor 被闭包捕获
return function (number) {
return number * factor
}
}
const double = createMultiplier(2)
const triple = createMultiplier(3)
console.log(double(5)) // 10
console.log(triple(5)) // 15
// 每次调用 createMultiplier 都创建新的闭包,factor 各自独立闭包的常见用途
数据封装(私有变量)
function createBankAccount(initialBalance) {
let balance = initialBalance // 私有变量,外部无法直接访问
return {
deposit(amount) {
if (amount > 0) balance += amount
return balance
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount
return balance
}
throw new Error('余额不足')
},
getBalance() {
return balance
},
}
}
const account = createBankAccount(1000)
account.deposit(500)
console.log(account.getBalance()) // 1500
// console.log(balance) // ❌ 无法直接访问函数工厂
// 创建一个验证器工厂:接收规则数组,返回验证函数
function createValidator(rules) {
// 返回的闭包函数:接收待验证的值
return function (value) {
// 遍历所有规则,有一条不通过就返回 false
for (const rule of rules) {
if (!rule(value)) return false
}
// 全部通过返回 true
return true
}
}
// 用工厂函数创建邮箱验证器,传入三条规则
const isValidEmail = createValidator([
(v) => v.includes('@'), // 必须包含 @ 符号
(v) => v.length > 5, // 长度必须大于 5
(v) => !v.includes(' '), // 不能包含空格
])
console.log(isValidEmail('[email protected]')) // true(三条规则都通过)
console.log(isValidEmail('invalid')) // false(缺少 @)闭包的坑:循环中的闭包
// ❌ 经典问题
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i) // 3, 3, 3(共享同一个 i)
}, 100)
}
// ✅ 方案一:用 let(块级作用域)
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i) // 0, 1, 2
}, 100)
}
// ✅ 方案二:用 IIFE 创建闭包
for (var i = 0; i < 3; i++) {
;(function (j) {
setTimeout(function () {
console.log(j) // 0, 1, 2
}, 100)
})(i)
}闭包的内存问题
function createLargeData() {
const largeArray = new Array(1000000).fill('data')
return function () {
// 即使只用了 largeArray.length,整个 largeArray 也被闭包引用
return largeArray.length
}
}
// ✅ 优化:只保留需要的值
function createLargeDataOptimized() {
const length = new Array(1000000).fill('data').length
return function () {
return length // 只捕获 length,largeArray 可被回收
}
}this 指向
绑定规则(优先级从高到低)
| 规则 | 场景 | this 指向 |
|---|---|---|
| new 绑定 | new Foo() | 新创建的对象 |
| 显式绑定 | call/apply/bind | 指定的对象 |
| 隐式绑定 | obj.fn() | 调用的对象 |
| 默认绑定 | fn() | 严格模式 undefined,非严格模式 window |
const obj = {
name: '张三',
greet() {
console.log(this.name)
},
}
obj.greet() // '张三'(隐式绑定)
const greetFn = obj.greet
greetFn() // undefined(默认绑定,严格模式 this 是 undefined)
const boundGreet = obj.greet.bind(obj)
boundGreet() // '张三'(显式绑定)call / apply / bind 的区别
| 方法 | 执行方式 | 参数格式 | 返回值 |
|---|---|---|---|
call | 立即执行 | 逐个传递 fn(obj, arg1, arg2) | 函数返回值 |
apply | 立即执行 | 数组传递 fn(obj, [arg1, arg2]) | 函数返回值 |
bind | 返回新函数 | 逐个传递 fn(obj, arg1) | 绑定后的新函数 |
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`
}
const person = { name: '张三' }
// call:立即执行,参数逐个传
greet.call(person, 'Hello', '!') // 'Hello, 张三!'
// apply:立即执行,参数用数组传
greet.apply(person, ['Hello', '!']) // 'Hello, 张三!'
// bind:返回新函数,不立即执行
const boundGreet = greet.bind(person, 'Hello')
boundGreet('!') // 'Hello, 张三!'
// 实际应用场景:类数组转真数组
const divs = document.querySelectorAll('div')
const divArray = Array.prototype.slice.call(divs) // 旧写法
const divArray2 = Array.from(divs) // ES2015 写法(推荐)箭头函数没有自己的 this
const obj = {
name: '张三',
// ❌ 普通函数:this 是调用者
greet() {
setTimeout(function () {
console.log(this.name) // undefined(this 是 window)
}, 100)
},
// ✅ 箭头函数:继承外层 this
greetArrow() {
setTimeout(() => {
console.log(this.name) // '张三'(继承 greetArrow 的 this)
}, 100)
},
}this 丢失问题
'use strict' // 以下示例在严格模式下(ES Modules 默认严格模式)
class User {
constructor(name) {
this.name = name
}
// ❌ 普通方法:传递后 this 丢失
greet() {
return `Hello, ${this.name}`
}
// ✅ 箭头函数属性(ES2022 类字段语法):this 始终是实例
greetArrow = () => {
return `Hello, ${this.name}`
}
}
const instance = new User('张三')
const greet = instance.greet
// greet() // ❌ TypeError: Cannot read properties of undefined (reading 'name')
// 严格模式下 this 是 undefined,非严格模式下 this 是 window
const greetArrow = instance.greetArrow
console.log(greetArrow()) // ✅ 'Hello, 张三'深拷贝 vs 浅拷贝
大白话解释: 浅拷贝就像"复印文件",只复印了第一层,如果文件里夹了照片(嵌套对象),复印的只是照片的"地址",改照片还是会影响原件。深拷贝就像"完全克隆",连照片都重新洗一份,互不影响。
为什么要区分?
- 赋值操作是引用传递:
const b = a只是让 b 指向同一个对象,修改 b 会影响 a - 浅拷贝只拷贝一层:嵌套对象仍是引用
- 深拷贝完全独立:修改副本不影响原件
浅拷贝方式
const original = { name: '张三', scores: [90, 85] }
// 方式一:展开运算符(ES2018)— 最常用
const copy1 = { ...original }
// 方式二:Object.assign — 旧写法
const copy2 = Object.assign({}, original)
// 方式三:数组的浅拷贝
const arr = [1, 2, 3]
const arrCopy1 = [...arr] // 展开运算符
const arrCopy2 = arr.slice() // slice 不传参
const arrCopy3 = Array.from(arr) // Array.from
// 浅拷贝的问题:嵌套对象仍是引用
copy1.scores.push(95)
console.log(original.scores) // [90, 85, 95] ❌ 原始对象也被修改了!深拷贝方式
const original = { name: '张三', scores: [90, 85], address: { city: '北京' } }
// 方式一:structuredClone(推荐,Web API,2022 年主流浏览器支持)
const deepCopy1 = structuredClone(original)
// 方式二:JSON.parse(JSON.stringify())(有局限性)
const deepCopy2 = JSON.parse(JSON.stringify(original))
// ⚠️ 局限性:
// - 不能处理 undefined、函数、Symbol、BigInt
// - 不能处理循环引用
// - Date 会变成字符串,RegExp 会变成空对象
// - Map/Set 会变成空对象
// 方式三:递归实现(理解原理)
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
const clone = Array.isArray(obj) ? [] : {}
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
clone[key] = deepClone(obj[key])
}
}
return clone
}
// 深拷贝后互不影响
deepCopy1.address.city = '上海'
console.log(original.address.city) // '北京' ✅ 原始对象不受影响对比总结
| 方式 | 深度 | 函数/Symbol | 循环引用 | 推荐场景 |
|---|---|---|---|---|
{ ...obj } | 浅 | ✅ 保留 | ✅ | 简单对象,只有一层 |
structuredClone | 深 | ❌ 丢失 | ✅ | 现代浏览器,通用深拷贝 |
JSON.parse/stringify | 深 | ❌ 丢失 | ❌ | 简单数据,兼容性好 |
| 递归实现 | 深 | ✅ 可处理 | ❌ | 需要处理特殊类型 |
原型链
大白话解释: 原型链就像"找不到就去爸爸那里找"。你问一个对象有没有某个属性,它自己没有,就去它的"爸爸"(原型对象)那里找,爸爸没有就去"爷爷"(Object.prototype)那里找,一直找到 null 为止。
为什么要理解原型链?
- 理解继承机制:JavaScript 没有传统的"类继承",而是通过原型链实现继承
- 排查属性问题:为什么某个对象能访问某个方法?顺着原型链就能找到答案
- 理解 class 本质:ES6 的 class 只是语法糖,底层还是原型链
原型链是 JavaScript 实现继承的核心机制,详细内容(原型三角关系、new 的过程、继承的 5 种方式、class 的原型本质、常见面试题)请阅读 JavaScript 原型与原型链。
快速理解
实例对象 ──[[Prototype]]──→ 原型对象(构造函数.prototype)
└── [[Prototype]] → Object.prototype
└── [[Prototype]] → null
注:[[Prototype]] 是规范表示,可通过 Object.getPrototypeOf() 访问
__proto__ 是非标准访问方式(教学演示用,生产代码不推荐)function Person(name) {
this.name = name
}
Person.prototype.sayHello = function () {
return `Hello, I'm ${this.name}`
}
const person = new Person('张三')
// 原型三角(推荐用 Object.getPrototypeOf,__proto__ 已废弃)
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
console.log(Person.prototype.constructor === Person) // true
// 原型链查找
console.log(person.sayHello()) // 'Hello, I'm 张三'(从 Person.prototype 上找到)
console.log(person.toString()) // '[object Object]'(从 Object.prototype 上找到)
// 沿原型链查找
console.log(Object.getPrototypeOf(Object.getPrototypeOf(person)) === Object.prototype) // true
console.log(Object.getPrototypeOf(Object.prototype) === null) // true(终点)
// ⚠️ __proto__ 已废弃,仅用于教学演示
console.log(person.__proto__ === Person.prototype) // true(不推荐,但面试常考)class 继承的原型链
class Animal {}
class Dog extends Animal {}
const dog = new Dog()
// 实例原型链(推荐用 Object.getPrototypeOf)
Object.getPrototypeOf(dog) === Dog.prototype // true
Object.getPrototypeOf(Dog.prototype) === Animal.prototype // true
Object.getPrototypeOf(Animal.prototype) === Object.prototype // true
// 构造函数继承(静态方法也能继承)
Object.getPrototypeOf(Dog) === Animal // true事件循环(Event Loop)
大白话解释: 事件循环就像餐厅的服务员。服务员(主线程)一次只能服务一位客人(执行一段代码)。如果有客人点了需要等待的菜(异步操作,如网络请求),服务员不会傻等,而是去服务下一位客人。等菜做好了(异步完成),再回来服务之前的客人。
为什么要理解事件循环?
- 理解异步代码执行顺序:为什么 setTimeout 不是立即执行?为什么 Promise 比 setTimeout 先执行?
- 避免常见坑:比如在循环中使用 var + setTimeout 的问题
- 排查性能问题:理解主线程阻塞会导致 UI 卡顿
JavaScript 是单线程语言,通过事件循环实现异步。
调用栈(Call Stack) → 同步代码,后进先出
任务队列(Task Queue) → 异步回调,先进先出
微任务队列(Microtask) → Promise 回调,优先级更高执行顺序
1. 执行同步代码(调用栈)
2. 执行所有微任务(Microtask)
3. 执行 requestAnimationFrame 回调
4. 浏览器渲染(Layout → Paint)
5. 取一个宏任务执行
6. 重复 2-5console.log('1') // 同步
setTimeout(() => {
console.log('2') // 宏任务
}, 0)
Promise.resolve().then(() => {
console.log('3') // 微任务
})
console.log('4') // 同步
// 输出:1, 4, 3, 2宏任务 vs 微任务
| 类型 | 代表 | 优先级 |
|---|---|---|
| 微任务 | Promise.then/catch/finally、MutationObserver、queueMicrotask | 高 |
| 宏任务 | setTimeout/setInterval、setImmediate(Node.js)、I/O、UI 渲染 | 低 |
console.log('start')
setTimeout(() => console.log('timeout 1'), 0)
Promise.resolve()
.then(() => console.log('promise 1'))
.then(() => console.log('promise 2'))
setTimeout(() => console.log('timeout 2'), 0)
console.log('end')
// 输出:start, end, promise 1, promise 2, timeout 1, timeout 2async/await 的执行顺序
async function async1() {
console.log('async1 start')
await async2() // 等价于 async2().then(() => { ... })
console.log('async1 end') // 这一行是微任务
}
async function async2() {
console.log('async2')
}
console.log('script start')
async1()
console.log('script end')
// 输出:script start, async1 start, async2, script end, async1 end经典面试题
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
return new Promise((resolve) => {
console.log('async2')
resolve()
})
}
console.log('script start')
setTimeout(() => console.log('setTimeout'), 0)
async1()
new Promise((resolve) => {
console.log('promise1')
resolve()
}).then(() => console.log('promise2'))
console.log('script end')
// 输出:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout垃圾回收
大白话解释: 垃圾回收就像"保洁阿姨打扫房间"。JavaScript 引擎会定期检查哪些东西还在用(有人引用),哪些东西没人用了(没有引用),然后把没人用的东西清理掉,释放内存。
为什么要了解垃圾回收?
- 避免内存泄漏:代码中不经意的引用可能导致内存越来越大,页面越来越卡
- 理解闭包的代价:闭包会持有外部变量的引用,变量不会被回收
- 优化性能:及时清理不用的引用,让引擎更快回收内存
标记清除(主流)
1. 从根对象(window)出发,标记所有可达对象
2. 清除所有未标记的对象常见内存泄漏
// 1. 未清理的定时器
setInterval(() => { /* ... */ }, 1000)
// ✅ 用完 clearInterval
// 2. 未移除的事件监听
element.addEventListener('click', handler)
// ✅ 用完 removeEventListener
// 3. 闭包持有大对象
function create() {
const largeData = new Array(1000000)
return function () {
return largeData.length // largeData 无法被回收
}
}
// 4. DOM 引用未清理
const elements = []
function addElement() {
const div = document.createElement('div')
document.body.appendChild(div)
elements.push(div) // 移除 DOM 后,数组仍持有引用
}Promise 详解
大白话解释: Promise 就像"外卖订单"。你下单后(创建 Promise),订单有三种状态:等待中(pending)、已完成(fulfilled)、已拒绝(rejected)。商家做好了(resolve)你会收到餐,做不了(reject)你会收到退款通知。你可以用 .then() 处理成功,.catch() 处理失败。
三种状态
| 状态 | 含义 | 触发方式 |
|---|---|---|
pending | 等待中 | 初始状态 |
fulfilled | 已完成 | resolve(value) |
rejected | 已拒绝 | reject(reason) |
基本用法
// 创建 Promise
const promise = new Promise((resolve, reject) => {
// 模拟异步操作
setTimeout(() => {
const success = true
if (success) {
resolve('成功的结果') // 状态变为 fulfilled
} else {
reject('失败的原因') // 状态变为 rejected
}
}, 1000)
})
// 使用 Promise
promise
.then((result) => {
console.log(result) // '成功的结果'
})
.catch((error) => {
console.error(error) // '失败的原因'
})
.finally(() => {
console.log('无论成功失败都会执行')
})链式调用
// .then() 返回新 Promise,可以链式调用
fetch('/api/user')
.then((response) => response.json()) // 解析 JSON
.then((user) => fetch(`/api/posts/${user.id}`)) // 获取用户文章
.then((response) => response.json())
.then((posts) => {
console.log(posts) // 最终结果
})
.catch((error) => {
// 任意一步出错都会到这里
console.error('请求失败:', error)
})常用静态方法
// Promise.all:所有都成功才成功,有一个失败就失败
const results = await Promise.all([
fetch('/api/user'),
fetch('/api/posts'),
fetch('/api/comments'),
])
// 适用场景:多个请求同时发出,全部完成后继续
// Promise.race:取最先完成的结果(无论成功失败)
const fastest = await Promise.race([
fetch('/api/server1'),
fetch('/api/server2'),
])
// 适用场景:超时控制
// Promise.allSettled:等所有都完成(不管成功失败)
const outcomes = await Promise.allSettled([
fetch('/api/maybe-fail-1'),
fetch('/api/maybe-fail-2'),
])
// outcomes: [{ status: 'fulfilled', value: ... }, { status: 'rejected', reason: ... }]
// 适用场景:批量操作,需要知道每个的结果
// Promise.any:取第一个成功的结果,全部失败才失败
const firstSuccess = await Promise.any([
fetch('/api/server1'),
fetch('/api/server2'),
fetch('/api/server3'),
])
// 适用场景:多个备用源,用最快成功的async/await
// async/await 是 Promise 的语法糖,让异步代码看起来像同步
async function getUserPosts(userId) {
try {
const userRes = await fetch(`/api/user/${userId}`)
const user = await userRes.json()
const postsRes = await fetch(`/api/posts/${user.id}`)
const posts = await postsRes.json()
return posts
} catch (error) {
console.error('获取失败:', error)
throw error // 向上抛出错误
}
}
// 等价于 Promise 写法(注意:await 会将后面的代码包装成微任务)
function getUserPostsPromise(userId) {
return fetch(`/api/user/${userId}`)
.then((res) => res.json())
.then((user) => fetch(`/api/posts/${user.id}`))
.then((res) => res.json())
.catch((error) => {
console.error('获取失败:', error)
throw error
})
}
// ⚠️ await 的本质:await asyncFn() 等价于 asyncFn().then(...)
// 但 await 后面的代码会被放入微任务队列,而非立即执行
async function demo() {
console.log('1')
await Promise.resolve()
console.log('2') // 微任务,不是同步执行
}
console.log('3')
// 输出:1, 3, 2模块化
大白话解释: 模块化就像"乐高积木"。每个文件是一块积木,有自己的功能,通过接口(export)暴露给别人用,通过接口(import)拿来别人的功能。这样代码不会乱成一团。
CommonJS(Node.js)
// math.js — 导出
const add = (a, b) => a + b
const multiply = (a, b) => a * b
module.exports = { add, multiply } // 导出对象
// 或者
exports.add = add // 逐个导出
// main.js — 导入
const { add, multiply } = require('./math') // 解构导入
const math = require('./math') // 整体导入ES Modules(浏览器/现代 Node.js)
// math.js — 导出
export const add = (a, b) => a + b // 命名导出
export const multiply = (a, b) => a * b // 命名导出
export default class Calculator {} // 默认导出(每个模块只能一个)
// main.js — 导入
import { add, multiply } from './math.js' // 命名导入
import Calculator from './math.js' // 默认导入
import * as math from './math.js' // 整体导入
// 动态导入(ES2020)— 按需加载
const module = await import('./math.js')
module.add(1, 2)对比
| 特性 | CommonJS | ES Modules |
|---|---|---|
| 环境 | Node.js | 浏览器 + 现代 Node.js |
| 加载方式 | 运行时同步加载 | 编译时静态分析 |
| 导入值 | 值的拷贝 | 值的引用(只读) |
| this 指向 | module.exports | undefined |
| Tree Shaking | ❌ | ✅ |
| 动态导入 | require()(同步,可在条件中使用) | import()(异步,返回 Promise) |
Vue 中的模块使用
// Vue 3 + Vite 项目中全部使用 ES Modules
import { ref, computed } from 'vue' // 导入 Vue API
import { useRouter } from 'vue-router' // 导入路由
import { useUserStore } from '@/stores/user' // 导入 Pinia Store
import axios from 'axios' // 导入第三方库
// 动态导入 — 路由懒加载
const Home = () => import('@/views/Home.vue')严格模式
大白话解释: 严格模式就像"考试的严格考场",不允许你犯一些常见的错误。启用后,一些原本会被忽略的问题会报错,帮你提前发现 bug。
启用方式
'use strict' // 在文件或函数顶部添加
// 整个文件启用
'use strict'
function demo() {
// 这里是严格模式
}
// 只在函数内启用
function demo() {
'use strict'
// 这里是严格模式
}
// ES Modules 默认就是严格模式,不需要手动添加
// <script type="module"> 默认严格模式严格模式的限制
'use strict'
// 1. 变量必须声明
undeclaredVar = 1 // ❌ ReferenceError(非严格模式会创建全局变量)
// 2. 禁止删除不可删除的属性
delete Object.prototype // ❌ TypeError
// 3. 函数参数名不能重复
function foo(a, a) {} // ❌ SyntaxError
// 4. this 不再默认指向 window
function foo() {
console.log(this) // undefined(非严格模式是 window)
}
foo()
// 5. 禁止八进制字面量
const num = 0123 // ❌ SyntaxError(用 0o123 替代)
// 6. eval 和 arguments 不能作为变量名
let eval = 1 // ❌ SyntaxError
let arguments = 1 // ❌ SyntaxError错误处理
大白话解释: 错误处理就像"买保险"。你不知道什么时候会出问题,但你可以提前准备好应对方案(catch),或者向上级汇报(throw)。
try/catch/finally
try {
// 可能出错的代码
const data = JSON.parse(invalidJSON)
} catch (error) {
// 出错时执行
console.error('解析失败:', error.message)
} finally {
// 无论成功失败都会执行(通常用于清理资源)
console.log('处理完成')
}错误类型
// 常见内置错误类型
new Error('通用错误')
new TypeError('类型错误') // 值的类型不对
new RangeError('范围错误') // 值超出范围
new ReferenceError('引用错误') // 引用不存在的变量
new SyntaxError('语法错误') // 语法不合法
new URIError('URI 错误') // URI 函数参数错误
// 判断错误类型
try {
// ...
} catch (error) {
if (error instanceof TypeError) {
console.error('类型错误')
} else if (error instanceof RangeError) {
console.error('范围错误')
} else {
console.error('其他错误')
}
}自定义错误
class ValidationError extends Error {
constructor(field, message) {
super(message)
this.name = 'ValidationError'
this.field = field
}
}
function validateAge(age) {
if (age < 0 || age > 150) {
throw new ValidationError('age', '年龄必须在 0-150 之间')
}
}
try {
validateAge(200)
} catch (error) {
if (error instanceof ValidationError) {
console.error(`${error.field}: ${error.message}`)
// 输出:age: 年龄必须在 0-150 之间
}
}异步错误处理
// Promise 的错误处理
fetch('/api/data')
.then((res) => res.json())
.catch((error) => {
console.error('请求失败:', error)
})
// async/await 的错误处理
async function fetchData() {
try {
const res = await fetch('/api/data')
const data = await res.json()
return data
} catch (error) {
console.error('请求失败:', error)
throw error // 向上抛出
}
}
// 全局未捕获错误处理(浏览器)
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise 错误:', event.reason)
})
window.addEventListener('error', (event) => {
console.error('未捕获的错误:', event.error)
})finally 与 return 的交互
// finally 中的 return 会覆盖 try/catch 中的 return
function demo() {
try {
return 'try 的值'
} finally {
return 'finally 的值' // ⚠️ 覆盖了 try 的 return
}
}
console.log(demo()) // 'finally 的值'
// finally 中的 throw 会覆盖 try/catch 中的 return
function demo2() {
try {
return 'try 的值'
} finally {
throw new Error('finally 的错误') // ⚠️ 覆盖了 try 的 return
}
}
// demo2() // ❌ Error: finally 的错误
// finally 中不 return 不会影响 try/catch 的返回值
function demo3() {
try {
return 'try 的值'
} finally {
console.log('finally 执行') // 不影响返回值
}
}
console.log(demo3()) // 'finally 执行' → 'try 的值'
// 实际应用:确保资源释放
async function fetchWithTimeout(url, ms) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), ms)
try {
const response = await fetch(url, { signal: controller.signal })
return await response.json()
} finally {
clearTimeout(timeoutId) // 无论成功失败都清理定时器
}
}模块循环依赖
// 场景:A 依赖 B,B 也依赖 A
// a.js
import { bFunc } from './b.js'
export function aFunc() {
return bFunc() + 1
}
// b.js
import { aFunc } from './a.js'
export function bFunc() {
return 10
}
// CommonJS 处理方式:
// require 是同步的,遇到循环依赖会返回已加载的部分(不完整)
// a.js 执行到 require('./b') 时,会去加载 b.js
// b.js 执行到 require('./a') 时,a.js 还没执行完,返回当前已导出的部分(可能是空对象)
// 解决方案:把 require 放到函数内部,延迟加载
// ES Modules 处理方式:
// import 是静态分析,会先建立模块依赖图,再执行
// 循环依赖时,后加载的模块会拿到先加载模块的"活引用"(live binding)
// 但如果在模块顶层互相调用,会抛出 ReferenceError
// ✅ 解决方案一:把调用放到函数内部(延迟执行)
// a.js
export function aFunc() {
return bFunc() + 1 // 函数调用时 bFunc 已经初始化完成
}
// ✅ 解决方案二:提取共同依赖到第三个模块
// shared.js
export const sharedValue = 42
// a.js
import { sharedValue } from './shared.js'
export function aFunc() { return sharedValue + 1 }
// b.js
import { sharedValue } from './shared.js'
export function bFunc() { return sharedValue * 2 }迭代器与生成器
大白话解释: 迭代器就像"自动售货机"。你按一下按钮(调用 next()),它就吐出一个商品(返回一个值),直到商品卖完(done: true)。生成器就是制造这种售货机的"工厂"。
可迭代协议
// 实现 Symbol.iterator 方法的对象是可迭代的
const iterable = {
data: [10, 20, 30],
[Symbol.iterator]() {
let index = 0
return {
next: () => {
if (index < this.data.length) {
return { value: this.data[index++], done: false }
}
return { value: undefined, done: true }
},
}
},
}
// 可以用 for...of 遍历
for (const item of iterable) {
console.log(item) // 10, 20, 30
}
// 可以用展开运算符
const arr = [...iterable] // [10, 20, 30]生成器函数
// 生成器函数用 function* 声明
function* countUp(max) {
for (let i = 1; i <= max; i++) {
yield i // 暂停执行,返回值
}
}
const counter = countUp(3)
counter.next() // { value: 1, done: false }
counter.next() // { value: 2, done: false }
counter.next() // { value: 3, done: false }
counter.next() // { value: undefined, done: true }
// 生成器是可迭代的
for (const num of countUp(5)) {
console.log(num) // 1, 2, 3, 4, 5
}实际应用场景
// 1. 生成无限序列
function* fibonacci() {
let a = 0, b = 1
while (true) {
yield a
;[a, b] = [b, a + b]
}
}
const fib = fibonacci()
fib.next().value // 0
fib.next().value // 1
fib.next().value // 1
fib.next().value // 2
fib.next().value // 3
// 2. 惰性求值(按需生成,节省内存)
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i
}
}
// 不会一次性生成 100 万个元素
for (const num of range(1, 1000000)) {
if (num > 5) break // 只生成到 5 就停止
console.log(num)
}
// 3. 异步生成器(async generator)— 处理异步数据流
async function* fetchPages(url) {
let page = 1
while (true) {
const response = await fetch(`${url}?page=${page}`)
const data = await response.json()
if (data.length === 0) return
yield data
page++
}
}
// 使用 for await...of 遍历异步迭代器
for await (const pageData of fetchPages('/api/items')) {
console.log(pageData) // 每页数据
}for...in vs for...of
大白话解释:for...in 就像"翻箱倒柜找标签",遍历对象的键名;for...of 就像"流水线取货",遍历可迭代对象的值。
对比
| 特性 | for...in | for...of |
|---|---|---|
| 遍历内容 | 键名(key) | 值(value) |
| 适用对象 | 普通对象 | 可迭代对象(Array、String、Map、Set 等) |
| 遍历原型链 | ✅ 会遍历原型链上继承的可枚举属性 | ❌ 只遍历自身(通过迭代器) |
| 推荐场景 | 遍历对象属性 | 遍历数组/集合 |
// for...in:遍历对象的键名
const user = { name: '张三', age: 25 }
for (const key in user) {
console.log(key) // 'name', 'age'
console.log(user[key]) // '张三', 25
}
// ⚠️ for...in 会遍历原型链上的属性
for (const key in user) {
if (Object.hasOwn(user, key)) { // 推荐:过滤掉继承的属性
console.log(key)
}
}
// ⚠️ 不要用 for...in 遍历数组!
// 1. 遍历的是键名(字符串),不是索引(数字)
// 2. 会遍历原型链上的自定义属性
// 3. 遍历顺序不保证
const arr = [10, 20, 30]
for (const key in arr) {
console.log(key) // '0', '1', '2'(字符串!)
}
// ✅ 遍历数组请用 for...of、forEach、for 循环
// for...of:遍历可迭代对象的值
for (const value of arr) {
console.log(value) // 10, 20, 30
}
// 遍历字符串
for (const char of 'hello') {
console.log(char) // 'h', 'e', 'l', 'l', 'o'
}
// 遍历 Map
const map = new Map([['a', 1], ['b', 2]])
for (const [key, value] of map) {
console.log(key, value) // 'a' 1, 'b' 2
}
// 遍历 Set
const set = new Set([1, 2, 3])
for (const value of set) {
console.log(value) // 1, 2, 3
}
// ❌ 普通对象不能用 for...of(不可迭代)
// for (const value of { a: 1 }) {} // TypeError
// ✅ 用 Object.keys/values/entries 转换
for (const [key, value] of Object.entries({ a: 1, b: 2 })) {
console.log(key, value) // 'a' 1, 'b' 2
}对象不可变性
大白话解释:Object.freeze 就像"把东西冻住",完全不能改;Object.seal 就像"把东西封起来",可以改现有属性的值,但不能增删属性。
对比
| 方法 | 修改属性值 | 增删属性 | 配置属性 | 深度冻结 |
|---|---|---|---|---|
Object.freeze | ❌ | ❌ | ❌ | ❌(只冻结一层) |
Object.seal | ✅ | ❌ | ❌ | ❌ |
Object.preventExtensions | ✅ | ❌ | ✅ | ❌ |
// Object.freeze:完全冻结
const config = { api: '/api', timeout: 5000 }
Object.freeze(config)
config.api = '/new-api' // 静默失败(严格模式下抛出 TypeError)
config.newProp = 'test' // 静默失败(严格模式下抛出 TypeError)
delete config.api // 静默失败(严格模式下抛出 TypeError)
console.log(config.api) // '/api'(未改变)
// ⚠️ 严格模式下会报错!
function strictDemo() {
'use strict'
const frozenObj = Object.freeze({ value: 1 })
// frozenObj.value = 2 // ❌ TypeError: Cannot assign to read only property 'value'
// frozenObj.newProp = 3 // ❌ TypeError: Cannot add property newProp, object is not extensible
// delete frozenObj.value // ❌ TypeError: Cannot delete property 'value'
}
// Object.seal:密封(可修改值,不能增删)
const settings = { theme: 'dark', lang: 'zh' }
Object.seal(settings)
settings.theme = 'light' // ✅ 可以修改
settings.newProp = 'test' // 静默失败
delete settings.theme // 静默失败
console.log(settings.theme) // 'light'(已改变)
// 检查状态
Object.isFrozen(config) // true
Object.isSealed(settings) // true
// ⚠️ freeze 只冻结一层(浅冻结)
const obj = { nested: { value: 1 } }
Object.freeze(obj)
obj.nested.value = 999 // ✅ 可以修改!嵌套对象未被冻结
// 深度冻结
function deepFreeze(obj) {
Object.freeze(obj)
for (const key of Object.keys(obj)) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
deepFreeze(obj[key])
}
}
return obj
}实际应用场景
// 1. 常量配置(防止意外修改)
const API_ENDPOINTS = Object.freeze({
USER: '/api/user',
POST: '/api/posts',
COMMENT: '/api/comments',
})
// 2. Redux reducer 中返回新状态(配合展开运算符)
function reducer(state, action) {
switch (action.type) {
case 'SET_NAME':
return { ...state, name: action.payload } // 返回新对象
default:
return state
}
}
// 3. Vue 3 中创建响应式只读数据
import { reactive, readonly } from 'vue'
const state = reactive({ count: 0 })
const readonlyState = readonly(state) // 类似 freeze,但是响应式的Symbol
大白话解释: Symbol 就像"身份证号",每个都是独一无二的。主要用来创建对象的唯一属性名,避免命名冲突。
基本用法
// 创建 Symbol
const id = Symbol('id') // 带描述(仅用于调试)
const name = Symbol('name')
// 每个 Symbol 都是唯一的
Symbol('id') === Symbol('id') // false(不同实例)
// 作为对象属性(唯一键)
const user = {
[id]: 12345,
[name]: '张三',
age: 25,
}
console.log(user[id]) // 12345
console.log(user.name) // undefined(普通属性)
console.log(user[name]) // '张三'(Symbol 属性)
// Symbol 属性不会被 for...in、Object.keys() 遍历到
for (const key in user) {
console.log(key) // 'age'(只有普通属性)
}
Object.keys(user) // ['age']
// 但 Object.getOwnPropertySymbols() 可以获取
Object.getOwnPropertySymbols(user) // [Symbol(id), Symbol(name)]全局 Symbol 注册表
// Symbol.for():全局注册表,相同描述返回同一个 Symbol
const s1 = Symbol.for('shared')
const s2 = Symbol.for('shared')
s1 === s2 // true
// Symbol.keyFor():获取全局 Symbol 的描述
Symbol.keyFor(s1) // 'shared'内置 Symbol(Well-known Symbols)
// Symbol.iterator:定义对象的迭代行为
// 详见"迭代器与生成器"章节
// Symbol.toPrimitive:定义对象转基本类型的逻辑
const money = {
amount: 100,
currency: 'CNY',
[Symbol.toPrimitive](hint) {
if (hint === 'number') return this.amount
if (hint === 'string') return `${this.amount} ${this.currency}`
return this.amount // default
},
}
console.log(+money) // 100(hint = 'number')
console.log(`${money}`) // '100 CNY'(hint = 'string')
console.log(money + 50) // 150(hint = 'default')
// Symbol.hasInstance:自定义 instanceof 行为
class EvenNumber {
static [Symbol.hasInstance](num) {
return typeof num === 'number' && num % 2 === 0
}
}
2 instanceof EvenNumber // true
3 instanceof EvenNumber // false
// Symbol.toStringTag:自定义 Object.prototype.toString 的标签
class MyCollection {
get [Symbol.toStringTag]() {
return 'MyCollection'
}
}
Object.prototype.toString.call(new MyCollection()) // '[object MyCollection]'实际应用场景
// 1. 防止属性名冲突(插件/库开发)
const MY_PLUGIN_STATE = Symbol('my-plugin-state')
function initPlugin(obj) {
obj[MY_PLUGIN_STATE] = { initialized: true }
}
// 2. 定义私有属性(约定俗成,不是真正的私有)
const _private = Symbol('private')
class ApiClient {
constructor(token) {
this[_private] = { token }
}
getData() {
return fetch('/api', { headers: { Authorization: this[_private].token } })
}
}
// 3. Vue 3 中的响应式标记
const RAW = Symbol('raw') // 原始对象标记
const REACTIVE = Symbol() // 响应式标记Proxy 与 Reflect
大白话解释: Proxy 就像"门卫",你访问对象的任何操作(读、写、删除等)都要经过它,它可以在中间做拦截、记录、修改。Reflect 就像"管理员手册",提供了操作对象的标准方法。
Proxy 基本用法
const handler = {
// 拦截属性读取
get(target, key, receiver) {
console.log(`读取 ${key}`)
return Reflect.get(target, key, receiver) // 默认行为
},
// 拦截属性设置
set(target, key, value, receiver) {
console.log(`设置 ${key} = ${value}`)
return Reflect.set(target, key, value, receiver) // 默认行为
},
}
const original = { name: '张三', age: 25 }
const proxy = new Proxy(original, handler)
proxy.name // 控制台输出:读取 name → '张三'
proxy.age = 26 // 控制台输出:设置 age = 26Vue 3 响应式原理
// Vue 3 的 reactive 就是基于 Proxy 实现的
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
track(target, key) // 收集依赖
return typeof result === 'object' ? reactive(result) : result
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key) // 触发更新
return result
},
})
}
// 使用
const state = reactive({ count: 0, user: { name: '张三' } })
// 读取时自动收集依赖,修改时自动触发更新Proxy 支持的拦截操作
const handler = {
get(target, key) {}, // 读取属性
set(target, key, value) {}, // 设置属性
has(target, key) {}, // in 操作符
deleteProperty(target, key) {},// delete 操作符
ownKeys(target) {}, // Object.keys()
getPrototypeOf(target) {}, // Object.getPrototypeOf()
setPrototypeOf(target, proto) {}, // Object.setPrototypeOf()
getOwnPropertyDescriptor(target, key) {}, // Object.getOwnPropertyDescriptor()
defineProperty(target, key, descriptor) {}, // Object.defineProperty()
apply(target, thisArg, args) {}, // 函数调用
construct(target, args) {}, // new 操作符
isExtensible(target) {}, // Object.isExtensible()
preventExtensions(target) {}, // Object.preventExtensions()
}实际应用场景
// 1. 数据验证
const validator = {
set(target, key, value) {
if (key === 'age' && (typeof value !== 'number' || value < 0)) {
throw new TypeError('年龄必须是非负数')
}
return Reflect.set(target, key, value)
},
}
const user = new Proxy({}, validator)
user.age = 25 // ✅
// user.age = -1 // ❌ TypeError: 年龄必须是非负数
// 2. 属性访问日志
const logger = {
get(target, key) {
console.log(`[${new Date().toISOString()}] 访问 ${key}`)
return Reflect.get(target, key)
},
}
const data = new Proxy({ count: 0 }, logger)
data.count // [2026-06-24T...] 访问 count
// 3. 默认值(类似 Python 的 defaultdict)
const defaults = {
get(target, key) {
return key in target ? Reflect.get(target, key) : 0
},
}
const counters = new Proxy({}, defaults)
counters.visits // 0(自动返回默认值)
counters.clicks // 0Proxy vs Object.defineProperty
| 特性 | Proxy | Object.defineProperty |
|---|---|---|
| 拦截操作 | 13 种(get/set/has/delete 等) | 2 种(get/set) |
| 数组索引 | ✅ 自动拦截 | ❌ 需要重写数组方法 |
| 新增属性 | ✅ 自动拦截 | ❌ 需要额外处理 |
| 删除属性 | ✅ 可拦截 | ❌ 无法拦截 |
| 性能 | 略低(惰性代理) | 略高 |
| 兼容性 | ES2015+ | ES5+ |
💡 Vue 2 用
Object.defineProperty,所以需要Vue.set添加新属性;Vue 3 用Proxy,不需要了。
可撤销代理(Revocable Proxy)
// Proxy.revocable:创建可撤销的代理
const target = { name: '张三', secret: '12345' }
const handler = {
get(target, key) {
console.log(`访问 ${key}`)
return Reflect.get(target, key)
},
}
const { proxy, revoke } = Proxy.revocable(target, handler)
// 使用代理
console.log(proxy.name) // 控制台输出:访问 name → '张三'
// 撤销代理(之后任何操作都会报错)
revoke()
// proxy.name // ❌ TypeError: Cannot perform 'get' on a proxy that has been revoked
// proxy.age = 26 // ❌ TypeError: Cannot perform 'set' on a proxy that has been revoked
// 实际应用场景:临时授权,用完即焚
function createTemporaryAccess(data, duration) {
const { proxy, revoke } = Proxy.revocable(data, {
get(target, key) {
// 可以在这里添加访问控制逻辑
if (key === 'password') throw new Error('无权访问')
return Reflect.get(target, key)
},
})
// 自动撤销
setTimeout(revoke, duration)
return proxy // 返回代理,duration 毫秒后自动失效
}
const access = createTemporaryAccess({ name: '张三', password: 'secret' }, 5000)
console.log(access.name) // '张三'
// 5 秒后 access 自动失效