变量对象
js 解析器是如何找到我们定义的函数和变量的?
实际是通过VO(Varible Object)来存储执行上下文所需要存储的比如变量、函数声明、函数参数。进一步我们还需要区分全局变量对象和函数变量对象。
我们定义一个全局变量就会有一个全局变量对象,里面有刚定义的全局变量
定义函数,除了有全局变量对象外,还有函数变量对象。
作用域
简单说分为全局作用域、和局部作用域。全局作用域对应全局变量,局部作用域对应局部变量。通常理解的局部作用域是定义在函数内部的作用域。
作用域链
当我们定义一个全局变量的时候,它就在全局环境里,作用域链只有一条。当我们定义一个函数,首先在定义的时候,或者说在js流执行的时候,会将函数和变量申明提前,而且函数申明比变量更提前。在申明函数的时候,会生成一个scope属性。此时,[[scope]]里面只包含了全局对象【Global Object】。而如果, 我们在A的内部定义一个B函数,那B函数同样会创建一个[[scope]]属性,B的[[scope]]属性包含了两个对象,一个是A的活动对象【Activation Object】【对于函数来说】一个是全局对象,A的活动对象上面,全局对象在下面。以此类摧,每个函数的都在定义的时候创建自己的[[scope]],里面保存着一个类似于栈的格式的数据。
// 外部函数
function A(){
// 内部函数
function B(){
}
}
执行环境
了解函数执行环境前先看下三个概念。
- 全局执行环境
我们通常可以在代码第一行使用类似String、Math等这样的全局函数,为何能直接使用到?
是因为我们在初始化代码以前,js引擎会把全局的一些东西,我理解成[[global]]会初始化到VO里面。
伪代码:
[[global]] = {
Math : ...,
String : ...,
window : global, // 执行全局对象本身
}
String(10) // [[global]].String(10)
window.a = 10 // [[global]].window.a = 10
this.b = 10 // [[global]].b = 10
- 激活对象(通常在函数中才有)
Active Object 缩写AO,该对象有函数的arguments类数组对象和this对象。
**慕课网这一章节说,函数里面 AO === VO
慕课网--Js深入浅出--闭包章节
- 变量初始化阶段
VO或者AO按照如下顺序初始化:
- 函数参数(如未传入,初始化该参数值为undefined)
- 函数声明(如发生命名冲突会覆盖)
- 变量声明(初始化变量值为undefined,若发生命名冲突会忽略)
通过一个实例来说明变量初始化阶段
function test (a, b) {
var c = 10
function d(){}
var e = function _e(){}
(function(){}) // 括号括起来的匿名函数,但并没执行,此时函数申明不会提前
// 这里多说下,括起来的不管是匿名函数,还是正常申明的函数,不仅不会申明提前,还会
// 形成一个闭包,只有通过自执行才能调用。在全局作用域或在申请的局部作用域调用都
// 是undefined.这里一个简单粗暴的解释是为何没有提前,如果提前就留下一个括号,岂不
// 是很奇怪。哈哈。
b = 20
}
test(10)
AO(test) = {
a: 10, // a 已传入,所以有值
b: undefined, // b 未传入,undefined,并且局部变量b发生命名冲突,但被忽略
c: undefined, // 局部变量
d: , // 函数申明
e: undefined // 命名函数_e,赋值给了局部变量e,undefined
}
// 分析发现,局部变量c和e两个局部变量都是undefined,因为开头就说了,变量申明被前置,所以在初始化的时候就是undefined,比如我们在var e = function _e() {} 前面调用e
//() 肯定会报错语法错误,e不是一个函数。
每个函数运行时都会产生一个执行环境,而且同一个函数运行多次时,会多次产生不同的执行环境。js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。 全局执行环境是最外围的执行环境,它所关联的对象就是我们熟知的window对象。js的执行顺序是根据函数的调用来决定的,当一个函数被调用时,该函数环境的变量对象就被压入一个环境栈中。而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。
var scope = "global";
function fn1(){
return scope;
}
function fn2(){
return scope;
}
fn1();
fn2();
当函数被执行的时候,就是进入这个函数的执行环境,首先会创建一个它自己的活动对象【Activation Object】(这个对象中包含了this、参数(arguments)、局部变量(包括命名的参数)的定义,当然全局对象是没有arguments的)和一个变量对象的作用域链[[scope chain]],然后,把这个执行环境的[scope]按顺序复制到[[scope chain]]里(也有博客说把作用域[[scope chain]]链赋值给[[scope]]),最后把这个活动对象推入到[[scope chain]]的顶部。这样[[scope chain]]就是一个有序的栈,这样保了对执行环境有权访问的所有变量和对象的有序访问。
分析下:
当执行fn1的时候,VO中fn1会指向fn1的执行环境,因为函数申明前置,所以在VO中已经存在function。
可以看到fn1活动对象里并没有scope变量,于是沿着作用域链(scope chain)向后寻找,结果在全局变量对象里找到了scope,所以就返回全局变量对象里的scope值。
闭包
引入闭包代码
function outer(){
var scope = "outer";
return function inner(){
return scope;
}
}
调用outer()
这里有点不解的是?如果outer()直接调用,然后再调用inner即outer()()不会造成闭包,但把outer赋值给一个全局变量,上面闭包代码的写法,就会成为一个闭包。
var fn = outer();
fn();
分析下:
执行outer函数,在VO对象中outer会指向它的执行环境,当因为变量申明前置,fn在初始化变量的时候是undefined。所以在outer函数执行中的时候,他依然是undefined.
如图,outer执行完后,执行环境不再被VO中的outer所指向,即执行环境已被销毁。
当第一次执行fn到底发生了什么?执行fn即执行inner函数,此时outer的执行环境已被销毁(而且只能有并且只有一个执行环境),而outer()执行后又赋值给了fn,所以可以这么理解,inner执行的时候执行环境也赋值给了fn。由于fn这个全局变量一直存在,除非你手动置为null或关闭浏览器,所以可以认为inner这个函数的执行环境就一直存在,那么inner函数执行环境的相关变量就一直存在。
通过上面加粗文字的理解,趁热打铁,再来看一个函数:
function outer() {
var a = 1
return function inner() {
a ++
}
}
var f = outer()
console.log(f()) // 2
console.log(f()) // 3
为何f()第二次执行的时候,会是3。结合上面加粗字体的理解,f()在执行完后,outer的AO并没有被释放,当第二次执行f()的时候,由于inner函数AO并没有变量a,所以沿着[[scope]]chain 中查找,结果在outer的AO中找到了变量a,但此时变量a已经是2了,所以再a ++ 后就是3了。除非我们关闭浏览器,或者将f = null,再次执行f(),就会回到初始化的状态。
理解了闭包再来看下面这个代码就很好理解了。
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
//定义一个带参函数
result[i] = function(num){
// num 形参
// 实际num这个形参拷贝了实参i这个变量的一个副本到arguments里。
// i 的变化就不会影响到这个副本的变化。
console.log('arguments', arguments)
// innerarg 的执行环境里面引用了result[i]这个数组函数的活动对象
// 当执行innerarg 的时候,作用域链会去找num,在result[i]这个数组活动对象的
// arguments 找到了
return function innerarg(){
console.log('num', num)
return num;
}
}(i);//预先执行函数写法
//把i当成参数传进去 实参
}
console.log('result', result)
return result;
}
var fn = outer()
fn[0]()
fn[1]()
起初我觉得上面那个代码实在是太复杂,为何要在result[i]这个函数数组里再返回一个函数。起初我是这么写的。结果发现我错了,但又对匿名函数自执行,又重新正确的认识了下。
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){//注:i是outer()的局部变量
result[i] = function(num){
console.log(num)
return i;
}(i)
// result[i]表面上是被一个函数所赋值,但这是一个自执行函数,自执行函数又
// 返回了它的实参。所以如果我们调用fn[0]()就会提示fn[0]不是一个function.
// 你修改成result[i] = (function(num){...}(i))也是一个自执行,只不过
// 多了一层闭包。
}
console.log('result', result)
return result;//返回一个函数对象数组
//这个时候会初始化result.length个关于内部函数的作用域链
}
var fn = outer();
fn[0]
fn[1]
参考文献
csdn博文
一篇cn博客--2012年对scope解释比较透彻