执行期上下文
定义:当函数执行时,会创建一个称为执行期上下文(AO对象)的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,它所产生的执行上下文被销毁。(⭐重点,后面要考的!!!)
预编译
预编译,就是编译前的过程。可以理解为:预编译就是做到函数声明整体提升,变量声明提升
详细的预编译知识点可以看JavaScript之预编译学习(内含多个面试题),这里只提及关键部分。
函数预编译
关于函数预编译我们只要学会怎么确定执行期上下文——AO(Activation Object)对象就行了。可能有的小伙伴还是对前面执行期上下文的定义有疑问:执行上下文在函数执行中起啥作用,多个执行上下文具体是指啥,独一无二又是怎么体现的呢?这个部分会解开你的疑惑。
如果我们要确定下面这个函数的执行期上下文和打印值,应该怎么做呢?
function test(a){
console.log(a);
var a=123;
console.log(a);
function a(){}
console.log(a);
var b=function() {}
console.log(b);
function d() {}
}
test(1);
- 创建AO对象
AO:{}
- 将形参和变量声明找出来作为AO的属性名,并且统一赋值undefined
AO: {
a: undefined,
b: undefined
}
- 将实参值赋值给形参
AO: {
a: 1,
b: undefined
}
- 从函数里找到函数声明,函数声明中的函数名作为AO的属性名,函数声明中的函数体作为这个属性名的值
AO: {
a: function a(){}, //因为函数声明a和变量声明a重名,对变量声明a进行覆盖
b: undefined, //如果以为此时b:function() {} ,就要理解函数声明!=变量声明
d: function d() {}
}
至此,AO对象已经确立好了。那么编译里怎么用到这个AO对象呢?你可以试着写出那些被打印出来的值,最终答案是这个:
或许和你的预期不一样?那是因为没有正确使用上AO对象。让我们重新分析一下:
function test(a){
console.log(a); //我们从AO里找有没有a,有的,值是function a(){},那么打印结果就是function a(){}
var a=123; //分为两部分:变量声明a,将a赋值为123。变量声明a我们在确定AO对象就已经看过了,忽略它。a被赋值123,那我们就修改AO中a的值为123
console.log(a); //我们从AO里找a的值,发现是123,那么就打印123
function a(){} //这条我们在确定AO对象就已经看过了,忽略它往下走
console.log(a); //AO里a的值仍为123,打印123
var b=function() {} //分为两部分:变量声明b,将b赋值为function() {}。变量声明b我们在确定AO对象就已经看过了,忽略它。b被赋值为function() {},因此AO中b的值不再是undefined,改为function() {}
console.log(b); //AO里b的值是function() {},那就输出function() {}
function d() {} //这条我们在确定AO对象就已经看过了,忽略它。分析到此结束。
}
test(1); //调用函数test,那我们开始分析这个函数叭⬆
一波分析下来,相信你对执行上下文有了更深的理解。另外,在确立AO对象与console.log()输出时,有两个小技巧:
- 函数声明与变量声明同名时,最终AO对象确定时只用管函数声明(因为函数声明在第四步,会覆盖之前同名的变量声明)
- console.log(xx)的前面一条代码如果是对变量xx赋值,那么输出的就是这个值
全局预编译
与函数预编译类似,关于全局预编译,只要知道怎么确定全局对象——GO(Global Object)对象就行了。(注:window对象和GO对象是同一个对象的不同名字,也就是说:window === GO)
GO对象的确立方法也和AO对象确立方法类似,只是修改了步骤二——形参去掉,少了步骤三(因为找的是全局变量),康康下面这个例子:
console.log(a);
var a=1;
console.log(a);
function test(a){
console.log(a);
var a=123;
console.log(a);
function a(){}
console.log(a);
console.log(b);
var b=function() {}
console.log(b);
}
test(2);
我们开始确定GO对象叭:
- 创建GO对象
GO:{}
- 将变量声明找出来作为GO的属性名,并且统一赋值undefined
GO:{
a:undefined,
c:undefined
}
- 从函数里找到函数声明,函数声明中的函数名作为GO的属性名,函数声明中的函数体作为这个属性名的值
GO:{
a:undefined,
c:undefined,
test:function test() {...}
}
为了解题,这里把AO对象也写出来:
AO:{
a: function a() {},
b: undefined
}
GO对象在编译时起到什么作用呢?看看解题过程叭
console.log(a); //GO里a的值是undefined,那么打印结果就为undefined
var a=1; //两部分:变量声明a,将a赋值为1。变量声明a在确定GO对象时看过了,忽略;a被赋值1,GO里a的值也从undefined变成1
var c=2; //忽略变量声明,GO里c的值从undefined变成2
console.log(a); //GO里a的值现在是1,打印结果为1
function test(a){ //注意了!这里进入了函数体,我们在函数体中首先看的都是AO对象
console.log(a); //AO对象里a的值是function a() {},输出它
var a=123; //两部分,变量声明忽略,GO里a的值从function a() {}变成123
console.log(a); //现在AO对象里a的值是123,输出它
function a(){} //确定AO对象已经看过了,忽略
console.log(a); //AO对象里a的值仍是123,输出它
console.log(b); //AO对象里b的值是undefined,输出它
var b=function() {} //变量声明忽略,GO里b的值从undefined变成function() {}
console.log(b); //现在AO对象里b的值是function() {},输出它
console.log(c); //注意了!AO对象中没有c怎么办,那就从GO对象中找,输出2。分析结束。
}
test(2); //进函数体里看看叭
总的来说,输出变量语句在函数体外面就从GO对象里找此变量,在函数体里优先从AO对象中找,找不到就去GO对象里找,GO对象也没有?那就会报错了。
作用域链
为什么找变量在函数体外面只能找GO对象,而在函数体里却优先找AO对象,找不到又可以去找GO对象呢?那这就有关变量作用域链了。
- 什么是作用域([[scope]])——存储了运行期上下文的集合的容器
- 什么是作用域链——作用域中所存储的执行期上下文对象的集合,这个集合呈链式链接,这种链式链接就是作用域链
函数定义时,会保留外层的作用域的变量,函数执行时,会创建执行期上下文,并且将执行期上下文放在作用域链的最顶端。
var c=3;
function a(){
var a=1
function b(){
var b=2;
}
}
var fun=a();
拿这个代码来说:函数a定义时,a的作用域链的0号位置会存放外层的GO对象,函数a执行时,会创建a的执行期上下文(以下称为aAO),并且放在最顶端,那么GO对象就往后移;函数b定义时,b的作用域链依次存放外层的aAO和GO,函数b执行时,创建b的执行期上下文(以下称为bAO),并且放在最顶端,其它对象就往后移。在函数a执行后的情况下作用域链如下图所示(红线需要被删去):
- 为什么a的作用域链和aAO断开?因为函数a执行完了,a的执行上下文(aAO)被销毁;
- 为什么b的作用域链和aAO断开(删去红线)?因为a的作用域链和aAO断开了,b保留a的作用域,自然也就和aAO断开了;
这幅图也可以解释为什么函数内部在AO中找不到所需变量时会去GO中找——查找变量从该函数作用域顶端依次向下查找,直到找到为止,而作用域链中GO在AO的后面,因此AO中找不到所需变量时自然会去GO中找。
来看个例子:
function a() {
function b() {
function c() {
}
c();
}
b();
}
a();
我们将函数定义为defined,函数执行定义为doing,最后的变量作用域链是这个样子的:
a defined a.[[scope]]-> 0:GO
a doing a.[[scope]]-> 0:aAO
1:GO
b defined b.[[scope]]-> 0:aAO
1:GO
b doing b.[[scope]]-> 0:bAO
1.aAO
2.GO
c defined c.[[scope]]-> 0:bAO
1.aAO
2.GO
c doing c.[[scope]]-> 0:cAO
1.bAO
2.aAO
3.GO
怎么样,是不是感觉其实不难,来写道题巩固巩固知识点叭
function a() {
var num=100;
function b() {
num++;
console.log(num);
}
return b;
}
var demo=a();
demo();
demo();
答案是这个:
解析:即使a函数执行完毕后执行上下文(aAO对象)被销毁,但由于return b,b函数的scope中仍含有aAO,aAO中的num在第一次调用demo()时就已经增加1变成101,因此再次调用会在101的基础上加1变成102。
闭包
好耶!终于来到闭包了!
什么是闭包呢?当内部函数被保存到外部时,将会生成闭包。闭包会导致原有作用域链不释放,造成内存泄漏。
这位大神的评论更容易理解:
其实前面的题目在return b时就生成了闭包——函数a执行到最后一条语句:return b,此时b函数被返回到函数a的外部,并且继承了上级(函数a)作用域变量,接着函数a执行完最后一条语句,a的执行上下文被销毁。a的作用域链和aAO连接的那条线断开了,但由于b函数返回到函数a外部先于a的执行上下文被销毁,所以b的作用域链和aAO连接的那条线仍然存在。造成上级(函数a)作用域变量依然在占用内存,使内存减少,导致内存泄漏。
来道题检验一下你是否真正理解了上面这段话叭:
function a(){
function b(){
var bbb=234;
console.log(aaa);
}
var aaa=123;
return b;
}
var glob=100;
var demo=a();
demo();
最后输出的是123
和上面的那段话一样,执行var demo=a();
时a函数开始执行,执行到var aaa=123;
时aAO中的aaa被赋值为123,return b;
返回b函数后函数a执行完了最后一条语句,a的aAO被销毁,b的作用域链和aAO连接的那条线仍然存在,所以它的作用域链中还有aAO,输出aaa时,b先去找作用域链最顶层的bAO有没有这个变量,没有,那就继续去找aAO,找到了,输出123。
再来看道关于闭包的题叭:
function test() {
var arr=[];
for (var i=0;i<10;i++) {
arr[i]=function() {
console.log(i);
}
}
return arr;
}
var myArr=test();
for(var j=0;j<10;j++){
myArr[j]();
}
答案和你预测的一样吗?
什么,你不会以为会从0一直输出到9吧?
如果和预测的结果有出入的话,那我们必须要解开两个问题:1.为什么输出结果是10?2.为什么输出了10个10?
- 先解决第一个问题:for循环的条件是i<10,也就是说当i==10时循环终止,所以最终i==10
- 再解决第二个问题:arr[i]等于一个函数体,直到调用它才会执行里面的语句,才会去找要输出的i是多少,在调用前i已经等于10了,所以最后也就只会输出10
所以这个过程就是:执行var myArr=test();
语句时a函数开始执行,但是a函数执行时并不会执行function() {console.log(i);}
,因为它并没有被调用,a函数只会执行外层的for循环,执行完循环后i等于10,接着return arr;
,执行完最后一条语句后a执行完毕,a与aAO之间的"线"断开,b与aAO之间的"线"继续存在。之后执行myArr[j]();
,就是执行function() {console.log(i);}
,i等于10,就输出10,执行了十次,就输出了10次10。
补充知识点:立即执行函数
那要是我们就是想在有闭包的情况下最终打印出0到9怎么办?用立即执行函数就行了
啊立即执行函数又是个什么东西?很简单,就是字面意思——用function定义函数之后,会立即调用该函数,并且执行完立即被释放。
这玩意要怎么用呢?
- 比较建议的格式是:(function(形参){函数体} (实参))
例:
var num=(function(a,b,c) {
var d=a+b+c*2-2;
return d;
} (1,2,3));
- 还有一种写法是:(function(){})()
要注意的是:(function(){})() 第二个()是执行符号,只有执行符号前面是表达式才能被执行。
至于为什么是这种写法,还有没有其它写法,这里没写,需要去看更多文章学习。
接下来我们就用立即执行函数做这道题叭:
function test() {
var arr=[];
for (var i=0;i<10;i++) {
//立即执行函数部分
(function(j){
arr[i]=function(){
console.log(j);
}
}(i))
}
return arr;
}
var myArr=test();
for(var j=0;j<10;j++){
myArr[j]();
}
解释一下立即执行函数部分在其中起到什么作用:每次都将for循环中的i单独赋值给了立即执行函数里的i:
for (var i=0;i<10;i++) {
(function(j){
arr[i]=function(){
console.log(j);
}
}(0))//for循环中的 i 循环到0给立即执行函数里的 i 赋值0
(function(j){
arr[i]=function(){
console.log(j);
}
}(1))//for循环中的 i 循环到1给立即执行函数里的 i 赋值1
......
(function(j){
arr[i]=function(){
console.log(j);
}
}(9))//for循环中的 i 循环到9给立即执行函数里的 i 赋值9
}
让我们看看最后的输出结果:
实现目标!
最后
到此,有关闭包的知识点就结束了。这篇文章我写得很啰嗦,因为想尽可能写得详细点,希望能够把我踩过的坑踏平。如果这篇文章能够帮到你就再好不过了!