@@ -162,3 +162,189 @@ while (node = node.next) {
162
162
```
163
163
164
164
## export default function() {}:你无法导出一个匿名函数表达式
165
+
166
+ - export ...语句通常是按它的词法声明来创建的标识符的,例如export var x = ...就意味着在当前模块环境中创建的是一个变量,并可以修改等等。但是当它被导入时,在import语句所在的模块中却是一个常量,因此总是不可写的。
167
+ - 由于export default ...没有显式地约定名字“default(或default)”应该按let/const/var的哪一种来创建,因此 JavaScript 缺省将它创建成一个普通的变量(var),但即使是在当前模块环境中,它事实上也是不可写的,因为你无法访问一个命名为“default”的变量——它不是一个合法的标识符。
168
+ - 所谓匿名函数,仅仅是当它直接作为操作数(而不是具有上述“匿名函数定义”的语法结构)时,才是真正匿名的,例如:
169
+
170
+ ``` js
171
+ console .log ((function (){}).name )); // ""
172
+ ```
173
+
174
+ - 由于类表达式(包括匿名类表达式)在本质上就是函数,因此它作为 default 导出时的性质与上面所讨论的是一致的。
175
+ - 导出项(的名字)总是作为词法声明被声明在当前模块作用域中的,这意味着它不可删除,且不可重复导出。亦即是说即使是用var x...来声明,这个x也是在 _ lexicalNames_ 中,而不是在 _ varNames_ 中。
176
+ - 所谓“某个名字表”,对于 export 来说是模块的导出表,对于 import 来说就是名字空间(名字空间是用户代码可以操作的组件,它映射自内部的模块导入名字表)。不过,如果用户代码不使用“import * as …”的语法来创建这个名字空间,那么该名字表就只存在于 JavaScript 的词法分析过程中,而不会(或并不必要)创建它在运行期的实例。这也是我一直用“某个名字表”来称呼它的原因,它并不总是以实体形式存在的。
177
+ - 没有模块会导出(传统意义上的)main(),因为 ECMAScript 为了维护模块的静态语义,而把执行过程及其入口的定义丢回给了引擎或宿主本身。
178
+
179
+ ** Q&A**
180
+
181
+ - ` export default function(){} ` 。这个语法本身没有任何的问题。但是他看似导出一个匿名函数表达式。其实他真正导出的是一个具有名字的函数,名字的default。
182
+ - ESModule 根据 import 构建依赖树,所以在代码运行前名字就是已经存在于上下文,然后在运行模块最顶层代码,给名字绑定值,就出现了‘变量提升’的效果。
183
+
184
+ ## for (let x of [ 1,2,3] ) ...:for循环并不比使用函数递归节省开销
185
+
186
+ 绝大多数 JavaScript 语句都并没有自己的块级作用域。从语言设计的原则上来看,越少作用域的执行环境调度效率也就越高,执行时的性能也就越好。
187
+
188
+ 基于这个原则,switch语句被设计为有且仅有一个作用域,无论它有多少个 case 语句,其实都是运行在一个块级作用域环境中的。例如:
189
+
190
+ ``` js
191
+ var x = 100 , c = ' a' ;
192
+ switch (c) {
193
+ case ' a' :
194
+ console .log (x); // ReferenceError
195
+ break ;
196
+ case ' b' :
197
+ let x = 200 ;
198
+ break ;
199
+ }
200
+ ```
201
+
202
+ 在这个例子中,switch 语句内是无法访问到外部变量x的,即便声明变量x的分支case 'b'永远都执行不到。这是因为所有分支都处在同一个块级作用域中,所以任意分支的声明都会给该作用域添加这个标识符,从而覆盖了全局的变量x。
203
+
204
+ 一些简单的、显而易见的块级作用域包括:
205
+
206
+ ``` js
207
+ // 例 1
208
+ try {
209
+ // 作用域 1
210
+ }
211
+ catch (e) { // 表达式 e 位于作用域 2
212
+ // 作用域 2
213
+ }
214
+ finally {
215
+ // 作用域 3
216
+ }
217
+ // 例 2
218
+ // (注:没有使用大括号)
219
+ with (x) /* 作用域 1 */ ; // <- 这里存在一个块级作用域
220
+ // 例 3, 块语句
221
+ {
222
+ // 作用域 1
223
+ // 除了这三个语句和“一个特例”之外,所有其它的语句都是没有块级作用域的。例如`if`条件语句的几种常见书写形式:
224
+ if (x) {
225
+ ...
226
+ }
227
+ // or
228
+ if (x) {
229
+ ...
230
+ } else {
231
+ ...
232
+ }
233
+ ` ` `
234
+
235
+ 这些语法中的“块级作用域”都是一对大括号表示的“块语句”自带的,与上面的“例 3”是一样的,而与if语句本身无关。
236
+
237
+ **循环语句中的块**
238
+
239
+ 并不是所有的循环语句都有自己的块级作用域,例如 ` while ` 和 ` do …while ` 语句就没有。而且,也不是所有 for 语句都有块级作用域。在 JavaScript 中,有且仅有:
240
+
241
+ ` ` ` js
242
+ for (< let / const >…) …
243
+ ` ` `
244
+
245
+ 这个语法有自己的块级作用域。当然,这也包括相同设计的` for await ` 和` for .. of / in ..` 。例如:
246
+
247
+ ` ` ` js
248
+ for await (< let / const >x of …) …
249
+ for (< let / const >x … in …) …
250
+ for (< let / const >x … of …) …
251
+ ` ` `
252
+
253
+ 语句` for (< const /let > x ... ) ... `语法中的标识符x 是一个词法名字,应该由for 语句为它创建一个(块级的)词法作用域来管理之。
254
+
255
+ ** 第二个作用域**
256
+
257
+ ` ` ` js
258
+ var x = 100;
259
+ for (let x = 102; x < 105; x++)
260
+ console.log('value:', x); // 显示“value: 102~104”
261
+ console.log('outer:', x); // 显示“outer: 100”
262
+ ` ` `
263
+
264
+ 因为for 语句的这个块级作用域的存在,导致循环体内访问了一个局部的x值(循环变量),而外部的(outer)变量x是不受影响的。
265
+
266
+ 那么在循环体内是否需要一个新的块级作用域呢?这取决于在语言设计上是否支持如下代码:
267
+
268
+ ` ` ` js
269
+ for (let x = 102; x < 105; x++)
270
+ let x = 200;
271
+ ` ` `
272
+
273
+ 也就是说,如果循环体(单个语句)允许支持新的变量声明,那么为了避免它影响到循环变量,就必须为它再提供另一个块级作用域。很有趣的是,在这里,JavaScript 是不允许声明新的变量的。上述的示例会抛出一个异常,提示“单语句不支持词法声明”:
274
+
275
+ ` ` ` bash
276
+ SyntaxError: Lexical declaration cannot appear in a single-statement context
277
+ ` ` `
278
+
279
+ ** for 循环的代价**
280
+
281
+ ` ` ` js
282
+ for (let i in x)
283
+ setTimeout(()=>console.log(i), 1000);
284
+ ` ` `
285
+
286
+ 这个例子创建了一些定时器。当定时器被触发时,函数会通过它的闭包(这些闭包处于 loopEnv 的子级环境中)来回溯,并试图再次找到那个标识符i。然而,当定时器触发时,整个 for 迭代有可能都已经结束了。这种情况下,要么上面的 forEnv 已经没有了、被销毁了,要么它即使存在,那个i的值也已经变成了最后一次迭代的终值。
287
+
288
+ 所以,要想使上面的代码符合预期,这个 loopEnv 就必须是“随每次迭代变化的”。也就是说,需要为每次迭代都创建一个新的作用域副本,这称为迭代环境(iterationEnv)。因此,每次迭代在实际上都并不是运行在 loopEnv 中,而是运行在该次迭代自有的 iterationEnv 中。
289
+
290
+ 也就是说,在语法上这里只需要两个“块级作用域”,而实际运行时却需要为其中的第二个块级作用域创建无数个副本。
291
+
292
+ 这就是 for 语句中使用“let / const ”这种块级作用域声明所需要付出的代价。
293
+
294
+ ** 总结**
295
+
296
+ - 当在这样的 for 循环中添加块语句时,块语句在每个迭代中都会都会创建一次它自己的** 块级作用域副本** 。这个循环体越大,支持的层次越多,那么这个环境的创建也就越频繁,代价越高昂。
297
+ - 无论用户代码是否直接引用 loopEnv 中的循环变量,这个过程都是会发生的。这是因为 JavaScript 允许动态的 eval (),所以引擎并不能依据代码文本静态地分析出循环体(ForBody)中是否引用哪些循环变量。
298
+ - ** “循环与函数递归在语义上等价”** 。所以在事实上,上述这种 for 循环并不比使用函数递归节省开销。在函数调用中,这里的循环变量通常都是通过函数参数传递来处理的。因而,那些支持“let / const ”的 for 语句,本质上也就与“在函数参数界面中传递循环控制变量的递归过程”完全等价,并且在开销上也是完全一样的。
299
+ - 因为每一次函数调用其实都会创建一个新的闭包——也就是函数的作用域的一个副本。
300
+
301
+ ** Q & A **
302
+
303
+ - for 只要写大括号就代表着块级作用域。所以只要写大括号,不管用let 还是 var ,一定是会创建相应循环数量的块级作用域的。
304
+ - 如果不用大括号,在for 中使用了let ,也会创建相应循环数量的块级作用域。也就是说,可以提高性能的唯一情况只有(符合业务逻辑的情况下),循环体是单行语句就不使用大括号且for 中使用var 。
305
+ - 因为单语句没有块级作用域,而词法声明是不可覆盖的,单语句后面的词法声明会存在潜在的冲突。
306
+
307
+ ## x: break x; 搞懂如何在循环外使用break ,方知语句执行真解
308
+
309
+ 1. “GOTO 语句是有害的”。1972 年图灵奖得主——迪杰斯特拉(Edsger Wybe Dijkstra, 1968 )
310
+ 2. 很多新的语句或语法被设计出来用来替代 GOTO 的效果的,但考虑到 GOTO 的失败以及无与伦比的破坏性,这些新语法都被设计为功能受限的了。
311
+ 3. 任何的一种 GOTO 带来的都是对“顺序执行”过程的中断以及现场的破坏,所以也都存在相应的执行现场回收的机制。
312
+ 4. 有两种中断语句,它们的语义和应用场景都不相同。
313
+ 5. 语句有返回值。
314
+ 6. 在顺序执行时,当语句返回 Empty 的时候,不会改写既有的其他语句的返回值。
315
+ 7. 标题中的代码,是一个“最小化的 break 语句示例”。
316
+
317
+ ## ` ${ 1 } ` :详解JavaScript中特殊的可执行结构
318
+
319
+ 模板字面量本身是一个特殊的可执行结构,但是它调动了包括引用、求值、标识符绑定、内部可执行结构存储,以及执行函数调用在内的全部能力。这是 JavaScript 厘清了所有基础的可执行结构之后,才在语法层面将它们融汇如一的结果。
320
+
321
+ ** 总结**
322
+
323
+ 1. 标题中的代码称为模板字面量,是一种可执行结构。JavaScript 中有许多类似的可执行结构,它们通常要用固定的逻辑、在确定的场景下交付 JavaScript 的一些核心语法的能力。
324
+ 2. 与参数表和赋值模板有相似的地方,模板字面量也是将它的形式规格(Formal)作为可执行结构来保存的。
325
+ 3. 只是参数表与赋值模板关注的是名字,因此存储的是“名字(lhs)”与“名字的值(rhs)的取值方法”之间的关系,执行的结果是 argArray 或在当前作用域中绑定的名字等。
326
+ 4. 而模板字面量关注的是值,它存储的是“结果”与“结果的计算过程”之间的关系。由于模板字面量的执行结果是一个字符串,所以当它作为值来读取时,就会激活它的运算求值过程,并返回一个字符串值。
327
+ 5. 模板字面量与所有其它字面量(能作为引用)相似,它也可以作为引用。
328
+ 1. ` ` ` js
329
+ 1=1
330
+ ` ` `
331
+ 2. “1 = 1 ”包括了“1 ”作为引用和值(lhs 和 rhs)的两种形式,在语法上是成立的。
332
+ 3. ` ` `
333
+ foo` ${1 }`
334
+ ` ` `
335
+ 4. 所以上面这行代码在语法上也是成立的。因为在这个表达式中,${1 }使用的不是模板字面量的值,而是它的一个“(类似于引用的)结构”。
336
+ 6. “模板字面量调用(TemplateLiteral Call)”是唯一一个会使用模板字面量的引用形态(并且也没有直接引用它的内部结构)的操作。这种引用形态的模板字面量也被称为“标签模板(Tagged Templates)”,主要包括模板的位置和那些可计算的标签的信息。例如:
337
+ 1. ` ` `
338
+ > var x = 1;
339
+ > foo = (...args) => console.log(...args);
340
+ > foo` ${x}`
341
+ [ '', '' ] 1
342
+ ` ` `
343
+ 2. 模板字面量的内部结构中,主要包括将模板多段截开的一个数组,原始的模板文本(raw)等等。在引擎处理模板时,只会将该模板解析一次,并将这些信息作为一个可执行结构缓存起来(以避免多次解析降低性能),此后将只使用该缓存的一个引用。当它作为字面量被取值时,JavaScript 会在当前上下文中计算各个分段中的表达式,并将表达式的结果值填回到模板从而拼接成一个结果值,最后返回给用户。
344
+
345
+ ## x => x:函数式语言的核心抽象:函数与表达式的同一性
346
+
347
+ 1. 传入参数的过程执行于函数之外,例如` f(a=100);` 绑定参数的过程执行于函数(的闭包)之内,例如` function foo(x=100) ..` 。
348
+ 2. ` x=>x` 在函数界面的两端都是值操作,也就是说 input/ output 的都是数据的值,而不是引用。
349
+ 3. 参数有两种初始化方法,它们根本的区别在于绑定初值的方式不同。
350
+ 4. 闭包是函数在运行期的一个实例。
0 commit comments