一、什么是作用域

什么是作用域?
作用域指的是程序源代码中定义变量的区域。它规定了当前执行代码对变量的执行权限。
在 JavaScript 这门语言中,采用了词法作用域(即静态作用域),而不是动态作用域。

二、静态作用域与动态作用域

诶,既然提到了这两个作用域,那两者的差别在哪里呢?
在词法作用域中,函数的作用域在函数定义的时候就决定了。
而在动态作用域中,函数的作用域在函数调用的时候才决定。
让我们来看个例子去理解一下这两句话:

  1. let tmp = 0;
  2. function foo() {
  3. console.log(tmp);
  4. }
  5. function bar() {
  6. let tmp = 1;
  7. foo();
  8. }
  9. bar();
  • 当 JavaScript 采用静态作用域:
    • 先执行foo函数,先从foo函数内部查找含有局部变量tmp,如果没有,则根据书写的位置,查找上面一层的代码,即tmp等于 0,所以打印结果为 0.
  • 当 JavaScript 采用动态作用域:
    • 先执行foo 函数,依然是从foo函数内部查找是否有局部变量tmp,如果没有,则从调用函数的作用域bar函数内部进行查找,所以打印结果 1.

讲到这里,相信大家都已经有所了解了。

思考题

那么接下来让我们看一个例子:

  1. var scope = "global scope";
  2. function checkscope(){
  3. var scope = "local scope";
  4. function f(){
  5. return scope;
  6. }
  7. return f();
  8. }
  9. checkscope(); // local scope
  1. var scope = "global scope";
  2. function checkscope(){
  3. var scope = "local scope";
  4. function f(){
  5. return scope;
  6. }
  7. return f;
  8. }
  9. checkscope()(); // local scope

在这两段代码中,打印结果均为local scope
原因也简单,因为JavaScript采用的是词法作用域,函数的作用域基于函数创建的位置。
而在《JavaScript 权威指南》中的回答则是:
JavaScript 函数执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数f()定义在这个作用域链里,其中的变量scope一定是局部变量,不管何时何地执行函数f(),这个绑定在执行f()时依然有效。
也就是说,词法作用域与动态作用域,决定的是作用域链的顺序。
虽然两段代码执行的结果一样,但这两段代码究竟有什么不同呢?这是值得我们思考的问题。

三、可执行代码

为了回答上面的问题,我们需要先了解 JavaScript 的可执行代码的类型有哪些?

  • 全局代码
  • 函数代码
  • eval 代码

当执行到一个函数的时候,就会进行准备工作,即所谓的“执行上下文”。每个上下文都有一个关联的对象,而这个上下文中定义的所有变量和函数都存在于这个对象中。变量或函数的上下文决定了它们可以访问哪些数据。

四、上下文执行栈

当我们写的函数越来越多的时候,我们需要用上下文执行栈去管理创建的执行上下文。
当 JS 引擎执行代码时,最先遇到全局代码,所以初始化的时候首先向执行上下文执行栈(ECStack)压入一个全局执行上下文,这里用globalContext表示。 :::info ECStack = [
globalContext
] ::: 此时遇到下面的代码:

  1. function fun3() {
  2. console.log('fun3')
  3. }
  4. function fun2() {
  5. fun3();
  6. }
  7. function fun1() {
  8. fun2();
  9. }
  10. fun1();

通常来说,每个函数调用都会有自己的上下文,当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。在函数执行完之后,上下文执行栈会弹出该函数上下文,将控制权返还给之前的上下文。所以上述的代码处理过程大致如下:

  1. // 伪代码
  2. // fun1()
  3. ECStack.push(<fun1> functionContext);
  4. // fun1中调用了fun2,还创建了fun2的执行上下文
  5. ECStack.push(<fun2> functionContext);
  6. // fun2调用了fun3
  7. ECStack.push(<fun3> functionContext);
  8. // fun3执行完毕
  9. ECStack.pop();
  10. // fun2执行完毕
  11. ECStack.pop();
  12. // fun1执行完毕
  13. ECStack.pop();
  14. // javascript接着执行下面的代码,但是ECStack底层永远有个globalContext

五、解答思考题

现在我们已经了解了执行上下文是如何处理执行上下文,是时候回答第二节的思考题所提出的疑问了。

  1. var scope = "global scope";
  2. function checkscope(){
  3. var scope = "local scope";
  4. function f(){
  5. return scope;
  6. }
  7. return f();
  8. }
  9. checkscope();
  1. var scope = "global scope";
  2. function checkscope(){
  3. var scope = "local scope";
  4. function f(){
  5. return scope;
  6. }
  7. return f;
  8. }
  9. checkscope()();

两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?
答案就是执行上下文栈的变化不一样
第一段代码变化如下:

  1. ECStack.push(<checkscope> functionContext);
  2. ECStack.push(<f> functionContext);
  3. ECStack.pop();
  4. ECStack.pop();

第二段代码变化如下:

  1. ECStack.push(<checkscope> functionContext);
  2. ECStack.pop();
  3. ECStack.push(<f> functionContext);
  4. ECStack.pop();

以上即是概括性的回答。
如果为了详尽讲解两个函数执行上的区别,我们需要去探究一下执行上下文包含了哪些内容,敬请期待下一篇文章。