1.前言

作用域概念是理解 JavaScript 的关键,不仅从性能的角度,而且从功能的角度。作用域对 JavaScript 有许多影响,从确定哪些变量可以被函数访问,到确定 this 的值。

2.变量

在 JavaScript 里,对象/函数都是变量。window对象是全局对象,全局变量是全局对象的属性。每个变量内部又有自己的变量,就是局部变量。

1
2
3
4
var age = 22; // 全局变量
function t(){
var num = 88; // 局部变量
}

每个变量都有自己的作用域,上面,num变量它的作用域为函数t,在t函数内部可以获取他,在函数外部则不行。基本上大多数的编程语言都遵循这种规律,然而JavaScript却有一个独特的地方,就是在变量内部可以访问到外部的变量。

以函数为例,在运行函数时,函数收到它运行时的环境影响,不同的外部环境可能产生的结果会不一样。

1
2
3
4
5
6
7
8
9
10
var age = 22;
var num = 99;
function t () {
var num = 88;
var str= 'hello';
alert(age); // 22
alert(str); // hello
alert(num); // 88
}
t();

来分析一下上面的例子,首先定义了全局变量age、num和函数t。在函数t中定义的局部变量num和str。
执行函数t时,局部变量里面没有age变量,于是就去函数t的外部去寻找,找到全局变量age,返回22;
局部变量里面有str和num变量,于是直接返回hello和88,不继续向外部寻找。

作用域分析

3.词法分析

一个栗子:

1
2
3
4
5
6
7
8
var str = 'global';
function t() {
alert(str); // undefined
var str = 'local';
alert(str); // local

}
t();

产生上述结果的原因是JavaScript并不是一句一句顺序执行的,先进行词法分析(预编译)。当函数运行时,它的作用域链被初始化,创建一个激活对象(Activation Object),以下简称AO。此激活对象作为函数执行期的一个可变对象,包含访问所有局部变量,命名参数,参数集合,和 this的接口。

js的执行顺序

词法分析阶段

  1. 分析参数
  2. 分析变量声明
  3. 分析函数声明

说明:
 1.先把接收到的参数放到AO上
 2.分析变量声明
  a: var xx = yy;
  做法:先分析var xx,声明一个xx属性在AO上,xx变量此时,没有赋值,值是undefined,但如果已经有xx,则不对xx操作。
  b:function t() {}
  做法:直接声明t属性,且内容是函数体

执行语句阶段

举个栗子:

1
2
3
4
5
6
function t(age) {
alert(age); // 99
var age = 12;
alert(age); // 12
}
t(99);

分析一下:

  1. 执行t函数,产生t的AO ==> t:AO:{}
  2. 词法分析形参得到 ==> t:AO:{age:undefined}
  3. 实参赋值 age属性 ==> t:AO:{age:99}
  4. 修改age的值 ==> t:AO:{age:12}

继续举个大栗子:

1
2
3
4
5
6
7
8
function a(b) {
alert(b); // function b(){}
function b(){
alert (b); // function b(){}
}
b();
}
a(1);

分析一下:

  1. 执行a函数,产生a的AO ==> a:AO:{}
  2. 词法分析形参得到 ==> a:AO:{b:undefined}
  3. 实参赋值 b属性 ==> a:AO:{b:1}
  4. 函数声明 b属性 ==> a:AO:{b:function}
  5. 执行b函数,产生b的AO ==> b:AO:{}
  6. 由于在b的AO中没有b属性,向b函数外部寻找到a的AO,找到b属性,为function。AO.b —>{}—>a:AO

有上面的例子可以看出,函数的作用域是和外部环境有着密切的关联,是一级一级通过激活对象向外部关联,一直到window对象为止,这样就形成了函数的作用域链。

4.闭包

闭包是 JavaScript 最强大的一个方面,它允许函数访问局部范围之外的数据。理解起来比较抽象,可以说闭包就是能够读取其他函数内部变量的函数,由于在JavaScript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。

闭包最明显的特点,一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

同样的举一个栗子:

1
2
3
4
5
6
7
8
9
10
11
function f1(){
var n=999;
function f2(){
n+=1;
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
result(); // 1000

上面f2就是一个闭包,运行了2次。通过两次的结果可以发现,f1函数内部的变量n并没有随着第一次运行结束而销毁,而是一直存在于内存中。

分析上述代码,当f1函数被执行,生成一个激活对象,里面包含了函数f2和变量n,而f2被返回并将它的引用给了全局变量result。由于全局变量result一直存在,所以f2也一直存在,f1和它内部的激活对象也一直存在,没有被销毁。从这里可以发现:

当函数f1的内部函数f2被函数f1外的一个变量result引用的时候,就创建了一个闭包。

通常,一个函数的激活对象在函数运行完会销毁。当涉及闭包时,激活对象就无法销毁了,因为引用仍然存在着。这意味着脚本中的闭包与非闭包函数相比,需要更多内存开销。所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。

5.总结

JavaScript并不是一句一句顺序执行的,会先进行词法分析,产出激活对象。在js执行时,变量的访问会沿着激活对象向外部延伸从而产生一条作用域链。当作用域链被销毁时,激活对象也一同销毁。