js深度剖析 : 执行环境和作用域链

执行上下文

execution context 又称执行上下文或者执行环境.

执行上下文是JavaScript中一个极为重要的概念. 我们可以简单的理解执行上下文就是我们代码执行时所在的环境. 这个环境中包含着许多对我们来说十分重要的数据. 例如作用域链 , 数据的访问权,执行代码等等 .

当我们的代码在JavaScript中运行时,便会进入不同的执行上下文.

其中Global execution context是最外围层的一个执行环境 , 即JavaScript代码开始运行的默认环境.也就是我们常说的全局环境. 这个执行上下文是由浏览器创建并初始化的 .

除此之外,每个函数都有自己的一个执行上下文. 当我们进行函数调用时, 其执行上下文便会创建. 这也是我们所认为的局部环境( local context) .

执行环境栈

JavaScript引擎在执行代码之前, 首先会创建一个执行环境栈(execution context stack) . 然后创建全局环境并将其压入栈中作为栈底. 再之后 , 随着执行流进入函数中,函数的执行上下文便会被推入 执行环境栈 中(即进入一个执行上下文). 而在函数执行完毕后, 执行环境栈便会把这个函数的环境弹出 , 并将控制权返回给之前的执行环境.

我们来分析一下如下代码中的执行环境栈是如何变化的:

<script>
  function fn() {
        function fn2() {};
        fn2();
    };
    fn();
</script> 

其大致流程如下图

javascript环境栈
javascript环境栈

variable object

javascript高级程序中是这样描述variable object的:

每个执行环境都有一个与之相关联的对象,并且环境中定义的变量和函数都在其中.

由此我们便知道,variable object中存储了在上下文中定义的变量和函数声明. 也就是说我们所使用的变量或者函数的标识符则都是在variable object上来查询其是否可用.

注: 下文会将其简写为VO

通常情况下,一个variable object有以下信息:

  • 变量 (var, Variable Declaration);
  • 函数声明 (Function Declaration, FD);
  • 函数的形参

并且因为执行环境的不同,所以variable object也有更细的划分或是不同的表现.其关系如下图:

variable object
  • global object

对于全局环境,浏览器中用window对象来作为它的variable object . 该全局对象中存储着JavaScript所定义的全部内置对象和全局函数,以及宿主环境的宿主对象和我们在全局环境中定义的函数和变量等.

这也是为什么我们在全局环境中声明一个变量和函数后,其会成为window对象的属性.

var a = 11;
function fn(){};

window.a ===a ; // -> true 
window.fn ===fn; // -> true 
  • activation object

对于局部环境,我们通常用activation object来作为variable object .并且这个对象我们是不能用代码访问到的.这个对象中存储着我们在函数内部声明的变量和函数以及参数(包含隐式参数arguments).

注: 下文会将其简写为AO

我们还要说明一下,activation object在最开始的时候只有一个变量,即arguments对象(这个对象在全局环境上是不存在的).

执行上下文的两个阶段

常言道: 兵马未动 粮草先行

因此当我们进入一个执行上下文的时候,js解释器并不会立即就去执行其内部代码, 而是先做一些预备工作来保证之后的执行无后顾之忧.

其大致可分为两个阶段:

  • 准备阶段
    • 创建activation object
    • 创建Scope chain(即作用域链)
    • 设置this的值
    • 抽离出执行代码.(剔除声明部分的代码)
  • 激活/执行阶段
    • 设置变量的值、函数的引用,然后解释/执行代码

然后下文中我们会着重讲一下准备阶段中一些过程的细节.

AO(activation object)的创建过程

  1. 在创建activation object之初,就为其添加了一个名为arguments对象,里面存储着函数调用时传入的所有参数.

  2. 扫描函数的形式参数

    • 对于所有找到的参数,将由参数名和其对应值以key-value的形式存入VO/AO中,如果没有传递对应参数的话,那么将参数名和undefined值以key-value的形式存入VO/AO中.
  3. 扫描函数内部代码,查找函数声明(Function declaration).

    • 对于所有找到的函数声明,将函数名和函数引用存入VO/AO中.
    • 并且如果VO/AO中已经有同名的标识符, 那么就对其标识符的引用进行覆盖.
  4. 扫描函数内部代码,查找变量声明(Variable declaration)

    • 对于所有找到的变量声明,将变量名存入VO/AO中,并初始化为"undefined".
    • 如果变量名称跟已经声明的参数或函数名相同,则变量声明不会干扰已经存在的标识符和其引用.

我们以如下代码为例:

function add(a,b){
    var z ;
    function fn(){
        console.log(a,b)
    }
    z = a+b;
    return z;
}

add(10,20)

接着我们来分析一下AO的创建过程, 如图:

创建AO过程
创建AO过程

然后我们来验证一下当标识符命名发生冲突时会怎样:

  • 参数名和函数名冲突
    function fn(x){
        function x(){};
        console.log(x)
    }
    
    fn(10) // -> ƒ x(){}

我们会发现当参数名和函数名冲突时,函数的引用值会覆盖参数名的引用值.

  • 参数名和变量名冲突
   function fn(x){
            var x ;
            console.log(x)
        }
        
    fn(10) // -> 10

我们会发现当参数名和变量名冲突时,变量声明不会干扰已经存在的标识符和其引用.

  • 变量名和函数名冲突
   function fn(){
             function x(){};
             var x ;
            console.log(x)
        }
        
    fn() // ->  ƒ x(){}

这一结果又刚好进一步证明了我们上一步得出结论.

我们再来看最后一段代码:

function fn(){
    console.log(x)
    var x = 10 ;
}
fn() // -> undefined

我们看见了,其输出的结果是undefined. 不知你有没有发现,上述代码与变量提升有关.而AO的创建过程则正是造成变量提升这一现象的根源.

函数对象的[[scope]]属性

在JavaScript中, 一切皆为对象

JavaScript 中每个函数都表示为一个函数对象(函数实例),既然是对象,就有相关的属性和方法。除了正常的属性,函数对象具有仅供 JavaScript 引擎内部使用,但不能通过代码访问的一系列内部属性。这些属性中,其中一个就是 [[scope]] 属性。

函数内部的 [[scope]]属性实际是一个variable object的集合.其包含了当前执行环境栈中所有执行上下文variable object. 而这也正是作用链的雏形.

注意: 因为该函数还未调用,所以执行环境栈中没有这个函数的执行上下文.

作用域链

正如上文所述, 执行上下文中有很多对我们来说十分重要的数据. 除了variable object以外 ,作用域链也是其中之一.

  • 创建作用域链的过程:
  1. 将函数内部[scope]属性中的所有对象按顺序复制到作用域链中,完成作用域链的初始化.
  2. 将当前执行上下文variable object放入作用域链的顶端,彻底完成作用域链的创建.

正因此,当前执行上下文的variable object一直都是其作用域链的开头 .

  • 标志符的查询

当在某个环境中为了读取或是写入而引用一个标志符,必须通过搜索来确定标识符代表什么. 搜索过程从作用域链的顶端开始,向上逐级查询与给定名字匹配的标识符.

在此我们再详细说明一下其搜索过程的细节. 如果在局部环境中找到了该标识符,搜索就会停止,变量就绪. 若是没有找到该变量名/函数名 ,那么就会沿着作用域链查找.搜索过程会一种追溯到全局对象上,若是还没有查找到该标识符,那么便意味着该变量/函数并未声明,输出错误信息.

var a = 10 ;
var b = 100;
function fn(){
     var b = 20 ;
     console.log(a) // -> 10 
     console.log(b) // -> 20
}
fn()

以上段代码为例,其在沿着作用域链上的查询过程如图:

作用域链
作用域链

结束语

写这篇文章的原因来自一次偶然的思考, 然后便是大量的翻阅相关资料.甚至为此我还用我那有些蹩脚的英文翻墙去查阅一些外文资料.其过程自然有些艰辛的,但是黄天不负有心人,我总算是理解了JavaScript中的一些核心概念.并且我能感受的到,一扇新的大门即将为我开启, 征程还未停止.不过我有些喜欢上了这种在路上的感觉了.

我在路上
我在路上
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容