以理解闭包为终点

疑惑一:JS有没有预编译 / 编译?
疑惑二:变量对象和活动对象的关系?

一、从**执行环境**开始
二、明确**window对象**
三、**变量对象**怎么样
四、从 **作用域** 到 **编译原理** 再到 **词法作用域**
五、先谈**作用域链**
六、再谈**垃圾收集**
七、终于等到**闭包**



一、从**执行环境**开始

《JavaScript高级程序设计》(第三版)

  1. 执行环境(Execution Context,EC,也叫执行上下文)是JavaScript中最为重要的一个概念。

读者注:
我们完全可以把执行环境类比于自己的生活环境——在家里是一个执行环境,在学校里也是一个执行环境,在公司又是一个执行环境——这些执行环境是相互独立的,还是嵌套的,取决于个人生活本身。对于生活在地球上的我们来说,地球完全能够作为我们的全局执行环境——不过,你怎么知道这个全局执行环境不是另一个更大的执行环境的局部呢?

  1. 全局执行环境是最外围的一个执行环境。根据ECMAScript实现所在的宿主环境不同,表示全局执行环境的对象也不一样。  在Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。
  2. 执行环境中的所有代码执行完毕后,该执行环境被销毁,保存在其中的所有变量和函数定义也随之销毁——全局执行环境直到应用程序退出(如关闭网页或浏览器)时才会被销毁。
  3. 每个函数都有自己的执行环境。
    (1)当JavaScript解释器初始化执行代码时,首先默认进入全局执行环境。从此刻开始,函数的每次调用都会创建一个新的执行环境。
    (2)每一个执行环境都会创建一个新的环境对象。当执行流进入一个函数时,函数的环境对象就会被推入一个执行环境栈(execution stack)中。 在函数执行完成之后,执行环境栈将该函数的执行环境对象弹出,并把控制权返回给之前的执行环境——ECMAScript 程序的执行流机制。
二、明确**window对象**

《JavaScript高级程序设计》(第三版)

  1. 在所有代码执行之前,作用域中就已经存在两个内置对象:Global和Math。
  2. Global(全局)对象ECMAScript中最特别的一个对象——不管从什么角度上看,Global对象都是不存在的。
  3. ECMASCript 中的 Global 对象在某种意义上是作为一个终极的 “兜底儿对象” 来定义的——事实上,没有全局变量或全局函数,所有在全局作用域中定义的属性和函数,都是Global对象的属性。
  4. 在大多数ECMAScript实现中,都不能直接访问Global对象。在浏览器中,window对象承担了Global对象的角色——全局变量和函数都是Global对象的属性。
  5. 在浏览器中,window对象有双重角色,它既是通过JavaScript访问浏览器窗口的一个接口,又是ECMAScript规定的Global对象。

《JavaScript权威指南》(第六版)
  JavaScript 全局变量是全局对象的属性,这是在ECMAScript 规范中强制规定的

《ES6标准入门》(第三版)

  1. ES 5顶层对象在各种环境下的实现是不统一的:
    (1)顶层对象在浏览器环境中,指的是 window 对象;
    (2)顶层对象在Node环境中,指的是 global 对象。
  2. 顶层对象的属性与全局变量相关,被认为是JavaScript语言中最大的设计败笔之一。
  3. ES 6 为了改变这一点,
    (1)一方面,为了保持兼容性,var 命令和function 命令声明的全局变量依旧是顶层对象的属性;
    (2)另一方面,规定:let 命令、const 命令、class 命令声明的全局变量不属于顶层对象的属性
    ——也就是说,从ES 6 开始,全局变量将逐步与顶层对象的属性隔离。
  4. 现在有一个提案,在语言标准的层面引入 global 作为顶层对象——也就是说,在所有环境下,global 都是存在的,都可以拿到顶层对象。
	var a = 1;
	console.log(window.a);  /* 1 */
	
	let b = 1;
	console.log(window.b);  /* undefined */
三、**变量对象**怎么样

《JavaScript权威指南》(第六版)

  1. 局部变量没有如 “全局变量是全局对象的属性” 的规定,但我们完全可以把局部变量当做跟函数调用相关的某个对象的属性:
    (1)在 ECMAScript 3 规范中称该对象为 “调用对象”(call object)
    (2)在ECMAScript 5 规范中称该对象为 “声明上下文对象”(declarative environment record)。
  2. JavaScript 可以允许使用 this 关键字来引用全局对象,却没有方法可以引用局部变量中存放的对象——这种存放局部变量的对象的特有性质,是一种对我们不可见的内部实现。

《JavaScript高级程序设计》(第三版)

  1. 每个执行环境都有一个与之关联的变量对象(Variable Object,VO),执行环境中定义的所有变量和函数都保存在变量对象中编写的代码无法访问变量对象,但 JavaScript 解析器在处理数据时会在后台使用变量对象。
  2. 如果把一个变量对象当做是普通的ECMAScript对象(VO),那么变量对象就是执行环境的一个属性。在全局执行环境中,全局对象自身就是变量对象。
    当然,变量对象只是内部机制的一个实现,
四、从 **作用域** 到 **编译原理** 再到 **词法作用域**

《JavaScript权威指南》(第六版)&《你不知道的JavaScript》(上卷)

  1. 一个变量的作用域(scope)是程序源代码中定义这个变量的区域。作用域规定了如何根据标识符名称进行变量查找,也就是确定当前执行代码对变量的访问权限
    (1)全局(global)变量拥有全局作用域,在JavaScript代码中的任何地方都有定义;
    (2)局部(local)变量(如函数内声明的变量和函数参数)拥有局部作用域——在函数体内,局部变量的优先级高于同名的全局变量;  对于嵌套函数的变量,其作用域一般是变量声明所在的函数体内以及其嵌套的函数体内(父级或父级以上的函数体内不起作用)。
  2. 传统编译语言的编译原理:

(1)分词(Tokenizing) / 词法分析(Lexing) 阶段
  将由字符组成的字符串分解成对编程语言来说有意义的代码块——这些代码块被称为词法单元(token)。对于词法分析来说,词法单元生成器通过有状态的解析规则识别词法单元,并且赋予单词以编程语言特有的语义。
(2)代码生成阶段
  将词法单元流(数组)转换成 “抽象语法树”(Abstract Synax Tree,SAT)——一个由元素逐级嵌套所组成的代表程序语法结构的树。
(3)解析 / 语法分析(Parsing)阶段
  将抽象代码树(AST)转换为可执行的代码,这个过程与编程语言、目标平台等息息相关。

具体分析:var a = 2;
第一阶段:分解成词法单元——(var)、(a)、(=)、(2)、(;)。
  空格是否会被当作词法单元,取决于空格在这门语言中是否具有意义。
第二阶段:转换成抽象语法树——
  (1. 顶级节点:VariableDeclaration 变量声明)、
  (1.1 子节点:Identifier 标识符值为a)、(1.2 子节点:AssignmentExpression 赋值表达式)、
  (1.2.1 子节点:NumericLiteral 数字字面量值为2)
第三阶段:转换成可执行代码——抛开具体细节,简单来说就是有某种方法可以将 var a = 2; 的抽象语法树转化为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中。

  1. 作用域主要有两种工作模型:词法作用域和动态作用域。
    词法作用域(lexical scoping),简单地说就是定义在词法分析阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。
  2. JavaScript是基于词法作用域的语言:通过阅读包含变量定义和函数定义在内的数行源码就能知道其作用域。换句话说,代码在编写过程中作用域就已经体现出来,而不用等到执行代码时才能确定。
五、先谈**作用域链**

《JavaScript高级程序设计》(第三版) & 《你不知道的JavaScript》(上卷)

  1. 当代码在一个执行环境中执行时,会创建变量对象的一个作用域链
  2. 作用域链本质上是一个指向变量对象的指针对象列表或链表(只引用而不实际包含变量对象)。
  3. 作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。
  4. 作用域链的组成
    (1)作用域链的前端,始终都是当前执行的代码所在执行环境的变量对象的引用。
    (2)如果执行环境是某个函数,则将其活动对象(Activation Object,AO)作为变量对象——活动对象在最开始时只包含一个变量,即arguments变量(这个对象在全局环境中是不存在的)
    (3)作用域链中的下一个变量对象的引用来自包含(外部)环境,而再下一个变量对象的引用则来自下一个包含环境。如此这般,一直延续到全局执行环境。
    (4)全局执行环境的变量对象的引用始终都是作用域链的末端。
  5. 标识符解析(又称作用域查找)中的作用域链:标识符解析就是沿着作用域链一级一级地搜索标识符的过程——标识符解析始终从作用域链的前端(即当前执行环境)开始搜索,然后逐级地向后回溯,直至找到第一个匹配的标识符为止;如果在作用域链的末端(即全局执行环境)也没有找到,则抛出一个引用错误(ReferenceError)异常。

标识符解析 / 作用域查找中的 LHS 和 RHS :
(1)LHS和RHS是JavaScrip引擎的两种查找类型
(2)LHS:Left Hand Side,表示赋值操作的左侧;RHS:Right Hand Side,表示赋值操作的右侧。
(3)注意,赋值操作并不狭义地意味着赋值操作符("="),赋值操作还有其他的形式。
(4)LHS 试图找到变量的容器本身,从而可以对其赋值。例如,a = 2; 变量在赋值操作的左侧,我们只想为 “=2” 这个赋值操作找到一个目标,而不关心当前的值是什么。
(5)RHS 与简单地查找某个变量的值别无二致,可以理解为 retrieve his value(获取源值)。比如,console.log( a ); 可以看出,我们并没有赋予变量 a 以任何值;相反,我们需要查找并取得 a 的值,并将其传递给 console.log( … )。
(6)具体分析:

   function foo(a){
   var b = a;
   return a + b;
	}
	var c = foo(2);

  ① var c = foo(2);
    首先对foo进行RHS查询,在作用域中找到编译器声明的foo的源值,随后对其进行函数引用;
    然后对形参 a = 2 的隐式赋值进行LHS查询,在作用域中为 “=2” 找到一个目标。
  ② var b = a;
    首先对a进行RHS查询,找到其源值为2;
    然后对b进行LHS查询,为 “=a” 找到一个目标。
  ③ return a + b; => var c = a + b;
    首先对 a 和 b 进行 RHS查询,找到其源值;
    然后对 c 进行 LHS查询,为 “= a + b” 找到一个目标。
  总结——3处LHS查询,4处RHS查询
(7)LHS 和 RHS 在任何作用域中都无法找到该变量(变量还没有声明)时的行为表现:
——RHS查询会让JavaScript引擎抛出ReferenceError异常;
——LHS查询在非严格模式下会让JavaScript引擎在全局作用域中创建一个具有该名称的变量,并将其返回给引擎,在严格模式下因为禁止自动或隐式地创建全局变量,因此JavaScript引擎会抛出ReferenceError异常;
(8)如果RHS查询找到了该变量,但是对这个变量的值进行了不合理的操作(比如,对一个非函数类型的值进行函数调用,或者引用 null 或 undefined 类型的值中的属性),JavaScript引擎会抛出TypeError异常。

  1. 函数定义和函数调用中的作用域链:
    (1)定义一个全局函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的 [[Scope]] 属性中。
    (2)调用一个全局函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起当前函数执行环境的作用域链。  然后,使用arguments和其他命名参数的值来初始化函数的活动对象,并将该活动对象(在此作为变量对象使用)推入当前函数执行环境作用域链的前端。
六、再谈**垃圾收集**

《JavaScript高级程序设计》(第三版)

  1. JavaScript 具有自动垃圾收集机制。也就是说,执行环境会负责管理代码执行过程中使用的内存——所需内存的分配以及无用内存的回收完全实现了自动管理。
  2. 垃圾收集机制的原理:
      找出那些不再继续使用的变量,然后释放其占用的内存——为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间)周期性地执行(找出无用变量和释放占用内存)这一操作。
  3. 函数中局部变量的正常生命周期:
    (1)局部变量只在函数执行的过程中存在。在这个过程中,会为局部变量在栈(或堆)内存中分配相应的空间,以便存储它们的值——基本数据类型把数据名和值直接存储在栈(stack)中,而复杂数据类型在栈中存储数据名和一个堆(heap)的地址,在这个地址指向的堆中存储属性及值
    (2)在函数执行的过程中会使用这些局部变量,直至函数执行结束。此时,局部变量就没有存在的必要了,因此可以释放它们的内存以供将来使用。
  4. 事实上,并非所有情况下都很容易判断变量是否还有存在的必要——为此,垃圾收集器必须跟踪哪个变量有用哪个变量没用——对于不再有用的变量打上标记,以备将来收回其占用的内存。
  5. 用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,通常有标记清除引用计数两个策略。
  6. JavaScript最常用的垃圾收集方式:标记清除(mark-and-sweep)
    (1)当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为 “进入环境”——从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。
    (2)当变量离开环境时,则将其标记为 “离开环境”。
    (3)可以使用任何方式来标记变量——比如,通过翻转某个特殊的位来记录一个变量何时进入环境,或者使用一个 “进入环境的” 变量列表及一个 “离开环境的” 变量列表来跟踪哪个变量发生了变化——说到底,如何标记变量其实并不重要,关键在于采取什么策略。
    (4)标记清除策略下垃圾收集器的运行
    ①垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记
    ②然后,垃圾收集器会去掉环境中的变量以及被环境中的变量引用的变量的标记
    ③在此之后,再被加上标记的变量将被视为准备删除的变量——原因是环境中的变量已经无法访问到这些变量了
    ④最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
    (5)到2008年为止,IE、Firefox、Opera、Chrome和Safari的JavaScript实现使用的都是标记清除式的垃圾收集策略(或类似的策略),只不过垃圾收集的时间间隔互有不同。
  7. JavaScript另一种不太常见的垃圾收集策略:引用计数(reference counting)
    (1)引用计数的含义是跟踪记录每个值被引用的次数
    (2)当声明了一个变量,并且将一个引用类型值赋给该变量是,则这个值的引用次数就是1。
    (3)如果同一个值又被赋给另一个变量,则该值的引用次数加1。
    (4)相反,如果包含这个值引用的变量又取得了另外一个值,则这个值的引用次数减1.
    (5)当这个值的引用次数变成0时,则说明没有办法再访问这个之,因而就可以将其占用的内存空间回收回来。
    (6)这样,当垃圾收集器下次再运行时,他就会释放那些引用次数为0的值所占用的内存。
七、终于等到**闭包**

《你不知道的JavaScript》(上卷)

  1. 闭包的实质:即使函数在词法作用域之外执行,也可以记住并访问所在的词法作用域

读者注:
理论联系实际,我们完全可以这样看待闭包:身在曹营心在汉——① 内层函数保持对外层函数的引用;② 内层函数和其上层执行上下文共同构成闭包;(身在曹营);③ 函数在其定义的作用域外进行访问(心在汉)。因此,我们不妨 以 “间谍” 手段、以浏览器(Chrome)为评判 来分析一下闭包。

注解:
曹营——外层函数词法作用域
曹敌——外层函数词法作用域中不构成闭包的其他代码
间谍——构成闭包的函数
间谍目的——暴露曹营(即外层函数的词法作用域)

(一)间谍手段一:反间计——用计谋离间敌人引起内讧
	function foo(){
	    var a = 2; 
	    function bar(){
	        console.log(a);
	    } 
	    bar();
	}
	foo();

直接通过在 foo 的词法作用域内的代码调用 bar() 函数,获得在 foo 词法作用域内的变量 a 的 RHS 引用。

(二)间谍手段二:瞒天过海——光天化日之下不让上天知道,就过了大海
    function foo(){
        var a = 2;
        function bar(){
            console.log(a);
        }
        return bar;
    }

    var baz = foo();
    baz();

瞒过 foo 的词法作用域,通过 return 将 bar 所引用的函数对象本身传递出去。

(三)间谍手段三:暗度陈仓——暗中进行突然袭击
	function bar(fn){
	    fn();
	}
	function foo(){
	    var a = 2;
	    function baz(){
	        console.log(a);
	    }
	    bar(baz);
	}
	
	foo();

明面上调用全局函数 bar(),暗中把 baz 所引用的函数对象本身传递出去。

(四)间谍手段四:假道伐虢——以借路为名,实际上要侵占该国领土
	var fn;
    function foo(){
        var a = 2;
        function baz(){
            console.log(a);
        }
        fn = baz;
    }
    function bar(){
        fn();
    }

    foo();
    bar();

调用 foo() 时,以全局变量 fn=baz 赋值为名,实际上在 bar() 函数中调用了 fn()。

  1. 为了实现词法作用域,JavaScript 函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链
    (未完待续)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值