【JS进阶-Day1】作用域、闭包与ES6语法

3 阅读6分钟

【JS进阶-Day1】作用域、闭包与ES6语法

📺 对应视频:P152-P163 | 🎯 核心目标:深入理解作用域链与垃圾回收、掌握闭包原理与应用、ES6常用语法


一、作用域链深入

1.1 词法作用域(静态作用域)

JS 采用词法 作用域:函数的作用域在定义时就确定了,而不是调用时。

let x = 'global'

function outer() {
  let x = 'outer'
  function inner() {
    console.log(x)  // 'outer'(定义时就确定了,向外找到 outer 的 x)
  }
  return inner
}

const fn = outer()
fn()  // 仍然输出 'outer',不是 'global'
// 因为 inner 的作用域链在 outer 内部定义时就绑定了

1.2 变量查找规则

作用域链查找过程:
当前作用域 → 外层作用域 → ... → 全局作用域 → 找不到 → ReferenceError

// 内层可以访问外层,外层不能访问内层
function outer() {
  let a = 1
  function inner() {
    let b = 2
    console.log(a)  // ✅ 1(内层访问外层)
  }
  console.log(b)  // ❌ ReferenceError(外层访问内层)
}

二、垃圾回收机制

2.1 内存生命周期

分配内存 → 使用内存 → 释放内存(垃圾回收)

2.2 引用计数法(旧,已废弃)

// 问题:循环引用导致内存泄漏
function createCircle() {
  let a = {}
  let b = {}
  a.ref = b    // a 引用 b
  b.ref = a    // b 引用 a(循环引用!引用计数永远不为0)
}
createCircle()
// 旧算法:ab 永远不会被回收

2.3 标记清除法(现代,主流)

1. 从根(window/全局)出发
2. 标记所有可达的对象
3. 清除未被标记的对象(不可达 = 垃圾)

2.4 内存泄漏常见场景

// 1. 意外的全局变量
function fn() {
  leak = '未声明的变量'  // 成为全局变量,永不释放
}

// 2. 忘记清除的定时器
const timer = setInterval(() => {
  // 持有 DOM 引用
  document.querySelector('.box').style.color = 'red'
}, 1000)
// 应在不需要时 clearInterval(timer)

// 3. 未移除的事件监听
const btn = document.querySelector('button')
function handler() { ... }
btn.addEventListener('click', handler)
// 组件销毁时应 btn.removeEventListener('click', handler)

// 4. 闭包持有不再需要的大对象
function createLeak() {
  const bigData = new Array(1000000).fill('*')
  return () => bigData.length  // 闭包持有 bigData
}

三、闭包(Closure)

3.1 什么是闭包?

闭包 = 函数 + 其能访问的外部作用域

当一个函数能够访问并记住其词法作用域中的变量,即使该函数在其词法作用域外执行,就产生了闭包。

function makeCounter() {
  let count = 0    // 外部函数的局部变量
  
  return function() {    // 返回的内部函数形成闭包
    count++              // 访问并记住了 count
    return count
  }
}

const counter = makeCounter()
counter()  // 1
counter()  // 2
counter()  // 3
// count 不会消失,被闭包保持着!

3.2 闭包的核心特性

// 1. 每次调用外部函数都创建独立的闭包
const counter1 = makeCounter()
const counter2 = makeCounter()

counter1()  // 1(counter1 自己的 count)
counter1()  // 2
counter2()  // 1(counter2 独立的 count,互不影响)

// 2. 闭包可以修改外部变量
function makeAdder(base) {
  return function(num) {
    base += num   // 修改外部变量 base
    return base
  }
}
const add = makeAdder(10)
add(5)   // 15(base 变成 15)
add(3)   // 18(base 变成 18)

3.3 闭包的实际应用

// 1. 私有变量(模块模式)
function createBankAccount(initialBalance) {
  let balance = initialBalance  // 私有变量,外部无法直接访问
  
  return {
    deposit(amount) {
      balance += amount
      console.log(`存入 ${amount},余额:${balance}`)
    },
    withdraw(amount) {
      if (amount > balance) {
        console.log('余额不足')
        return
      }
      balance -= amount
      console.log(`取出 ${amount},余额:${balance}`)
    },
    getBalance() {
      return balance
    }
  }
}

const account = createBankAccount(1000)
account.deposit(500)     // 存入 500,余额:1500
account.withdraw(200)    // 取出 200,余额:1300
// account.balance       // undefined(私有,访问不到!)

// 2. 函数工厂
function multiplier(factor) {
  return num => num * factor
}
const double = multiplier(2)
const triple = multiplier(3)
double(5)   // 10
triple(5)   // 15

// 3. 记忆化(Memoization)
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 expensiveCalc = memoize((n) => {
  // 假设是耗时计算
  return n * n
})
expensiveCalc(5)  // 25(计算)
expensiveCalc(5)  // 25(从缓存取)

四、ES6 重要语法

4.1 解构赋值

// 数组解构
const [a, b, c] = [1, 2, 3]        // a=1, b=2, c=3
const [x, , z] = [1, 2, 3]         // 跳过:x=1, z=3
const [first, ...rest] = [1, 2, 3, 4]  // first=1, rest=[2,3,4]
const [p = 10, q = 20] = [5]        // 默认值:p=5, q=20

// 交换变量(经典用法)
let m = 1, n = 2
;[m, n] = [n, m]  // m=2, n=1

// 对象解构
const { name, age } = { name: '张三', age: 18 }
const { name: myName, age: myAge } = { name: '张三', age: 18 }  // 重命名
const { address: { city } } = { address: { city: '北京' } }      // 嵌套解构
const { score = 100, grade = 'A' } = { score: 85 }  // 默认值:grade='A'

// 函数参数解构(非常常用!)
function showUser({ name, age, gender = '未知' }) {
  console.log(`${name}${age}岁,${gender}`)
}
showUser({ name: '张三', age: 18 })  // '张三,18岁,未知'

4.2 展开运算符与剩余参数

// 展开运算符(...):将可迭代对象展开
const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
const merged = [...arr1, ...arr2]  // [1, 2, 3, 4, 5, 6]

// 展开对象(浅拷贝)
const obj1 = { a: 1, b: 2 }
const obj2 = { ...obj1, c: 3 }   // { a: 1, b: 2, c: 3 }
const clone = { ...obj1 }         // 浅拷贝

// 函数调用时展开
Math.max(...[1, 5, 3, 2])   // 5

// 剩余参数(...args):收集剩余参数为数组
function sum(first, ...others) {
  return others.reduce((acc, n) => acc + n, first)
}
sum(1, 2, 3, 4)  // 10

4.3 箭头函数进阶

// 箭头函数的 this 继承自定义时的外层作用域(词法this)
class Timer {
  constructor() {
    this.seconds = 0
  }
  start() {
    // 普通函数:this 丢失(undefined 或 window)
    setInterval(function() {
      this.seconds++  // ❌ this 不是 Timer 实例
    }, 1000)
    
    // 箭头函数:this 正确(继承自 start 方法的 this = Timer 实例)
    setInterval(() => {
      this.seconds++  // ✅ this 是 Timer 实例
    }, 1000)
  }
}

4.4 Symbol(简介)

// Symbol:唯一的原始值,常用作对象的唯一属性键
const id = Symbol('id')
const id2 = Symbol('id')
id === id2   // false(永远不相等)

const obj = {
  [id]: 123,
  name: '张三'
}
obj[id]   // 123(只能通过 Symbol 访问)
// for...in 和 Object.keys 不会遍历 Symbol 属性

五、知识图谱

作用域、闭包与ES6
├── 作用域链
│   ├── 词法作用域(定义时确定)
│   └── 查找规则(内→外→全局→报错)
├── 垃圾回收
│   ├── 标记清除(主流)
│   └── 内存泄漏:全局变量/定时器/监听器/闭包
├── 闭包
│   ├── 本质:函数+词法环境
│   ├── 特性:记住外部变量、每次调用独立
│   └── 应用:私有变量/函数工厂/记忆化
└── ES6 语法
    ├── 解构:数组/对象/函数参数解构
    ├── 展开运算符(...):数组合并、对象拷贝
    ├── 剩余参数(...args):收集剩余
    └── 箭头函数(词法 this

六、高频面试题

Q1:什么是闭包?

闭包是指函数能够访问并记住其定义时所在词法作用域中的变量,即使函数在词法作用域外执行。常见用途:私有变量、函数工厂、记忆化、模块化。

Q2:闭包会引起内存泄漏吗?

闭包本身不会导致内存泄漏,但如果闭包持有不再需要的大对象引用,且闭包自身一直存在(如绑定在全局事件上),就会导致内存无法回收。解决方案:及时解除引用、移除事件监听。

Q3: var 在循环中的闭包陷阱?

// 经典问题
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)
}
// 输出:3 3 3(var 没有块级作用域,所有回调共享同一个 i)

// 解决方案一:改用 let
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)  // 0 1 2
}

// 解决方案二:IIFE
for (var i = 0; i < 3; i++) {
  ;(function(j) {
    setTimeout(() => console.log(j), 0)  // 0 1 2
  })(i)
}

⬅️ 上一篇Web APIs Day6&7 - 正则与综合实战 ➡️ 下一篇JS进阶Day2 - 构造函数与内置对象方法