作用域和闭包(Scope and Closures)

JavaScript 的作用域机制和闭包概念:从基础使用到底层原理


📖 入门:基础使用

1. 作用域(Scope)

作用域定义:作用域是变量和函数的可访问范围。

1.1 全局作用域(Global Scope)

特点

  • 在函数外部声明的变量
  • 在整个程序中都可以访问

示例

let globalVar = "I'm global";  // 全局变量
 
function test() {
  console.log(globalVar);  // ✅ 可以访问
}
 
test();
console.log(globalVar);  // ✅ 可以访问

1.2 函数作用域(Function Scope)

特点

  • 在函数内部声明的变量
  • 只能在函数内部访问
  • var 声明的变量属于函数作用域

示例

function test() {
  var functionVar = "I'm in function";  // 函数作用域
  console.log(functionVar);  // ✅ 可以访问
}
 
// console.log(functionVar);  // ❌ 报错:functionVar is not defined
test();

1.3 块级作用域(Block Scope)

特点

  • {} 代码块内声明的变量
  • 只能在代码块内访问
  • letconst 声明的变量属于块级作用域

示例

if (true) {
  let blockVar = "I'm in block";  // 块级作用域
  console.log(blockVar);  // ✅ 可以访问
}
 
// console.log(blockVar);  // ❌ 报错:blockVar is not defined

2. 作用域链(Scope Chain)

概念:当前作用域找不到变量时,会向上级作用域查找,形成作用域链。

示例

let global = "global";
 
function outer() {
  let outerVar = "outer";
  
  function inner() {
    let innerVar = "inner";
    console.log(innerVar);  // "inner"(当前作用域)
    console.log(outerVar);  // "outer"(上级作用域)
    console.log(global);    // "global"(全局作用域)
  }
  
  inner();
}
 
outer();

3. 闭包(Closure)

概念:闭包是函数能够访问其外部作用域变量的能力。

3.1 闭包的基本使用

示例

function outer() {
  let outerVar = "I'm outside";
  
  function inner() {
    console.log(outerVar);  // 访问外部变量
  }
  
  return inner;  // 返回内部函数
}
 
let closure = outer();
closure();  // "I'm outside"(仍然可以访问 outerVar)

3.2 闭包的常见应用

1. 数据私有化

function createCounter() {
  let count = 0;  // 私有变量
  
  return {
    increment() {
      count++;
      return count;
    },
    decrement() {
      count--;
      return count;
    },
    getCount() {
      return count;
    }
  };
}
 
let counter = createCounter();
console.log(counter.increment());  // 1
console.log(counter.increment());  // 2
console.log(counter.getCount());   // 2

2. 函数工厂

function createMultiplier(multiplier) {
  return function(number) {
    return number * multiplier;
  };
}
 
let double = createMultiplier(2);
let triple = createMultiplier(3);
 
console.log(double(5));  // 10
console.log(triple(5));  // 15

🚀 提高:底层原理

1. 词法作用域(Lexical Scope)

1.1 词法作用域的定义

概念:词法作用域(静态作用域)是在代码编写时确定的作用域,而不是运行时。

原理

  • JavaScript 使用词法作用域
  • 作用域由代码的书写位置决定
  • 函数的作用域在定义时就确定了

示例

let x = "global";
 
function outer() {
  let x = "outer";
  
  function inner() {
    console.log(x);  // "outer"(词法作用域,查找定义时的外层作用域)
  }
  
  return inner;
}
 
let x = "new global";
let func = outer();
func();  // "outer"(不是 "new global")

1.2 作用域链的构建

原理

  • 每个函数都有内部属性 [[Scope]]
  • [[Scope]] 保存了函数定义时的作用域链
  • 函数执行时,会创建新的执行上下文
  • 新的执行上下文的作用域链 = 当前变量对象 + [[Scope]]

示例分析

let global = "global";
 
function outer() {
  let outerVar = "outer";
  
  function inner() {
    let innerVar = "inner";
    console.log(innerVar, outerVar, global);
  }
  
  return inner;
}
 
let func = outer();
func();

作用域链结构

inner 执行上下文
├── 变量对象(VO)
│   └── innerVar: "inner"
└── 作用域链
    ├── outer 变量对象
    │   └── outerVar: "outer"
    └── 全局对象
        └── global: "global"

2. 执行上下文(Execution Context)

2.1 执行上下文的创建

创建阶段

  1. 创建变量对象(Variable Object)
  2. 建立作用域链(Scope Chain)
  3. 确定 this 绑定(This Binding)

执行阶段

  1. 变量赋值
  2. 函数引用
  3. 执行代码

示例

function test(a, b) {
  var c = 10;
  function inner() {}
  return a + b + c;
}
 
test(1, 2);

执行上下文创建过程

创建阶段:
1. 创建变量对象
   - arguments: { a: 1, b: 2 }
   - c: undefined
   - inner: function reference
2. 建立作用域链
   - [当前 VO, 全局对象]
3. 确定 this 绑定

执行阶段:
1. c = 10
2. 执行 return a + b + c

3. 闭包的原理

3.1 闭包的形成机制

原理

  • 当函数返回内部函数时,内部函数会保留对外部作用域的引用
  • 即使外部函数执行完毕,其变量对象也不会被销毁
  • 内部函数仍然可以访问外部函数的变量

示例

function outer() {
  let outerVar = "outer";
  
  function inner() {
    console.log(outerVar);
  }
  
  return inner;  // 返回内部函数,形成闭包
}
 
let closure = outer();
closure();  // 仍然可以访问 outerVar

内存模型

outer 执行上下文(即使执行完毕,仍保留在内存中)
└── 变量对象
    └── outerVar: "outer"

inner 函数对象
└── ``[[Scope]]``: [outer 的变量对象, 全局对象]

closure 变量
└── 引用 inner 函数对象

3.2 闭包的内存管理

内存泄漏风险

function createHandler() {
  let largeData = new Array(1000000).fill(0);  // 大数组
  
  return function() {
    // 即使不使用 largeData,闭包仍然持有引用
    console.log("Handler");
  };
}
 
let handler = createHandler();
// largeData 不会被垃圾回收,因为闭包持有引用

解决方案

function createHandler() {
  let largeData = new Array(1000000).fill(0);
  
  return function() {
    console.log("Handler");
  };
  
  // 如果不再需要 largeData,可以显式设置为 null
  // largeData = null;  // 但这样会失去闭包的作用
}
 
let handler = createHandler();
// 如果不再需要 handler,应该设置为 null
handler = null;  // 释放闭包

4. 变量提升和闭包

4.1 变量提升对闭包的影响

示例

var functions = [];
 
for (var i = 0; i < 3; i++) {
  functions.push(function() {
    console.log(i);  // 所有函数都引用同一个 i
  });
}
 
functions[0]();  // 3
functions[1]();  // 3
functions[2]();  // 3

问题:所有函数都引用同一个 i,因为 var 是函数作用域。

解决方案 1:使用 let

let functions = [];
 
for (let i = 0; i < 3; i++) {  // let 创建块级作用域
  functions.push(function() {
    console.log(i);  // 每个函数引用不同的 i
  });
}
 
functions[0]();  // 0
functions[1]();  // 1
functions[2]();  // 2

解决方案 2:使用 IIFE

var functions = [];
 
for (var i = 0; i < 3; i++) {
  (function(j) {  // 立即执行函数,创建新的作用域
    functions.push(function() {
      console.log(j);
    });
  })(i);
}
 
functions[0]();  // 0
functions[1]();  // 1
functions[2]();  // 2

5. 闭包的性能考虑

5.1 内存占用

闭包会占用更多内存

  • 闭包会保留外部作用域的变量对象
  • 即使外部函数执行完毕,变量对象也不会被销毁
  • 需要谨慎使用,避免内存泄漏

5.2 性能优化

避免不必要的闭包

// 不好的做法
function processData(data) {
  return data.map(function(item) {
    return item * 2;  // 不需要闭包
  });
}
 
// 更好的做法
function processData(data) {
  return data.map(item => item * 2);  // 使用箭头函数
}

📝 最佳实践

  1. 理解作用域:清楚变量的作用域范围
  2. 合理使用闭包:利用闭包实现数据私有化
  3. 避免内存泄漏:注意闭包的内存占用
  4. 使用 letconst:避免 var 带来的作用域问题

🔗 相关链接


javascript 作用域 闭包 词法作用域 作用域链