JavaScript作用域链分析
1.前言
作用域概念是理解 JavaScript 的关键,不仅从性能的角度,而且从功能的角度。作用域对 JavaScript 有许多影响,从确定哪些变量可以被函数访问,到确定 this 的值。
2.变量
在 JavaScript 里,对象/函数都是变量。window对象是全局对象,全局变量是全局对象的属性。每个变量内部又有自己的变量,就是局部变量。
1 | var age = 22; // 全局变量 |
每个变量都有自己的作用域,上面,num
变量它的作用域为函数t,在t函数内部可以获取他,在函数外部则不行。基本上大多数的编程语言都遵循这种规律,然而JavaScript却有一个独特的地方,就是在变量内部可以访问到外部的变量。
以函数为例,在运行函数时,函数收到它运行时的环境影响,不同的外部环境可能产生的结果会不一样。
1 | var age = 22; |
来分析一下上面的例子,首先定义了全局变量age、num和函数t。在函数t中定义的局部变量num和str。
执行函数t时,局部变量里面没有age变量,于是就去函数t的外部去寻找,找到全局变量age,返回22;
局部变量里面有str和num变量,于是直接返回hello和88,不继续向外部寻找。
3.词法分析
一个栗子:
1 | var str = 'global'; |
产生上述结果的原因是JavaScript并不是一句一句顺序执行的,先进行词法分析(预编译)。当函数运行时,它的作用域链被初始化,创建一个激活对象(Activation Object),以下简称AO。此激活对象作为函数执行期的一个可变对象,包含访问所有局部变量,命名参数,参数集合,和 this的接口。
js的执行顺序
词法分析阶段
- 分析参数
- 分析变量声明
- 分析函数声明
说明:
1.先把接收到的参数放到AO上
2.分析变量声明
a: var xx = yy;
做法:先分析var xx,声明一个xx属性在AO上,xx变量此时,没有赋值,值是undefined,但如果已经有xx,则不对xx操作。
b:function t() {}
做法:直接声明t属性,且内容是函数体
执行语句阶段
举个栗子:
1 | function t(age) { |
分析一下:
- 执行t函数,产生t的AO ==> t:AO:{}
- 词法分析形参得到 ==> t:AO:{age:undefined}
- 实参赋值 age属性 ==> t:AO:{age:99}
- 修改age的值 ==> t:AO:{age:12}
继续举个大栗子:
1 | function a(b) { |
分析一下:
- 执行a函数,产生a的AO ==> a:AO:{}
- 词法分析形参得到 ==> a:AO:{b:undefined}
- 实参赋值 b属性 ==> a:AO:{b:1}
- 函数声明 b属性 ==> a:AO:{b:function}
- 执行b函数,产生b的AO ==> b:AO:{}
- 由于在b的AO中没有b属性,向b函数外部寻找到a的AO,找到b属性,为function。AO.b —>{}—>a:AO
有上面的例子可以看出,函数的作用域是和外部环境有着密切的关联,是一级一级通过激活对象向外部关联,一直到window对象为止,这样就形成了函数的作用域链。
4.闭包
闭包是 JavaScript 最强大的一个方面,它允许函数访问局部范围之外的数据。理解起来比较抽象,可以说闭包就是能够读取其他函数内部变量的函数,由于在JavaScript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。
闭包最明显的特点,一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
同样的举一个栗子:
1 | function f1(){ |
上面f2就是一个闭包,运行了2次。通过两次的结果可以发现,f1函数内部的变量n并没有随着第一次运行结束而销毁,而是一直存在于内存中。
分析上述代码,当f1函数被执行,生成一个激活对象,里面包含了函数f2和变量n,而f2被返回并将它的引用给了全局变量result。由于全局变量result一直存在,所以f2也一直存在,f1和它内部的激活对象也一直存在,没有被销毁。从这里可以发现:
当函数f1的内部函数f2被函数f1外的一个变量result引用的时候,就创建了一个闭包。
通常,一个函数的激活对象在函数运行完会销毁。当涉及闭包时,激活对象就无法销毁了,因为引用仍然存在着。这意味着脚本中的闭包与非闭包函数相比,需要更多内存开销。所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。
5.总结
JavaScript并不是一句一句顺序执行的,会先进行词法分析,产出激活对象。在js执行时,变量的访问会沿着激活对象向外部延伸从而产生一条作用域链。当作用域链被销毁时,激活对象也一同销毁。