相信学前端的兄弟,都看过JS高程这本书。这本书中讲到了作用域、作用域链、变量对象和闭包的种种含义。刚开始看的时候觉得说的很有道理,直到出现了如下的情况:
代码1:
function test(){ // 代码1
var b = 1;
return function (){ // 这里对函数的命名可以是除了b以外的名称,我们假设是一个匿名函数
console.log(b); // b = 1;
}
}
test()();
看到这里,大家都会认为这不就是一个闭包嘛。这有什么好说的呢,那么下面这个情况呢?
代码2:
function test(){ // 代码2
var b = 1;
return function b(){
console.log(b); // b = Function: b
}
}
test()();
大家也许会认为这个我也能解释,这个跟:
代码3:
function b(){ // 代码3
console.log(b); // b = Function: b
}
b();
有什么区别嘛?
没错,这里有区别,你可能会说,代码3中的b
是在全局环境下声明的,而上面代码2中的b
它是在test
当中声明的。所以代码3在执行b
的时候,向它的父作用域去查找,也就是全局环境下,查找到b
是一个函数。
这里似乎说通了,但是代码2当中的b
真的是在test
当中声明了嘛?按照书中作用域链的说法,我们应该能在test
当中打印出b
,并且它是一个函数。说到这里,有的小伙伴一定会在var b = 1;
之前打印一下b
,但实际的结果是undefined
。也就是说test
函数中并没有b=Function: b
。
这里就要说一下return
了,函数的创建阶段是没有管这个return
的,当在执行阶段中才真正去解释这个return
。通过Chrome
打断点我们看到:
当代码2中的test
函数执行到return
这里的时候,会在Local
中声明一个Return value
的属性,它的属性值就是函数b
。那么,它是在这里声明了函数b
嘛?我们接着往下看,你可能还会去用书上的那套理论解释代码2中最后为什么b
打印出来是一个函数,那么接下来,我们看一个恐怖的东西:
这里的Local
为什么有个b
的属性?而且属性值是Function b
,这是为什么?调用栈里面也是b
函数进栈,我并没有在b
当中声明b
这个属性,它是从哪里冒出来的呢?是不是感觉书上说的那一套理论已经无法解释这里的现象了。我们都知道这个b
函数是在test
函数中声明的,但是在test
里面我们也打印不出来,在b
里面却出现了,无限怀疑人生。。。。。。。
那么接下来就是我的理解了~~~
作用域:是在代码编译阶段制定的规则
作用域链:是在代码执行阶段对作用域规则的具体实现。
这两句话乍一看没什么问题,细思极恐啊!!!
现在我们也不要管什么作用域链,闭包,还有什么执行上下文生命周期这一类的说法了。我们换个角度,站在V8引擎上去看这段代码。
我们知道V8引擎是由C++实现的,那么V8在编译这段代码的时候,规定了,b
应该是哪个一个值,如果让编写V8的人自己解释为什么b
就是这个值而不是其他的值,他肯定会说,我就是这么规定的,所以为了让大家更好的理解应该是哪个一个值,就引入了变量提升,作用域链之类的说法。因此,当遇到这种问题时,我们就找到了规律,这里就是闭包,那里就是变量提升等等。下面我们再来看看代码1和代码2:
function test() 1. 第一步遇到了函数声明test,编译器会在这里打上标记这里是函数声明
{ 2. 第二步遇到了大括号,表示函数体,编译器打上标记下面是函数体
var b = 1; 3. 第三步又遇到了变量声明var b,好,编译器打上标记这里是var声明
return function b()4. 第四步遇到了return,表示test函数要返回了,编译器就要查看这里返回的啥,这里
又遇到函数声明了,所以打上标记b函数声明了,
{ 5. 第五步遇到了大括号,表示函数体
console.log(b); 6. 第六步遇到了打印b,编译器就会打上标记这里的b,就是前面函数声明的b
} 7. 第七步遇到了大括号,没有return,b函数体结束了
} 8. 第八步遇到了大括号,test函数体结束了
test()(); 9. 第九步遇到了小括号,表示有函数在这里要调用。
以上就是编译阶段所干的事情,下面就是执行代码了。
(这里的test()()
是为了简写,也可以拆开来,即执行完test
函数,再执行test
返回的函数(在这里返回的是一个函数),但是不方便看这一类的问题)。
- 首先,
test
会在全局环境中声明test = Function:test
。 - 然后
test
函数执行,创建上下文执行环境,开始执行创建阶段的操作,变量b
会被声明在这个地方。 - 再然后就是
return
,为return value
赋值对b
函数的引用。 - 接下来,函数
b
声明了:b = Function:b
。 - 此时
test
函数全部执行完成,退出栈。 - 然后,
b
函数入栈。 - 请注意代码是从
return value
那一行执行过来的,因此b
进栈的时候,发现已经有一个b = Function:b
,所以在Chrome
的Local
那里会出现b = Function:b
。 b
的函数体中只有一个打印语句,所以执行该代码,并且在编译阶段已经打上标记了,这里的b
就是在test
函数return
的时候声明的。
至此,我对这个现象的理解已经解释完了。相信这里面也存在较多说明上的漏洞,毕竟我没看过V8的引擎源码也不知道他的工作流程,仅仅是对这个现象的理解。欢迎大家评论交流,共同学习进步。