Js 执行原理:预编译、作用域、This 指向

57 阅读4分钟

JavaScript的执行原理主要包括以下几个步骤:

  1. 解析代码:浏览器会解析你的JavaScript代码,并在内部创建一个AST(抽象语法树)和符号表。
  2. 预处理:这一步包括创建作用域,变量和函数的声明提升等。
  3. 解释执行:代码被逐行解释并执行,如果遇到函数调用则进入调用对象的作用域。

v8引擎动态解析和执行:(边解析边执行)

解析器 -> 解释器 -> 编译器 (js代码 -> 抽象语法树AST -> 字节码 -> 机器码)

解析:堆内存中创建执行上下文GO(全局+函数执行)

执行:栈内存在执行栈中,纵向栈底为全局执行期上下文VO,其他函数依次压入栈,执行时从上往下栈顶先执行先销毁

执行上下文三属性: 变量对象、作用域链、this 指向

变量对象

全局对象 GO [堆内存]:存放可被JS直接调用的变量和方法,比方window、setTimeout、Date等

变量对象 VO  [执行栈]:存储了在执行上下文中定义的所有变量和函数声明,将各种变量和函数声明进行提升的环节

活动对象 AO [执行栈]:在函数执行上下文里面的,也是变量对象,只是需要在函数被调用时才被激活,而且初始化 arguments

在新的ECMA规范中用变量环境VE代替了VO

预编译

全局预编译发生在js文件加载完毕:

  1. 创建GO对象(window)

  2. 找变量声明,值为undefined

  3. 找函数声明,赋值函数体(如果后面存在同名重复的函数声明则赋值后面的函数体)

  4. 逐行执行

(执行遇到函数则先进入该函数的局部预编译AO)

局部预编译发生在函数执行的前一刻:

  1. 创建AO对象(执行期上下文/作用域)

  2. 找形参、变量声明作为AO属性名,值为undefined

  3. 实参形参相统一

  4. 函数体内找函数声明,赋值函数体(如果后面存在同名重复的函数声明则赋值后面的函数体)

  5. 执行

暗示全局变量:未经声明就赋值的变量

预编译特点:函数声明整体提升,变量的声明提升

执行期:提升过后函数执行不再看被提升过的变量,只看赋值

函数表达式和函数声明之间的主要区别在于他们的执行时间:

  • 函数声明会在代码执行之前被加载,可以在任何地方调用。
  • 函数表达式必须等到代码执行到对应行时才会被解析执行。

另外,函数声明还可以通过名称进行提升,即在声明之前就可以调用该函数。而函数表达式则必须等到赋值语句执行完毕后才可以使用。

作用域

函数产生作用域

test.[[scope]] 隐式属性,仅供js引擎存取

存储了函数执行时所在环境产生的执行期上下文(如果全局就是先存GO)

函数内的函数创建时会存储GO+父函数的AO,执行时在此基础上再加个自己的AO

函数执行完毕执行期上下文被销毁

作用域链(scope chain):[[scope]]中存储的执行期上下文集合,为什么函数中能够访问函数外部的变量,因为有作用域链,在自身找不到就顺着作用域链往上找,找到全局对象中还没有就会报错未定义

image.png

image.png

其中b的[[scope]] [1]拿的是a的AO的引用

b执行完毕后b的AO被销毁,等待下一次执行

a执行完a的AO被销毁,AO内的b也全部被销毁,等待下一次执行

a下一次执行前又生成新的AO……

如果b被a函数retrun出去(或者赋值给外部变量去执行),b则赋值给某个全局变量,则a的AO不会被销毁,b还是能访问到a的AO,形成闭包

This指向

  • 预编译和全局执行上下文(全局作用域):this 的值是 window
  • 函数执行上下文(函数作用域):
    • 函数直接作为某对象的方法被调用则函数的 this 指向该对象。
    • 函数作为函数直接独立调用(不是某对象的方法,或直接在对象外部把对象的方法赋值给一个变量去执行,或者没有this.test()而是直接test()执行),或是函数中的函数、闭包,其 this 指向 window。
    • 函数通过 call()或apply()或bind()改变 this指向给某对象