JavaScript作用域及作用域链的详解

1. JavaScript作用域

  作用域就是变量与函数的可访问范围,JavaScript的变量作用域分为两种:全局作用域局部作用域。另外JavaScript块级作用域

2.全局作用域

  在代码中任何地方都可以访问到的对象拥有全局作用域。全局作用域里的变量能够在其他作用域中被访问和修改。

  • 最外层函数在最外层函数外定义的变量拥有全局作用域
  • 所有未定义直接赋值的变量自动声明为拥有全局作用域
  • 所有window对象的属性拥有全局作用域
1
2
3
4
5
6
7
8
9
10
11
var str1 = "jessy1"; //定义一个全局变量
function func(){ //最外层函数,拥有全局作用域
var str2 = "jessy2" //局部作用域
str3 = "jessy3" //未定义直接赋值的变量
window.str4 = "jessy4" //window对象的属性
}
func(); //全局作用域
console.log(str1) // jessy1
console.log(str2) // str2 is not defined 局部作用域
console.log(str3) //jessy3
console.log(str4) // jessy4

3.局部作用域

  定义在函数中的变量就在局部作用域中。并且函数在每次调用时都有一个不同的作用域。这意味着同名变量可以用在不同的函数中。因为这些变量绑定在不同的函数中,拥有不同作用域,彼此之间不能访问。

1
2
3
4
5
6
7
8
9
function func1(){
var str = "jessy1" //局部作用域
console.log(str) //jessy1
}
function func2(){
var str = "jessy2" //局部作用域
console.log(str) //jessy2
}
console.log(str) // str is not defined 局部作用域

  以上例子可以看出,func1和func2函数都绑定了str变量。他们拥有不同的作用域,只可以访问自身的str。彼此不能访问。

4. JavaScripte没有块级作用域

  块级声明包括ifswitch,以及forwhile循环,和函数不同,它们不会创建新的作用域。在块级声明中定义的变量从属于该块所在的作用域。

1
2
3
4
5
6
7
8
9
10
11
if(true){
var str1 = "Jessy1";
}
function func(){
if(true){
var str2 = "Jessy2";
}
console.log(str2) //if块的作用域是局部,因此局部内可以访问str2
}
console.log(str1) //str1所属的块的作用域是全局作用域
console.log(str2) //str2 is not defined,str2所属块是局部作用域

  ECMAScript 6 引入了let关键字,用来声明变量。let 声明的变量只在它所在的代码块有效。

1
2
3
4
5
6
if(true){
let str1 = "Jessy1";
var str2 = "Jessy2"
}
console.log(str1) //str1 is not defined
console.log(str2) //Jessy2

  上面代码在代码块之中,分别用letvar声明了两个变量。然后在代码块之外调用这两个变量,结果let声明的变量报错,var 声明的变量返回了正确的值。这表明,let 声明的变量只在它所在的代码块有效。

5.执行环境(execution context)

  每个执行环境都有一个与之关联的变量对象,环境中定义的所有的变量和函数都保存在这个变量对象中。

5.1 执行环境

在JavaScript中有三种代码运行环境:

  • Gloal Code
      JavaScript代码开始运行的默认环境
  • Function Code
      代码进入一个JavaScript函数
  • Eval Code
      使用eval()执行代码

  为了表示不同的运行环境,JavaScript中有一个执行上下文(Execution context,EC)的概念也就是说,当JavaScript代码执行的时候,会进入不同的执行上下文,这些执行上下文就构成了一个执行上下文栈(Execution context stack,ECS)
例如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var a = "global var";
debugger;
function foo(){
console.log(a);
}
function outerFunc(){
var b = "var in outerFunc";
console.log(b);
eval("var e='eee'");
function innerFunc(){
var c = "var in innerFunc";
console.log(c);
foo();
}
innerFunc();
}
eval("var d='ddd'");
outerFunc();

  代码首先进入Global Execution Context,然后依次进入outerFuncinnerFuncfoo的执行上下文,执行上下文栈可使用Chrome浏览器按F12查看,如下图:
image

  当JavaScript代码执行的时候,第一个进入的总是默认的Global Execution Context,所以说它总是在ECS的最底部
  对于每个Execution Context都有三个重要的属性,变量对象(Variable object,VO),作用域链(Scope chain)和this。

5.2 变量对象(Variable object)

  变量对象是与执行上下文相关的数据作用域。它是一个与上下文相关的特殊对象,其中存储了在上下文中定义的变量和函数声明。也就是说,一般VO中会包含以下信息:

  • 变量 (var, Variable Declaration);
  • 函数声明 (Function Declaration, FD);
  • 函数的形参
      当JavaScript代码运行中,如果试图寻找一个变量的时候,就会首先查找VO。对于前面例子中的代码,Global Execution Context中的VO就可以表示如下:
    image

5.3 活动对象(Activation object)

  只有全局上下文的变量对象允许通过VO的属性名称间接访问;在函数执行上下文中,VO是不能直接访问的,此时由激活对象(Activation Object,缩写为AO)扮演VO的角色。激活对象 是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。
  Arguments Objects 是函数上下文里的激活对象AO中的内部对象,它包括下列属性:

  • callee: 指向当前函数的引用
  • length: 真正传递的参数的个数
  • properties-indexes: 就是函数的参数值(按参数列表从左到右排列)

  对于VO和AO的关系可以理解为,VO在不同的Execution Context中会有不同的表现:当在Global Execution Context中,可以直接使用VO;但是,在函数Execution Context中,AO就会被创建。
当上面的例子开始执行outerFunc的时候,就会有一个outerFunc的AO被创建:
image

6. 作用域链

  了解变量对象和活动对象之后,我们来了解下什么是作用域链。
  在执行环境创建阶段,作用域链在变量对象之后创建。作用域链包含变量对象。作用域链用于解析变量。当解析一个变量时,JavaScript 开始从最内层沿着父级寻找所需的变量或其他资源。作用域链包含自己执行环境以及所有父级环境中包含的变量对象。
  内部环境可以通过作用域链访问所在的外部环境,但是外部环境不能访问内部环境的任何变量和函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var level1 = "爷爷";
function outerFunc() {
var level2 = "爸爸";
function innerFunc() {
var level3 = "自己";
console.log(level1)
console.log(level2)
console.log(level3)
//这里可以访问level1,level2,level3
//在swapColor执行环境中找不到level1,level2变量对象,从父级环境中找到
}
innerFunc();
console.log(level1)
console.log(level2)
//这里可以访问level1,level2,但是不能访问level3
//(即在changeColor这个大函数的作用域内)
}
//这里只能访问level1 (即全局作用域)
outerFunc();

  从以上例子可以看出,在innerFunc执行环境中,可以访问自身的变量level1.。接着访问level2,找不到自身环境的level2,于是从父级outerFunc执行环境中开始查找到level2。访问level3时,在自身执行环境和父级执行环境都找不到,继续往父级的父级(爷爷级),最后找到爷爷级的level3。也就是说作用域链是从自己执行环境然后从父级环境一层层往上查找的。
  在outerFunc执行环境中,可以访问自身的变量level2以及父级变量level3。不能访问内部环境的变量level1。


作者: Jessy Hong