Skip to content

作用域和闭包

作用域(Scope)和闭包(Closure)是JavaScript中的核心概念,它们对于理解变量的可访问性和函数的行为至关重要。本文将详细介绍JavaScript的作用域和闭包,以及它们在实际开发中的应用。

作用域

作用域是指变量和函数的可访问范围,它决定了代码在何处可以访问特定的变量和函数。JavaScript中有三种类型的作用域:全局作用域、函数作用域和块级作用域。

1. 全局作用域

全局作用域是最外层的作用域,在任何地方都可以访问。在浏览器环境中,全局作用域由window对象表示;在Node.js环境中,全局作用域由global对象表示。

使用方式

javascript
// 全局变量
var globalVar = 'Global variable';
let globalLet = 'Global let';
const globalConst = 'Global const';

// 全局函数
function globalFunction() {
  console.log('Global function');
}

// 在任何地方都可以访问
console.log(globalVar); // Global variable
console.log(globalLet); // Global let
console.log(globalConst); // Global const
globalFunction(); // Global function

// 在浏览器中,全局变量和函数会成为window对象的属性
console.log(window.globalVar); // Global variable
console.log(window.globalFunction); // ƒ globalFunction() { ... }

特点

  • 全局作用域中的变量和函数在任何地方都可以访问。
  • 在浏览器环境中,全局变量和函数会成为window对象的属性。
  • 全局作用域中的变量会持续存在,直到页面关闭或进程结束。
  • 过多的全局变量会污染全局命名空间,容易导致命名冲突。

2. 函数作用域

函数作用域是指在函数内部创建的作用域,只有在函数内部才能访问。

使用方式

javascript
function outerFunction() {
  // 函数作用域变量
  var functionVar = 'Function variable';
  let functionLet = 'Function let';
  const functionConst = 'Function const';
  
  // 函数内部可以访问全局变量
  console.log(globalVar); // Global variable
  
  // 函数内部的函数
  function innerFunction() {
    // 内部函数可以访问外部函数的变量
    console.log(functionVar); // Function variable
    console.log(functionLet); // Function let
    console.log(functionConst); // Function const
  }
  
  innerFunction();
}

outerFunction();

// 外部无法访问函数内部的变量
console.log(functionVar); // ReferenceError: functionVar is not defined
console.log(functionLet); // ReferenceError: functionLet is not defined
console.log(functionConst); // ReferenceError: functionConst is not defined

特点

  • 函数作用域中的变量和函数只能在函数内部访问。
  • 函数内部可以访问全局变量和外部函数的变量。
  • 函数执行完毕后,函数作用域中的变量会被销毁(除非被闭包引用)。
  • 使用var声明的变量会被提升到函数顶部。

3. 块级作用域

块级作用域是ES6引入的新特性,使用letconst声明的变量会创建块级作用域,块级作用域由{}包围。

使用方式

javascript
// 块级作用域
if (true) {
  var blockVar = 'Block var'; // 不创建块级作用域,会被提升到全局
  let blockLet = 'Block let'; // 创建块级作用域
  const blockConst = 'Block const'; // 创建块级作用域
  
  console.log(blockVar); // Block var
  console.log(blockLet); // Block let
  console.log(blockConst); // Block const
}

// 外部可以访问var声明的变量
console.log(blockVar); // Block var

// 外部无法访问let和const声明的变量
console.log(blockLet); // ReferenceError: blockLet is not defined
console.log(blockConst); // ReferenceError: blockConst is not defined

// 循环中的块级作用域
for (var i = 0; i < 3; i++) {
  // var声明的变量会泄露到外部
}
console.log(i); // 3

for (let j = 0; j < 3; j++) {
  // let声明的变量不会泄露到外部
}
console.log(j); // ReferenceError: j is not defined

特点

  • 使用letconst声明的变量会创建块级作用域。
  • 块级作用域中的变量只能在块内部访问。
  • 块级作用域可以防止变量泄露到外部作用域。
  • 块级作用域中的变量不会被提升。

4. 作用域链

作用域链是指当代码在某个作用域中访问变量时,JavaScript会先在当前作用域中查找,如果找不到,就会向上查找父级作用域,直到找到该变量或到达全局作用域。

使用方式

javascript
// 全局作用域
var globalVar = 'Global';

function outer() {
  // 外部函数作用域
  var outerVar = 'Outer';
  
  function inner() {
    // 内部函数作用域
    var innerVar = 'Inner';
    
    // 访问变量,会沿着作用域链查找
    console.log(innerVar); // Inner(当前作用域)
    console.log(outerVar); // Outer(父级作用域)
    console.log(globalVar); // Global(全局作用域)
    console.log(nonExistentVar); // ReferenceError: nonExistentVar is not defined
  }
  
  inner();
}

outer();

特点

  • 作用域链是由嵌套的作用域组成的,从当前作用域开始,向上延伸到全局作用域。
  • 当访问变量时,JavaScript会沿着作用域链查找,直到找到该变量或到达全局作用域。
  • 如果在全局作用域中也找不到该变量,会抛出ReferenceError错误。
  • 作用域链的长度取决于函数的嵌套深度。

5. 变量提升

变量提升(Hoisting)是JavaScript的一个特性,它指的是变量和函数声明会被提升到它们所在作用域的顶部。

使用方式

javascript
// 变量提升
console.log(hoistedVar); // undefined(变量声明被提升,但赋值未被提升)
var hoistedVar = 'Hoisted';

// 函数声明提升
hoistedFunction(); // Function hoisted(函数声明被完整提升)
function hoistedFunction() {
  console.log('Function hoisted');
}

// 函数表达式不会被提升
console.log(hoistedExpression); // undefined
hoistedExpression(); // TypeError: hoistedExpression is not a function
var hoistedExpression = function() {
  console.log('Function expression');
};

// let和const不会被提升
console.log(hoistedLet); // ReferenceError: Cannot access 'hoistedLet' before initialization
let hoistedLet = 'Hoisted let';

console.log(hoistedConst); // ReferenceError: Cannot access 'hoistedConst' before initialization
const hoistedConst = 'Hoisted const';

特点

  • 使用var声明的变量会被提升到作用域顶部,但其赋值不会被提升。
  • 函数声明会被完整提升到作用域顶部,可以在声明之前调用。
  • 函数表达式不会被提升,因为它们是变量赋值的一部分。
  • 使用letconst声明的变量不会被提升,它们会处于"暂时性死区"(Temporal Dead Zone),直到声明语句执行。

闭包

闭包是指一个函数可以访问其词法作用域之外的变量,即使该函数在其词法作用域之外执行。

1. 闭包的概念

当一个函数被定义在另一个函数内部,并且内部函数引用了外部函数的变量,那么内部函数就形成了一个闭包。闭包会保留对外部函数作用域的引用,即使外部函数已经执行完毕。

使用方式

javascript
function outer() {
  var outerVar = 'Outer variable';
  
  // 内部函数形成闭包
  function inner() {
    // 访问外部函数的变量
    console.log(outerVar);
  }
  
  return inner;
}

// 外部函数执行完毕,返回内部函数
const closure = outer();

// 调用内部函数,仍然可以访问外部函数的变量
closure(); // Outer variable

特点

  • 闭包是由内部函数形成的,它可以访问外部函数的变量。
  • 即使外部函数已经执行完毕,闭包仍然可以访问外部函数的变量。
  • 闭包会保留对外部函数作用域的引用,不会释放外部函数的变量。
  • 每个闭包都有自己的词法环境,它们不会共享变量的值。

2. 闭包的应用场景

闭包在JavaScript中有很多应用场景,以下是一些常见的例子:

2.1 数据私有化

闭包可以用于创建私有变量,防止外部访问和修改。

javascript
function createCounter() {
  // 私有变量
  let count = 0;
  
  return {
    increment: function() {
      count++;
      return count;
    },
    decrement: function() {
      count--;
      return count;
    },
    getCount: function() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount()); // 1
console.log(counter.count); // undefined(无法直接访问私有变量)

2.2 函数工厂

闭包可以用于创建函数工厂,根据不同的参数创建不同的函数。

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

2.3 事件处理

闭包可以用于事件处理,保存事件处理函数所需的上下文。

javascript
function setupEventHandlers() {
  let count = 0;
  
  document.getElementById('button').addEventListener('click', function() {
    count++;
    console.log(`Button clicked ${count} times`);
  });
}

setupEventHandlers();

2.4 模块模式

闭包可以用于实现模块模式,将代码组织成独立的模块。

javascript
const module = (function() {
  // 私有变量和函数
  let privateVar = 'Private variable';
  
  function privateFunction() {
    console.log('Private function');
  }
  
  // 暴露公共接口
  return {
    publicVar: 'Public variable',
    publicFunction: function() {
      console.log('Public function');
      // 可以访问私有变量和函数
      console.log(privateVar);
      privateFunction();
    }
  };
})();

console.log(module.publicVar); // Public variable
module.publicFunction(); // Public function, Private variable, Private function
console.log(module.privateVar); // undefined(无法访问私有变量)
module.privateFunction(); // TypeError: module.privateFunction is not a function

2.5 缓存

闭包可以用于实现缓存,保存计算结果,避免重复计算。

javascript
function createCache() {
  const cache = {};
  
  return function(key, calculate) {
    if (cache[key]) {
      console.log('Using cached value');
      return cache[key];
    } else {
      console.log('Calculating new value');
      const value = calculate();
      cache[key] = value;
      return value;
    }
  };
}

const memoize = createCache();

// 第一次计算
const result1 = memoize('fibonacci-10', function() {
  function fib(n) {
    return n <= 1 ? n : fib(n - 1) + fib(n - 2);
  }
  return fib(10);
});
console.log(result1); // Calculating new value, 55

// 第二次使用缓存
const result2 = memoize('fibonacci-10', function() {
  function fib(n) {
    return n <= 1 ? n : fib(n - 1) + fib(n - 2);
  }
  return fib(10);
});
console.log(result2); // Using cached value, 55

3. 闭包的优缺点

3.1 优点

  • 数据私有化:闭包可以创建私有变量,防止外部访问和修改,提高代码的安全性。
  • 函数工厂:闭包可以用于创建函数工厂,根据不同的参数创建不同的函数。
  • 事件处理:闭包可以用于事件处理,保存事件处理函数所需的上下文。
  • 模块模式:闭包可以用于实现模块模式,将代码组织成独立的模块。
  • 缓存:闭包可以用于实现缓存,保存计算结果,避免重复计算。

3.2 缺点

  • 内存泄漏:闭包会保留对外部函数作用域的引用,不会释放外部函数的变量,可能导致内存泄漏。
  • 性能问题:闭包会增加内存使用,可能影响性能,特别是在频繁创建闭包的情况下。
  • 可读性问题:过度使用闭包会使代码难以理解和维护。

4. 闭包的内存管理

为了避免闭包导致的内存泄漏,应该注意以下几点:

  1. 及时释放引用:当不再需要闭包时,应该将其引用设置为null,以便垃圾回收器回收。
javascript
function createClosure() {
  const largeObject = new Array(1000000).fill('Large object');
  
  return function() {
    console.log(largeObject.length);
  };
}

let closure = createClosure();
closure(); // 1000000

// 不再需要闭包时,释放引用
closure = null; // 现在可以被垃圾回收
  1. 避免不必要的闭包:只在必要时使用闭包,避免过度使用。

  2. 使用块级作用域:使用letconst声明变量,利用块级作用域自动释放变量。

  3. 使用WeakMap:对于需要存储对象引用的情况,可以使用WeakMap,它不会阻止垃圾回收。

javascript
const weakMap = new WeakMap();

function storeObject(obj) {
  weakMap.set(obj, 'value');
}

const obj = {};
storeObject(obj);

// 当obj不再被引用时,会被垃圾回收,WeakMap中的条目也会被自动删除
obj = null;

面试常见问题

1. 什么是作用域?JavaScript中有哪些类型的作用域?

作用域是指变量和函数的可访问范围,它决定了代码在何处可以访问特定的变量和函数。

JavaScript中有三种类型的作用域:

  • 全局作用域:最外层的作用域,在任何地方都可以访问。
  • 函数作用域:在函数内部创建的作用域,只有在函数内部才能访问。
  • 块级作用域:ES6引入的新特性,使用letconst声明的变量会创建块级作用域,由{}包围。

2. 什么是变量提升?var、let和const的变量提升有什么区别?

变量提升是JavaScript的一个特性,它指的是变量和函数声明会被提升到它们所在作用域的顶部。

  • var:使用var声明的变量会被提升到作用域顶部,但其赋值不会被提升。
  • letconst:使用letconst声明的变量不会被提升,它们会处于"暂时性死区"(Temporal Dead Zone),直到声明语句执行。

3. 什么是闭包?闭包是如何形成的?

闭包是指一个函数可以访问其词法作用域之外的变量,即使该函数在其词法作用域之外执行。

闭包的形成:当一个函数被定义在另一个函数内部,并且内部函数引用了外部函数的变量,那么内部函数就形成了一个闭包。闭包会保留对外部函数作用域的引用,即使外部函数已经执行完毕。

4. 闭包有哪些应用场景?

闭包的应用场景包括:

  • 数据私有化:创建私有变量,防止外部访问和修改。
  • 函数工厂:根据不同的参数创建不同的函数。
  • 事件处理:保存事件处理函数所需的上下文。
  • 模块模式:将代码组织成独立的模块。
  • 缓存:保存计算结果,避免重复计算。

5. 闭包的优缺点是什么?

优点

  • 数据私有化,提高代码的安全性。
  • 函数工厂,根据不同的参数创建不同的函数。
  • 事件处理,保存事件处理函数所需的上下文。
  • 模块模式,将代码组织成独立的模块。
  • 缓存,保存计算结果,避免重复计算。

缺点

  • 内存泄漏,闭包会保留对外部函数作用域的引用,不会释放外部函数的变量。
  • 性能问题,闭包会增加内存使用,可能影响性能。
  • 可读性问题,过度使用闭包会使代码难以理解和维护。

6. 如何避免闭包导致的内存泄漏?

避免闭包导致的内存泄漏的方法:

  • 及时释放引用:当不再需要闭包时,将其引用设置为null
  • 避免不必要的闭包:只在必要时使用闭包。
  • 使用块级作用域:使用letconst声明变量,利用块级作用域自动释放变量。
  • 使用WeakMap:对于需要存储对象引用的情况,使用WeakMap,它不会阻止垃圾回收。

7. 下面的代码输出什么?为什么?

javascript
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

输出

3
3
3

原因:使用var声明的变量i是函数作用域,会被提升到全局作用域。当setTimeout的回调函数执行时,循环已经结束,i的值已经变成了3,所以三个回调函数都会输出3。

8. 如何修改上面的代码,使其输出0、1、2?

可以使用以下方法:

方法1:使用立即执行函数表达式(IIFE)创建闭包

javascript
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j);
    }, 1000);
  })(i);
}

方法2:使用let声明变量,利用块级作用域

javascript
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

方法3:使用forEach方法

javascript
[0, 1, 2].forEach(function(i) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
});

9. 什么是作用域链?它是如何工作的?

作用域链是由嵌套的作用域组成的,从当前作用域开始,向上延伸到全局作用域。

工作原理:当访问变量时,JavaScript会沿着作用域链查找,直到找到该变量或到达全局作用域。如果在全局作用域中也找不到该变量,会抛出ReferenceError错误。

10. 闭包与垃圾回收的关系是什么?

垃圾回收是JavaScript的自动内存管理机制,它会回收不再被引用的变量和对象。

闭包与垃圾回收的关系:闭包会保留对外部函数作用域的引用,不会释放外部函数的变量,这意味着即使外部函数已经执行完毕,其变量仍然会被保留在内存中,直到闭包不再被引用。如果闭包被长时间引用,可能会导致内存泄漏。

为了避免闭包导致的内存泄漏,应该在不再需要闭包时释放其引用,以便垃圾回收器回收外部函数的变量。

11. 什么是暂时性死区(Temporal Dead Zone)?

暂时性死区是ES6引入的概念,它指的是使用letconst声明的变量在声明之前的时间段。在这段时间内,变量已经被创建,但不能被访问,否则会抛出ReferenceError错误。

javascript
console.log(hoistedLet); // ReferenceError: Cannot access 'hoistedLet' before initialization
let hoistedLet = 'Hoisted let';

暂时性死区的存在是为了避免变量在声明之前被使用,提高代码的可靠性和可读性。

12. 模块模式的原理是什么?如何使用闭包实现模块模式?

模块模式的原理是使用闭包创建私有变量和函数,只暴露公共接口。

使用闭包实现模块模式

javascript
const module = (function() {
  // 私有变量和函数
  let privateVar = 'Private variable';
  
  function privateFunction() {
    console.log('Private function');
  }
  
  // 暴露公共接口
  return {
    publicVar: 'Public variable',
    publicFunction: function() {
      console.log('Public function');
      // 可以访问私有变量和函数
      console.log(privateVar);
      privateFunction();
    }
  };
})();

console.log(module.publicVar); // Public variable
module.publicFunction(); // Public function, Private variable, Private function
console.log(module.privateVar); // undefined(无法访问私有变量)
module.privateFunction(); // TypeError: module.privateFunction is not a function

模块模式的优点是可以创建私有变量和函数,防止外部访问和修改,提高代码的安全性和可维护性。

总结

作用域和闭包是JavaScript中的核心概念,它们对于理解变量的可访问性和函数的行为至关重要。

  • 作用域决定了变量和函数的可访问范围,包括全局作用域、函数作用域和块级作用域。
  • 作用域链是由嵌套的作用域组成的,当访问变量时,JavaScript会沿着作用域链查找。
  • 变量提升是JavaScript的一个特性,它指的是变量和函数声明会被提升到它们所在作用域的顶部。
  • 闭包是指一个函数可以访问其词法作用域之外的变量,即使该函数在其词法作用域之外执行。
  • 闭包的应用场景包括数据私有化、函数工厂、事件处理、模块模式和缓存。
  • 闭包的优缺点:优点是可以创建私有变量、实现函数工厂等;缺点是可能导致内存泄漏、性能问题和可读性问题。
  • 避免闭包导致的内存泄漏:及时释放引用、避免不必要的闭包、使用块级作用域和WeakMap。

通过理解作用域和闭包,你可以更好地控制变量的访问范围,创建更加安全和高效的JavaScript代码。

好好学习,天天向上