作用域和作用域链

作用域是每种计算机语言最重要的基础之一,当然它也是JavaScript最重要的概念之一。要想真正的深入了解JavaScript,了解JavaScript的作用域和作用域链非常必要。

JavaScript的作用域是什么

简单的说,作用域就是变量和函数可访问的范围,即作用域控制着变量和函数的可见性和生命周期。在javascript的作用域有全局作用域和局部作用域两种,局部作用域也叫函数作用域。

全局作用域

在代码中任何地方都能访问到的对象拥有全局作用域,以下几种情况拥有全局作用域。
1、程序最外层定义的函数或变量

1
2
3
4
5
6
var a = 'dogjun'
function hello () {
alert(a)
}
console.log(a) // 'dogjun'
hello() // 能访问到 'dogjun'

2、所有未定义直接赋值的变量

1
2
3
4
5
6
function hello () {
a = 'dogjun'
var b = 'dogjun1'
}
console.log(a) // 'dogjun'
console.log(b) // 报错

3、所有window对象的属性和方法
一般情况下,window对象的内置属性都拥有全局作用域,例如window.name、window.location、window.top等等。

局部作用域(函数作用域)

局部作用域在函数内创建,函数内可访问,函数外不可访问。

1
2
3
4
5
6
functon hello () {
var a = 'dogjun'
alert(a)
}
hello() // 能访问到'dogjun'
alert(a) // error not defined

作用域链是什么

了解作用域链之前要先知道几个概念

  • 变量和函数的声明
  • 函数的生命周期
  • Activetion Object(AO), Variable Object(VO)

    变量和函数的声明

    在javascript引擎解析javascript代码的时候,会把变量和函数的声明进行预解析,然后再执行其他代码。

变量声明:只有一种声明方式,就是使用var关键字进行声明。直接赋值不是一种声明方式,仅仅实在全局对象上创建了新的属性(而不是变量)。它们有以下区别:

(1)因为只是一种赋值,所以不会声明提前

1
2
3
4
alert(a) // undefined
alert(b) // error 'b' is not defined
var a = 'dogjun'
b = 'dogjun1'

(2)直接赋值形式是在执行阶段创建

1
2
3
4
5
alert(a) // undefined
b = 10
alert(10) // 10, 执行阶段创建
var a = 'dogjun'
alert(a) // 'dogjun', 执行阶段修改

(3)变量不能删除(delete),属性可以删除

1
2
3
4
5
6
7
8
a = 10
alert(window.a) // 10
alert(delete a) // true
alert(window.a) // undefined
var b = 20
alert(window.b) // 20
alert(delete b) // false
alert(window.b) // 20, 变量不能被删除

这里有个意外情况,在eval的上下文中,变量可以删除。

1
2
3
4
eval('var a = 10')
alert(window.a) // 10
alert(delete a) // true
alert(window.a) // undefined

有些debug工具也是可以删除的,因为他们用了eval来执行代码。

函数声明:有三种声明方式

(1)function name () {}直接创建

1
2
3
4
function add (a, b) {
return a + b
}
add(4, 5)

(2) new Function 构造函数创建

1
2
var add = new Function('a', 'b', 'return a+b')
add(4, 5)

(3)给变量赋值匿名函数方法创建

1
2
3
4
var add = function (a, b) {
return a + b
}
add(4, 5)

后面两种方法,在声明前访问,返回的都是一个undefined的变量。
注意:如果变量名和函数名声明时相同,函数优先声明。

1
2
3
4
5
6
alert(x); // function
var x = 10;
alert(x); // 10
x = 20;
function x() {};
alert(x); // 20

函数的生命周期

函数的生命周期分为创建和执行两个阶段。

在函数创建阶段,js解析引擎进行预解析,会将函数声明提前,同时将函数放到全局作用域中或当前函数的上一级函数的局部作用域中。

在函数执行阶段,js引擎会将当前函数的局部作用域和内部函数进行提前声明,然后再执行业务代码,当函数执行完毕后退出时,释放该函数的执行上下文,并注销该函数的局部变量。

什么是AO、VO

VO(Variable Object 变量对象) 对应的就是函数创建阶段,js解析引擎进行预解析的时候,所有的变量和函数的声明,统称为VO。该变量与执行上下文有关,知道自己的数据存储在哪里,并且知道如何访问。VO是一个与执行上下文有关的特殊对象,它存储着在上下文中声明的以下内容:

  • 变量
  • 函数声明
  • 函数的形参
    1
    2
    3
    4
    5
    6
    7
    8
    function add (a, b) {
    var sum = a + b
    function say () {
    alert(sum)
    }
    return sum
    }
    // add, sum, a, b组合的对象就是VO,不过该对象上的值基本都是undefined

AO(Activetion Object 活动对象) 对应的就是函数执行阶段,当函数变调用执行的时候,会创建一个执行上下文。该执行上下文包含了函数所需的所有变量,这些变量共同组成了一个新的对象就是AO。该对象包含了:

  • 函数的所有局部变量
  • 函数的所有命名参数
  • 函数的参数集合
  • 函数的this指向
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function add (a, b) {
    var sum = a + b
    function say () {
    alert(sum)
    }
    return sum
    }
    add(4, 5)
    /*
    AO = {
    this: window,
    arguments: [4, 5],
    a: 4,
    b: 5,
    say: undefined,
    sum: undefined
    }
    */

javascript的作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链来保证对执行环境有权访问的变量和函数的有序访问。作用域第一个对象始终是当前执行代码所在环境的变量对象VO。

1
2
3
4
function add (a, b) {
var sum = a + b
return sum
}

假设函数在全局作用域中创建的,在函数add创建的时候,它的作用域链填入全局对象,全局对象中有所有全局变量,此时的全局变量就是VO。作用域链就是:

1
2
3
4
5
6
// 此时作用域链只有一级,就是Global Object
scope(add) -> Global Object (VO)
VO = {
this: window,
add:
}

如果是函数执行阶段,那么将其AO作为作用域链的第一个对象,第二个对象就是上级函数的执行上下文VO,下一个对象以此类推。

1
add(4, 5)

调用add后的作用域链是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 此时作用域有两级,第一级为AO,然后Global Object(VO)
scope(add) -> AO -> VO
AO = {
this: window,
arguments: [4, 5],
a: 4,
b: 5,
sum: undefined
}
VO = {
this: window,
add:
}

在函数运行过程中标识符的解析是沿着作用域链一级一级搜索的过程,从第一个对象开始,逐级向后回溯,直到找到同名标识符为止,找到后不再继续遍历,找不到就报错。

1
2
3
4
5
6
7
8
9
10
var x = 10
function foo() {
var y = 20
function bar() {
var z = 30
console.log(x + y + z)
}
bar()
}
foo()

上面代码的输出结果为”60″,函数bar可以直接访问”z”,然后通过作用域链访问上层的”x”和”y”。此时的作用域链为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 此时作用域链有三级:第一级为bar AO,第二级为foo AO,第三级为Global Object(VO)
scope -> bar.AO -> foo.AO -> Global Object
bar.AO = {
z : 30,
__parent__ : foo.AO
}
foo.AO = {
y : 20,
bar : ,
__parent__ :
}
Global Object = {
x : 10,
foo : ,
__parent__ : null
}