【Javascript核心原理】04. export default function() {}:你无法导出一个匿名函数
Kumakii
编辑于 2020年12月21日 11:11
收录于文集
共7篇

    本文不讨论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&#​34;: function(){}   

        }

        console.log(obj.default.name); // default

    补充:

        在import语句所在模块中,导入的名字是一个常量,所以总是不可写的。

        由于export default ...没有显式地约定名字“default”应该按let/const/var的哪一种来创建,因此 js缺省将它创建成一个普通的变量(var),但即使是在当前模块环境中,它事实上也是不可写的,因为你无法访问一个命名为“default”的变量——它不是一个合法的标识符。

        所谓匿名函数,仅仅是当它直接作为操作数(而不是具有上述“匿名函数定义”的语法结构)时,才是真正匿名的,例如:

            console.log((function(){}).name); // "&#​34;

        由于类表达式(包括匿名类表达式)在本质上就是函数,因此它作为 default 导出时的性质与上面所讨论的是一致的。

        导出项(的名字)总是作为词法声明被声明在当前模块作用域中的,这意味着它不可删除,且不可重复导出。亦即是说即使是用var x...来声明,这个x也是在 _lexicalNames_ 中,而不是在 _varNames_ 中。

        所谓“某个名字表”,对于 export 来说是模块的导出表,对于 import 来说就是名字空间(名字空间是用户代码可以操作的组件,它映射自内部的模块导入名字表)。不过,如果用户代码不使用“import * as …”的语法来创建这个名字空间,那么该名字表就只存在于 JavaScript 的词法分析过程中,而不会(或并不必要)创建它在运行期的实例。

        

2020/12/21 03:06

熬夜不可取。