zoukankan      html  css  js  c++  java
  • 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等等问题了。

    穷则独善其身,达则兼济天下……
  • 相关阅读:
    Linux下C语言的调试--转
    linux下c的网络编程---转载
    redis学习资料
    Keepalived配置与使用--转载
    Redis configuration
    keepalived程序包
    Keepalived 使用指南
    myeclipse解决JSP文件script调整背景颜色
    java 面试题汇总(未完成)
    c++ primer plus(文章6版本)中国版 编程练习答案第八章
  • 原文地址:https://www.cnblogs.com/hmy-666/p/14436783.html
Copyright © 2011-2022 走看看