Skip to content

JavaScript 核心概念

JavaScript 语言中必须理解的核心机制,包括数据类型、执行上下文、闭包、原型链、this 指向、事件循环、Promise、模块化、严格模式、错误处理、迭代器、for...in/for...of、对象不可变性、Symbol、Proxy。


数据类型

JavaScript 有 7 种基本类型和引用类型,以及丰富的类型检测、类型转换机制和各类型原型链上的方法。详细内容请阅读 JavaScript 数据类型与内置方法

速查

基本类型示例typeof
string'hello''string'
number42, NaN, Infinity'number'
booleantrue, false'boolean'
undefinedundefined'undefined'
nullnull'object'(bug,见下方说明)
symbolSymbol('id')'symbol'
bigint42n'bigint'
引用类型示例typeof
Object{}'object'
Array[]'object'
Functionfunction(){}'function'
Datenew Date()'object'
RegExp/\d+/'object'
Map / Setnew Map()'object'
js
// 假值(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)。

text
全局执行上下文
  ↓ 函数调用
函数执行上下文
  ↓ 函数调用
函数执行上下文
  ↓ 执行完毕出栈
...

每个执行上下文包含:

  • 变量环境var 声明的变量、函数声明
  • 词法环境let/const 声明的变量
  • this 绑定

作用域链

js
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 的区别

特性varletconst
作用域函数级块级块级
变量提升✅ 初始化为 undefined✅ 存在但不初始化(暂时性死区)✅ 存在但不初始化
重复声明✅ 允许❌ 报错❌ 报错
重新赋值
全局属性挂载到 window不挂载不挂载
js
// 暂时性死区(TDZ)
console.log(a) // undefined(var 提升)
// console.log(b) // ❌ ReferenceError(let 暂时性死区)
let b = 1
js
// 块级作用域
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 有块级作用域,每次循环创建新变量)

闭包

什么是闭包

大白话解释: 闭包就像一个函数"记住了"它出生时的环境。就像你小时候在家学会了一个秘密配方,长大后离家了,但你仍然记得那个配方,可以用它。

为什么要用闭包?

  • 数据私有化:想隐藏变量,不让外部直接访问,只能通过特定方法操作
  • 保持状态:函数执行完毕后,某些变量还想保留下来继续用
  • 函数工厂:根据参数生成不同的函数

闭包 = 函数 + 其引用的外部变量。函数能够"记住"它创建时的作用域。

js
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 变量被闭包"记住",不会被回收

闭包的本质

js
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 各自独立

闭包的常见用途

数据封装(私有变量)

js
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) // ❌ 无法直接访问

函数工厂

js
// 创建一个验证器工厂:接收规则数组,返回验证函数
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(缺少 @)

闭包的坑:循环中的闭包

js
// ❌ 经典问题
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)
}

闭包的内存问题

js
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
js
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)绑定后的新函数
js
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

js
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 丢失问题

js
'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
  • 浅拷贝只拷贝一层:嵌套对象仍是引用
  • 深拷贝完全独立:修改副本不影响原件

浅拷贝方式

js
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] ❌ 原始对象也被修改了!

深拷贝方式

js
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 原型与原型链

快速理解

text
实例对象  ──[[Prototype]]──→  原型对象(构造函数.prototype)
                                   └── [[Prototype]] → Object.prototype
                                                           └── [[Prototype]] → null

注:[[Prototype]] 是规范表示,可通过 Object.getPrototypeOf() 访问
    __proto__ 是非标准访问方式(教学演示用,生产代码不推荐)
js
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 继承的原型链

js
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 是单线程语言,通过事件循环实现异步。

text
调用栈(Call Stack)    → 同步代码,后进先出
任务队列(Task Queue)  → 异步回调,先进先出
微任务队列(Microtask) → Promise 回调,优先级更高

执行顺序

text
1. 执行同步代码(调用栈)
2. 执行所有微任务(Microtask)
3. 执行 requestAnimationFrame 回调
4. 浏览器渲染(Layout → Paint)
5. 取一个宏任务执行
6. 重复 2-5
js
console.log('1')              // 同步

setTimeout(() => {
  console.log('2')            // 宏任务
}, 0)

Promise.resolve().then(() => {
  console.log('3')            // 微任务
})

console.log('4')              // 同步

// 输出:1, 4, 3, 2

宏任务 vs 微任务

类型代表优先级
微任务Promise.then/catch/finallyMutationObserverqueueMicrotask
宏任务setTimeout/setIntervalsetImmediate(Node.js)、I/OUI 渲染
js
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 2

async/await 的执行顺序

js
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

经典面试题

js
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 引擎会定期检查哪些东西还在用(有人引用),哪些东西没人用了(没有引用),然后把没人用的东西清理掉,释放内存。

为什么要了解垃圾回收?

  • 避免内存泄漏:代码中不经意的引用可能导致内存越来越大,页面越来越卡
  • 理解闭包的代价:闭包会持有外部变量的引用,变量不会被回收
  • 优化性能:及时清理不用的引用,让引擎更快回收内存

标记清除(主流)

text
1. 从根对象(window)出发,标记所有可达对象
2. 清除所有未标记的对象

常见内存泄漏

js
// 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)

基本用法

js
// 创建 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('无论成功失败都会执行')
  })

链式调用

js
// .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)
  })

常用静态方法

js
// 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

js
// 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)

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)

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)

对比

特性CommonJSES Modules
环境Node.js浏览器 + 现代 Node.js
加载方式运行时同步加载编译时静态分析
导入值值的拷贝值的引用(只读)
this 指向module.exportsundefined
Tree Shaking
动态导入require()(同步,可在条件中使用)import()(异步,返回 Promise)

Vue 中的模块使用

js
// 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。

启用方式

js
'use strict'  // 在文件或函数顶部添加

// 整个文件启用
'use strict'
function demo() {
  // 这里是严格模式
}

// 只在函数内启用
function demo() {
  'use strict'
  // 这里是严格模式
}

// ES Modules 默认就是严格模式,不需要手动添加
// <script type="module"> 默认严格模式

严格模式的限制

js
'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

js
try {
  // 可能出错的代码
  const data = JSON.parse(invalidJSON)
} catch (error) {
  // 出错时执行
  console.error('解析失败:', error.message)
} finally {
  // 无论成功失败都会执行(通常用于清理资源)
  console.log('处理完成')
}

错误类型

js
// 常见内置错误类型
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('其他错误')
  }
}

自定义错误

js
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 之间
  }
}

异步错误处理

js
// 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 的交互

js
// 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) // 无论成功失败都清理定时器
  }
}

模块循环依赖

js
// 场景: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)。生成器就是制造这种售货机的"工厂"。

可迭代协议

js
// 实现 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]

生成器函数

js
// 生成器函数用 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
}

实际应用场景

js
// 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...infor...of
遍历内容键名(key)值(value)
适用对象普通对象可迭代对象(Array、String、Map、Set 等)
遍历原型链✅ 会遍历原型链上继承的可枚举属性❌ 只遍历自身(通过迭代器)
推荐场景遍历对象属性遍历数组/集合
js
// 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
js
// 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
}

实际应用场景

js
// 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 就像"身份证号",每个都是独一无二的。主要用来创建对象的唯一属性名,避免命名冲突。

基本用法

js
// 创建 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 注册表

js
// 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)

js
// 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]'

实际应用场景

js
// 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 基本用法

js
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 = 26

Vue 3 响应式原理

js
// 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 支持的拦截操作

js
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()
}

实际应用场景

js
// 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   // 0

Proxy vs Object.defineProperty

特性ProxyObject.defineProperty
拦截操作13 种(get/set/has/delete 等)2 种(get/set)
数组索引✅ 自动拦截❌ 需要重写数组方法
新增属性✅ 自动拦截❌ 需要额外处理
删除属性✅ 可拦截❌ 无法拦截
性能略低(惰性代理)略高
兼容性ES2015+ES5+

💡 Vue 2 用 Object.defineProperty,所以需要 Vue.set 添加新属性;Vue 3 用 Proxy,不需要了。

可撤销代理(Revocable Proxy)

js
// 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 自动失效

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准