JavaScript 的运行上下文和执行栈

在这往篇译文中,将会深入探讨 JS 最基础的一部分:Execution Context ,目的是理解 JS 引擎的运行,为什么一些函数和变量可以在声明之前就可以使用,并且他们的值是怎么确定下来的。

什么是运行上下文?

js 代码执行的时候,运行环境尤为重要,一般被概括为以下三种:

  • 全局环境 : 代码第一次运行的默认环境
  • 函数环境 : 执行到函数体的时候
  • Eval 环境 : eval 函数所执行的文本

我们把术语 “运行上下文” 看成是当前代码所在的 “环境/作用域”。

下面看一个包含全局和函数上下文的例子

// global context
var sayHello = 'hello';
function person(){ // execution context
  var first = 'David',
  last = 'Shariff';
  function firstName(){// execution context
    return first;
  }
  function lastName(){// execution context
    return last;
  }
  alert(sayHello+firstName()+''+lastName());
}

这里有一个全局上下文和三个不同的函数上下文。

其中全局上下文只能有一个,但函数上下文可以有任意多个,并可以访问全局上下文。

对于函数上下文来说,伴随着每一次函数调用,一个新的上下文就会被创建,同时一个私有的作用域也会被创建,这个私有作用域不能被外部直接访问。

在上图的例子中,一个函数可以访问当前上下文环境外部声明的变量,但是外部的上下文却不能访问此函数内部声明的变量和方法。为什么会这样?这些代码具体是怎么被解析的?

运行上下文栈

浏览器中的 JS 引擎是单线程的,也就是说它在某一时刻只能干一件事,其它的操作和事件需要以队列的形式呈现,这个队列就叫做运行栈或者执行栈。下图是一个单线程栈的抽象表示。

在这里插入图片描述

当浏览器第一次加载代码的时候,默认情况下就处于全局运行上下文,当有函数被调用的时,一个新的运行上下文就被创建并推入运行栈的栈顶。

如果在当前函数中调用另一个函数,同样的道理,执行流就会进入被调用的函数,新建运行上下文并推入执行栈。浏览器总是运行栈顶的上下文,一旦当前运行上下文运行完毕,它就会被推出栈顶,向下方的运行上下文交出控制权。一个递归调用的例子如下:

(function foo(i){
  if(i===3){
    return 
  }else{
    foo(++i);
  }
}(0))

在这里插入图片描述

上面的函数自己调了自己 3 次,每次 foo 方法被调用都会创建一个新的运行上下文,一旦当前的上下文运行完毕,它就会被弹出,然后控制权被转到它下面的运行上下文,直到再次回到全局运行上下文。

有 5 个需要注意的关键点:

  • 单线程
  • 同步执行
  • 1 个全局上下文
  • 不限个数的函数上下文
  • 每次函数调用都创建一个新的运行上下文,不管是不是自己调用自己。

运行上下文详解

既然已经知道每次函数调用都会新建运行上下文,然而在引擎内部,从每次函数调用到运行上下文的创建成功需要经历两个步骤:

  1. 创建阶段 【函数被调用之后,运行之前】
    • 初始化作用域链
    • 创建需要的变量、函数和参数
    • 确定 this 指向
  2. 代码运行阶段
    • 对变量和函数赋值,解析并执行代码

可以用下面带有3个属性的对象来对运行上下文做一下概念上的描述

executionContextObj = {
  'scopeChain':{/*variableObject + all parent execution contextis variableObject*/},
  'variableObject':{/* function arguments/parameters,inner varibale and function declarations*/},
  'this':{}
}

VariableObject 变量对象

在函数被调用之后的阶段一,也就是创建阶段,引擎遍历传入函数的参数,函数内其它函数、函数变量的声明,遍历的结果用于生成 executionContextObj 对象中的 variableObject

大致的遍历过程如下:

  • 创建 arguments 对象, 初始化函数参数并创建一个引用副本。
  • 遍历函数体中的函数声明
    • 每次发现函数声明后都会在 variable object 中添加一个属性,属性名是函数名,属性的值是函数的引用
    • 如果函数名称已经存在于 variable object 中,则函数的引用就会被后来者覆盖
  • 遍历函数体中的变量声明
    • 每次发现变量声明都会在 variable object 中添加属性,属性名是变量名,属性值是 undefined。
    • 如果变量名已经存在于 variable object 中,直接忽略并继续遍历。

再看一个例子:

function foo(i){
  var a = 'hello';
  var b = function privateB(){
  };
  function c(){
  }
}

如果执行 foo(22), 那么创建阶段的运行上下文就会是这样:

fooExecutionContext = {
	scopeChain = {...},
  variableObject:{
    arguments:{
      0:22,
      length:1
    },
    i:22,
    c:pointer to function c(),
    a:undefined,
    b:undefined
  },
  this:{}
}

可以看出来, 创建阶段主要是处理属性名的定义,且除了arguments 之外并不进行任何赋值操作。

当创建阶段完成后,执行流就进入函数内部开始运行阶段,运行阶段完成后的 运行上下文 如果下所示:

fooExecutionContext = {
	scopeChain = {...},
  variableObject:{
    arguments:{
      0:22,
      length:1
    },
    i:22,
    c:pointer to function c(),
    a:'hello',
    b:pointer to function privateB()
  },
  this:{}
}

变量提升

在很多地方都可以看到变量提升的概念,意思就是变量和函数的声明被提升到了函数作用域的顶部,但很少看到对此现象的详细解释。其实很容易解释这个现象,以下面的代码为例:

(function() {

    console.log(typeof foo); // function pointer
    console.log(typeof bar); // undefined

    var foo = 'hello',
        bar = function() {
            return 'world';
        };

    function foo() {
        return 'hello';
    }

}());
  • 为什么在 foo 函数声明前就可以被访问到?

    如果回头看看在创建阶段js引擎做的事情,就会明白变量在代码运行阶段之前就已经被创建了,所以当代码开始运行时,foo 已经被定义了。

  • foo 被定义了两次,为什么最后它是函数而不是 undefined 或者 string?
    即使 foo 被定义了两次,从创建阶可知:函数先于变量被遍历,并且对于变量来说如果名称已经存在于 variableObject 中 ,则直接忽略。

  • 那为什么 bar 是undefined 呢?

    bar 其实是一个拥有函数赋值的变量,并且变量是在创建阶段被创建,同时被初始化为 undefined

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值