漫谈JS引擎的运行机制 你应该知道什么


Posted in Javascript onJune 15, 2016

javascript从定义到执行,JS引擎在实现层做了很多初始化工作,因此在学习JS引擎工作机制之前,我们需要引入几个相关的概念:执行环境栈、全局对象、执行环境、变量对象、活动对象、作用域和作用域链等,这些概念正是JS引擎工作的核心组件。这篇文章的目的不是孤立的为你讲解每一个概念,而是通过一个简单的demo来展开分析,全局讲解JS引擎从定义到执行的每一个细节,以及这些概念在其中所扮演的角色。 

var x = 1; //定义一个全局变量 x
function A(y){
  var x = 2; //定义一个局部变量 x
  function B(z){ //定义一个内部函数 B
    console.log(x+y+z);
  }
  return B; //返回函数B的引用
}
var C = A(1); //执行A,返回B
C(1); //执行函数B

这个demo是一个闭包,执行结果是4,下面我们将分全局初始化、执行函数A、执行函数B 三个阶段来分析JS引擎的工作机制:

 一、全局初始化
 JS引擎在进入一段可执行的代码时,需要完成以下三个初始化工作: 
首先,创建一个全局对象(Global Object) , 这个对象全局只存在一份,它的属性在任何地方都可以访问,它的存在伴随着应用程序的整个生命周期。全局对象在创建时,将Math,String,Date,document 等常用的JS对象作为其属性。由于这个全局对象不能通过名字直接访问,因此还有另外一个属性window,并将window指向了自身,这样就可以通过window访问这个全局对象了。用伪代码模拟全局对象的大体结构如下:

//创建一个全局对象
var globalObject = { 
  Math:{},
  String:{},
  Date:{},
  document:{}, //DOM操作
  ...
  window:this //让window属性指向了自身
}

然后,JS引擎需要构建一个执行环境栈( Execution Context Stack) ,与此同时,也要创建一个全局执行环境(Execution Context)EC ,并将这个全局执行环境EC压入执行环境栈中。执行环境栈的作用是为了保证程序能够按照正确的顺序被执行。在javascript中,每个函数都有自己的执行环境,当执行一个函数时,该函数的执行环境就会被推入执行环境栈的顶部并获取执行权。当这个函数执行完毕,它的执行环境又从这个栈的顶部被删除,并把执行权并还给之前执行环境。我们用伪代码来模拟执行环境栈和EC的关系: 

var ECStack = []; //定义一个执行环境栈,类似于数组

var EC = {};  //创建一个执行空间,
//ECMA-262规范并没有对EC的数据结构做明确的定义,你可以理解为在内存中分配的一块空间

ECStack.push(EC); //进入函数,压入执行环境
ECStack.pop(EC); //函数返回后,删除执行环境

最后,JS引擎还要创建一个与EC关联的全局变量对象(Varibale Object) VO,  并把VO指向全局对象,VO中不仅包含了全局对象的原有属性,还包括在全局定义的变量x 和函数 A,与此同时,在定义函数A的时候,还为 A 添加了一个内部属性scope,并将scope指向了VO。每个函数在定义的时候,都会创建一个与之关联的scope属性,scope总是指向定义函数时所在的环境。此时的ECStack结构如下: 

ECStack = [  //执行环境栈
  EC(G) = {  //全局执行环境
    VO(G):{ //定义全局变量对象
      ... //包含全局对象原有的属性
      x = 1; //定义变量x
      A = function(){...}; //定义函数A
      A[[scope]] = this; //定义A的scope,并赋值为VO本身
    }
  }
];

二、 执行函数A
 当执行进入A(1) 时,JS引擎需要完成以下工作:
 首先,JS引擎会创建函数A的执行环境EC,然后EC推入执行环境栈的顶部并获取执行权。此时执行环境栈中有两个执行环境,分别是全局执行环境和函数A执行环境,A的执行环境在栈顶,全局执行环境在栈的底部。然后,创建函数A的作用域链(Scope Chain) ,在javascript中,每个执行环境都有自己的作用域链,用于标识符解析,当执行环境被创建时,它的作用域链就初始化为当前运行函数的scope所包含的对象。  
 接着,JS引擎会创建一个当前函数的活动对象(Activation Object) AO,这里的活动对象扮演着变量对象的角色,只是在函数中的叫法不同而已(你可以认为变量对象是一个总的概念,而活动对象是它的一个分支), AO中包含了函数的形参、arguments对象、this对象、以及局部变量和内部函数的定义,然后AO会被推入作用域链的顶端。需要注意的是,在定义函数B的时候,JS引擎同样也会为B添加了一个scope属性,并将scope指向了定义函数B时所在的环境,定义函数B的环境就是A的活动对象AO, 而AO位于链表的前端,由于链表具有首尾相连的特点,因此函数B的scope指向了A的整个作用域链。 我们再看看此时的ECStack结构: 

ECStack = [  //执行环境栈
  EC(A) = {  //A的执行环境
    [scope]:VO(G), //VO是全局变量对象
    AO(A) : { //创建函数A的活动对象
      y:1,
      x:2, //定义局部变量x
      B:function(){...}, //定义函数B
      B[[scope]] = this; //this指代AO本身,而AO位于scopeChain的顶端,因此B[[scope]]指向整个作用域链
      arguments:[],//平时我们在函数中访问的arguments就是AO中的arguments
      this:window //函数中的this指向调用者window对象
    },
    scopeChain:<AO(A),A[[scope]]> //链表初始化为A[[scope]],然后再把AO加入该作用域链的顶端,此时A的作用域链:AO(A)->VO(G)
  },
  EC(G) = {  //全局执行环境
    VO(G):{ //创建全局变量对象
      ... //包含全局对象原有的属性
      x = 1; //定义变量x
      A = function(){...}; //定义函数A
      A[[scope]] = this; //定义A的scope,A[[scope]] == VO(G)
    }
  }
];

三、 执行函数B
 函数A被执行以后,返回了B的引用,并赋值给了变量C,执行 C(1) 就相当于执行B(1),JS引擎需要完成以下工作: 
首先,还和上面一样,创建函数B的执行环境EC,然后EC推入执行环境栈的顶部并获取执行权。 此时执行环境栈中有两个执行环境,分别是全局执行环境和函数B的执行环境,B的执行环境在栈顶,全局执行环境在栈的底部。(注意:当函数A返回后,A的执行环境就会从栈中被删除,只留下全局执行环境)然后,创建函数B的作用域链,并初始化为函数B的scope所包含的对象,即包含了A的作用域链。最后,创建函数B的活动对象AO,并将B的形参z, arguments对象 和 this对象作为AO的属性。此时ECStack将会变成这样: 

ECStack = [  //执行环境栈
  EC(B) = {  //创建B的执行环境,并处于作用域链的顶端
    [scope]:AO(A), //指向函数A的作用域链,AO(A)->VO(G)
    var AO(B) = { //创建函数B的活动对象
      z:1,
      arguments:[],
      this:window
    }
    scopeChain:<AO(B),B[[scope]]> //链表初始化为B[[scope]],再将AO(B)加入链表表头,此时B的作用域链:AO(B)->AO(A)-VO(G)
  },
  EC(A), //A的执行环境已经从栈顶被删除,
  EC(G) = {  //全局执行环境
    VO:{ //定义全局变量对象
      ... //包含全局对象原有的属性
      x = 1; //定义变量x
      A = function(){...}; //定义函数A
      A[[scope]] = this; //定义A的scope,A[[scope]] == VO(G)
    }
  }
];

当函数B执行“x+y+z”时,需要对x、y、z 三个标识符进行一一解析,解析过程遵守变量查找规则:先查找自己的活动对象中是否存在该属性,如果存在,则停止查找并返回;如果不存在,继续沿着其作用域链从顶端依次查找,直到找到为止,如果整个作用域链上都未找到该变量,则返回“undefined”。从上面的分析可以看出函数B的作用域链是这样的:
 AO(B)->AO(A)->VO(G)
因此,变量x会在AO(A)中被找到,而不会查找VO(G)中的x,变量y也会在AO(A)中被找到,变量z 在自身的AO(B)中就找到了。所以执行结果:2+1+1=4. 

简单的总结语
 了解了JS引擎的工作机制之后,我们不能只停留在理解概念的层面,而要将其作为基础工具,用以优化和改善我们在实际工作中的代码,提高执行效率,产生实际价值才是我们的真正目的。就拿变量查找机制来说,如果你的代码嵌套很深,每引用一次全局变量,JS引擎就要查找整个作用域链,比如处于作用域链的最底端window和document对象就存在这个问题,因此我们围绕这个问题可以做很多性能优化的工作,当然还有其他方面的优化,此处不再赘述,本文仅当作抛砖引玉吧!

by @一像素 2015

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Javascript的getYear、getFullYear、getUTCFullYear异同分享
Nov 30 Javascript
JavaScript中判断对象类型的几种方法总结
Nov 11 Javascript
js将控件隐藏及display属性的使用介绍
Dec 30 Javascript
JavaScript实现两个Table固定表头根据页面大小自行调整
Jan 03 Javascript
Javascript自定义函数判断网站访问类型是PC还是移动终端
Jan 10 Javascript
jQuery Masonry瀑布流插件使用详解
Nov 17 Javascript
使用Javascript写的2048小游戏
Nov 25 Javascript
jQuery+css实现的换页标签栏效果
Jan 27 Javascript
基于jQuery实现弹幕APP
Feb 10 Javascript
js禁止浏览器的回退事件
Apr 20 Javascript
js模仿微信朋友圈计算时间显示几天/几小时/几分钟/几秒之前
Apr 27 Javascript
jQuery封装placeholder效果实现方法,让低版本浏览器支持该效果
Jul 08 jQuery
JavaScript操作 url 中 search 部分方法函数
Jun 15 #Javascript
JS实现动态表格的添加,修改,删除功能(推荐)
Jun 15 #Javascript
JS封装的自动创建表格的实现代码
Jun 15 #Javascript
基于JavaScript代码实现自动生成表格
Jun 15 #Javascript
JavaScript生成带有缩进的表格代码
Jun 15 #Javascript
全面解析JavaScript中apply和call以及bind(推荐)
Jun 15 #Javascript
JavaScript仿flash遮罩动画效果
Jun 15 #Javascript
You might like
浅析PHP中的UNICODE 编码与解码
2013/06/29 PHP
Smarty局部缓存的几种方法简介
2014/06/17 PHP
php cookie名使用点号(句号)会被转换
2014/10/23 PHP
基于jquery的内容循环滚动小模块(仿新浪微博未登录首页滚动微博显示)
2011/03/28 Javascript
基于jquery的lazy loader插件实现图片的延迟加载[简单使用]
2011/05/07 Javascript
jquery中的查找parents与closest方法之间的区别
2013/12/02 Javascript
JS检测输入字符是否包含非法字符的示例代码
2014/02/11 Javascript
超级好用的jQuery圆角插件 Corner速成
2014/08/31 Javascript
面向切面编程(AOP)的理解
2015/05/01 Javascript
js下拉选择框与输入框联动实现添加选中值到输入框的方法
2015/08/17 Javascript
JS简单实现仿百度控制台输出信息效果
2016/09/04 Javascript
Vue2.0+ElementUI实现表格翻页的实例
2017/10/23 Javascript
webpack打包多页面的方法
2018/11/30 Javascript
vue实现一拉到底的滑动验证
2019/07/25 Javascript
浅谈Vue.js之初始化el以及数据的绑定说明
2019/11/14 Javascript
[34:39]DOTA2上海特级锦标赛主赛事日 - 4 败者组第四轮#1COL VS EG第二局
2016/03/05 DOTA
[37:23]DOTA2上海特级锦标赛主赛事日 - 3 胜者组第二轮#2Secret VS EG第二局
2016/03/04 DOTA
Python translator使用实例
2008/09/06 Python
Python爬虫爬取新浪微博内容示例【基于代理IP】
2018/08/03 Python
matplotlib给子图添加图例的方法
2018/08/03 Python
在Python 字典中一键对应多个值的实例
2019/02/03 Python
python异常处理、自定义异常、断言原理与用法分析
2020/03/23 Python
Python Numpy中数据的常用保存与读取方法
2020/04/01 Python
Numpy一维线性插值函数的用法
2020/04/22 Python
基于python连接oracle导并出数据文件
2020/04/28 Python
css 省略号 css3让多余的字符串消失并附加省略号的实现代码
2013/02/07 HTML / CSS
详解如何在css3打包后自动追加前缀插件:autoprefixer
2018/12/18 HTML / CSS
anello泰国官方网站:日本流行包包品牌
2019/08/08 全球购物
夜大自我鉴定
2013/10/31 职场文书
材料会计岗位职责
2014/03/06 职场文书
高中同学会活动方案
2014/08/14 职场文书
党校党性分析材料
2014/12/19 职场文书
房地产销售员岗位职责
2015/04/11 职场文书
劳动保障事务所个人工作总结
2015/08/12 职场文书
python 遍历磁盘目录的三种方法
2021/04/02 Python
Java数组详细介绍及相关工具类
2022/04/14 Java/Android