5.4.4. 作用域与闭包

5.4.4.1. 作用域与作用域链

5.4.4.1.1. 作用域

简单来说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。JavaScript的作用域是靠函数来形成的,也就是说一个函数的变量在函数外不可以访问。

作用域可以分为全局作用域、局部作用域和块级作用域,其中全局作用域主要有以下三种情况:

  • 函数外面定义的变量拥有全局作用域

  • 未定义直接赋值的变量自动声明为拥有全局作用域

  • window对象的属性拥有全局作用

局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部,所以也会把这种作用域称为函数作用域。

5.4.4.1.2. 作用域泄漏

在ES5标准时,只有全局作用域和局部作用域,没有块级作用域,这样可能会造成变量泄漏的问题。例如:

var i = 1;
function f() {
    console.log(i)
    if (true) {
        var i = 2;
    }
}
f(); // undefined

5.4.4.1.3. 作用域提升(var Hoisting)

在JavaScript中,使用var在函数或全局内任何地方声明变量相当于在其内部最顶上声明它,这种行为称为Hoisting。例如下面这段代码等效于第二段代码

function foo() {
    console.log(x); // => undefined
    var x = 1;
    console.log(x); // => 1
}
foo();
function foo() {
    var x;
    console.log(x); // => undefined
    x = 1;
    console.log(x); // => 1
}
foo();

5.4.4.1.4. 作用域链

当函数被执行时,总是先从函数内部找寻局部变量,如果找不到相应的变量,则会向创建函数的上级作用域寻找,直到找到全局作用域为止,这个过程被称为作用域链。

5.4.4.2. 闭包

函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在JavaScript,函数在每次创建时生成闭包。

在JavaScript中,并没有原生的对private方法的支持,即一个元素/方法只能被同一个类中的其它方法所调用。而闭包则是一种可以被用于模拟私有方法的方案。另外闭包也提供了管理全局命名空间的能力,避免非核心的方法或属性污染了代码的公共接口部分。下面是一个简单的例子:

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }
})();

console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

5.4.4.3. 全局对象

全局对象是一个特殊的对象,它的作用域是全局的。

全平台可用的全局对象是 globalThis ,它跟全局作用域里的this值相同。另外在浏览器中存在 selfwindow 全局对象,Web Workers中存在 self 全局对象,Node.js 中存在 global 全局对象。