JS教程:词法作用域和闭包

  1. var classA = function(){
  2.     this.prop1 = 1;
  3. }
  4. classA.prototype.func1 = function(){
  5.     var that = this,
  6.         var1 = 2;
  7.        
  8.     function a(){
  9.         return function(){
  10.             alert(var1);
  11.             alert(this.prop1);
  12.         }.apply(that);
  13.     };
  14.     a();
  15. }
  16. var objA = new ClassA();
  17. objA.func1();

大家应该写过上面类似的代码吧,其实这里我想要表达的是有时候一个方法定义的地方和使用的地方会相隔十万八千里,那方法执行时,它能访问哪些变量,不能访问哪些变量,这个怎么判断呢?这个就是我们这次需要分析的问题—词法作用域

词法作用域:变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。 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 是同一个存储空间吗?

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,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);

疑问:上面的代码又又会输出什么呢?(( undefined 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. 思考:到底怎么回事儿?

5、经典案例五…………..N

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

解析过程

1、执行顺序

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

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

  1. 步骤1. 读入第一个代码段(js执行引擎并非一行一行地执行程序,而是一段一段地分析执行的)
  2. 步骤2. 做词法分析和语法分析,有错则报语法错误(比如括号不匹配等),并跳转到步骤5
  3. 步骤3. 对【var】变量和【function】定义做“预解析“(永远不会报错的,因为只解析正确的声明)
  4. 步骤4. 执行代码段,有错则报错(比如变量未定义)
  5. 步骤5. 如果还有下一个代码段,则读入下一个代码段,重复步骤2
  6. 步骤6. 结束

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

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

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

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

4、实体表示

解析模拟

估计,看到这儿,大家还是很朦胧吧,什么是语法分析树,语法分析树到底长什么样子,作用域链又怎么实现的,活动对象又有什么内容等等,还是不是太清晰,下面我们就通过一段实际的代码来模拟整个解析过程,我们就把语法分析树,活动对象实实在在的创建出来,理解作用域,作用域链的到底是怎么实现的
1、模拟代码

  1. /*全局(window)域下的一段代码*/
  2. var i = 1,j = 2,k = 3;
  3. function a(o,p,x,q){
  4.     var x = 4;
  5.         alert(i);
  6.     function b(r,s) {
  7.         var i = 11,y = 5;
  8.             alert(i);
  9.         function c(t){
  10.           var z = 6;
  11.                 alert(i);
  12.         };
  13.             //函数表达式
  14.         var d = function(){
  15.                 alert(y);
  16.             };
  17.             c(60);
  18.             d();
  19.     };
  20.         b(40,50);
  21. }
  22. a(10,20,30);

2、语法分析树
上面的代码很简单,就是先定义了一些全局变量和全局方法,接着在方法内再定义局部变量和局部方法,现在JS解释器读入这段代码开始解析,前面提到 JS 引擎会先通过语法分析和预解析得到语法分析树,至于语法分析树长什么样儿,都有些什么信息,下面我们以一种简单的结构:一个 JS 对象(为了清晰表示个各种对象间的引用关系,这里的只是伪对象表示,可能无法运行)来描述语法分析树(这是我们比较熟悉的,实际结构我们不去深究,肯定复杂得多,这里是为了帮助理解解析过程而特意简化)

  1. /**
  2. * 模拟建立一棵语法分析树,存储function内的变量和方法
  3. */
  4. var SyntaxTree = {
  5.         // 全局对象在语法分析树中的表示
  6.     window: {
  7.         variables:{
  8.             i:{ value:1},
  9.             j:{ value:2},
  10.             k:{ value:3}
  11.         },
  12.         functions:{
  13.             a: this.a
  14.         }
  15.     },
  16.  
  17.     a:{
  18.         variables:{
  19.             x:"undefined"
  20.         },
  21.         functions:{
  22.             b: this.b
  23.         },
  24.         scope: this.window
  25.     },
  26.  
  27.     b:{
  28.         variables:{
  29.             y:"undefined"
  30.         },
  31.         functions:{
  32.             c: this.c,
  33.             d: this.d
  34.         },
  35.         scope: this.a
  36.     },
  37.  
  38.     c:{
  39.         variables:{
  40.             z:"undefined"
  41.         },
  42.         functions:{},
  43.         scope: this.b
  44.     },
  45.  
  46.     d:{
  47.         variables:{},
  48.         functions:{},
  49.         scope: {
  50.            myname:d,
  51.            scope: this.b
  52.         }
  53.     }
  54. };

上面就是关于语法分析树的一个简单表示,正如我们前面分析的,语法分析树主要记录了每个 function 中的变量集(variables),方法集(functions)和作用域(scope)
语法分析树关键点

  • 1变量集(variables)中,只有变量定义,没有变量值,这时候的变量值全部为“undefined”
  • 2作用域(scope),根据词法作用域的特点,这个时候每个变量的作用域就已经明确了,而不会随执行时的环境而改变。【什么意思呢?就是我们经常将一个方法 return 回去,然后在另外一个方法中去执行,执行时,方法中变量的作用域是按照方法定义时的作用域走。其实这里想表达的意思就是不管你在多么复杂,多么远的地方执行该方法,最终判断方法中变量能否被访问还是得回到方法定义时的地方查证】
  • 3作用域(scope)建立规则
  • a对于函数声明和匿名函数表达式来说,[scope]就是它创建时的作用域
  • b对于有名字的函数表达式,[scope]顶端是一个新的JS对象(也就是继承了Object.prototype),这个对象有两个属性,第一个是自身的名称,第二个是定义的作用域,第一个函数名称是为了确保函数内部的代码可以无误地访问自己的函数名进行递归。

3、执行环境与活动对象
语法分析完成,开始执行代码。我们调用每一个方法的时候,JS 引擎都会自动为其建立一个执行环境和一个活动对象,它们和方法实例的生命周期保持一致,为方法执行提供必要的执行支持,针对上面的几个方法,我们这里统一为其建立了活动对象(按道理是在执行方法的时候才会生成活动对象,为了便于演示,这里一下子定义了所有方法的活动对象),具体如下:
执行环境

  1. /**
  2. * 执行环境:函数执行时创建的执行环境
  3. */
  4. var ExecutionContext = {
  5.     window: {
  6.         type: "global",
  7.         name: "global",
  8.         body: ActiveObject.window
  9.     },
  10.  
  11.     a:{
  12.         type: "function",
  13.         name: "a",
  14.         body: ActiveObject.a,
  15.         scopeChain: this.window.body
  16.     },
  17.  
  18.     b:{
  19.         type: "function",
  20.         name: "b",
  21.         body: ActiveObject.b,
  22.         scopeChain: this.a.body
  23.     },
  24.  
  25.     c:{
  26.         type: "function",
  27.         name: "c",
  28.         body: ActiveObject.c,
  29.         scopeChain: this.b.body
  30.     },
  31.  
  32.     d:{
  33.         type: "function",
  34.         name: "d",
  35.         body: ActiveObject.d,
  36.         scopeChain: this.b.body
  37.     }
  38. }

上面每一个方法的执行环境都存储了相应方法的类型(function)、方法名称(funcName)、活动对象(ActiveObject)、作用域链(scopeChain)等信息,其关键点如下:

  • body属性,直接指向当前方法的活动对象
  • scopeChain属性,作用域链,它是一个链表结构,根据语法分析树中当前方法对应的scope属性,它指向scope对应的方法的活动对象(ActivceObject),变量查找就是跟着这条链条查找的

活动对象

  1. /**
  2. * 活动对象:函数执行时创建的活动对象列表
  3. */
  4. var ActiveObject = {
  5.         window: {
  6.         variables:{
  7.             i: { value:1},
  8.             j: { value:2},
  9.             k: { value:3}
  10.         },
  11.         functions:{
  12.             a: this.a
  13.         }
  14.     },
  15.  
  16.     a:{
  17.         variables:{
  18.             x: {value:4}
  19.         },
  20.         functions:{
  21.             b: SyntaxTree.b
  22.         },
  23.         parameters:{
  24.             o: {value: 10},
  25.             p: {value: 20},
  26.             x: this.variables.x,
  27.             q: "undefined"
  28.         },
  29.         arguments:[this.parameters.o,this.parameters.p,this.parameters.x]
  30.     },
  31.  
  32.     b:{
  33.         variables:{
  34.             y:{ value:5}
  35.         },
  36.         functions:{
  37.             c: SyntaxTree.c,
  38.             d: SyntaxTree.d
  39.         },
  40.         parameters:{
  41.             r:{value:40},
  42.             s:{value:50}
  43.         },
  44.         arguments:[this.parameters.r,this.parameters.s]
  45.     },
  46.  
  47.     c:{
  48.         variables:{
  49.             z:{ value:6}
  50.         },
  51.         functions:{},
  52.         parameters:{
  53.             u:{value:70}
  54.         },
  55.         arguments:[this.parameters.u]
  56.     },
  57.  
  58.     d:{
  59.         variables:{},
  60.         functions:{},
  61.         parameters:{},
  62.         arguments:[]
  63.     }
  64. }

上面每一个活动对象都存储了相应方法的内部变量集(variables)、内嵌函数集(functions)、形参(parameters)、实参(arguments)等执行所需信息,活动对象关键点

  • 创建活动对象,从语法分析树复制方法的内部变量集(variables)和内嵌函数集(functions)
  • 方法开始执行,活动对象里的内部变量集全部被重置为 undefined
  • 创建形参(parameters)和实参(arguments)对象,同名的实参,形参和变量之间是【引用】关系
  • 执行方法内的赋值语句,这才会对变量集中的变量进行赋值处理
  • 变量查找规则是首先在当前执行环境的 ActiveObject 中寻找,没找到,则顺着执行环境中属性 ScopeChain 指向的 ActiveObject 中寻找,一直到 Global Object(window)
  • 方法执行完成后,内部变量值不会被重置,至于变量什么时候被销毁,请参考下面一条
  • 方法内变量的生存周期取决于方法实例是否存在活动引用,如没有就销毁活动对象
  • 6和7 是使闭包能访问到外部变量的根本原因

重释经典案例

案列一二三

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

案例四

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

总结

以上是我在学习和使用了JS一段时间后,为了更深入的了解它, 也为了更好的把握对它的应用, 从而在对闭包的学习过程中,自己对于词法作用域的一些理解和总结,中间可能有一些地方和真实的JS解释引擎有差异,因为我只是站在一个刚入门的前端开发人员而不是系统设计者的角度上去分析这个问题,希望能对JS开发者理解此法作用域带来一些帮助!

时间: 2024-10-01 11:46:55

JS教程:词法作用域和闭包的相关文章

javascript 词法作用域和闭包分析说明_javascript技巧

复制代码 代码如下: var classA = function(){ this.prop1 = 1; } classA.prototype.func1 = function(){ var that = this, var1 = 2; function a(){ return function(){ alert(var1); alert(this.prop1); }.apply(that); }; a(); } var objA = new ClassA(); objA.func1(); 大家应

柯里化的前生今世(六):词法作用域和闭包

关于 在上一篇中,我们介绍了动态作用域,并进行了相关名词的解释. 我们解释了什么是环境,什么是帧,如何在一个环境中对表达式求值. 我们用一个内部结构表示了函数,实现了一个微型的支持动态作用域的解释器. 这一篇,我们将再往前一步,实现词法作用域(lexical scope). 动态作用域 vs 词法作用域 作用域(scope)的概念大家可能比较陌生, 但是闭包(closure)的概念在这几年非常流行,几乎已经没有什么主流语言不支持它了. 从实现角度,和函数一样我们将用另外一种内部结构表示闭包, ;

JavaScript 中词法作用域、闭包与跳出闭包详解

Lexical Scope:词法作用域 functions are executed using the scope chain that was in effect when they were defined 一般来说,在编程语言里我们常见的变量作用域就是词法作用域与动态作用域(Dynamic Scope),绝大部分的编程语言都是使用的词法作用域.词法作用域注重的是所谓的Write-Time,即编程时的上下文,而动态作用域以及常见的this的用法,都是Run-Time,即运行时上下文.词法作

JS语法作用域与词法作用域

原文地址:http://blog.csdn.net/huli870715/article/details/6387243 <script type="text/javascript"> var ClassA = function(){ this.prop1 = 1; }; ClassA.prototype.func1 = function(){ var that = this, var1 = 2; function a(){ return function(){ alert

网易JS面试题与Javascript词法作用域说明_javascript技巧

调用对象位于作用域链的前端,局部变量(在函数内部用var声明的变量).函数参数及Arguments对象都在函数内的作用域中--这意味着它们隐藏了作用域链更上层的任何同名的属性. 2010年9月14日,我去参加网易网页工程师招聘会,应聘JS工程师职位.有幸参加笔试,然后有幸栽在笔试,呵呵.废话少说,抓出音响极深的一题重新研究研究. 题目大概是:写出如下代码的输出结果并进行分析 复制代码 代码如下: var tt = 'aa'; function test(){ alert(tt); var tt

几句话带你理解JS中的this、闭包、原型链_javascript技巧

原型链 所有对象都是基于Object.prototype,Object.prototype就是JavaScript的根对象,在Object.prototype中定义的方法都可以被其它对象访问到,当然也可以被重写了,所以直接在Object.prototype上调用的是原始功能的toString()方法,该方法会放回参数对象的内置属性[[class]]的值,这个值是个字符串,比如'[Object String]' 要理解原型链机制的话,首先得知道根本原因:JavaScript中的对象都有一个内置属性

JavaScript作用域和闭包

作用域和闭包在JavaScript里非常重要.但是在我最初学习JavaScript的时候,却很难理解.这篇文章会用一些例子帮你理解它们. 我们先从作用域开始. 作用域 JavaScript的作用域限定了你可以访问哪些变量.有两种作用域:全局作用域,局部作用域. 全局作用域 在所有函数声明或者大括号之外定义的变量,都在全局作用域里. 不过这个规则只在浏览器中运行的JavaScript里有效.如果你在Node.js里,那么全局作用域里的变量就不一样了,不过这篇文章不讨论Node.js. `const

[JavaScript]JavaScript高级之词法作用域和作用域链

主要内容: 分析JavaScript的词法作用域的含义 解析变量的作用域链 变量名提升时什么 一.关于块级作用域         说到JavaScript的变量作用域,与咱们平时使用的类C语言不同. 例如C#中下面代码: static void Main(string[] args) {         if(true)         {                 int num = 10;         }         System.Console.WriteLine(num);

深入理解JavaScript高级之词法作用域和作用域链_基础知识

主要内容:1.分析JavaScript的词法作用域的含义 2.解析变量的作用域链 3.变量名提升时什么 最近在传智播客讲解JavaScript的课程,有不少朋友觉得JavaScript是如此的简单,但是又如此的不知如何使用,因此我准备了一些内容给大家分享一下. 这个系列主要讲解JavaScript的高级部分的内容,包括作用域链.闭包.函数调用模式.原型以及面向对象的一些东西. 在这里不包含JavaScript的基本语法,如果需要了解基础的同学可以到http://net.itcast.cn里面去下