一、从代码运行看作用域

  • js引擎在执行js代码之前,编译器会先对js代码进行词法分析、语法分析和代码生成。
  • 在词法分析会把一段代码分解成词法单元,然后把词法单元(token)解析成树结构
  • 在代码生成式时遇到变量,会把变量存在当前环境作用域内并生成引擎可以执行的代码
  • 在执行代码时会先去作用域中查当前变量是否存在,之后进行下一步操作。
  • 比如遇到var a=1;这段程序时。
    • 编译阶段,编译器遇到var a的时候会先进行变量提升,把a放到当前执行环境的作用域内。
    • 执行阶段 遇到a=1;会先去作用域内查,是否有变量a,如果有,就把1赋值给它。

二、作用域链

1
2
3
4
5
6
function f1(a){
var c=1
console.log(a+b+c)
}
var b=2;
f1(1);//1
  • 编译阶段,编译器遇到function和var时,会先获取这些变量的定义,把b和f1函数放入全局作用域内。
  • 引擎执行代码时,
    • 遇到b=2,在作用域内查找变量b,并将2赋值给b。
    • 遇到f1(1)时,会进入f1函数的执行上下文,进行执行f1,当执行到console.log时,调用栈的环境如下图所示。

在执行console.log( a + b +c )时,引擎查找b时,会现在f1函数的作用域范围内查找,找不到就会去全局作用里查找。在全局作用域里,找到b=2。这就是所谓的作用域链式查找。

三、作用域链形成规则

1
2
3
4
5
6
7
8
9
function f2() {
console.log(myName)
}
function f1() {
var myName = "f1变量"
f2()
}
var myName = "全局变量"
f1() //全局变量
  • 我们执行后发现f2虽然嵌入在f1函数里,但是f2里的myName取值并不是f1中的myName,而是全局的变量myName。

  • 注意这里就涉及一个重要的概念:

    • 作用域链是由词法作用域决定的,而词法作用域就是指作用域由代码中函数声明的位置来决定的,所以词法作用域也成为静态作用域。(也可以理解为在词法生成阶段就已经决定了作用域的位置。也就决定了作用域链)
  • 当执行到console.log(myname),调用栈信息如下图所示

  • 每个作用域里都有一个outer指向它的上一级作用域,这个指向是按照代码书写位置来决定的。
  • 也就是说作用域链由代码书写位置决定,和函数调用没有关系
1
2
3
4
5
6
7
8
9
10
11
12
13
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)