3.作用域、作用域提升、执行上下文

374 阅读6分钟

JavaScript在V8引擎中执行过程

非函数执行过程

var num1 = 20;
var num2 = 30;
var result = num1 + num2;
console.log(result);
  1. 代码进行解析,V8引擎内部在解析成AST语法树之前会帮助我们创建一个GlobalObject(GO)对象,内容如下:
var GlobalObject = {
    String: "字符串类",
    Date:"日期类",
    Number,
    setTimeout,
    num1: undefined,
    num2: undefined,
    result: undefined,
    window: GlobalObject
}

编译阶段所有变量都会进入预编译阶段,没有赋值

  1. 运行代码
    • V8为了执行代码,内部会有一个执行上下文栈(函数调用栈:Execution Context Stack, ECStack)
    • ECStack一般用于执行函数,因为我们执行的全局代码中没有函数,因为我们的全局代码中需要创建全局作用域上下文(Global Execution Context,GEC)
    • 全局作用域包含:Variable Object:GO

image.png 准备工作完成,开始执行代码,开始给GlobalObject内部属性进行赋值,num1 = 20,num2 = 30,result = 50,打印50,GO会在解析阶段存放所有的变量,并且初始值为undefined,代码执行开始进行相关的赋值,得到真正的结果,上述GO解析过程其实就是变量提升.

函数执行过程

var name = '123';
//执行:foo() => foo
function foo (){
    console.log('foo');
}

编译阶段:

var GlobalObject = {
    String: "字符串类",
    Date:"日期类",
    Number,
    setTimeout,
    name: undefined,
    foo: undefined,
    window: GlobalObject
}

image.png 编译阶段如果发现是函数,会在ECStack重新创建一个内存空间用于存放函数对象,函数对象包含了两个属性:

  • [[sope]]:parent scope
  • 函数的执行体 在原来堆空间该函数属性存放了函数对象的地址,属性元素指向该地址。
foo(123);
function foo(num){
    console.log(m);
    var m = 10;
    var n = 20;
    console.log('foo');
}
foo(321);

image.png 上述代码执行过程: fooGO空间存储的依然是内存地址,该地址指向了一个新开辟的函数存储空间,遇到执行时期在ECStack调用栈内生成一块FECFEC指向了新开辟的地(VO环境),传值为123时,AO空间内num就有了赋值,此时undefined还处于提升阶段结果为undefined,函数内部开始执行,执行赋值打印结束以后,函数外部发现没有引用该函数的地址,所以函数就会被销毁。 如果再次发生调用,在ECStack内部会重新开辟一块创建FEC对象,重复上述操作,直到函数空间被销毁为止.

作用域链

var name = 'why';
foo(123);
function foo(num){
    console.log(m);
    var m = 10;
    var n = 20;
    console.log(name);
}

image.png 查找变量的过程其实是沿着作用域链进行查找的。 当前作用域链scope chain包含了两块内容:VOParentScope两块节点,name变量在执行的过程中发现AO环境没有定义该变量,会去上层作用域ParentScope去进行查找,而ParentSocpe则是在函数创建的时候就已经指向了GlobalObject,所以会引用上层父级作用域的var name = "why"

var name = 'why'
foo(123);
function foo(num){
    function bar(){
        console.log(name)
    }
}
foo();

image.png 内存空间执行过程:

image.png 函数执行完成以后发现没有引用就会从栈内弹出来然后删除掉。

作用域链查找过程:

  • 现在自己的AO空间进行查找
  • 如果AO空间没有就会去上层作用域GO空间进行查找
  • 如果上层作用域GO空间也没有就会再去上上层空间查找直至找到为止
  • 如果最外层空间都没有找到变量的赋值就会报错:Uncaguht ReferenceError: xx is not defined 建议慎用name属性,可能会导致name属性本身存在于window对象上

全局作用域分析

var message = "Hello Global";
function foo (){
    console.log(message);
}
function bar (){
    var message = "Hello bar";
    foo();
}
foo();

image.png image.png 易混淆的点是:函数在编译阶段就已经确定父级作用域,不是在执行阶段才知道.查找变量阶段不是查找运行处的变量,而是去父级作用域进行查找.

变量环境和记录

EOAO的规范只适用于早期的ECMA(5)规范,在最新的ECMA规范中,每一个执行上下文会关联到一个变量环境(VariableEnviroment)作为环境记录添加到变量环境中,对于函数来说,形参也会被作为环境记录添加到变量环境中.

作用域面试题

题目一:

var n = 100;
function foo(){
    n = 200;
}
foo();
console.log(n);

image.png

题目二:

function foo(){
    console.log(n); //undefined
    var n = 200;
    console.log(n); //200
}
var n = 100;
foo();

解析:

  • 首先会创建一个GlobalObject对象,里面存储了foo的地址,n的变量(undefined)
  • GO对象开始赋值,n赋值为100,foo函数依然保存地址
  • 函数foo新开辟一个空间AO,开始执行代码,执行函数从上到下执行
  • 打印n,发现在自己的内部空间AO已经提前解析了n变量,结果是undefiend,第二次打印n结果就是上面定义的200

题目三:

var n = 100;
function foo1(){
    console.log(n)
}
function foo2(){
    var n = 200;
    console.log(n);
    foo1();
}
foo2();
console.log(n);

解析:

  • 首先在堆内存开辟一个空间GlobalObject用于存储上述变量,n是undefined,foo1,foo2均是将要新开辟的内存地址
  • 新开辟foo1,foo2内存空间,进行变量解析,nundefined
  • 代码进入执行阶段
  • foo2内部的n会先查找自己内部的AO空间是否有n变量的赋值,查找发现有:n=200
  • foo1内部打印n,发现是否n变量,发现没有,然后去自己内存上层空间查找,上层空间GlobalObject是有的,赋值为n=100
  • 最后打印n,此时的n是全局变量n=100

题目四:

var a = 100;
function foo(){
  console.log(a);
  return;
  var a = 100;
}

与上面比较类似的是,虽然return后面的代码不会进行执行,但是在编译阶段依然会给a进行赋值为undefined,对于foo这个函数来说内部的空间已经有了赋值的变量a,就不会去上层作用域进行查找,因为结果就是undefined. 补充:

function other(){
  m = 200; //意思默认JS引擎对m进行了一个处理,将其添加到全局变量`GlobalObject`中去了
}

题目五:

function foo(){
    var a = b = 100;
}
foo();
console.log(a);
console.log(b);
/*
 * 等价于 var a = 100;  b=100;
 */

结果打印: 在AO空间内部是有a的变量定义的undefined,但是在GO空间内a是not defined,b是100