JS函数执行上下文与变量提升

首页 > JS函数执行上下文与变量提升 > 列表

JS函数执行上下文与变量提升

variable Object:以下称VO.

作者:柳兮
链接:https://zhuanlan.zhihu.com/p/26011572
来源:知乎

执行上下文看似很好理解,可是当深入之后其实里面还有很多值得学习的地方,并且与很多我们耳熟能详的概念,譬如提升(hoisting)联系紧密。我的理解可能有所欠缺,只能把自己浅薄的理解说出来,大家仅供参考哇。也欢迎大家来找我讨论哇。

什么是执行上下文

JavaScript是一个单线程语言,意味着同一时间只能执行一个任务。当JavaScript解释器初始化执行代码时, 它首先默认进入全局执行环境(execution context),从此刻开始,函数的每次调用都会创建一个新的执行环境。

执行环境的分类

  • 全局环境——JavaScript代码运行时首次进入的环境。
  • 函数环境——当函数被调用时,会进入当前函数中执行代码。
  • Eval——eval内部的文本被执行时(因为eval不被鼓励使用,此处不做详细介绍)。

执行上下文栈

当JavaScript代码执行的时候,会进入不同的执行上下文,这些执行上下文会构成了一个执行上下文栈(Execution context stack,ECS)。栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文。

代码在执行过程时遇到以上三种执行环境的代码时,都会生成一个对应的执行上下文,压入执行上下文栈中,当栈顶的上下文执行完毕之后,会自动出栈。下面用一个例子说明。

var a = 1;
function fn1() {
  function fn2() {
   console.log(a);
 }
 fn2();
}
fn1();

第一步,全局执行上下文入栈。

第二步,遇到fn1(),执行代码,创建自己的执行上下文,入栈。

第三步,fn1的上下文入栈之后,接着执行其中的代码,遇到fn2(),创建自己的执行上下文,入栈。

第四步,在fn2的执行上下文中未创建新的执行上下文,代码执行完毕之后,fn2的执行上下文出栈。

第五步,fn2的执行上下文出栈之后,继续执行fn1r的可执行代码,也未创建新的执行上下文,出栈。这个时候栈中只剩下全局执行上下文了。

有5个需要记住的关键点,关于执行栈(调用栈):

  • 单线程。
  • 同步执行。所有的执行上下文都得等到栈顶的执行之后才能顺序执行
  • 只有一个全局执行上下文。
  • 函数上下文是无限制的。
  • 每次函数被调用时都会创建新的执行上下文,包括调用自己。

深入了解执行上下文

执行上下文的构成

可以将每个执行上下文抽象为一个对象并有三个属性。

executionContextObj = {
    scopeChain: { /* 变量对象(variableObject)+ 所有父执行上下文的变量对象*/ }, 
    variableObject: { /*函数 arguments/参数,内部变量和函数声明 */ }, 
    this: {} 
}

执行上下文的产生

在JavaScript解释器内部,每次调用执行上下文,分为两个阶段:

创建阶段(此时函数被调用,但未执行内部代码):

  • 设置[[Scope]]属性的值
  • 设置变量对象VO,创建变量,函数和参数。
  • 设置this的值。

激活/代码执行阶段:

在当前上下文上运行/解释函数代码,并随着代码一行行执行指派变量的值和函数的引用。

创建阶段

1.根据函数的参数,创建并初始化arguments object。

2.扫描上下文的函数声明:对于找到的函数声明,将函数名和函数引用存入VO中,如果VO中已经有同名函数,那么就进行覆盖。

3.扫面上下文的变量声明:对于找到的每个变量声明,将变量名存入VO中,并且将变量的值初始化为undefined。如果变量的名字已经在变量对象里存在,不会进行任何操作并继续扫描。

要记住:函数扫描是在变量之前。

让我们举一个栗子来说明:

function person(age) {
    var name = 'abby';
    var getName = function getName() {
    };
    function getAge() {
    	return age
    }
}
person(20);

首先,当我调用person(20)的时候,创建的状态是这样:

PersonExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 20,
            length: 1
        },
        age: 20,
        getAge: pointer to function getAge(),
        name: undefined,
        getName: undefined,
    },
    this: { ... }
}

刚创建的时候,首先是指出函数的引用,然后按顺序对变量进行定义,初始化为undefined。当创建完成之后,执行流进入函数并且在上下文中运行/解释代码,指定函数的引用和变量的值,如下:

PersonExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 20,
            length: 1
        },
        age: 20,
        getAge: pointer to function getAge(),
        name: 'Abby',
        getName: pointer to function getName(),
    },
    this: { ... }
}

提升(Hoisting)(注:函数提升优于变量提升执行

很多书上只说了变量提升是将变量提至当前上下文的最顶端,却未说明原因,现在理解了执行环境的创建、激活阶段,由此也可以解释函数、变量的提升了。

(function() {
    console.log(typeof name); // function
    console.log(typeof another); // undefined
    var name = 'Abby';
    var another = function() {
            return 'Lucky';
        };
    function name() {
        return 'Abby';
    }
    console.log(typeof name); // string
    console.log(typeof another); // function
}()); 

此时的创建阶段的过程是:

1.函数name和其引用被存入到VO之中。

2.变量name发现在VO之中存在同名的属性,因此忽略。

3.变量another存入到VO之中,并赋值为undefined。(这也是函数表达式不会提升的原因)

此时代码从上到下执行的时候激活阶段的过程是:

1.console.log(typeof name); 此时name在VO中是函数。

2.console.log(typeof another); 此时another在VO中的值是undefined。

3.指出函数name的引用。

4.将name赋值为’hello’。

5.将another赋值为函数表达式的值。

6.console.log(typeof name); 此时的name由于被函数被字符串赋值覆盖因此是string类型。

7.console.log(typeof another); 此时的another被赋值成函数表达式因此是function类型。

上面变量提升后的代码:

(function () {
  function name() {
    return 'Abby';
  }
  var name;//因为前面name已经赋值,此时再定义变量的默认值还是上面的值
  var another;
  console.log(typeof name); // function
  console.log(typeof another); // undefined
  name = 'Abby',
  another = function () {
      return 'Lucky';
    };
  console.log(typeof name); // string
  console.log(typeof another); // function
}());

因此理解执行上下文之后也就很好理解了为什么我们能在name声明之前访问它,为什么之后的name的类型值发生了变化,为什么another第一次打印的时候是undefined等等问题了。