# 概念

由多个执行上下文的变量对象构成的链表就叫做作用域链

# 作用域链

# 函数创建时

《词法作用域和动态作用域》中提到,函数的作用域在定义的时候就已经决定了。

这是因为函数有一个内部属性[[scope]],在函数创建的时候,就会保存所有父级的变量对象到其中,可以理解 [[scope]] 是父级变量对象的层级链:

function foo() {
  function bar() {
    // 此处省略代码
  }
}
1
2
3
4
5

函数创建时,各自的[[scope]]为:

foo[[scope]] = [globalContext.VO];

bar[[scope]] = [fooContext.AO, globalContext.VO];
1
2
3

# 函数激活时

函数激活时,进入执行上下文,创建 VO/AO,此时会将行数的变量对象放入作用域链最前端,例如 foo 函数:

Scope = [AO].concat(foo.[[scope]])

至此,作用域链创建完成

# 总结

总结一下,如下代码在执行过程中执行上下文和变量对象的创建过程:

var a = "global a";
function foo() {
  var b = "local b";
  return b;
}
foo();
1
2
3
4
5
6

# 分析过程

  1. foo 函数被创建,作用域链保存父级变量对象:
foo[[scope]] = [globalContext.VO];
1

2.执行 foo 函数,创建 foo 函数执行上下文,foo 函数的执行上下文被压如执行上下文栈:

ECSTack = [fooContext, globalContext];
1
  1. 此时 foo 函数还未执行,开始准备工作,第一步:复制函数的[[scope]]属性创建作用域链:
fooContext = {
  Scope: foo[[scope]],
};
1
2
3

4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数申明、变量申明

fooContext = {
  AO: {
    arguments: {
      length: 0,
    },
    b: undefined,
  },
  Scope: foo[[scope]],
};
1
2
3
4
5
6
7
8
9

5.第三步:将活动对象压入作用域链顶端

fooContext = {
  AO: {
    arguments: {
      length: 0,
    },
    b: undefined,
  },
  Scope: [AO, foo[[scope]]],
};
1
2
3
4
5
6
7
8
9

6.准备工作完毕,开始执行函数,变量对象被激活,修改 AO 的值:

fooContext = {
  AO: {
    arguments: {
      length: 0,
    },
    b: "local b",
  },
  Scope: [AO, foo[[scope]]],
};
1
2
3
4
5
6
7
8
9

7.查找 b 的值,返回后函数执行结束,函数上下文从执行上下文栈中移除:

ECSTack = [ globalContext ]

# 疑问

1.函数创建的保存作用域链和函数执行前准备工作中,复制函数作用域链到 Scope 属性中有什么区别?

函数创建的时候,保存的是根据词法所生成的作用域链,执行时会复制这个作用域链,作为自己作用域链的初始化, 然后生成变量对象,并添加到这个复制的作用域链中,这才完整构建了自己的作用域链。 至于为什么会有两个作用域链,是因为函数在创建的时候并不能最终确定作用域的样子

# 别人的总结

源代码中当你定义(书写)一个函数的时候(并未调用),js 引擎也能根据你函数书写的位置,函数嵌套的位置,给你生成一个[[scope]],作为该函数的属性存在(这个属性属于函数的)。即使函数不调用,所以说基于词法作用域(静态作用域)。

然后进入函数执行阶段,生成执行上下文,执行上下文你可以宏观的看成一个对象,(包含 vo,scope,this),此时,执行上下文里的 scope 和之前属于函数的那个[[scope]]不是同一个,执行上下文里的 scope,是在之前函数的[[scope]]的基础上,又新增一个当前的 AO 对象构成的。

函数定义时候的[[scope]]和函数执行时候的 scope,前者作为函数的属性,后者作为函数执行上下文的属性。

# JS 中没有块级作用域

if (true) {
  var color = "red";
}
console.log(color); // red
1
2
3
4

如果把 var 换成 let 打印 color 时则会报错

ReferenceError: a is not defined
1

# 词法作用域

javascript 采用 词法作用域 也就是 静态作用域

# 静态作用域

因为 javascript 采用的是词法作用域,所以函数的作用域在定义的时候就决定了

看下面的例子

var value = 1;
function foo() {
  console.log(value);
}

function bar() {
  var value = 2;
  foo();
}

bar();
1
2
3
4
5
6
7
8
9
10
11

结果打印为 1,因为函数的作用域在定义的时候就决定了,所以 foo 函数是在全局定义的, 因此作用域便是全局,foo 先从内部查找有没有局部变量 value ,如果没有,就从它的作用域全局去找, 结果找到,所以打印 1

# 思考题

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
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

上面 2 段代码都会打印 local scope, 因为 js 采用词法作用域,函数的作用域基于其创建的位置, 而上面的代码最终都是调用函数 f ,而函数 f 是在 checkscope 函数内部定义的, 其作用域在 checkscope 内部,无论何时何地调用 f 都无法改变,都会打印 local scope

# 动态作用域

函数的作用域是在函数调用的时候才决定的

按照动态作用域,则上面代码打印 2, 因为 foo 内部找不到 value,便会从函数调用的作用域查找,所以会打印 2

# 扩展阅读

和大多数的现代化编程语言一样,JavaScript 是采用词法作用域的,这就意味着函数的执行依赖于函数定义的时候所产生(而不是函数调用的时候产生的)的变量作用域。为了去实现这种词法作用域,JavaScript 函数对象的内部状态不仅包含函数逻辑的代码,除此之外还包含当前作用域链的引用。函数对象可以通过这个作用域链相互关联起来,如此,函数体内部的变量都可以保存在函数的作用域内,这在计算机的文献中被称之为闭包。

从技术的角度去将,所有的 JavaScript 函数都是闭包:他们都是对象,他们都有一个关联到他们的作用域链。绝大多数函数在调用的时候使用的作用域链和他们在定义的时候的作用域链是相同的,但是这并不影响闭包。当调用函数的时候闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域链的时候,闭包 become interesting。这种 interesting 的事情往往发生在这样的情况下: 当一个函数嵌套了另外的一个函数,外部的函数将内部嵌套的这个函数作为对象返回。一大批强大的编程技术都利用了这类嵌套的函数闭包,当然,javascript 也是这样。可能你第一次碰见闭包觉得比较难以理解,但是去明白闭包然后去非常自如的使用它是非常重要的。

通俗点说,在程序语言范畴内的闭包是指函数把其的变量作用域也包含在这个函数的作用域内,形成一个所谓的“闭包”,这样的话外部的函数就无法去访问内部变量。所以按照第二段所说的,严格意义上所有的函数都是闭包。

需要注意的是:我们常常所说的闭包指的是让外部函数访问到内部的变量,也就是说,按照一般的做法,是使内部函数返回一个函数,然后操作其中的变量。这样做的话一是可以读取函数内部的变量,二是可以让这些变量的值始终保存在内存中。