
本文不讨论nodejs里的模块装载和es6的模板导入之间的区别。
前面也提到过import也是声明语法,而本质上export也只能导出这6种声明语法锁声明的标识符,在导出时统一称为名字。
在语言设计里,“标识符”和“名字”是有语义差别的,而export将之称为名字,就意味着这是一个标识符的子集。类似的其它子集也是存在的,例如“保留字是标识符名,但不能用作标识符(A reserved word is an IdentifierName that cannot be used as an Identifier)”。
在js语言设计上,除了预设的标点符号(大括号、运算符),以及部分保留字、关键字意外,用户代码书写的只有三种:
- 标识符: 一个名字(通常情况下)。
- 字面量:表明由它的字面含义所决定的值。
- 模板:一个可计算结果的字符串值。
如果在这个层面上解构你所写的js代码,那你所能书写/声明的就只有一个“名字和值”。
而export事实上也之只能导出“名字和值”。
在语言设计层面,代码其实就是文本,是没有应用逻辑的。而你所写的代码绝大部分是应用逻辑,去掉这些逻辑之后,只剩下纯粹的符号-“代码文本”。
事实上,es6 中的模块就是用来理解你程序中的那些静态代码文本的,因此它也就只能理解上面所谓的 6 种声明,以及它们声明出来的那些“名字和值”。
export
语法分类:
- 导出“声明的名字”
1. export <let/const/var> x ...;
2. export function x() ...;
3. export class x ...;
4. export {x, y, z, ...};
- 导出“重命名的名字”:
1. export { x as y, ...};
2. export { x as default, ... };
- 导出“(其它模块的)名字”:
1. export ... from ...;
- 导出“值”:
1. export default <expression>
导出声明的、重命名的、其他模块的名字,这三种情况,其实就是形成一个名字表,让外部模块可以查看。
但是导出值的形式比较特殊,因为要是导出的值没有名字,也就没法访问了。
所以es6模块约定了一个统称为“default”的名字。
在js中,一般字面量也是值、也是单值表达式,因此导出一个字面量也是合法的。比如:
export default 114514;
而js中的对象也是字面量、也是值、也是单值表达式。而对象成员可以组合其他任何数据。也可以写成下面的方式:
export default {
a: 1,
b: function(){},
c(){},
...
}
那么标题里的语法到底导出了什么呢?
首先我们要思考“export如何导出名字”。
如果只是导出一个名字,那么它其实在“某个名字表”中做一个登记项就可以了。并且 JavaScript 中也的确是这样处理的。但是实际使用的时候,这个名字还是要绑定一个具体的值才是可以使用的。因此,一个 export 也必须理解为这样两个步骤:
1. 导出一个名字
2. 为上述名字绑定一个值
而从另一端(import端)的角度来看,以import {x} from ...来分析:
1. (与 export 类似)按照语法在当前模块中声明名字,例如上面的x;
2. 添加一个当前模块对目标模块的依赖项。
有了上述的操作,js就可以在依据所有它能在静态文本中发现的import语句来形成模块依赖树,最后找到这个模块依赖树的顶端根模块,尝试加载。
关键在于js是依赖import来形成依赖树的,与export无关,所以,到目前为止(指找到所有导入导出的名字,并完成所有模块的装配),没有任何一行用户的js代码被执行过。因为源代码只被理解为静态、没有逻辑的“代码文本”。
而导出名字和导出值的差异只在于,导出值的形式其实是导出了“default”这个特殊的名字。但它们都是确定的、符合语法规范的标识符。
接下来思考“function() {}”这个匿名函数表达式
按照js的约定,匿名函数表达式可以理解为一个函数的“字面量(值)”。
当它作为右操作数的单值表达式时,就意味着执行这个单值表达式不会在当前作用域产生一个名字,即使这个函数是具名的也必然如此。即:具名函数作为表达式时,名字在块级作用域中无意义。例如:
var x1 = function x2() {}
x2(); // ReferenceError: x2 is not defined
而当我们导出一个匿名函数或者具名函数时这两种情况时不同的。如下面两个例子:
1. export default function() { }
2. export default function x() { }
如果执行的表达式是匿名函数声明,那么它将强制在当前作用域中登记“default”这样一个特殊名字,并且在执行时绑定该匿名函数。
需要注意的是,这是一个匿名函数定义,而不是匿名函数表达式。一般函数的语句则被称为声明。匿名函数定义表述为:
aName = FunctionExpression
它可以用在一般的赋值表达式、变量声明的右操作数,以及对象成员的初始值等等位置。
在这些位置上,函数表达式总是被关联给一个名字。而这种关联不是严格意义上的“名字->值”的绑定语义。当该函数关联给名字(aName)时,js又会反向地处理该函数(作为对象f)的属性f.name,使该名字指向aName。
所以本文标题中的代码,严格意义上来说:
它并不是导出了一个匿名函数表达式,而是导出了一个匿名函数定义(Anonymous Function Definition)。
因此,该匿名函数初始化时才会绑定给它左侧的名字“default”,这会导致import f from ...之后访问f.name值会得到“default”这个名字。
使用下面的代码也会得到这个“default”:
var obj = {
"default": function(){}
}
console.log(obj.default.name); // default
补充:
在import语句所在模块中,导入的名字是一个常量,所以总是不可写的。
由于export default ...没有显式地约定名字“default”应该按let/const/var的哪一种来创建,因此 js缺省将它创建成一个普通的变量(var),但即使是在当前模块环境中,它事实上也是不可写的,因为你无法访问一个命名为“default”的变量——它不是一个合法的标识符。
所谓匿名函数,仅仅是当它直接作为操作数(而不是具有上述“匿名函数定义”的语法结构)时,才是真正匿名的,例如:
console.log((function(){}).name); // ""
由于类表达式(包括匿名类表达式)在本质上就是函数,因此它作为 default 导出时的性质与上面所讨论的是一致的。
导出项(的名字)总是作为词法声明被声明在当前模块作用域中的,这意味着它不可删除,且不可重复导出。亦即是说即使是用var x...来声明,这个x也是在 _lexicalNames_ 中,而不是在 _varNames_ 中。
所谓“某个名字表”,对于 export 来说是模块的导出表,对于 import 来说就是名字空间(名字空间是用户代码可以操作的组件,它映射自内部的模块导入名字表)。不过,如果用户代码不使用“import * as …”的语法来创建这个名字空间,那么该名字表就只存在于 JavaScript 的词法分析过程中,而不会(或并不必要)创建它在运行期的实例。
2020/12/21 03:06
熬夜不可取。