Skip to content

JavaScript 闭包和作用域学习笔记

作用域(Scope)

作用域决定了变量和函数的可访问性。JavaScript 有三种作用域:全局作用域、函数作用域和块作用域。

全局作用域

javascript
var globalVar = "我是全局变量";

function test() {
  console.log(globalVar); // "我是全局变量"
}

test();
console.log(globalVar); // "我是全局变量"

函数作用域

javascript
function test() {
  var functionVar = "我是函数作用域变量";
  console.log(functionVar); // "我是函数作用域变量"
}

test();
// console.log(functionVar); // ReferenceError: functionVar is not defined

块作用域(ES6+)

javascript
if (true) {
  let blockVar = "我是块作用域变量";
  const blockConst = "我也是块作用域变量";
  console.log(blockVar); // "我是块作用域变量"
}

// console.log(blockVar); // ReferenceError: blockVar is not defined

TIP

letconst 具有块作用域,而 var 只有函数作用域。

作用域链

JavaScript 引擎通过作用域链查找变量。

javascript
var globalVar = "全局变量";

function outer() {
  var outerVar = "外部变量";
  
  function inner() {
    var innerVar = "内部变量";
    console.log(innerVar);  // 内部变量
    console.log(outerVar);  // 外部变量
    console.log(globalVar); // 全局变量
  }
  
  inner();
}

outer();

变量查找规则

  1. 首先在当前作用域查找
  2. 如果找不到,向上一级作用域查找
  3. 一直查找到全局作用域
  4. 如果全局作用域也找不到,抛出 ReferenceError

闭包(Closure)

闭包是指函数能够访问其外部作用域中的变量,即使外部函数已经执行完毕。

基本示例

javascript
function outer() {
  var outerVar = "外部变量";
  
  function inner() {
    console.log(outerVar); // 访问外部变量
  }
  
  return inner;
}

const innerFunc = outer();
innerFunc(); // "外部变量"

闭包的经典应用:计数器

javascript
function createCounter() {
  let count = 0;
  
  return function() {
    count++;
    return count;
  };
}

const counter1 = createCounter();
const counter2 = createCounter();

console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 (独立的计数器)
console.log(counter1()); // 3

闭包实现私有变量

javascript
function createBankAccount(initialBalance) {
  let balance = initialBalance; // 私有变量
  
  return {
    deposit: function(amount) {
      balance += amount;
      return balance;
    },
    withdraw: function(amount) {
      if (amount <= balance) {
        balance -= amount;
        return balance;
      } else {
        return "余额不足";
      }
    },
    getBalance: function() {
      return balance;
    }
  };
}

const account = createBankAccount(1000);
console.log(account.getBalance()); // 1000
console.log(account.deposit(500)); // 1500
console.log(account.withdraw(200)); // 1300
// console.log(balance); // ReferenceError: balance is not defined

闭包在循环中的问题

javascript
// 问题代码
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 输出: 3, 3, 3
  }, 1000);
}

DANGER

使用 var 在循环中创建闭包会导致所有闭包共享同一个变量。

解决方案 1:使用 let

javascript
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 输出: 0, 1, 2
  }, 1000);
}

解决方案 2:使用 IIFE(立即执行函数)

javascript
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j); // 输出: 0, 1, 2
    }, 1000);
  })(i);
}

解决方案 3:使用 bind

javascript
for (var i = 0; i < 3; i++) {
  setTimeout(function(j) {
    console.log(j); // 输出: 0, 1, 2
  }.bind(null, i), 1000);
}

闭包的实际应用

函数工厂

javascript
function createMultiplier(multiplier) {
  return function(number) {
    return number * multiplier;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

模块模式

javascript
const myModule = (function() {
  let privateVar = 0;
  
  function privateFunction() {
    return privateVar;
  }
  
  return {
    publicMethod: function() {
      privateVar++;
      return privateFunction();
    },
    anotherPublicMethod: function() {
      return privateVar;
    }
  };
})();

console.log(myModule.publicMethod()); // 1
console.log(myModule.anotherPublicMethod()); // 1
// console.log(myModule.privateVar); // undefined

防抖(Debounce)

javascript
function debounce(func, delay) {
  let timeoutId;
  
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

const debouncedSearch = debounce(function(query) {
  console.log("搜索:", query);
}, 300);

// 用户输入时,只有停止输入 300ms 后才会执行搜索
debouncedSearch("JavaScript");

节流(Throttle)

javascript
function throttle(func, limit) {
  let inThrottle;
  
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  };
}

const throttledScroll = throttle(function() {
  console.log("滚动事件");
}, 1000);

内存泄漏注意事项

闭包可能导致内存泄漏,因为闭包会保持对外部变量的引用。

javascript
// 可能导致内存泄漏的示例
function attachHandler() {
  const largeData = new Array(1000000).fill("data");
  
  document.getElementById("button").addEventListener("click", function() {
    // 这个闭包持有 largeData 的引用
    console.log("按钮被点击");
  });
}

WARNING

如果闭包持有大量数据的引用,可能导致内存泄漏。使用完毕后记得清理引用。

总结

  • 作用域:决定了变量的可访问性
  • 作用域链:JavaScript 通过作用域链查找变量
  • 闭包:函数能够访问外部作用域的变量
  • 应用:闭包常用于实现私有变量、函数工厂、防抖节流等