《你必须知道的javascript(上)》- 1.作用域和闭包

1 作用域是什么

1.1 编译原理

  • 分词/词法分析(Tokenizing/Lexing)
    将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。
  • 解析/语法分析(Parsing)
    这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。
  • 代码生成
    将AST(抽象语法树)转换为可执行代码的过程称被称为代码生成

1.2 理解作用域

1.2.1 演员表

引擎(导演) 编译器(男主) 作用域(女主)

1.2.2 对话

1.遇到var a,编译器会问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。
2.接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理a = 2这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。

1.2.3 编译器有话说

当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询。
考虑以下代码:
console.log( a );
其中对a的引用是一个RHS引用,因为这里a并没有赋予任何值。

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

function foo(a)  { 
    console.log( a ); // 2 
} 
 
foo( 2 ); 

最后一行foo(..)函数的调用需要对foo进行RHS引用,意味着“去找到foo的值,并把它给我”。并且(..)意味着foo的值需要被执行,因此它最好真的是一个函数类型的值!这里还有一个容易被忽略却非常重要的细节。

代码中隐式的a=2操作可能很容易被你忽略掉。这个操作发生在2被当作参数传递给foo(..)函数时,2会被分配给参数a。为了给参数a(隐式地)分配值,需要进行一次LHS查询。

这里还有对a进行的RHS引用,并且将得到的值传给了console.log(..)。console.log(..)本身也需要一个引用才能执行,因此会对console对象进行RHS查询,并且检查得到的值中是否有一个叫作log的方法。

最后,在概念上可以理解为在LHS和RHS之间通过对值2进行交互来将其传递进log(..)(通过变量a的RHS查询)。假设在log(..)函数的原生实现中它可以接受参数,在将2赋值给其中第一个(也许叫作arg1)参数之前,这个参数需要进行LHS引用查询。

1.2.5 小测验

  1. 找到其中所有的LHS查询。(这里有3处!)
  2. 找到其中所有的RHS查询。(这里有4处!)

1.4 异常

如果RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError异常。值得注意的是,ReferenceError是非常重要的异常类型。

相较之下,当引擎执行LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下

1.5 小结

如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。

不成功的RHS引用会导致抛出ReferenceError异常。
不成功的LHS引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用LHS引用的目标作为标识符,或者抛出ReferenceError异常(严格模式下)。

2 词法作用域

作用域共有两种主要的工作模型。

  • 词法作用域,被大多数编程语言所采用。
  • 动态作用域,仍有一些编程语言在使用(比如Bash脚本、Perl中的一些模式等)

2.1 词法阶段

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

在这个例子中有三个逐级嵌套的作用域。

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

2.2 欺骗词法

怎样才能在运行时来“修改”(也可以说欺骗)词法作用域呢?

欺骗词法作用域会导致性能下降。

2.2.1 eval(不推荐使用)

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

eval(..)可以在运行期修改书写期的词法作用域。

在程序中动态生成代码的使用场景非常罕见,因为它所带来的好处无法抵消性能上的损失。

2.2.2 with(不推荐使用,严格模式下完全禁止)

var obj = { 
    a: 1, 
    b: 2, 
    c: 3 
}; 
 
// 单调乏味的重复"obj" 
obj.a = 2; 
obj.b = 3; 
obj.c = 4; 
 
// 简单的快捷方式 
with (obj) { 
    a = 3; 
    b = 4; 
    c = 5; 
}

但实际上这不仅仅是为了方便地访问对象属性

    function foo(obj) {
        with (obj) {
            a = 2;
        }
    }

    var o1 = {
        a: 3
    };

    var o2 = {
        b: 3
    };

    foo(o1);
    console.log(o1.a); // 2 

    foo(o2);
    console.log(o2.a); // undefined   a = 2赋值操作创建了一个全局的变量a
    console.log(a); // 2——不好,a被泄漏到全局作用域上了!

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

3 函数作用域和块作用域

究竟是什么生成了一个新的气泡?只有函数会生成新的气泡吗?JavaScript中的其他结构能生成作用域气泡吗?

3.1 函数中的作用域


在这个代码片段中,foo(..)的作用域气泡中包含了标识符a、b、c和bar。
bar(..)拥有自己的作用域气泡。全局作用域也有自己的作用域气泡,它只包含了一个标识符:foo。
由于标识符a、b、c和bar都附属于foo(..)的作用域气泡,因此无法从foo(..)的外部对它们进行访问。也就是说,这些标识符全都无法从全局作用域中进行访问,因此下面的代码会导致ReferenceError错误:

bar(); // 失败 
console.log( a, b, c ); // 三个全都失败

但是,这些标识符(a、b、c、foo和bar)在foo(..)的内部都是可以被访问的,同样在bar(..)内部也可以被访问(假设bar(..)内部没有同名的标识符声明)。

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

3.2 隐藏内部实现

从所写的代码中挑选出一个任意的片段,然后用函数声明对它进行包装,实际上就是把这些代码“隐藏”起来了。

为什么“隐藏”变量和函数是一个有用的技术?
有很多原因促成了这种基于作用域的隐藏方法。它们大都是从最小特权原则中引申出来的,也叫最小授权或最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的API设计。

例如:

function doSomething(a) { 
    b = a + doSomethingElse( a * 2 ); 
 
    console.log( b * 3 ); 
} 
 
function doSomethingElse(a) { 
    return a - 1; 
} 
 
var b; 
 
doSomething( 2 ); // 15

在这个代码片段中,变量b和函数doSomethingElse(..)应该是doSomething(..)内部具体实现的“私有”内容。给予外部作用域对b和doSomethingElse(..)的“访问权限”不仅没有必要,而且可能是“危险”的,因为它们可能被有意或无意地以非预期的方式使用,从而导致超出了 doSomething(..)的适用条件。更“合理”的设计会将这些私有的具体内容隐藏在doSomething(..)内部,例如:

function doSomething(a) { 
    function doSomethingElse(a) { 
        return a - 1; 
    } 
 
    var b; 
 
    b = a + doSomethingElse( a * 2 ); 
 
    console.log( b * 3 ); 
} 
 
doSomething( 2 ); // 15

bdoSomethingElse(..)都无法从外部被访问,而只能被doSomething(..)所控制。功能性和最终效果都没有受影响,但是设计上将具体内容私有化了,设计良好的软件都会依此进行实现。

3.3 函数作用域

在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来。

var a = 2; 
 
function foo() { // <-- 添加这一行 
 
    var a = 3; 
    console.log( a ); // 3 
 
} // <-- 以及这一行 
foo(); // <-- 以及这一行 
 
console.log( a ); // 2

必须声明一个具名函数foo(),意味着foo这个名称本身“污染”了所在作用域。其次,必须显式地通过函数名(foo())调用这个函数才能运行其中的代码。

如果函数不需要函数名(或者至少函数名可以不污染所在作用域),并且能够自动运行,这将会更加理想。

javascript提供这两个问题的解决方案:

var a =2;
(function foo(){ // <-- 添加这一行 
 
    var a = 3; 
    console.log( a ); // 3 
 
})(); // <-- 以及这一行 
 
console.log( a ); // 2

包装函数的声明以(function...而不仅是以function...开始。函数会被当作函数表达式而不是一个标准的函数声明来处理。
(function foo(){ .. })作为函数表达式意味着foo只能在..所代表的位置中被访问,外部作用域则不行。foo变量名被隐藏在自身中意味着不会非必要地污染外部作用域。

3.3.1 匿名和具名

匿名函数表达式 缺点:

  1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
  2. 如果没有函数名,当函数需要引用自身时只能使用已经过期的arguments.callee引用,比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。
  3. 匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。

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

setTimeout( function timeoutHandler() { // <-- 快看,我有名字了! 
    console.log( "I waited 1 second!" ); 
}, 1000 );

3.3.2 立即执行函数表达式

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

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

这种模式很常见,几年前社区给它规定了一个术语:IIFE,代表立即执行函数表达式(Immediately Invoked Function Expression);
相较于传统的IIFE形式,很多人都更喜欢另一个改进的形式:(function(){ .. }())。这两种形式在功能上是一致的。

IIFE的另一个非常普遍的进阶用法是把它们当作函数调用并传递参数进去。

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,因此在代码风格上对全局
对象的引用变得比引用一个没有“全局”字样的变量更加清晰。

IIFE还有一种变化的用途是倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行之后当作参数传递进去。这种模式在UMD(Universal Module Definition)项目中被广泛使用。尽管这种模式略显冗长,但有些人认为它更易理解。

var a= 2;
(function IIFE( def ) { 
    def( window ); 
})(function def( global ) { 
 
    var a = 3; 
    console.log( a ); // 3 
    console.log( global.a ); // 2 
 
});

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

3.4 块作用域

我们在for循环的头部直接定义了变量i,通常是因为只想在for循环内部的上下文中使用i,而忽略了i会被绑定在外部作用域(函数或全局)中的事实。

例子:

for (var i=0; i<10; i++) { 
    console.log( i ); 
}

为什么要把一个只在for循环内部使用(至少是应该只在内部使用)的变量i污染到整个函数作用域中呢?

3.4.1 with

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

3.4.2 try/catch

try { 
    undefined(); // 执行一个非法操作来强制制造一个异常 
}  
catch (err) { 
    console.log( err ); // 能够正常执行! 
} 
 
console.log( err ); // ReferenceError: err not found

err仅存在catch分句内部,当试图从别处引用它时会抛出错误。

3.4.3 let

3.4.4 const

之后任何试图修改值的操作都会引起错误。

4 提升

4.1 先有鸡还是先有蛋

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

考虑另外一段代码:

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

那么到底发生了什么?看起来我们面对的是一个先有鸡还是先有蛋的问题。到底是声明(蛋)在前,还是赋值(鸡)在前?

4.2 编译器再度来袭

正确的思考思路是,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。

当你看到var a = 2;时,可能会认为这是一个声明。但JavaScript实际上会将其看成两个声明:var a;a = 2;。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段

第一段执行过程:

第二段执行过程:

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

换句话说,先有蛋(声明)后有鸡(赋值)。

1.每个作用域都会进行提升操作。
2.函数声明会被提升,但是函数表达式却不会被提升。

foo(); // 不是ReferenceError, 而是TypeError! 
 
var foo = function bar() { 
    // ... 
};

3.具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用。

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

4.3 函数优先

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

  1. var重复声明会被忽略。
  2. 函数声明会覆盖前面的。

4.3.1 var声明重复

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

会输出1而不是2!这个代码片段会被引擎理解为如下形式:

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

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

4.3.2 函数声明重复

在同一个作用域中进行重复定义是非常糟糕的,而且经常会导致各种奇怪的问题。

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

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

因此应该尽可能避免在块内部声明函数。

5 作用域闭包

5.2 实质问题

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

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

在这个例子中,我们将bar所引用的函数对象本身当作返回值。

在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。

而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是bar()本身在使用。

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

bar()依然持有对该作用域的引用,而这个引用就叫作闭包

5.3 现在我懂了

function wait(message) { 
 
    setTimeout( function timer() { 
        console.log( message ); 
    }, 1000 ); 
 
} 
 
wait( "Hello, closure!" );

timer具有涵盖wait(..)作用域的闭包,因此还保有对变量message的引用。wait(..)执行1000毫秒后,它的内部作用域并不会消失,timer函数依然保有wait(..)作用域的闭包。

或者,如果你很熟悉jQuery(或者其他能说明这个问题的JavaScript框架),可以思考下面
的代码:

function setupBot(name, selector) { 
    $( selector ).click( function activator() { 
        console.log( "Activating: " + name ); 
    } ); 
} 
 
setupBot( "Closure Bot 1", "#bot_1" ); 
setupBot( "Closure Bot 2", "#bot_2" );

本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

5.4 循环和闭包

要说明闭包,for循环是最常见的例子。

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

正常情况下,我们对这段代码行为的预期是分别输出数字1~5,每秒一次,每次一个。

但实际上,这段代码在运行时会以每秒一次的频率输出五次6。

首先解释6是从哪里来的。这个循环的终止条件是i不再<=5。条件首次成立时i的值是6。因此,输出显示的是循环结束时i的最终值。延迟函数的回调会在循环结束时才执行。

缺陷:我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个i的副本。
实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。

我们需要更多的闭包作用域,特别是在循环的过程中每个迭代都需要一个闭包作用域。

我们的IIFE只是一个什么都没有的空作用域。它需要包含一点实质内容才能为我们所用。它需要有自己的变量,用来在每个迭代中储存i的值:

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

或者

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


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

重返块作用域

第3章介绍了let声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。

本质上这是将一个块转换成一个可以被关闭的作用域.

for (var i=1; i<=5; i++) { 
    let j = i; // 是的,闭包的块作用域! 
    setTimeout( function timer() { 
        console.log( j ); 
    }, j*1000 ); 
}


但是,这还不是全部!

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

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

5.5 模块

    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

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

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

当通过返回一个含有属性引用的对象的方式来将函数传递到词法作用域外部时,我们已经创造了可以观察和实践闭包的条件。

两个必要条件:

  1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

单例模式:

上一个示例代码中有一个叫作CoolModule()的独立的模块创建器,可以被调用任意多次,每次调用都会创建一个新的模块实例。当只需要一个实例时,可以对这个模式进行简单的改进来实现单例模式:

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

模块模式另一个简单但强大的变化用法是,命名将要作为公共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.5.1 现代的模块机制

        //首先实例化我们的模块管理器,取名myModules
        var MyModules = (function Manager() {

            //作为我们的模块池,保存所有定义的模块
            var modules = {};

            /**
             *使用类似AMD的方式定义新模块,接收3个参数
             *name:模块名
             *deps:数组形式表示所依赖的其他模块
             *impl:模块功能的实现函数
            **/
            function define(name, deps, impl) {

                //遍历依赖模块数组的每一项,从程序池中取出对应的模块,并赋值.
                //循环完后,deps由保存模块名的数组变成了保存对应模块的数组.
                for (var i = 0; i < deps.length; i++) {
                    deps[i] = modules[deps[i]];
                }
                //将新模块存储进模块池,并通过apply注入它所依赖的模块(即遍历后的deps,实际上就是用deps作为impl的传入参数)
                modules[name] = impl.apply(impl, deps);
            }
            //从模块池中取出对应模块
            function get(name) {
                return modules[name];
            }
            //暴露定义模块和获取模块的两个api
            return {
                define: define,
                get: get
            }
        })()


        MyModules.define('bar', [], function () {
            function hello(who) {
                return "Let me introduce: " + who;
            }
            //返回公共API 即提供一个hello的接口
            return {
                hello: hello
            };
        });


        MyModules.define('foo', ['bar'], function (anybar) {
            var hungry = "hippo";

            function awesome() {
                console.log(anybar.hello(hungry).toUpperCase());
            }

            //返回公共API 即提供一个awesome的接口
            return {
                awesome: awesome
            }
        })

        var bar = MyModules.get('bar');//通过管理器获取模块'bar'
        var foo = MyModules.get('foo');//通过管理器获取模块'foo'

        console.log(
            //调用模块bar的hello接口
            bar.hello("hippo")
        ); // Let me introduce: hippo 

        //调用模块foo的awesome接口
        foo.awesome(); // LET ME INTRODUCE: HIPPO

deps[i] = modules[deps[i]];的细节

foo的模块调用

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

为我们自己着想,应该多花一点时间来研究这些示例代码并完全理解闭包的作用吧。最重要的是要理解模块管理器没有任何特殊的“魔力”。它们符合前面列出的模块模式的两个特点:为函数定义引入包装函数,并保证它的返回值和模块的API保持一致。

换句话说,模块就是模块,即使在它们外层加上一个友好的包装工具也不会发生任何变化。

5.5.2 未来的模块机制

ES6中为模块增加了一级语法支持。但通过模块系统进行加载时,ES6会将文件当作独立的模块来处理。每个模块都可以导入其他模块或特定的API成员,同样也可以导出自己的API成员。

bar.js

function hello(who) { 
    return "Let me introduce: " + who; 
} 
 
export hello;

foo.js

// 仅从"bar"模块导入hello() 
import hello from "bar"; 
 
var hungry = "hippo"; 
 
function awesome() { 
    console.log( 
        hello( hungry ).toUpperCase() 
    ); 
} 
 
export awesome;

baz.js

// 导入完整的"foo"和"bar"模块 
module foo from "foo"; 
module bar from "bar"; 
 
console.log( 
    bar.hello( "rhino" ) 
); // Let me introduce: rhino 
 
foo.awesome(); // LET ME INTRODUCE: HIPPO

需要用前面两个代码片段中的内容分别创建文件foo.js和bar.js。然后如第三个代码片段中展示的那样,bar.js中的程序会加载或导入这两个模块并使用它们。

import可以将一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上(在我们的例子里是hello)。module会将整个模块的API导入并绑定到一个变量上(在我们的例子里是foo和bar)。export会将当前模块的一个标识符(变量、函数)导出为公共API。这些操作可以在模块定义中根据需要使用任意多次。

模块文件中的内容会被当作好像包含在作用域闭包中一样来处理,就和前面介绍的函数闭包模块一样。

posted @ 2019-08-07 23:34  【唐】三三  阅读(341)  评论(0编辑  收藏  举报