• javascript之词法作用域及函数的运行过程


    词法作用域:变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。 with和eval除外,所以只能说JS的作用域机制非常接近词法作用域(Lexical scope)。

    下面通过几个小小的案例,开始深入的了解对理解词法作用域和闭包必不可少的,JS执行时底层的一些概念和理论知识。

    经典案列重现

    1、经典案例一

    1 /*全局(window)域下的一段代码*/
    2 function a(i) {
    3     var i;
    4     alert(i);
    5 };
    6 a(10);

    疑问:上面的代码会输出什么呢?
    答案:没错,就是弹出10。具体执行过程应该是这样的

    1. a 函数有一个形参 i,调用 a 函数时传入实参 10,形参 i=10
    2. 接着定义一个同名的局部变量 i,未赋值
    3. alert 输出 10
    4. 思考:局部变量 i 和形参 i 是同一个存储空间吗?
    5. 按照定义来理解:局部变量 i 和形参 i 是同一个存储空间(引用同一个内存地址)。ECMAScript中,函数执行时,传入函数的实际参数会在函数内部用一个数组来表示,可以通过arguments对象来访问这个参数数组。命名的形参仅是提供便利,但不是必需的。javascript权威指南里说道:Arguments对象有一个非同寻常的特性。当函数具有了命名了的参数时,Arguments对象的数组元素是存放函数参数的局部变量的同义词。arguments[]数组和命名了的参数是引用同一变量的两种不同方法。用参数名改变一个参数的值时同时会改变通过arguments[]数组获得的值,反之亦然。所以可以把函数的参数想象成一早就声明了的局部变量并已赋值(如果传入参数的话),而且此变量不管写入的值是基本类型还是引用类型,都会改变arguments[]数组对应的值,所以上面案列第3行代码定义了一个同名的局部变量i且未赋值是会被忽略的,因为ECMAScript规定在同一作用域里,如果重复声明一个变量并赋予初始值,那么它担当的不过是一个赋值语句的角色;如果重复声明一个变量但没有初始值,那么它不会对原来存在的变量有任何的影响。如下图:

              

               从上图很明显看出,在语句var i;未执行时,i的值已经是10了。另一种理解是:对【var】变量做“预解析“,也就是说在函数执行之前,【var】变量就已经声明了但未赋值,当执行到var语句时仅仅是赋值而已。所以在函数声明局部变量时,一般都写在函数体的开头,以免影响理解,如经典案例四。

    2、经典案例二

    1 /*全局(window)域下的一段代码*/
    2 function a(i) {
    3     alert(i);
    4     alert(arguments[0]); //arguments[0]应该就是形参 i
    5     var i = 2;
    6     alert(i);
    7     alert(arguments[0]);
    8 };
    9 a(10);

    疑问:上面的代码又会输出什么呢?(10,10,2,2 )
    答案:在FireBug中的运行结果是第二个10,10,2,2,猜对了… ,下面简单说一下具体执行过程

    1. a 函数有一个形参i,调用 a 函数时传入实参 10,形参 i=10
    2. 第一个 alert 把形参 i 的值 10 输出
    3. 第二个 alert 把 arguments[0] 输出,应该也是 i
    4. 接着定义个局部变量 i 并赋值为2,这时候局部变量 i=2
    5. 第三个 alert 就把局部变量 i 的值 2 输出
    6. 第四个alert再次把 arguments[0] 输出
    7. 思考:这里能说明局部变量 i 和形参 i 的值相同吗?

    3、经典案例三

    1 /*全局(window)域下的一段代码*/
    2 function a(i) {
    3     var i = i;
    4     alert(i);
    5 };
    6 a(10);

    疑问:上面的代码又又会输出什么呢?(10 )
    答案:在FireBug中的运行结果是 10,下面简单说一下具体执行过程

    1. 第一句声明一个与形参 i 同名的局部变量 i,根据结果我们知道,后一个 i 是指向了
    2. 形参 i,所以这里就等于把形参 i 的值 10 赋了局部变量 i
    3. 第二个 alert 当然就输出 10
    4. 思考:结合案列二,这里基本能说明局部变量 i 和形参 i 指向了同一个存储地址!

    4、经典案例四

    1 /*全局(window)域下的一段代码*/
    2 var i=10;
    3 function a() {
    4     alert(i);
    5     var i = 2;
    6     alert(i);
    7 };
    8 a();

    疑问:上面的代码又会输出什么呢?
    答案:在FireBug中的运行结果是 undefined, 2,下面简单说一下具体执行过程

    1. 第一个alert输出undefined
    2. 第二个alert输出 2
    3. 思考:到底怎么回事儿?           

     

    看到上面的几个例子,你可能会弄错。原因是:我们能很快的写出一个方法,但到底方法内部是怎么执行的呢?执行的细节又是怎么样的呢?你可能没有进行过深入的学习和了解。要了解这些细节,那就需要了解 JS 引擎的工作方式,所以下面我们就把 JS 引擎对一个方法的解析过程进行一个稍微深入一些的介绍

    解析过程

    1、执行顺序

    1. 编译型语言,编译步骤分为:词法分析、语法分析、语义检查、代码优化和字节生成。
    2. 解释型语言,通过词法分析和语法分析得到语法分析树后,就可以开始解释执行了。这里是一个简单原始的关于解析过程的原理,仅作为参考,详细的解析过程(各种JS引擎还有不同)还需要更深一步的研究

      JavaScript执行过程,如果一个文档流中包含多个script代码段(用script标签分隔的js代码或引入的js文件),它们的运行顺序是:

      步骤1. 读入第一个代码段(js执行引擎并非一行一行地执行程序,而是一段一段地分析执行的)

      步骤2. 做词法分析和语法分析,有错则报语法错误(比如括号不匹配等),并跳转到步骤5

      步骤3. 对【var】变量和【function】定义做“预解析“(永远不会报错的,因为只解析正确的声明)

      步骤4. 执行代码段,有错则报错(比如变量未定义)

      步骤5. 如果还有下一个代码段,则读入下一个代码段,重复步骤2

      步骤6. 结束

    2、特殊说明
      全局域(window)域下所有JS代码可以被看成是一个“匿名方法“,它会被自动执行,而此“匿名方法“内的其它方法则是在被显示调用的时候才被执行
    3、关键步骤
      上面的过程,我们主要是分成两个阶段

    1. 解析:就是通过语法分析和预解析构造合法的语法分析树。
    2. 执行:执行具体的某个function,JS引擎在执行每个函数实例时,都会创建一个执行环境(ExecutionContext)和活动对象(activeObject)(它们属于宿主对象,与函数实例的生命周期保持一致)

    3、关键概念
      到这里,我们再更强调以下一些概念,这些概念都会在下面用一个一个的实体来表示,便于大家理解

    1. 语法分析树(SyntaxTree)可以直观地表示出这段代码的相关信息,具体的实现就是JS引擎创建了一些表,用来记录每个方法内的变量集(variables),方法集(functions)和作用域(scope)等
    2. 执行环境(ExecutionContext)可理解为一个记录当前执行的方法【外部描述信息】的对象,记录所执行方法的类型,名称,参数和活动对象(activeObject)
    3. 活动对象(activeObject)可理解为一个记录当前执行的方法【内部执行信息】的对象,记录内部变量集(variables)、内嵌函数集(functions)、实参(arguments)、作用域链(scopeChain)等执行所需信息,其中内部变量集(variables)、内嵌函数集(functions)是直接从第一步建立的语法分析树复制过来的
    4. 词法作用域:变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。 with和eval除外,所以只能说JS的作用域机制非常接近词法作用域(Lexical scope)
    5. 作用域链:词法作用域的实现机制就是作用域链(scopeChain)。作用域链是一套按名称查找(Name Lookup)的机制,首先在当前执行环境的 ActiveObject 中寻找,没找到,则顺着作用域链到父 ActiveObject 中寻找,一直找到全局调用对象(Global Object)

    4、实体表示

      

    5、函数的运行过程

    1. 建立执行环境(execution context)的阶段,函数将初始化各种变量,并将它们记录在一个内部的变量对象(variable object)中。记录在该变量对象中的变量依次有下面三种:(a)函数的实际参数;(b)内部的函数声明;(c)内部变量集。此时前面两种变量有了具体的值,内部变量集的值未undefined。
    2. 创建实参(arguments)对象,同名的实参,形参和变量之间是【引用】关系
    3. 执行方法内的赋值语句,这才会对变量集中的变量进行赋值处理
    4. 变量查找规则是首先在当前执行环境的 ActiveObject 中寻找,没找到,则顺着执行环境中属性 ScopeChain 指向的 ActiveObject 中寻找,一直到 Global Object(window)
    5. 方法执行完成后,内部变量值不会被重置,至于变量什么时候被销毁,请参考下面一条
    6. 方法内变量的生存周期取决于方法实例是否存在活动引用,如没有就销毁活动对象
    7. 6和7 是使闭包能访问到外部变量的根本原因

    6、重释经典案例

      案列一二三:根据【在一个方法中,同名的实参,形参和变量之间是引用关系,也就是JS引擎的处理是同名变量和形参都引用同一个内存地址】,所以才会有案例二中的修改arguments会影响到局部变量的情况出现

      案例四:根据【JS引擎变量查找规则,首先在当前执行环境的 ActiveObject 中寻找,没找到,则顺着执行环境中属性 ScopeChain 指向的 ActiveObject 中寻找,一直到 Global Object(window)】,所以在案例四中,因为在当前的ActiveObject中找到了有变量 i 的定义,只是值为 “undefined”,所以直接输出 “undefined” 了 

  • 相关阅读:
    LeetCode Merge Two Sorted Lists 归并排序
    LeetCode Add Binary 两个二进制数相加
    LeetCode Climbing Stairs 爬楼梯
    034 Search for a Range 搜索范围
    033 Search in Rotated Sorted Array 搜索旋转排序数组
    032 Longest Valid Parentheses 最长有效括号
    031 Next Permutation 下一个排列
    030 Substring with Concatenation of All Words 与所有单词相关联的字串
    029 Divide Two Integers 两数相除
    028 Implement strStr() 实现 strStr()
  • 原文地址:https://www.cnblogs.com/leolai/p/2553906.html
Copyright © 2020-2023  润新知