代码的运行(执行)环境
在JS中有三种代码运行环境
Global Code
JavaScript代码开始运行的默认环境
Function Code
代码进入一个JavaScript函数
Eval Code
使用eval()执行代码
执行上下文
【要知道的:】
JS代码在执行前,JS引擎总要做一番准备工作,这份工作其实就是创建对应的执行上下文
同时也是为了表示代码处在不同的运行环境,才引出了执行上下文(Execution context,EC)的概念
执行上下文栈(Execution context stack,ECS):即当JS代码在执行的时,会在执行环境中进入到不同的执行上下文。而这些执行上下文就构成了一个ECS
执行上下文有且只有三类:
- 全局执行上下文
- 函数上下文
- 与eval上下文
由于eval一般不会使用,这里不做讨论。
例如对如下面的JavaScript代码:
var a = "global var";
function foo(){
console.log(a);
}
function outerFunc(){
var b = "var in outerFunc";
console.log(b);
function innerFunc(){
var c = "var in innerFunc";
console.log(c);
foo();
}
innerFunc();
}
outerFunc()
代码首先进入Global Execution Context,然后依次进入outerFunc,innerFunc和foo的执行上下文,执行上下文栈就可以表示为:
当JavaScript代码执行的时候,第一个进入的总是默认的Global Execution Context,所以说它总是在ECS的最底部。
执行上下文创建阶段
JS执行上下文的创建阶段主要负责三件事:
- 确定this
- 创建词法环境组件(LexicalEnvironment)
- 创建变量环境组件(VariableEnvironment)
确定this
创建词法环境组件
词法环境是一个包含标识符变量映射的结构( 标识符:变量)
这里的标识符表示变量/函数的名称;
变量是对实际对象【包括函数类型对象】或原始值的引用。即:age(变量或函数的名称): 0x88 (对象或原始值的引用)
词法环境由环境记录与对外部环境引入记录两个部分组成。
【环境记录】:存储当前环境中的变量和函数声明的实际位置
【外部环境引入记录】:保存自身环境可以访问的其它外部环境
由于不同的执行上下文,因此词法环境组件分为两大类:
全局执行上下文 =》全局词法环境组件
对外部环境的引入记录为null,因为它本身就是最外层环境;除此之外(对象)环境记录包含了当前环境下的所有(使用let和const声明的)属性、方法位置
函数执行上下文 =》函数词法环境组件
函数词法环境的外部环境引入可以是全局环境,也可以是其它函数环境,这个根据实际代码而来;(声明性)环境记录包含了用户在函数中定义的所有(使用let、const声明的)属性方法,和一个arguments对象。
创建变量环境组件
变量环境可以说也是词法环境,它具备词法环境所有属性,一样有环境记录与外部环境引入。在ES6中唯一的区别在于:
-
词法环境用于存储函数声明与let const声明的变量
-
变量环境仅仅存储var声明的变量。
举个栗子
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
不知道你有没有发现,在执行上下文创建阶段,函数声明与var声明的变量在创建阶段已经被赋予了一个值,var声明被设置为了undefined,函数被设置为了自身函数,而let const被设置为未初始化。
现在你总知道变量提升与函数声明提前是怎么回事了吧,以及为什么let const为什么有暂时性死域,这是因为作用域创建阶段JS引擎对两者初始化赋值不同。
执行上下文执行阶段
代码执行时根据之前的环境记录对应进行赋值
比如早期var在创建阶段为undefined,如果有值就对应赋值;
像let const值为未初始化,如果有值就赋值,无值则赋予undefined。
案例分析
【案例】
<script>
var scope = "global";
function fn1(){
return scope;
}
function fn2(){
return scope;
}
fn1();
fn2();
</script>
上面代码执行如下:
【案例】
function foo(i) {
var a = 'hello';
var b = function privateB() {
...
};
function c() {
...
}
}
foo(22);
对于上面的代码,可以用下面的伪代码来描述执行上下文的创建过程:
【案例】
var a = 1
let b = 2
function foo(i) {
var a = 'hello';
let d = 2
var b = function privateB() {
...
};
function c() {
...
}
}
foo(22);
执行环境创建的分析图解如下:
【案例】
(function(){
console.log(bar);
console.log(baz);
bar = 20;
console.log(window.bar);
console.log(bar);
function baz(){
console.log("baz");
}
})()
运行这段代码会得到"bar is not defined(…)"错误。当代码执行到"console.log(bar);“的时候,会去AO中查找"bar”。但是,根据前面的解释,函数中的"bar"并没有通过var关键字声明,所有不会被存放在AO中,也就有了这个错误。
注释掉"console.log(bar);",再次运行代码,可以得到下面结果。"bar"在"激活/代码执行阶段"被创建。
【案例】
<script>
var c = 1;
function c(c){
var c = 2;
console.log(c);
}
c(3);
</script>
运行结果: c is not a function
上面的代码相当于如下所示:
<script>
//变量提升
var c;
//函数提升
function c(c){
var c = 2;
console.log(c);
}
c = 1;
c(3);
</script>
【案例】运行下面的代码,会依次输出什么,创建了几个执行上下文
<script>
console.log('gb begin:'+i);
var i = 1;
foo(1);
function foo(i){
if(i == 3){
return;
}
console.log('foo() begin:'+i);
foo(i+1);
console.log('foo() end:'+i);
}
console.log('gb end:'+i);
</script>
执行结果:
gb begin:undefined
demon.html:38 foo() begin:1
demon.html:38 foo() begin:2
demon.html:40 foo() end:2
demon.html:40 foo() end:1
demon.html:42 gb end:1
闭包
<script>
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){//注:i是outer()的局部变量
result[i] = function(){
return i;
}
}
return result;//返回一个函数对象数组
//这个时候会初始化result.length个关于内部函数的作用域链
}
var fn = outer();
console.log(fn[0]());//result:2
console.log(fn[1]());//result:2
</script>
返回结果很出乎意料吧,你肯定以为依次返回0,1,但事实并非如此
来看一下调用fn0的作用域链图:
可以看到result[0]函数的活动对象里并没有定义i这个变量,于是沿着作用域链去找i变量,结果在父函数outer的活动对象里找到变量i(值为2),而这个变量i是父函数执行结束后将最终值保存在内存里的结果。
由此也可以得出,js函数内的变量值不是在编译的时候就确定的,而是等在运行时期再去寻找的。
那怎么才能让result数组函数返回我们所期望的值呢?
看一下result的活动对象里有一个arguments,arguments对象是一个参数的集合,是用来保存对象的。
那么我们就可以把i当成参数传进去,这样一调用函数生成的活动对象内的arguments就有当前i的副本。
【改进之后:】
<script>
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
//定义一个带参函数
function arg(num){
return num;
}
//把i当成参数传进去
result[i] = arg(i);
}
return result;
}
var fn = outer();
console.log(fn[0]);//result:0
console.log(fn[1]);//result:1
</script>
虽然的到了期望的结果,但是又有人问这算闭包吗?调用内部函数的时候,父函数的环境变量还没被销毁呢,而且result返回的是一个整型数组,而不是一个函数数组!
确实如此,那就让arg(num)函数内部再定义一个内部函数就好了:
这样result返回的其实是innerarg()函数
<script>
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
//定义一个带参函数
function arg(num){
function innerarg(){
return num;
}
return innerarg;
}
//把i当成参数传进去
result[i] = arg(i);
}
return result;
}
var fn = outer();
console.log(fn[0]());
console.log(fn[1]());
</script>
当调用outer,for循环内i=0时的作用域链图如下:
由上图可知,当调用innerarg()时,它会沿作用域链找到父函数arg()活动对象里的arguments参数num=0.
上面代码中,函数arg在outer函数内预先被调用执行了,对于这种方法,js有一种简洁的写法
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
//定义一个带参函数
result[i] = function(num){
function innerarg(){
return num;
}
return innerarg;
}(i);//预先执行函数写法
//把i当成参数传进去
}
return result;
}