你不知道的JS(上)
本文是借鉴《你不知道的JS》这本书的内容的。
也就是自己对这本书每章节的分解,标注了重点,更容易理解。
文章目录
第一部分 作用域和闭包
第一章 作用域
1.1 编译原理
JavaScript 归类为“动态” 或“解释执行” 语言, 但事实上它是一门编译语言。
“编译”
-
分詞/詞法分析
这个过程会将由字符组成的字符串分解成(对编程语言来说) 有意义的代码块, 这些代码块被称为词法单元(token)。
例如, 考虑程序 var a = 2;。 这段程序通常会被分解成为下面这些词法单元: var、 a、 =、 2 、 ;。 空格是否会被当作词法单元, 取决于空格在这门语言中是否具有意义。
分词(tokenizing) 和词法分析(Lexing) 之间的区别是非常微妙、 晦涩的,
主要差异在于词法单元的识别是通过有状态还是无状态的方式进行的。
简单来说, 如果词法单元生成器在判断 a 是一个独立的词法单元还是其他词法单元的一部分时, 调用的是有状态的解析规则, 那么这个过程就被称为词法分析。
-
解析/语法分析
这个过程是将词法单元流(数组) 转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。
这个树被称为“抽象语法树”(Abstract Syntax Tree, AST)。
var a = 2; 的抽象语法树中可能会有一个叫作 VariableDeclaration 的顶级节点, 接下来是一个叫作 Identifier(它的值是 a) 的子节点, 以及一个叫作 AssignmentExpression的子节点。 AssignmentExpression 节点有一个叫作 NumericLiteral( 它的值是 2) 的子节点。 -
代码生成
将 AST 转换为可执行代码的过程称被称为代码生成。
这个过程与语言、 目标平台等息息相关 。
简单来说就是有某种方法可以将 var a = 2; 的 AST 转化为一组机器指令, 用来创建一个叫作 a 的变量(包括分配内存等), 并将一个值储存在 a 中 。
引擎可以根据需要创建并储存变量。
JS引擎相对编译过程只有三个步骤的语言的编译器要复杂很多;例如,在语法分析和代码生成的阶段,都需要有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。
- JS引擎没有很多的时间来用于优化。因为JS的编译过程不是发生在构建之前的。
- 对于JS 来说, 大部分情况下编译发生在代码执行前的几微秒(甚至更短! ) 的时间内。 在我们所要讨论的作用域背后, JavaScript 引擎用尽了各种办法(比如 JIT, 可以延迟编译甚至实施重编译) 来保证性能最佳。
简单来说:任何 JS代码片段在执行前都要进行编译( 通常就在执行前)。 因此,JS编译器首先会对 var a = 2; 这段程序进行编译, 然后做好执行它的准备, 并且通常马上就会执行它。
1.2 理解作用域
处理var a = 2;
1.2.1 成员
-
引擎
从头到尾负责整个 JavaScript 程序的编译及执行过程。
-
编译器
引擎的好朋友之一,负责语法分析及代码生成等脏活累活。
-
作用域
引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量) 组成的一系列查询, 并实施一套非常严格的规则, 确定当前执行的代码对这些标识符的访问权限。
1.2.2 过程
我们会认为var a = 2是一句声明;
但是引擎认为有两个完全不同的声明:一个是编译器在编译时处理,另一个由引擎在运行时处理。
- 遇到 var a, 编译器会询问作用域**是否已经有一个该名称的变量存在于同一个作用域的集合中**。 如果是, 编译器会忽略该声明, 继续进行编译; 否则它会要求作用域在当前作用域的集合中声明一个新的变量, 并命名为 a。
- 接下来编译器会为引擎生成运行时所需的代码, 这些代码被用来处理 a = 2 这个赋值操作。 引擎运行时会首先询问作用域, 在当前的作用域集合中是否存在一个叫作 a 的变量。 如果是, 引擎就会使用这个变量; 如果否, 引擎会继续查找该变量( 查看 1.3节)。
如果引擎最终找到了 a 变量, 就会将 2 赋值给它。 否则引擎就会举手示意并抛出一个异常!!
总结: 变量的赋值操作会执行两个动作, 首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量, 如果能够找到就会对它赋值 。
1.2.3 编译器
编译器在编译过程的第二步中生成了代码, 引擎执行它时, 会通过查找变量 a 来判断它是否已声明过。 查找的过程由作用域进行协助, 但是引擎执行怎样的查找, 会影响最终的查找结果。
LHS查询、RHS查询
-当变量出现在赋值操作的左侧时进行 LHS 查询,出现在右侧时进行 RHS 查询;
console.log(a);//对a的引用是RHS查询,因为a并没有被赋予任何值;
a=2;//LHS引用,因为实际上我们并不关心当前的值是什么, 只是想要为 =2 这个赋值操作找到一个目标。
LHS 和 RHS 的含义是“赋值操作的左侧或右侧” 并不一定意味着就是“=赋值操作符的左侧或右侧”。 赋值操作还有其他几种形式, 因此在概念上最好将其理解为“赋值操作的目标是谁(LHS)” 以及“谁是赋值操作的源头(RHS)”。
1.3 作用域嵌套
遍历嵌套作用域链的规则很简单: 引擎从当前的执行作用域开始查找变量, 如果找不到,就向上一级继续查找。 当抵达最外层的全局作用域时, 无论找到还是没找到, 查找过程都会停止。
1.4 异常
function foo(a) {
console.log( a + b );
b = a;
}
foo( 2 );
/*
第一次对 b 进行 RHS 查询时是无法找到该变量的。 也就是说, 这是一个“未声明” 的变量, 因为在任何相关的作用域中都无法找到它。
如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量, 引擎就会抛出 ReferenceError异常。
*/
ReferenceError
当引擎执行 LHS 查询时, 如果在顶层(全局作用域) 中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量, 并将其返还给引擎, 前提是程序运行在非“严格模式” 下。
ES5 中引入了“严格模式”。 同正常模式, 或者说宽松 / 懒惰模式相比, 严格模式在行为上有很多不同。 其中一个不同的行为是严格模式禁止自动或隐式地创建全局变量。 因此, 在严格模式中 LHS 查询失败时, 并不会创建并返回一个全局变量, 引擎会抛出同 RHS 查询失败时类似的 ReferenceError 异常。
TypeError
如果 RHS 查询找到了一个变量, 但是你尝试对这个变量的值进行不合理的操作,比如试图对一个非函数类型的值进行函数调用, 或着引用 null 或 undefined 类型的值中的属性,那么引擎会抛出另外一种类型的异常, 叫作 TypeError。
ReferenceError 同作用域判别失败相关, 而 TypeError 则代表作用域判别成功了, 但是对结果的操作是非法或不合理的。
1.5 小结
作用域是一套规则, 用于确定在何处以及如何查找变量(标识符)。
- 如果查找的目的是对变量进行赋值, 那么就会使用 LHS 查询;
- 如果目的是获取变量的值, 就会使用 RHS 查询 ;
LHS 和 RHS 查询都会在当前执行作用域中开始, 如果有需要(也就是说它们没有找到所需的标识符), 就会向上级作用域继续查找目标标识符, 这样每次上升一级作用域(一层楼), 最后抵达全局作用域(顶层), 无论找到或没找到都将停止。
不成功的 RHS 引用会导致抛出 ReferenceError 异常。 不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下), 该变量使用 LHS 引用的目标作为标识符, 或者抛出 ReferenceError 异常(严格模式下)。
第二章 词法作用域
作用域的两种主要工作模型:
- (普遍)词法作用域
- 动态作用域
2.1 词法阶段
作用域气泡由其对应的作用域块代码写在哪里决定, 它们是逐级包含的。
词法作用域完全由写代码期间函数所声明的位置来定义 。
查找
作用域气泡的结构和互相之间的位置关系给引擎提供了足够的位置信息, 引擎用这些信息来查找标识符的位置。
作用域查找会在找到第一个匹配的标识符时停止。 在多层的嵌套作用域中可以定义同名的标识符, 这叫作“遮蔽效应”(内部的标识符“遮蔽” 了外部的标识符)。 抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始, 逐级向外或者说向上进行, 直到遇见第一个匹配的标识符为止。
全局变量会自动成为全局对象(比如浏览器中的 window 对象) 的属性, 因此可以不直接通过全局对象的词法名称, 而是间接地通过对全局对象属性的引用来对其进行访问。
window.a
通过这种技术可以访问那些被同名变量所遮蔽的全局变量。 但非全局的变量如果被遮蔽了, 无论如何都无法被访问到。
无论函数在哪里被调用, 也无论它如何被调用, 它的词法作用域都只由函数被声明时所处的位置决定。
词法作用域查找只会查找一级标识符, 比如 a、 b 和 c。 如果代码中引用了 foo.bar.baz,词法作用域查找只会试图查找 foo 标识符, 找到这个变量后, 对象属性访问规则会分别接管对 bar 和 baz 属性的访问。
2.2 欺骗词法
在运行时来“修改”(也可以说欺骗) 词法作用域 。但是欺骗词法作用域会导致性能下降!!!
两种机制实现“欺骗”:
2.2.1 eval
类似:setTimeout(…) 和setInterval(…) 的第一个参数可以是字符串, 字符串的内容可以被解释为一段动态生成的函数代码。但已过时,并不提倡使用!!!
JS中的 eval(…) 函数可以接受一个字符串为参数, 并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。 换句话说, 可以在你写的代码中用程序生成代码并运行, 就好像代码是写在那个位置的一样 。
在执行 eval(…) 之后的代码时, 引擎并不“知道” 或“在意” 前面的代码是以动态形式插入进来, 并对词法作用域的环境进行修改的。 引擎只会如往常地进行词法作用域查找。
function foo(str, a) {
eval( str ); // 欺骗!!!遮蔽了外部(全局) 作用域中的同名变量
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
在严格模式的程序中, eval(…) 在运行时有其自己的词法作用域, 意味着其中的声明无法修改所在的作用域。
function foo(str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2" );
new Function(…) 函数的行为也很类似, 最后一个参数可以接受代码字符串, 并将其转化为动态生成的函数(前面的参数是这个新生成的函数的形参)。 这种构建函数的语法比eval(…) 略微安全一些, 但也要尽量避免使用。
2.2.2 with
不推荐使用!!!
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
console.log( a ); // 2——不好, a 被泄漏到全局作用域上了!
/*
其中一个具有 a 属性, 另外一个没有。 foo(..) 函
数接受一个 obj 参数, 该参数是一个对象引用, 并对这个对象引用执行了 with(obj) {..}。
在 with 块内部, 我们写的代码看起来只是对变量 a 进行简单的词法引用, 实际上就是一个
LHS 引用(查看第 1 章), 并将 2 赋值给它。
当我们将 o1 传递进去, a= 2 赋值操作找到了 o1.a 并将 2 赋值给它, 这在后面的 console.
log(o1.a) 中可以体现。 而当 o2 传递进去, o2 并没有 a 属性, 因此不会创建这个属性,
o2.a 保持 undefined。
*/
with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域, 因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符。
eval(…) 函数如果接受了含有一个或多个声明的代码, 就会修改其所处的词法作用域, 而
with 声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域 。
2.2.3 性能
JS 引擎会在编译阶段进行数项的性能优化。
但如果引擎在代码中发现了 eval(…) 或 with, 它只能简单地假设关于标识符位置的判断是无效的, 因为无法在词法分析阶段明确知道 eval(…) 会接收到什么代码, 这些代码会如何对作用域进行修改, 也无法知道传递给 with 用来创建新词法作用域的对象的内容到底是什么。
最悲观的情况是如果出现了 eval(…) 或 with, 所有的优化可能都是无意义的, 因此最简单的做法就是完全不做任何优化 。
2.3 小结
词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。 编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的, 从而能够预测在执行过程中如何对它们进行查找。
JS中有两个机制可以**“欺骗” 词法作用域**: eval(…) 和 with。
**eval(…)**可以对一段包含一个或多个声明的“代码” 字符串进行演算, 并借此来修改已经存在的词法作用域(在
运行时)。with本质上是通过将一个对象的引用当作作用域来处理, 将对象的属性当作作用域中的标识符来处理, 从而创建了一个新的词法作用域(同样是在运行时)。
这两个机制的副作用是引擎无法在编译时对作用域查找进行优化, 因为引擎只能谨慎地认为这样的优化是无效的。 使用这其中任何一个机制都将导致代码运行变慢。 不要使用它们!!!。
2.4 动态作用域
词法作用域是一套关于引擎如何寻找变量以及会在何处找到变量的规则。
词法作用域的重要特征是:在它的定义过程发生在代码的书写阶段(没有使用eval和with的情况)。
动态作用域是JS中this的表亲,让作用域作为一个在运行时就被动态确定的形式, 而
不是在写代码时进行静态确定的形式 。
function foo() {
console.log( a ); // 2
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
//词法作用域让 foo() 中的 a 通过 RHS 引用到了全局作用域中的 a, 因此会输出 2。
而动态作用域并不关心函数和作用域是如何声明以及在何处声明的, 只关心它们从何处调
用。 换句话说, 作用域链是基于调用栈的, 而不是代码中的作用域嵌套 .
//如果 JavaScript 具有动态作用域, 理论上, 下面代码中的 foo() 在执行时将会输出 3。
function foo() {
console.log( a ); // 3(不是 2 ! )
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
因为当 foo() 无法找到 a 的变量引用时, 会顺着调用栈在调用 foo() 的地方查找 a, 而不是在嵌套的词法作用域链中向上查找。 由于 foo() 是在 bar() 中调用的,引擎会检查 bar() 的作用域, 并在其中找到值为 3 的变量 a。
但是JavaScript 并不具有动态作用域。 它只有词法作用域, 简单明了。
但是 this 机制某种程度上很像动态作用域。
区别:
词法作用域是在写代码或者说定义时确定的, 而动态作用域是在运行时确定的。(this 也是! )
词法作用域关注函数在何处声明, 而动态作用域关注函数从何处调用。
this词法(箭头函数)
ES6 添加了一个特殊的语法形式用于函数声明, 叫作箭头函数
var foo = a => {
console.log( a );
};
foo( 2 ); // 2
问题代码:
var obj = {
id: "awesome",
cool: function coolFn() {
console.log( this.id );
}
};
var id = "not awesome"
obj.cool(); // 酷
setTimeout( obj.cool, 100 ); // 不酷
问题在于 cool() 函数丢失了同 this 之间的绑定。 解决这个问题有好几种办法, 但最长用
的就是 var self = this; 利用词法作用域:
// 使用起来如下所示:
var obj = {
count: 0,
cool: function coolFn() {
var self = this;
if (self.count < 1) {
setTimeout( function timer(){
self.count++;
console.log( "awesome?" );
}, 100 );
}
}
};
obj.cool(); // 酷吧?
self 只是一个可以通过词法作用域和闭包进行引用的标识符,
不关心 this 绑定的过程中发生了什么 。
正确使用和包含 this 机制 :
var obj = {
count: 0,
cool: function coolFn() {
if (this.count < 1) {
setTimeout( function timer(){
this.count++; // this 是安全的
// 因为 bind(..)
console.log( "more awesome" );
}.bind( this ), 100 ); // look, bind()!
}
}
};
obj.cool(); // 更酷了。
第三章 函数作用域和块作用域
3.1 函数中的作用域
函数作用域的含义是指, 属于这个函数的全部变量都可以在整个函数的范围内使用及复用( 事实上在嵌套的作用域中也可以使用)。
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(..) 都无法从外部被访问
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
doSomething( 2 ); // 15
规避冲突(避免同名标识符之间的冲突)
-
全局命名空间
当程序中加载了多个第三方库时, 如果它们没有妥善地将内部私有的函数或变量隐藏起来, 就会很容易引发冲突。这些库通常会在全局作用域中声明一个名字足够独特的变量, 通常是一个对象。 这个对象
被用作库的命名空间, 所有需要暴露给外界的功能都会成为这个对象( 命名空间) 的属性, 而不是将自己的标识符暴漏在顶级的词法作用域中。 -
模块管理
通过依赖管理器的机制将库的标识符显式地导入到另外一个特定的作用域中。 这些工具并没有能够违反词法作用域规则的“神奇” 功能。 它们只是利用作用域的规则强制所有标识符都不能注入到共享作用域中, 而是保持在私有、 无冲突的作用域中, 这样可以有效规避掉所有的意外冲突 。
3.3 函数作用域
在任意代码片段外部添加包装函数, 可以将内部的变量和函数定义“隐藏” 起来, 外部作用域无法访问包装函数内部的任何内容 。
var a = 2;
function foo() { // <-- 添加这一行
var a = 3;
console.log( a ); // 3
} // <-- 以及这一行
foo(); // <-- 以及这一行
console.log( a ); // 2
/*
必须声明一个具名函数 foo(), 意味着 foo 这个名称本身“污染” 了所在作用域(在这个
例子中是全局作用域)。 其次, 必须显式地通过函数名(foo()) 调用这个函数才能运行其
中的代码。
*/
解决方案:
var a=2;
(function foo(){ // <-- 添加这一行
var a = 3;
console.log( a ); // 3
})(); // <-- 以及这一行
console.log( a ); // 2
/*
首先, 包装函数的声明以 (function... 而不仅是以 function... 开始。 尽管看上去这并不
是一个很显眼的细节, 但实际上却是非常重要的区别。 函数会被当作函数表达式而不是一
个标准的函数声明来处理。
*/
区分函数声明和表达式最简单的方法是看 function 关键字出现在声明中的位置(不仅仅是一行代码, 而是整个声明中的位置)。 如果 function 是声明中的第一个词, 那么就是一个函数声明, 否则就是一个函数表达式。
代码比较结论:
第一个片段中 foo 被绑定在所在作用域中, 可以直接通过foo() 来调用它。
第二个片段中 foo 被绑定在函数表达式自身的函数中而不是所在作用域中。
换句话说, (function foo(){ … }) 作为函数表达式意味着 foo 只能在 … 所代表的位置中被访问, 外部作用域则不行。 foo 变量名被隐藏在自身中意味着不会非必要地污染外部作用域。
3.3.1 匿名和具名
//函数表达式最熟悉的场景是回调参数
setTimeout( function() {//匿名函数表达式
console.log("I waited 1 second!");
}, 1000 );
函数声明不可以省略函数名 ;
函数表达式省略函数名,即没有名称标识符,即为匿名函数表达式。
匿名函数表达式缺点:
- 匿名函数在栈追踪中不会显示出有意义的函数名, 使得调试很困难;
- 如果没有函数名, 当函数需要引用自身时只能使用已经过期的 arguments.callee 引用,比如在递归中。 另一个函数需要引用自身的例子, 是在事件触发后事件监听器需要解绑自身;
- 匿名函数省略了对于代码可读性 / 可理解性很重要的函数名。 一个描述性的名称可以让代码不言自明;
行内函数表达式 非常强大且有用——匿名和具名之间的区别并不会对这点有任何影响。 给函
数表达式指定一个函数名可以有效解决以上问题。
3.3.2 立即执行函数表达式(IIFE)
IIFE,代表立即执行函数表达式 (Immediately Invoked Function Expression )
var a = 2;
(function foo() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
由于函数被包含在一对 ( ) 括号内部, 因此成为了一个表达式, 通过在末尾加上另外一个( ) 可以立即执行这个函数, 比如 (function foo(){ … })()。 第一个 ( ) 将函数变成表达式, 第二个 ( ) 执行了这个函数。
改进形式:
(function(){ .. }())
进阶用法:
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, 因此在代码风格上对全局对象的引用变得比引用一个没有“全局” 字样的变量更加清晰。
*/
不常用——> 解决 undefined 标识符的默认值被错误覆盖导致的异常。
做法:
可以在一开始将一个参数命名为undefined,但是在对应的位置不传入任何值,这样就保证了在代码块中,undefined标识符的值是真的undefined:
危险操作,禁用!!
undefined=true;//挖坑
(function IIFE( undefined ) {
var a;
if (a === undefined) {
console.log( "Undefined is safe here!" );
}
})();
IIFE的另外一种用途:倒置代码的运行顺序。
var a =2;
(function IIFE(def){
def(window);
})(function def(global){
var a=3;
console.log(a);//3
console.log(gloable.a);//2
});
/*
函数表达式 def 定义在片段的第二部分, 然后当作参数(这个参数也叫作 def) 被传递进IIFE 函数定义的第一部分中。 最后, 参数 def( 也就是传递进去的函数) 被调用, 并将window 传入当作 global 参数的值.
*/
3.4 块作用域
块作用域是一个用来对之前的最小授权原则进行扩展的工具, 将代码从在函数中隐藏信息扩展为在块中隐藏信息。
但是, 当使用 var 声明变量时, 它写在哪里都是一样的, 因为它们最终都会函数作用域和块作用域 属于外部作用域。
for(var i=0;i<10;i++)//这里的i污染到了整个函数作用域;现在可以用let
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
尽管这个行为已经被标准化, 并且被大部分的标准 JavaScript 环境(除了老版本的 IE 浏览器) 所支持, 但是当同一个作用域中的两个或多个 catch 分句用同样的标识符名称声明错误变量时, 很多静态检查工具还是会发出警告。实际上这并不是重复定义, 因为所有变量都被安全地限制在块作用域内部,但是静态检查工具还是会很烦人地发出警告。为了避免这个不必要的警告, 很多开发者会将 catch 的参数命名为 err1、err2 等。 也有开发者干脆关闭了静态检查工具对重复变量名的检查。
3.4.3 let
let 关键字可以将变量绑定到所在的任意作用域中(通常是{…}内部)。
也就是说let为其声明的变量隐式的创建了所在的块作用域;
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
console.log( bar ); // ReferenceError
用 let 将变量附加在一个已经存在的块作用域上的行为是隐式的 ,但是容易导致代码变得混乱。
所以——>为块作用域显式地创建块可以部分解决这个问题 。
在声明中的任意位置都可以使用 { … } 括号来为 let 创建一个用于绑定的块。
var foo = true;
if (foo) {
{ // <-- 显式的块
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
}
console.log( bar ); // ReferenceError
替代方案:(ES6之前的环境中实现块作用域)使用catch
try{throw 2;}catch(a){
console.log( a ); // 2
}
console.log( a ); // ReferenceError
所以需要利用工具, 在构建时通过工具来对代码进行预处理, 使之可以在部署时正常工作 。
例如: Traceur (Google)维护
转换后的代码:
{
try {
throw undefined;
} catch (a) {
a = 2;
console.log( a );
}
}
console.log( a );
提升
提升是指声明会被视为存在于其所出现的作用域的整个范围内 。
使用 let 进行的声明不会在块作用域中进行提升。 声明的代码被运行之前, 声明并不“存在”。
{
console.log( bar ); // ReferenceError!
let bar = 2;
}
- 垃圾收集
另一个块作用域非常有用的原因和闭包及回收内存垃圾的回收机制相关 。(详见第五章)
function process(data) {
// 在这里做点有趣的事情
}
var someReallyBigData = { .. };
process( someReallyBigData );
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt) {
console.log("button clicked");
}, /*capturingPhase=*/false )
/*
click 函数的点击回调并不需要 someReallyBigData 变量。 理论上这意味着当 process(..) 执
行后, 在内存中占用大量空间的数据结构就可以被垃圾回收了。 但是, 由于 click 函数形成了一个覆盖整个作用域的闭包, JavaScript 引擎极有可能依然保存着这个结构(取决于具体实现)。
*/
块作用域可以打消这种顾虑, 可以让引擎清楚地知道没有必要继续保存 someReallyBigData 了:
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 );
为变量显式声明块作用域, 并对变量进行本地绑定是非常有用的工具。
let循环
典型例子:for循环
for (let i=0; i<10; i++) {
console.log( i );
}
console.log( i ); // ReferenceError
for循环头部的let不仅将i绑定到了for循环的块中,其实它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。
{
let j;
for (j=0; j<10; j++) {
let i = j; // 每个迭代重新绑定!
console.log( i );
}
}
隐式和显式作用域
let 作用域或 let 声明 区别于let定义
let (a = 2) {
console.log( a ); // 2
}
console.log( a ); // ReferenceError
let声明会创建一个显示的作用域并与其进行绑定,在语法上强制性的将所有变量声明提升到块的顶部来使代码简洁。
let声明不包含在ES6中,官方的Traceur 也不接受,需要使用合法的ES6语法并且在代码规范性上做出妥协:
/*let*/ { let a = 2;
console.log( a );
}
console.log( a ); // ReferenceError
二是编写显式 let 声明, 然后通过工具将其转换成合法的、 可以工作的代码。
工具:let-er
let-er 是一个构建时的代码转换器,但它唯一的作用就是找到 let 声明并对其进行转换。 它不会处理包括 let 定义在内的任何其他代码。 你可以安全地将 let-er 应用在 ES6 代码转换的第一步, 如果有必要, 接下来也可以把代码传递给 Traceur 等工具 。
let-er 还有一个设置项 --es6, 开启它(默认是关闭的) 会改变生成代码的种类。 开启这个设置项时 let-er 会生成完全标准的 ES6 代码, 而不会生成通过 try/catch 进行 hack
的 ES3 替代方案:{ let a = 2; console.log( a ); } console.log( a ); // ReferenceError
性能(try/catch)
try/catch 带来的性能问题, “为什么不直接使用 IIFE 来创建作用域?”
IIFE 和 try/catch 并不是完全等价的 ——> 因为如果将一段代码中的任意一部分拿出来
用函数进行包裹, 会改变这段代码的含义, 其中的 this、 return、 break 和 contine 都会
发生变化。 IIFE 并不是一个普适的解决方案, 它只适合在某些情况下进行手动操作。
3.4.4 const
ES6引入,可创建块作用域变量,但值是固定的常量。之后任何试图修改值得操作都会引起错误。
var foo = true;
if (foo) {
var a = 2;
const b = 3; // 包含在 if 中的块作用域常量
a = 3; // 正常 !
b = 4; // 错误 !
}
console.log( a ); // 3
console.log( b ); // ReferenceError!
3.5 小结
**函数是JS中最常见的作用域单元。**但不是唯一的作用域单元。
本质:声明一个函数内部的变量或者函数,会在所处的作用域中“隐藏”起来,意为良好软件的设计原则。
块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指 { … } 内部)。
ES3中,try/catch结构在catch分句中具有块作用域。
ES6中,引入了let,用来在任意代码块中声明变量。
if(...){let a=2;}//声明一个劫持了if的{...}块的变量,并将变量添加到了这个块中
函数作用域和块作用域的行为是一样的,
可以总结为: 任何声明在某个作用域内的变量, 都将附属于这个作用域。
但是作用域同其中的变量声明出现的位置有某种微妙的联系 ~(即第四章)
第四章 提升
4.1 先有鸡还是先有蛋(我们的固有思维)
我们以为:js代码在执行的时候是由上到下一行一行执行的。但并不完全对,有一种假设:
a = 2;
var a;
console.log( a );//2
/*
很多开发者会认为是 undefined, 因为 var a 声明在 a = 2 之后, 他们自然而然地认为变量
被重新赋值了, 因此会被赋予默认值 undefined。 但是, 真正的输出结果是 2。
*/
console.log( a );//2?没有先声明?ReferenceError?
//正确答案:undefind
var a = 2;
4.2 编译器
在第一章里,曾经描述过:
引擎会在解释js之前首先对其进行编译。
编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。
SO:新思维——>
包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
var a=2;
//我们的思想:这是一句声明
//JS的想法:这是两个声明,var a 和 a=2;
//var a 在编译阶段进行,a=2留在原地等待执行。
我们再重新看下4.1的代码:
var a ;
a=2;
console.log(a);
var a;
console.log(a);
a=2;
第一部分是编译,第二部分是执行。
这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动”到了最上面。而这个过程就叫作提升。
也就是先有蛋(声明)后有鸡(赋值)。
tips:
只有声明本身会被提升, 而赋值或其他运行逻辑会留在原地。 如果提升改变了代码执行的顺序, 会造成非常严重的破坏。
注意:
**每个作用域都会进行提升操作。**但是 函数声明会被提升, 但是函数表达式却不会被提升。
foo();
function foo() {
console.log( a ); // undefined
var a = 2;
}
提升后:
function foo() {
var a;
console.log( a ); // undefined
a = 2;
}
foo()
foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
// ...
};
/*
这段程序中的变量标识符 foo() 被提升并分配给所在作用域(在这里是全局作用域), 因此foo() 不会导致 ReferenceError。 但是 foo 此时并没有赋值(如果它是一个函数声明而不是函数表达式, 那么就会赋值)。 foo() 由于对 undefined 值进行函数调用而导致非法操作,因此抛出 TypeError 异常。
*/
即使是具名的函数表达式, 名称标识符在赋值之前也无法在所在作用域中 使用:
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};
//提升后:
var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
var bar = ...self...
// ...
}
4.3 函数优先
函数声明和变量声明都会被提升 ,但是函数会首先被提升,其次是变量;
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
//提升后
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 );
}
说明了在同一个作用域中进行重复定义是非常糟糕的, 而且经常会导致各种奇怪的问题 。。。
验证后机制已改变:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4t0UTZsd-1590569885088)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20200526172524346.png)]
4.4 总结
我们习惯将 var a = 2; 看作一个声明, 而实际上 JavaScript 引擎并不这么认为。
它将 var a和 a = 2 当作两个单独的声明, 第一个是编译阶段的任务, 而第二个则是执行阶段的任务。
即: 无论作用域中的声明出现在什么地方, 都将在代码本身被执行前首先进行处理。
可以将这个过程形象地想象成所有的声明(变量和函数) 都会被“移动” 到各自作用域的
最顶端, 这个过程被称为提升 。
但:声明本身会被提升, 而包括函数表达式的赋值在内的赋值操作并不会提升 。
注意: 避免重复声明, 特别是当普通的 var 声明和函数声明混合在一起的时候
第五章 作用域闭包
js中闭包无处不在!!!!
我自己也一直很模糊的知道闭包,看这次能不能帮大家一起大彻大悟一次~~
可以返回第二章回顾一下词法。接下来继续:
闭包是基于词法作用域书写代码时所产生的自然结果!!
不要为了闭包而闭包。不要为了利用它们而故意创建闭包。
5.1 闭包的实质
当函数可以记住并能够访问所在的词法作用域的时候,就产生了闭包,即使函数是在当前作用域以外去执行。
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
/*
基于词法作用域的查找规则, 函数bar() 可以访问外部作用域中的变量 a(这个例子中的是一个 RHS 引用查询)。
*/
上面的函数是闭包吗?技术上看:是;定义上看:不是。
bar() 对 a 的引用的方法是词法作用域的查找规则, 而这些规则只是闭包的一部分。
在上面的代码片段中, 函数 bar() 具有一个涵盖 foo() 作用域的闭包(事实上, 涵盖了它能访问的所有作用域, 比如全局作用域 )。也可以认为 bar() 被封闭在了 foo() 的作用域中。 为什么呢? 原因简单明了, 因为 bar() 嵌套在 foo() 内部。
清晰的闭包:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 这就是闭包的效果。
函数 bar() 的词法作用域能够访问 foo() 的内部作用域。 然后我们将 bar() 函数本身当作
一个值类型进行传递。 在这个例子中, 我们将 bar 所引用的函数对象本身当作返回值 。
在foo()执行后,其返回值 (也就是内部的 bar() 函数) 赋值给变量 baz 并调用 baz(), 实
际上只是通过不同的标识符引用调用了内部的函数 bar()。
在foo()执行后,引擎的垃圾回收器用来释放不再使用的内存空间,这时侯foo()的整个内部作用域应该都被销毁。而闭包可以阻止这件事的发生。
因为内部作用域的存在(bar()本身在使用这个),所以没有被回收。
又因为bar()声明的位置,所以它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以提供bar()在之后的任何时间进行引用。
所以 bar() 依然持有对该作用域的引用, 而这个引用就叫作闭包。
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn(); // 快看呀, 这就是闭包!
}
/*
把内部函数baz传递给bar,当调用了这个内部函数的时候,及fn,它涵盖了foo()内部作用域的闭包就已经被观察到了,因为它能访问a;
*/
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // 将 baz 分配给全局变量
}
function bar() {
fn(); // 快看呀, 这就是闭包!
}
foo();
bar(); // 2
无论通过何种手段将内部函数传递到所在的词法作用域以外, 它都会持有对原始定义作用
域的引用, 无论在何处执行这个函数都会使用闭包。
5.2 解释
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
/*
将一个内部函数( 名为 timer) 传递给 setTimeout(..)。
timer 具有涵盖 wait(..) 作用域的闭包, 因此还保有对变量 message 的引用。
wait(..) 执行 1000 毫秒后, 它的内部作用域并不会消失, timer 函数依然保有 wait(..)
作用域的闭包。
*/
内置的工具函数 setTimeout(..) 持有对一个参数的引用, 这个参数也许叫作 fn 或者 func, 或者其他类似的名字。
引擎会调用这个函数, 在例子中就是内部的 timer 函数, 而词法作用域在这个过程中保持完整。
这就是闭包。
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 或者任何其他的异步(或者同步) 任务中, 只要使用了回调函数, 实际上就是在使用闭包!
至此,我理解了,你能理解吗???多读几遍~~~嘻嘻
现在还记得IIFE是啥不(CTRL+F搜索一下~~),它就是个典型的闭包,但是从闭包的定义来说,并不是很严格。
var a = 2;
(function IIFE() {
console.log( a );
})();
/*
虽然这段代码可以正常工作, 但严格来讲它并不是闭包。 为什么?
因为函数(示例代码中的 IIFE) 并不是在它本身的词法作用域以外执行的。 它在定义时所在的作用域中执行(而外部作用域, 也就是全局作用域也持有 a)。
a 是通过普通的词法作用域查找而非闭包被发现的。
*/
尽管 IIFE 本身并不是观察闭包的恰当例子, 但它的确创建了闭包, 并且也是最常用来创建可以被封闭起来的闭包的工具。
因此 IIFE 的确同闭包息息相关, 即使本身并不会真的使用闭包。
是不是突然发现,自己写的代码里其实用了好多闭包,但是你当时并不知道它就是闭包,哈哈哈~~~
5.3 循环和闭包
最常见的例子还是for循环
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
预期:分别输出1~5,每秒一次,每次一个。
实际:每秒一次的频率输出五次6。
/*
原因:循环的终止条件是 i 不再 <=5。
条件首次成立时 i 的值是6。 因此, 输出显示的是循环结束时 i 的最终值。
延迟函数的回调会在循环结束时才执行。
事实上,当定时器运行时即使每个迭代中执行的是 setTimeout(.., 0), 所有的回调函数依然是在循
环结束后才会被执行, 因此会每次输出一个 6 出来。
*/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wjXXg4Ed-1590569885093)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20200526190939821.png)]
我们试图假设:循环中的每个迭代在运行时都会给自己“捕获” 一个 i 的副本。
但是根据作用域的工作原理, 实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,
但是它们都被封闭在一个共享的全局作用域中, 因此实际上只有一个 i。
所以通过更多的闭包作用域,在循环的过程中每个迭代都使用一次闭包作用域。
test:
前提: IIFE 会通过声明并立即执行一个函数来创建作用域。
for (var i=1; i<=5; i++) {
(function() {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mUIbheYT-1590569885099)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20200526192101731.png)]
依然不行。
虽然每个延迟函数都会将IIFE在每次迭代中创建的作用域封闭起来,但是每个作用域都是空的,没有作用。
所以:它们需要有自己的变量,用来在每个迭代中储存i的值。
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FPKxF091-1590569885105)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20200526192352169.png)]
完美!!!
改进一下:
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}
在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,
使得延迟函数的回调可以将新的作用域封闭在每个迭代内部, 每个迭代中都会含有一个具有正确值的变量供我们访问。
所以我们利用IIFE在每次迭代的时候都创建了一个新的作用域。
也就是每次迭代的时候我们都需要一个块作用域,块作用域?那就可以用let来声明啊,用来劫持块作用域,并且在块作用域中声明一个变量。
本质上来说:就是将一个块转换成一个可以被关闭的作用域。
for (var i=1; i<=5; i++) {
let j = i; // 是的, 闭包的块作用域!
setTimeout( function timer() {
console.log( j );
}, j*1000 );
}
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
/*
for 循环头部的 let 声明还会有一个特殊的行为。 这个行为指出变量在循环过程中不止被声明一次, 每次迭代都会声明。 随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
*/
块作用域和闭包联手便可天下无敌 !!! 哈哈哈~~
5.4 模块
function foo() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
}
/*
这里并没有明显的闭包, 只有两个私有数据变量 something和 another, 以及 doSomething() 和 doAnother() 两个内部函数,
但是它们的词法作用域(而这就是闭包) 也就是 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
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
这个模式在 JavaScript 中被称为模块。
最常见的实现模块模式的方法通常被称为模块暴露,这里展示的是其变体。
解释:
-
CoolModule() 只是一个函数, 必须要通过调用CoolModule来创建一个模块实例。
如果不执行外部函数, 内部作用域和闭包都无法被创建。
-
CoolModule() 返回一个用对象字面量语法 { key: value, … } 来表示的对象。
这个返回的对象中含有对内部函数的引用而不是内部数据变量的引用。
我们能保持内部数据变量是隐藏且私有的状态。 可以将这个对象类型的返回值看作本质上是模块的公共 API。
这个对象类型的返回值最终被赋值给外部的变量 foo, 然后就可以通过它来访问 API 中的
属性方法, 比如 foo.doSomething()。从模块中返回一个实际的对象并不是必须的, 也可以直接返回一个内部函数。
jQuery 就是一个很好的例子。 jQuery 和 $ 标识符就是 jQuery 模块的公共 API,
但它们本身都是函数(由于函数也是对象, 它们本身也可以拥有属性)。
-
doSomething() 和 doAnother() 函数具有涵盖模块实例内部作用域的闭包( 通过调用CoolModule() 实现)。 当通过返回一个含有属性引用的对象的方式来将函数传递到词法作
用域外部时, 我们已经创造了可以观察和实践闭包的条件。
简要概述:
模块模式需要具备两个必要条件 。
- 必须有外部的封闭函数, 该函数必须至少被调用一次(每次调用都会创建一个新的模块
实例)。 - 封闭函数必须返回至少一个内部函数, 这样内部函数才能在私有作用域中形成闭包, 并
且可以访问或者修改私有的状态。
一个具有函数属性的对象本身并不是真正的模块。
从方便观察的角度看, 一个从函数调用所返回的, 只有数据属性而没有闭包函数的对象并不是真正的模块 。
单例模式
CoolModule() 是独立的模块创建器,可以被调用任意多次,每次调用都会创建一个新的模块实力。
所以当只需要一个实例时,可以改为单例模式:
//原版
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
//单例模式
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
//立即调用这个函数并将返回值直接赋值给单例的模块实例标识符 foo
tips:
模块也是普通的函数,因此可以接收参数。
**另外一种用法:**命名将要作为公共 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 模块机制
5.5.1 现代的模块机制
大多数模块依赖加载器 / 管理器本质上都是将这种模块定义封装进一个友好的 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]];
}
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 //这里是通过一个返回公共 API 的函数来定义的
};
} );
MyModules.define( "foo", ["bar"], function(bar) {
var hungry = "hippo";
function awesome() {
console.log( bar.hello( hungry ).toUpperCase() );
}
return {
awesome: awesome //和这里,都是通过一个返回公共 API 的函数来定义的
};
//foo" 甚至接受 "bar" 的示例作为依赖参数, 并能相应地使用它
} );
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
体现了:
模块模式的两个特点 (必要条件)
为函数定义引入包装函数, 并保证它的返回值和模块的 API 保持一致。
大白话:模块就是模块, 即使在它们外层加上一个友好的包装工具也不会发生任何变化。
5.5.2 未来的模块机制
ES6中为模块增加了一级语法支持。
但通过模块系统进行加载时,ES6会将文件当做独立的模块来处理。
每个模块都可以导入其他模块或特定的API成员,同样也可以导出自己的。
基于函数的模块并不是一个能被稳定识别的模式(编译器无法识别), 它们的 API 语义只有在运行时才会被考虑进来。 因此可以在运行时修改一个模块的 API 。
相比之下, ES6 模块 API 更加稳定(API 不会在运行时改变)。
由于编辑器知道这一点, 因此可以在(的确也这样做了) 编译期检查对导入模块的 API 成
员的引用是否真实存在。如果 API 引用并不存在, 编译器会在运行时抛出一个或多个“早期” 错误, 而不会像往常一样在运行期采用动态的解决方案 。
ES6 的模块没有“行内” 格式, 必须被定义在独立的文件中(一个文件一个模块)。
浏览器或引擎有一个默认的“模块加载器”(可以被重载, 但这远超出了我们的讨论范围,不做解释) 我们可以在导入模块时异步地加载模块文件。
//bar.js
function hello(who) {
return "Let me introduce: " + who;
}
//foo.js
// 仅从 "bar" 模块导入 hello()
import hello from "bar";
var hungry = "hippo";
function awesome() {
console.log(hello( hungry ).toUpperCase());
}
export awesome;
export hello;
//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
import
可以将一个模块中的一个或多个 API 导入到当前作用域中, 并分别绑定在一个变量
上(在我们的例子里是 hello)。
module
会将整个模块的 API 导入并绑定到一个变量上(在我们的例子里是 foo 和 bar)。
export
会将当前模块的一个标识符(变量、 函数) 导出为公共 API。
模块文件中的内容会被当作好像包含在作用域闭包中一样来处理, 就和前面介绍的函数闭包模块一样。
5.6 小结
闭包其实是一个标准。(关于如何在函数作为值按需传递的词法环境中书写代码)
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
模块的主要特征:
- 为创建内部作用域而调用了一个包装函数;
- 包装函数的返回值必须至少包括一个对内部函数的引用, 这样就会创建涵盖整个包装函数内部作用域的闭包。