# 词法作用域和动态作用域

# 什么是作用域

作用域 是定义变量的区域,规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。作用域是确定在何处以及如何查找变量的一套规则。JavaScrip 采用词法作用域(lexical scoping),也就是静态作用域。

# 两作用域的区别

词法作用域(静态作用域)和动态作用域的区别:

词法作用域:因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。函数的作用域基于函数创建的位置

动态作用域:而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的

在全局作用域中“定义”一个函数到时候,只会创建包含全局作用域的作用域链。只有“执行”该函数的时候,才会复制创建时的作用域,并将当前函数的局部作用域放在作用域链的顶端。 词法作用域是通过作用域链来实现的

动态作用域和静态作用域,规定变量访问权,决定的作用域链的顺序。

举例:

// 例1:
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

// 例2:
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

两段代码都会打印:local scope。因为JavaScript采用的是词法作用域,函数的作用域基于函数创建的位置。JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。

但这两段代码的执行原理有本质区别,见下节从底层执行上下文栈方面深刻理解。

# 闭包与作用域的理解

JavaScript 是采用词法作用域的,这就意味着 函数的执行依赖于函数定义的时候所产生(而不是函数调用的时候产生的)的变量作用域。为了去实现这种词法作用域,JavaScript 函数对象的内部状态不仅包含函数逻辑的代码,除此之外还 包含当前作用域链的引用。函数对象可以通过这个 作用域链 相互关联起来,如此,函数体内部的变量都可以保存在函数的作用域内,这在计算机的文献中被称之为 闭包。所以闭包是指一个函数能够访问其定义时的作用域中的变量,即使在其定义的作用域已经执行完毕后。这意味着闭包可以让函数“记住”它被创建时的环境,即使它在另一个环境被调用,而这种“记住”正是通过作用域链实现

从技术的角度去讲,所有的 JavaScript 函数都是闭包:他们都是对象,他们都有一个关联到他们的作用域链。绝大多数函数在调用的时候使用的作用域链和他们在定义的时候的作用域链是相同的,但是这并不影响闭包。当 调用函数的时候闭包所指向的作用域链定义函数时的作用域链 不是同一个作用域链的时候,闭包 become interesting。这种 interesting 的事情往往发生在这样的情况下: 当一个函数嵌套了另外的一个函数,外部的函数将内部嵌套的这个函数作为对象返回。一大批强大的编程技术都利用了这类嵌套的函数闭包。

在 JavaScript 中,外部函数无法直接访问内部函数的变量,这是由于作用域链的单向性决定的。当函数嵌套时,内部函数可以访问外部函数的变量(包括参数、局部变量和外部函数的内部函数的变量),但外部函数不能访问内部函数的变量。这是因为内部函数的作用域链包含了外部函数的作用域,而外部函数的作用域链中并不包含内部函数的作用域。因此,内部函数可以访问外部函数的变量,但外部函数不能访问内部函数的变量。

尽管外部函数不能直接访问内部函数的变量,但可以通过返回内部函数来创建闭包,从而间接访问这些变量。闭包使得内部函数即使在其外部函数执行完毕后,仍然可以访问外部函数的作用域中的变量。使内部函数返回一个函数形成闭包,通过闭包,外部函数可操作内部变量。这样做的话一是可以读取函数内部的变量,二是可以让这些变量的值始终保存在内存中。

//示例 1: 外部函数不能直接访问内部函数的变量
function outerFunction() {
  var outerVar = 'I am outer';

  function innerFunction() {
    var innerVar = 'I am inner';
    console.log(outerVar); // 可以访问外部函数的变量
  }

  innerFunction();
  console.log(innerVar); // ReferenceError: innerVar is not defined  因为 innerVar 不在 outerFunction 的作用域链中
}

outerFunction();

//示例 2: 返回内部函数形成闭包,外部间接访问这些变量
function outerFunction() {
  var outerVar = 'I am outer';

  function innerFunction() {
    console.log(outerVar);
  }

  return innerFunction; // 返回内部函数,形成闭包
}

var myClosure = outerFunction(); // 获取闭包
myClosure(); // 输出:I am outer 即使 outerFunction 已经执行完毕
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

综上,每个函数都有自己的作用域及作用域链,它决定了变量的可见性。当函数执行时,会创建一个执行上下文(execution context),其中包括一个作用域链。作用域链包含了函数的局部变量、父函数的变量,以及全局变量。闭包因为保留了这个作用域链,所以可以访问定义这个闭包函数时,它所在位置的外部函数的变量(词法作用域)。当外部函数执行时,它的变量被存储在内存中。当外部函数返回一个内部函数时,这个内部函数会保留对外部函数变量环境的引用。即使外部函数执行完毕,这些变量也不会被垃圾回收,因为内部函数(闭包)仍然引用着它们。JavaScript 的垃圾回收机制通常通过引用计数或者标记-清除算法来回收不再使用的内存。由于闭包仍然引用着外部函数的变量,这些变量不会被垃圾回收,直到闭包本身不再被引用。