JS基础03 -- 执行机制

195 阅读7分钟

一些概念

JS引擎 = 调用栈 +

调用栈

ECStack(execution context stack)。用于执行全局代码的地方。

  • 调用栈会负责如下事情
  1. 函数即将执行,函数执行上下文入栈(称为调用帧)
  2. 函数代码逐行执行
  3. 函数返回,若不写return就是return undefined
  4. 函数执行上下文从栈中弹出
  5. 错误追踪:根据调用栈记录,从顶到底依次弹出错误位置。即从错误最近点向外辐射

堆内存空间

heap,函数创建、对象创建完成后存储的地方,被执行栈中的变量引用其地址。堆内存是否能被垃圾回收就看此个堆内存是否被某栈内存变量引用

全局对象

GO(global Object),在浏览器渲染js时,默认创建这个对象。存储在堆内存中,有一个十六进制的空间地址; 这里存储了很多内置的属性和方法;在创建全局执行上下文时,默认window指向这个全局对象。浏览器默认会自带很多供JS调取使用的内置API,这些属性方法都在GO中存储着,在浏览器端,把GO对象赋值给window,在node端把GO赋值给了global。它一个对象。注意区别GO与VO

var globalObject = {
  Math: {},
  String: {},
  document: {},
  ……
  window: this
}

全局变量对象

VO(G)(variable object)。当全局代码执行过程中,会声明一些变量,这些变量存储在全局变量对象VO中。VO是我们自己写代码创建的变量要存储的地方;是栈内存。window是VO(G)中的一个变量,这个变量指向全局对象GO地址,所以通过window.xxx可以访问浏览器提供的全局API

函数活动对象

函数f的活动对象AO(active object)函数执行时,创建它自己的EC,编译完成后也有若干函数的私有变量被创建,他们的集合为活动对象

执行上下文

执行上下文EC是一段代码执行时所处的范围,eg:一个全局环境EC(G)、一个函数执行环境EC(f)。函数执行上下文将被作为调用帧创建并压入调用栈执行

1. 创建EC

在上下文的创建阶段,会完成几个重要的事情

完成this值的绑定

  • 在全局执行上下文中,this 的值指向全局对象。(在浏览器中,this引用 Window 对象)。
  • 在函数执行上下文中,this 的值取决于该普通函数是如何被调用的。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined(在严格模式下)。

创建词法环境组件

词法解析阶段:基于HTTP返回的js代码其实是一些字符串,浏览器会按照ECMAScript规则将字符串变为C++可识别解析的一套对象。

  • 创建环境记录器,存储变量和函数声明的实际位置,在全局环境中,环境记录器是对象环境记录器(定义出现在全局上下文中的变量和函数的关系)。在函数环境中,环境记录器是声明式环境记录器(存储变量、函数和参数arguments)。
  • 创建 外部父环境的引用,意味着它可以访问其父级词法环境(作用域链的形成)
  • 存储的变量是let/const变量,函数声明,放在相应的VOAO

创建变量环境组件

同上存储,但只存储var声明的变量

2. 执行代码

完成对所有这些变量的分配,最后执行代码

声明

ES5及以前:var、function

ES6+:let/const/import(模块导入声明)

var

var声明有变量提升,允许重复声明覆盖,全局var声明不在VO(G),而放在GO中(变量环境组件)

// EC(G)全局上下文中
// window.x = 10,而不是VO(G)中添加变量x = 10。相当于直接x = 10
var x = 10;
// 查找链  VO(G) --> GO,查找window的属性值  --> 报错xx is not defined
console.log(x);     //10
console.log(window.x);       //10

let

let不允许重复声明的。在当前上下文中,不管用什么方式,只要声明了这个变量,都不能基于let重复声明了,会报错。let检查在词法检查阶段,在代码执行阶段之前,若发现let带重复声明,则直接报错。函数创建也推荐使用函数表达式的形式,因为不存在变量提升全局上下文let声明的变量会存放到VO(G)

/**
 * 执行前进行词法解析,发现有变量a是基于ES6规范的变量,且并未重复声明,不报词法错误
 * 代码开始执行:
 * 1. b = 12 => 给window对象添加属性b
 * 2. 打印变量b,但在VO中没有b,所以到GO中找到b并打印
 * 3. 执行a等于12时,发现变量a符合ES6规范但还未定义就赋值,报错Cannot access 'a' before initialization
 * 4. 程序中断
 */
b = 12;
console.log(b);
a = 12;
console.log(a);
let a = 13;
console.log(a);

const

const声明的其实也是一个变量,并不是常量,只是说变量指向的地址不能变,但若是个引用类型,则内部值可变

const m = 13
//报错
m = 12

const obj = {name = 'aaa'}
//只要obj的指针指向就不会报错,此时可以通过obj操作指向对象的属性
obj.name = 'bbb'

块级作用域

正常跨级

es5中只有两种作用域:全局上下文和函数执行私有上下文,但有了let,则产生了块级作用域。

  • 若除函数和对象{}以外的{}中出现了let/const/function 则,此个{}独立为一个块级作用域。
  1. var不受块级上下文的限制
  2. 若块级上下文中的值被外部引用,则也会形成闭包而不被销毁
  3. 块级上下文不会初始化this,所以块级上下文中的this处理指向上级上下文(即在哪被创建)
  4. 在上述大括号块级域中,出现let/const声明语句,大括号会变成私有上下文,这些声明的变量都会在块级Block作用域中
  5. 在上述大括号块级域中,出现function foo(){}声明语句,IE6/7/8中块级无效,正常变量提升在上层作用域(函数环境则AO顶部,全局则在VO顶部)。新版浏览器中,变量提升阶段,function既声明提升到上层作用域而不赋值,也声明加赋值提升到块级作用域。执行时,又执行到function foo(){}时,会将此时块级中的function的值映射给全局一份。其他操作均在块级中进行

function额外块级

新版浏览器中,执行函数时,不论函数是function声明还是函数表达式声明,如果满足以下两个条件,将会形成函数私有AO和函数块级VO

  1. 函数声明的()只要使用了es6的形参默认值
  2. 函数体内再次声明了变量,必须是let/const/var声明,但function必须声明和形参相同的名字才会有以下机制,且块级的此个function声明的变量值直接为这个函数
  • VO的创建时刻是函数私有上下文形参赋值完成后的变量提升阶段进行,在函数体内声明的变量都会存在函数块级VO中,这些变量与AO没有关系。VO实在AO中创建,AO是VO的上级作用域。
  • 变量提升后,代码执行前,会将AO中和VO重名的变量(形参)值直接赋给VO中刚提升而未赋值的变量,函数体内代码执行,此时代码执行的直接环境就是块级上下文,变量的寻找也会遵循作用域链机制