JavaScript的执行原理主要包括以下几个步骤:
- 解析代码:浏览器会解析你的JavaScript代码,并在内部创建一个AST(抽象语法树)和符号表。
- 预处理:这一步包括创建作用域,变量和函数的声明提升等。
- 解释执行:代码被逐行解释并执行,如果遇到函数调用则进入调用对象的作用域。
v8引擎动态解析和执行:(边解析边执行)
解析器 -> 解释器 -> 编译器 (js代码 -> 抽象语法树AST -> 字节码 -> 机器码)
解析:堆内存中创建执行上下文GO(全局+函数执行)
执行:栈内存在执行栈中,纵向栈底为全局执行期上下文VO,其他函数依次压入栈,执行时从上往下栈顶先执行先销毁
执行上下文三属性: 变量对象、作用域链、this 指向
变量对象
全局对象 GO [堆内存]:存放可被JS直接调用的变量和方法,比方window、setTimeout、Date等
变量对象 VO [执行栈]:存储了在执行上下文中定义的所有变量和函数声明,将各种变量和函数声明进行提升的环节
活动对象 AO [执行栈]:在函数执行上下文里面的,也是变量对象,只是需要在函数被调用时才被激活,而且初始化 arguments
在新的ECMA规范中用变量环境VE代替了VO
预编译
全局预编译发生在js文件加载完毕:
-
创建GO对象(window)
-
找变量声明,值为undefined
-
找函数声明,赋值函数体(如果后面存在同名重复的函数声明则赋值后面的函数体)
-
逐行执行
(执行遇到函数则先进入该函数的局部预编译AO)
局部预编译发生在函数执行的前一刻:
-
创建AO对象(执行期上下文/作用域)
-
找形参、变量声明作为AO属性名,值为undefined
-
实参形参相统一
-
函数体内找函数声明,赋值函数体(如果后面存在同名重复的函数声明则赋值后面的函数体)
-
执行
暗示全局变量:未经声明就赋值的变量
预编译特点:函数声明整体提升,变量的声明提升
执行期:提升过后函数执行不再看被提升过的变量,只看赋值
函数表达式和函数声明之间的主要区别在于他们的执行时间:
- 函数声明会在代码执行之前被加载,可以在任何地方调用。
- 函数表达式必须等到代码执行到对应行时才会被解析执行。
另外,函数声明还可以通过名称进行提升,即在声明之前就可以调用该函数。而函数表达式则必须等到赋值语句执行完毕后才可以使用。
作用域
函数产生作用域
test.[[scope]] 隐式属性,仅供js引擎存取
存储了函数执行时所在环境产生的执行期上下文(如果全局就是先存GO)
函数内的函数创建时会存储GO+父函数的AO,执行时在此基础上再加个自己的AO
函数执行完毕执行期上下文被销毁
作用域链(scope chain):[[scope]]中存储的执行期上下文集合,为什么函数中能够访问函数外部的变量,因为有作用域链,在自身找不到就顺着作用域链往上找,找到全局对象中还没有就会报错未定义
其中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指向给某对象