JS:深入理解词法作用域

@一棵菜菜  August 6, 2020

说明

最近在复看红皮书的部分内容,发现之前对词法作用域理解的有点片面,所以今天单独成文整理下,方便以后查阅理解。

相关概念

  1. 执行环境(分类:全局/函数/Eval,浏览器下全局是window对象)
  2. 变量对象:作用:存储对应执行环境的变量和函数
  3. 执行环境:执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。
  4. 作用域链:当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。用途:保证对执行环境有权访问的所有变量和函数的有序访问。

任何js代码片段在执行前都要进行编译,通常就在执行前几微秒,甚至更短。即:(编译器)先编译,(JS引擎)再执行。

js 编译的三个阶段:

  1. 词法阶段:字符串分解成词法单元;生成词法作用域
  2. 语法阶段:把词法单元流(数组)转换成AST;
  3. 生成阶段:把 AST 转换成可执行的代码。

作用域

作用域是指程序源代码中定义变量的区域。

作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

JavaScript 采用词法作用域,也就是静态作用域

词法作用域【必须理解!】

概念:定义在词法阶段的作用域。

Javascript 采用的就是词法作用域,所以变量和函数的作用域在定义的时候就决定了。

【理解】词法作用域是由你在写代码时将变量和块作用域 写在哪里 来决定的,因此当词法分析器处理代码时会保持作用域不变(欺骗词法除外),不会因为函数调用的位置发生改变
【牢记】内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。每个环境都可以向上搜索作用域链,以查询变量和函数名,但不能向下!

典例:

function foo() {
    console.log(a);
}
function bar() {
    var a = 2;
    foo();
}
var a = 1;
bar()

图解作用域链:

window
 |—— a
 |—— foo()
 |—— bar()
   |-a
结果为:1

分析:

  • 假设javascript采用静态作用域,让我们分析下执行过程:

    ...,开始执行foo函数,先从foo函数内部查找是否有局部变量a => 没有。再根据代码书写的位置,向上查找作用域链,就找到了window.a,也就是value等于1,所以结果会打印1。与foo函数在哪里执行没有任何关系。

  • 假设javascript采用动态作用域,让我们分析下执行过程:

    执行foo函数,依然是从foo函数内部查找是否有局部变量value,如果没有,就从调用函数的作用域,也就是bar函数内部查找value变量,所以结果会打印 2。

由于javascript采用的是词法作用域,所以这个例子的结果是1。

动态作用域

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

理解即可,具体内容请查看《你不知道的js》上 中 P59 的介绍。

思考题

来自《javascript权威指南》中的例子:
eg1:

var scope = "global scope";
function checkscope(){
  var scope = "local scope";
  function f(){
    return scope;
  }
  return f()
}
checkscope()

eg2:

var scope = "global scope";
function checkscope(){
  var scope = "local scope";
  function f(){
    return scope;
  }
  return f;
}
checkscope()();
结果:两段代码都会打印:local scope

分析:因为javascript采用的是词法作用域,函数的作用域基于函数创建的位置。

引用《javascript权威指南》的回答是:
javascript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数f()定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行f()时依然有效。

参考

《你不知道的JS》上-词法作用域
参考文献《JavaScript深入之词法作用域和动态作用域》
我的相关文章《JS:4.2 执行环境及作用域》

添加新评论