[ffxixslh] 你不知道的JavaScript - 作用域和闭包 - 读书笔记 (1)

1 作用域

1.1 编译流程

  • 分词/词法分析(Tokenizing):
    将由字符组成的字符串分解成有意义的代码块,这些代码块称为词法单元(token)

  • 解析/语法分析(Parsing):
    将词法单元流(数组)转换成“抽象语法树”(Abstract Syntax Tree, AST)

  • 代码生成:
    将 AST 转换为可执行代码

1.2 处理角色

  • 引擎
    负责整个JavaScript程序的编译及执行过程。

  • 编译器
    负责语法分析及代码生成。

  • 作用域
    负责收集并维护由所有什么的标识符(变量)组成的一系列查询,并实施一套严格的规则,缺点当前执行的代码对这些标识符的访问权限。

1.3 处理流程

例如,将 var a = 2;分解:

  1. 编译器将这段程序分解成词法单元,然后将词法单元解析成AST

  2. 为一个变量分配内存,将其命名为 a ,然后将值 2 保存进这个变量。

该步骤称为 编译过程

  1. 遇到 var a,编译器询问作用域是否由有个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为 a 。

  2. 生成引擎运行所需的代码,代码被用来处理 a = 2 这个赋值操作。引擎运行时首先询问作用域是否在当前的作用域集合中存在一个叫 a 的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续寻找该变量。

  3. 如果最终找到 a 变量,将值 2 赋值给它,否则引擎会抛出一个异常。

1.4 LHS & RHS

在编译器生成代码的过程中,引擎会(通过作用域)为变量 a 进行LHS查询。另一个查找的类型叫做RHS

L? R? 表示为一个赋值操作的左侧右侧

LHS可以理解为查询容器,而RHS理解为获取源值

例如:

console.log(a); // RHS,查询 a 的源值

a = 2; // LHS,查询可以进行 "= 2" 这一操作的容器

LHS 和 RHS 的含义是“赋值操作的左侧或右侧”,并不一定意味着就是“‘=’赋值操作符的左侧或右侧”。 赋值操作还有其他几种形式, 因此在概念上最好将其理解为“赋值操作的目标是谁(LHS)” 以及“谁是赋值操作的源头(RHS)”。

1.5 小结

  • 作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。

  • = 操作符会调用函数传入参数的操作都会导致关联作用域的赋值操作。

  • JavaScript 引擎首先会在代码执行前对其进行编译, 在这个过程中, 像 var a = 2 这样的声明会被分解成两个独立的步骤:

    1. 首先,var a在其作用域中声明型变量。这会在最开始的阶段,也就是代码执行前进行。

    2. 然后, a = 2会查询(LHS查询)变量 a 并对其进行赋值。

  • LHS 和 RHS 查询都会在当前执行作用域中开始,如果查找不到所需要的标识符,就会向上级作用域继续寻找没目标标识符,这样每次上升一级作用域,知道最后抵达全局作用域,无论找没找到都将停止。

  • 在非严格模式下,不成功的 LHS 引用会导致自动隐式地创建一个全局变量(值为undefined),而严格模式下会抛出 ReferenceError 异常;不成功的 RHS 引用则会导致抛出 ReferenceError 异常(通常抛出 ReferenceError 与作用域判别失败相关)


2 词法作用域

2.1 词法阶段

大部分标准语言编译器的第一个工作阶段叫做词法化(单词化)。词法化的过程会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义。

词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域就是由你在写代码时变量和块作用域写在哪里来决定的, 因此当词法分析器处理代码时会保持作用域不变。

2.2.1 查找

作用域查找会在找到第一个匹配的标识符时停止。 作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者向上进行,直到遇见第一个匹配的标识符为止。

无论函数在 哪里 被调用,也无论它 如何 被调用,它的词法作用域都 只由 函数被声明时所处的位置决定。

词法作用域查找 只会 查找一级标识符,如果代码中引用了 foo.bar.baz ,词法作用域只会试图查找 foo 标识符,找到这个变量后,对象属性访问规则会分别接管对 bar 和 baz 属性的访问。

2.2.2 欺骗词法

不推荐使用的最重要的点:欺骗词法作用域会导致性能下降。

另外一个不推荐使用的原因:它们会被严格模式所影响(限制)。在严格模式下, with 被完全禁止, 而在保留核心功能的前提下, 间接或非安全地使用 eval(…) 也被禁止了。

eval函数

JavaScript 中的 eval(…) 函数可以接受一个字符串为参数, 并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。 换句话说, 可以在你写的代码中用程序生成代码并运行, 就好像代码是写在那个位置的一样。

在执行 eval(…) 之后的代码时, 引擎并不 “知道” 或 “在意” 前面的代码是以动态形式插入进来, 并对词法作用域的环境进行修改的。 引擎只会如往常地进行词法作用域查找。

function foo(str, a) {
    eval(str); // 欺骗!
    console.log(a, b); // 1, 3
}
var b = 2;
foo('var b = 3;', 1); 

在实际情况中, 可以非常容易地根据程序逻辑动态地将字符拼接在一起之后再传递进去, 所以, eval(…) 通常被用来执行动态创建的代码,

默认情况下, 如果 eval(..) 中所执行的代码包含一个或多个声明(无论是变量还是函数),就会对 eval(..) 所在的词法作用域进行修改。

with

with 通常被当作重复引用同一个对象中的多个属性的快捷方式,可以 不需要 重复引用对象的本身。

with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符。

尽管 with 块可以将一个对象处理为词法作用域,但是这个块内部正常的 var 声明并不会被限制在这个块的作用域中,而是被添加到到 with 所处的函数作用域中。

在非严格模式下,当执行赋值语句时,正常的 LHS 查找都找不到所需要的变量/属性的话,会自动地在全局作用域中创建该变量/属性,并赋值给它。

区别

eval(…) 函数如果接受了含有一个或多个声明的代码, 就会修改其所处的词法作用域, 而 with 声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域。

2.2 性能

JavaScript 引擎会在编译阶段进行数项的性能优化。 其中有些优化依赖于能够根据代码的词法进行静态分析, 并预先确定所有变量和函数的定义位置, 才能在执行过程中快速找到标识符。

如果引擎在代码中发现了 eval(…) 或 with, 它只能简单地 假设 关于标识符位置的判断都是无效的, 因为无法在词法分析阶段明确知道 eval(…) 会接收到什么代码, 这些代码会如何对作用域进行修改, 也无法知道传递给 with 用来创建新词法作用域的对象的内容到底是什么。

如果代码中大量使用 eval(…) 或 with, 那么运行起来一定会变得非常慢。 无论引擎多聪明, 试图将这些悲观情况的副作用限制在最小范围内, 也无法避免如果没有这些优化, 代码会运行得更慢这个事实。

2.3 小结

  • 词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。

  • 编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。

  • 在程序运行时可以 “欺骗” 词法作用域的两个机制: eval(..)with
    前者可以对一个或多个声明的 “代码” 字符串进行演算,并以此来修改已存在的词法作用域;
    后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域的标识符来处理,从而创建了一个性的词法作用域。

  • 两个机制会导致引擎无法在编译时对作用域查找进行优化,所以最好不要使用它们。


3 函数作用域和块作用域

3.1 函数中的作用域

属于这个函数的全部变量都可以在整个函数的范围内使用及复用(在嵌套的作用域中也可以使用)。

3.2 隐藏内部实现

从所写的代码中选出一个任意的片段,然后用函数声明对它包装,实际的结果就是在这个代码片段的周围创建了一个作用域气泡,在代码中的任何变量和函数声明都绑定在这个新创建的函数的作用域中,实现了 “隐藏” 的功能。

最小授权原则:指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都 “隐藏” 起来,比如某个模块或对象的 API 设计。

这个原则可以保证 “私有” 内容的 “访问特权” ,即不被外部作用域影响

规避冲突

“隐藏” 作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突。可以通过声明一个本地变量来满足要求,也可以采用一个完全不同的标识符名称解决。

1.全局命名空间

为解决引进多个第三方库导致变量冲突的问题,这些第三方库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。

这个对象被用作库的 命名空间 ,所以需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将所有标识符暴露在顶级的词法作用域中。

2.模块管理

避免冲突的办法和现代的 模块 机制很接近,就是从众多的管理器中挑选一个来使用。通过依赖管理器的机制将库的标识符显式地导入到另外一个特定的作用域中。

这些管理器利用作用域的规则强制所有标识符都不能注入到共享作用域中,而是保持在私有、无冲突的作用域中,可以有效规避所有的意外冲突。

3.3 函数作用域

给任意代码片段添加包装函数可以将内部的变量和函数定义给 “隐藏” 起来,但会导致一些额外的问题。首先,需要声明一个具名函数,这个具名函数的名称本身 “污染” 了这个作用域,其次,要显式地调用改函数才能运行代码。

以下函数表达式可以自动执行:

var a = 2;

(function foo() { // <--注意圆括号
    var a = 3;
    console.log( a ); // 3
})(); // <-- 需要调用该函数表达式

console.log( a ); // 2

区分函数声明和表达式的方法:

function 关键字出现的位置,如果是第一个词,那么就是一个函数声明;否则就是一个函数表达式。

3.3.1 匿名和具名

setTimeout(function() {
    console.log('I waited 1 second');
}) 

例子中,setTimeout 的回调参数没有名称,叫做 匿名函数表达式 ,因为 function().. 没有名称标识符。

函数表达式可以是匿名的,但函数声明则不可以省略函数名。

匿名函数表达式也有缺点:

  1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试困难;

  2. 因为没有函数名,函数应用自身时只能使用已经过期的 arguments.callee 引用;

  3. 匿名函数表达式降低了代码可读性;

行内函数表达式非常强大且有用——匿名和具名之间的区别并不会对这点有任何影响。 给函数表达式指定一个函数名可以有效解决以上问题。

3.3.2 立即执行函数表达式(IIFE)

var a = 2;

(function foo() { 
    var a = 3;
    console.log( a ); // 3
})();

console.log( a ); // 2

由于函数被包含在一对 ( ) 括号内部, 因此成为了一个表达式, 通过在末尾加上另外一个 ( ) 可以立即执行这个函数, 比如 (function foo(){ .. })()。 第一个 ( ) 将函数变成表达式, 第二个 ( ) 执行了这个函数。

IIFE的另外一种形式:(function(){ .. }())

这两种形式在功能上是一致的。 选择哪个全凭个人喜好。

进阶用法:

  1. 把它当作函数调用并传递参数进去,例如:
var a = 2;  
(function IIFE( global ) {  
    var a = 3;  
    console.log( a ); // 3  
    console.log( global.a ); // 2  
})( window );  
console.log( a ); // 2

我们将 window 对象的引用传递进去, 但将参数命名为 global, 因此在代码风格上对全局对象的引用变得比引用一个没有“全局” 字样的变量更加清晰。 当然可以从外部作用域传递任何你需要的东西, 并将变量命名为任何你觉得合适的名字。 这对于改进代码风格是非常有帮助的。

  1. 解决 undefined 标识符的默认值被错误覆盖导致的异常

将一个参数命名为 undefined, 但是在对应的位置不传入任何值, 这样就可以保证在代码块中 undefined 标识符的值真的是 undefined:

undefined = true; // 给其他代码挖了一个大坑! 绝对不要这样做!  
(function IIFE( undefined ) {  
    var a;  
    if (a === undefined) {  
        console.log( "Undefined is safe here!" );  
    }  
})();
  1. 倒置代码的运行顺序, 将需要运行的函数放在第二位, 在 IIFE 执行之后当作参数传递进去。
var a = 2;
(function IIFE(def) {
    def( window );
})(function def( global )) {
    var a = 3;
    console.log( a ); // 3
    console.log( global.a ); // 2  
})

这种模式在 UMD(Universal Module Definition) 项目中被广泛使用。

函数表达式 def 定义在片段的第二部分, 然后当作参数(这个参数也叫作 def) 被传递进 IIFE 函数定义的第一部分中。 最后, 参数 def( 也就是传递进去的函数) 被调用, 并将 window 传入当作 global 参数的值。

3.4 块作用域

如果在 { } 内部定义一个变量,那么该变量只在{ }内部(变量声明时的上下文)才能使用。但是用 var 声明的变量,它写在哪里都是一样的,因为它们最终都会属于外部作用域。

块作用域是一个用来对 最小授权原则 进行扩展的工具, 将代码从在函数中隐藏信息扩展为在块中隐藏信息。

3.4.1 with

with从对象中创建出的作用域仅在with声明中而非外部作用域中有效。

3.4.2 try/catch

try/catchcatch分句会创建一个块作用域,其中声明的变量仅在catch内部有效。

3.4.3 let

let 关键字可以将变量绑定到所在的任意作用域中(通常是 { .. } 内部)。 换句话说, let为其声明的变量隐式地附加到了所在的块作用域。

只要声明是有效的, 在声明中的任意位置都可以使用 { … } 括号来为 let 创建一个用于绑定的块。

块作用域的用途

  1. 垃圾收集

块作用域可以让引擎清楚地知道有没必要继续保留一个可能存在的数据。

function process(data) {
    // 在这里做点有趣的事情
} 
// 在这个块中定义的内容可以销毁了!
{
    let someReallyBigData = { .. };
    process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
    console.log("button clicked");
}, /*capturingPhase=*/false );

为变量显式声明块作用域, 并对变量进行本地绑定是非常有用的工具, 可以把它添加到你的代码工具箱中了。

  1. let 循环
for (let i = 0; i < 10; i++) {
    console.log(i);
}
console.log(i); // ReferenceError

循环头部的let将 i 绑定到了 for 循环的块中,事实上它将其重新绑定到了循环的每一个 迭代 中,确保使用上一个循环迭代结束时的值重新进行赋值。

let 声明附属于一个新的作用域而不是当前的函数作用域( 也不属于全局作用域),代码中存在对于函数作用域中 var 声明的隐式依赖时, 就会有很多隐藏的陷阱, 如果用let 来替代 var 则需要在代码重构的过程中付出额外的精力。

3.4.4 const

const声明的值是固定的(常量),任何试图修改值的操作都会引起错误。

3.5 小结

  • 声明在一个函数内部的变量或函数会在所处的作用域中“隐藏” 起来, 这是有意为之的良好软件的设计原则。

  • 块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指 { … } 内部)。

  • 从 ES3 开始, try/catch 结构在 catch 分句中具有块作用域。

  • let会声明一个劫持了其所在块的变量,并将变量添加到这个块中。

  • 块作用域和函数作用域都是创建可读、可维护的优良代码的方案。


4 提升

函数作用域和块作用域的行为是一样的,即任何声明在某个作用域内的变量,都将附属于这个作用域.

4.1 变量提升

变量和函数在内的所有声明都会在任何代码被执行前首先被处理

例如, var a = 2会看成两个声明:var a;a = 2;

var a;
a = 2;
console.log( a );

其中第一部分是编译,而第二部分是执行。

这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动”到了最上面。 这个过程就叫作提升

只有声明本身会被提升, 而赋值或其他运行逻辑会留在原地。 如果提升改变了代码执行的顺序, 会造成非常严重的破坏。

// 1.
foo();
function foo() {    
    console.log( a ); // undefined
    var a = 2;
}


// equals 1
foo();
function foo() {
    var a;    
    console.log( a ); // undefined
    a = 2;
}


// 2.
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar(){ 
    // ... 
}

// equals 2
var foo;

foo(); // TypeError
bar(); // ReferenceError


foo = function() {
    var bar = ...self...
    // ...
}

可以看到, 函数声明会被提升, 但是函数表达式却不会被提升。

foo() 由于对 undefined 值进行函数调用而导致非法操作,因此抛出 TypeError 异常。即使是具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用。

4.2 函数优先

函数声明和变量声明都会被提升。但是函数会首先被提升,然后才是变量。

例如:

foo(); // 1

var foo;

function foo() {
    console.log(1);
}

foo = function() {
    console.log(2);
};


// equals
function foo() {
    console.log(1);
}

foo(); // 1

foo = function() {
    console.log(2);
};

var foo 尽管出现在 function foo()... 的声明之前, 但它是重复的声明(因此被忽略了), 因为函数声明会被提升到普通变量之前。

尽管重复的 var 声明会被忽略掉, 但出现在后面的函数声明还是可以覆盖前面的。

foo(); // 3
function foo() {    
    console.log( 1 );
}
var foo = function() {
    console.log( 2 );
};
function foo() {
    console.log( 3 );
}

一个普通块内部的函数声明通常会被提升到所在作用域的顶部, 这个过程不会像下面的代码暗示的那样可以被条件判断所控制:

foo(); // 'b'

var a = true;
if(a) {
    function foo() { console.log('a') };
} else {
    function foo() { console.log('b') };
}

即它会不管执行顺序而是先提升函数声明。

4.3 小结

  • var a = 2;实际上是两个单独的声明:var aa = 2,第一个是编译阶段的任务,第二个是执行阶段的任务;

  • 无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理,即提升,而函数表达式的赋值操作不会被提升;

  • 函数的重复声明会覆盖之前的声明;


5 作用域闭包

5.1 启示

闭包是基于词法作用域书写代码时所产生的自然结果, 你甚至不需要为了利用它们而有意识地创建闭包。 闭包的创建和使用在你的代码中随处可见。 你缺少的是根据你自己的意愿来识别、 拥抱和影响闭包的思维环境。

5.2 实质

定义:当函数可以记住并访问所在的词法作用域时, 就产生了闭包, 即使函数是在当前词法作用域之外执行。

下面代码清晰地展示了闭包:

function foo() {
    var a = 2;
    function bar() {
        console.log( a );
    }
    return bar;
}
var baz = foo();
baz(); // 2 —— 朋友, 这就是闭包的效果。

以上代码可以描述成如下操作:

  1. 函数bar()的词法作用域能够访问foo()的内部作用域,然后我们将bar()函数本身当作一个值类型进行传递;

  2. foo()执行后,其返回值(bar()函数)赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数bar()

  3. foo()执行后,其整个内部作用域并不会被销毁,因为bar()引用着foo的内部作用域,使得其没有被回收;

bar() 所声明的位置所赐, 它拥有涵盖 foo() 内部作用域的闭包, 使得该作用域能够一直存活, 以供 bar() 在之后任何时间进行引用。
bar() 依然持有对该作用域的引用, 而这个引用就叫作闭包。

这个函数在定义时的词法作用域以外的地方被调用。 闭包使得函数可以继续访问定义时的词法作用域。

更多例子:

// 1
function foo() {
    var a = 2;
    function baz() {
        console.log( a ); // 2
    } 
    bar( baz );
}
function bar(fn) {
    fn(); // 妈妈快看呀, 这就是闭包!
}


// 2
var fn;
function foo() {
    var a = 2;
    function baz() {
        console.log( a );
    }

    fn = bar;
}
function bar() {
    fn(); // 妈妈快看呀, 这就是闭包!
}
foo();
bar(); // 2

无论通过何种手段将内部函数传递到所在的词法作用域以外, 它都会持有对原始定义作用域的引用, 无论在何处执行这个函数都会使用闭包。

5.3 循环与闭包

闭包中,for循环是常见的例子:

for (var i = 1; i <= 5; i++) {
    setTimeout( function timer() {
        console.log(i); 
    }, i * 1000)
}

正常情况下,我们对这段代码行为的预期是分别输出数字 1~5,每秒一次,每次一个。但实际上,这段代码在运行时会以每秒一次的频率输出五次 6。

原因:循环中的每个迭代在运行时都会给自己 “捕获” 一个 i 的副本,但是根据作用域的工作原理,这些迭代都被封闭在一个共享的全局作用域中,因此实际上它们在共用一个 i ,尽管每次迭代都会定义新的函数。

实际上,这样的代码等价于将延迟函数的回调重复定义五次,完全没使用循环。

那么,给它们各自开辟一个新的作用域是很有必要的,用 IIFE 可以实现:

for (var i = 1; i <= 5; i++) {
    (function () {
        setTimeout(function timer() {
            console.log( i );
        }, i * 1000);
    })();
}

但是,这样是行不通的。循环的每次迭代仍然在共享同一个 i ,尽管它们各自有自己的作用域。这样相当于给它们创建了一个空的作用域,仅此而已。

更正代码,给它们添加它们各自需要的变量:

for (var i = 1; i <= 5; i++) {
    (function () {
        var j = i;
        setTimeout(function timer() {
            console.log( j );
        }, j * 1000);
    })();
}

这样,它们各自的作用域中都保存了每次迭代后更新的 i 值,还可以再改进一下:

for (var i = 1; i <= 5; i++) {
    (function (j) {
        setTimeout(function timer() {
            console.log( j );
        }, j * 1000);
    })(i);
}

根据 IIFE 是函数的特性,将 i 当作参数传递进去,并将形参命名为 j ,当然你想命名为什么都可以,现在代码可以工作了。

在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域, 使得延迟函数的回调可以将新的作用域封闭在每个迭代内部, 每个迭代中都会含有一个具有正确值的变量供我们访问。

重返块作用域

let 可以用来劫持块作用域, 并且在这个块作用域中声明一个变量:

for (var i = 1; i <= 5; i++) {
    let j = i;
    setTimeout(function timer() {
        console.log( j );
    }, j * 1000);
}

for 循环头部的 let 声明还会有一个特殊的行为。 这个行为指出变量在循环过程中不止被声明一次, 每次迭代都会声明。 随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

for (let i = 0; i <= 5; i++) {
    setTimeout(function timer() {
        console.log( i );
    }, i * 1000);
}

这就是块作用域和闭包联手的强大所在。

5.4 模块

function CoolModule() {
    var something = 'cool';
    var another = [1, 2, 3];

    function doSomething() {
        console.log( something );
    }

    function doAnother() {
        console.log( another.join(' ! ') );
    }

    return {
        doSomething: doSomething,
        doAnother: doAnother,
    }
}


var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3 ! 

这个模式在 JavaScript 中被称为模块。

首先, CoolModule()只是一个函数,必须通过调用它来创建一个模块实例,如果不执行外部函数,那么内部作用域和闭包都无法被创建。

其次, CoolModule()返回一个对象字面量语法 { key: value, ... }来表示的对象。这个对象中含有对内部函数而不是内部数据变量的引用。这样可以保持内部数据变量隐蔽且私有的状态。可以将这个对象类型的返回值看作是 模块的公共API

最后,这个对象类型的返回值被赋值给外部的变量foo,然后可以通过它来访问 API 中的属性方法,比如 foo.doSomething()

模块模式的两个必要条件:

  1. 必须有外部的封闭函数,该函数至少被调用一次(每次调用都会创建一个新的模块实例)。

  2. 封闭函数必须返回至少应一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或修改私有的状态。

上面的模块函数代码还可以改进:

var foo = (function CoolModule() {
    var something = 'cool';
    var another = [1, 2, 3];

    function doSomething() {
        console.log( something );
    }

    function doAnother() {
        console.log( another.join(' ! ') );
    }

    return {
        doSomething: doSomething,
        doAnother: doAnother,
    }
})();

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3 ! 

当然,模块也是普通的函数,因此可以接收参数:

function CoolModule(id) {
    function identify() {
        console.log( id );
    }

    return {
        identify: identify,
    }
}

var foo1 = CoolModule('foo 1');
var foo2 = CoolModule('foo 2');
foo1.identify(); // 'foo 1'
foo2.identify(); // 'foo 2'

模块模式另一个简单且强大的变化用法是,命名将要作为公共 API 返回的对象:

var foo = (function CoolModule(id) {
    function change() {
        // 修改公共API
        publicAPI.identify = identify2;
    }

    function identify1() {
        console.log( id );
    }

    function identify2() {
        console.log( id.toUpperCase() );
    }

    var publicAPI = {
        change: change,
        identify: identify1,
    };

    return publicAPI;
})('foo module');

foo.identify(); // foo module
foo.change(); 
foo.identify(); // FOO MODULE

通过在模块实例内部保留对公共 API 对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改它们的值。

5.4.1 现代的模块机制

ydkjs p54

大多数模块依赖加载器 / 管理器本质上都是将这种模块定义封装进一个友好的 API。

var MyModules = (function Manager() {
  var modules = {};
  function define(name, deps, impl) {
    for (var i = 0; i < deps.length; i++) {
      deps[i] = modules[deps[i]];
      console.log(deps, modules);
    }
    modules[name] = impl.apply(impl, deps);
  }

  function get(name) {
    return modules[name];
  }

  return {
    define: define,
    get: get,
  };
})();

这段代码的核心是 modules[name] = impl.apply( impl, deps ) 。为了模块的定义引入了包装函数(可以引入任何依赖)并且将返回值(模块的 API )储存在一个根据名字来管理的模块列表中。

下面演示如何使用它来定义模块:

MyModules.define("bar", [], function () {
  function hello(who) {
    return "let me introduce: " + who;
  }
  return {
    hello: hello,
  };
});

MyModules.define("foo", ["bar"], function (bar) {
  var hungry = "hippo";
  function awesome() {
    console.log(bar.hello(hungry).toUpperCase());
  }
  return {
    awesome: awesome,
  };
});

var bar = MyModules.get("bar");
var foo = MyModules.get("foo");

console.log(bar.hello("hippo")); // let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO

"foo""bar"模块都是通过一个返回公共API的函数来定义的。 "foo"甚至能接收"bar"的示例作为依赖参数,并且相应地使用它。

5.4.2 未来的模块机制

ES6 将模块添加了一级语法支持,它会将文件当作单独的模块来处理。每个模块都可以导入其他模块或特定的 API 成员,同样也可以导出自己的 API 成员。

基于函数的模块并不被稳定地识别(编译器无法识别),它们的 API 语义仅在运行时才能够被考虑进来。因此可以在运行时修改一个模块的 API。

相比之下,ES6 模块 API 更加稳定(即 API 不会在运行时改变)。所以在编译期时会检查对导入模块的 API 或成员的引用是否真实存在。如果 API 引用不存在,编译器会在运行时抛出 “早期" 错误,而不是在运行期采用动态的解决方式。

ES6 的模块没有“行内” 格式,必须被定义在独立的文件中(一个文件一个模块)。

导入有两种方式:

module foo from 'foo';

import foo from 'foo';

module会将整个模块的 API 导入并绑定到一个变量上,而import可以将一个或多个 API 导入到当前作用域。

export 会将当前模块的一个标识符(变量、 函数) 导出为公共 API,如:

function foo() { ... }

export foo;

5.5 小结

  • 当函数可以记住并访问所在的词法作用域, 即使函数是在当前词法作用域之外执行, 这时就产生了闭包

  • 闭包也是一个非常强大的工具, 可以用多种形式来实现模块等模式;

  • 模块有两个主要特征:

    1. 为创建内部作用域而调用了一个包装函数;

    2. 包装函数的返回值必须至少包括一个对内部函数的引用, 这样就会创建涵盖整个包装函数内部作用域的闭包。

  • closure is everywhere…


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值