zoukankan      html  css  js  c++  java
  • 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” 了 

  • 相关阅读:
    TeamViewer的替代品:realVNC
    Introduction of Generator in Python
    Excel: assign label to scatter chart using specific cell values
    reverse/inverse a mapping but with multiple values for each key
    虚拟化与云计算
    现代计算机简介
    CentOS 7 安装中网络设置111
    机械硬盘原理
    文件系统
    最重要的块设备——硬盘
  • 原文地址:https://www.cnblogs.com/leolai/p/2553906.html
Copyright © 2011-2022 走看看