ESM模块系统之关于 export和import 的基础知识

1,580 阅读7分钟
  • ES6 导入导出形式汇总

    • 导入

        /* 1、与下面是等价的,下面才是全称定义 */
        import A from 'xx'; //对应默认导出,这个A只是别名,可以随便起,一般取导出时的定义名
        import { default as A } from 'xx';//对应默认导出,这个A只是别名,可以随便起,一般取导出时的定义名
    
        /* 2、常规导出 */
        import { B } from 'xx';//对应命名导出
        import { B as C } from 'xx';//对应命名导出,导入时可以给予别名C
        import A,{ B as C, D } from 'xx';//可以同时导入默认导出和命名导出甚至和别名处理
    
        /* 3、不是必须通过导出的成员才能导入模块。如果不需要模块的特定导出,但仍想加载和执行模块以利用其副作用,可以只通过路径加载它 */
        import 'xx.(js|ts|css|less)';//将模块整体导入,执行模块中的副作用/副效应
    
        /* 4、命名导出可以使用*批量获取并赋值给保存导出集合的别名,而无须列出每个标识符 */
        import * as Foo from 'xx';
    
    • 导出

        /* 1、命名导出-行内导出【导出和定义在一行】 */
        export const A = 'xx'; // 使用时: import { A } from 'xx';
    
        /* 2、命名导出-export子句导出【子句导出语法:{},和行内导出等效】 */
        const A = 'xx'; 
        export { A } // 使用时: import { A } from 'xx';
    
        /* 3、命名导出-别名导出【别名导出语法:{ xx as yy },必须在字句导出大括号内】 */
        const A = 'xx'; 
        export { A as B } // 使用时: import { B } from 'xx';
    
        /* 4、默认导出 */
        const A = 'xx'; 
        export default A
    
        /* 4、默认导出配合别名 */
        const A = 'xx'; 
        export {A as default} // 等效于:export default A
    
        /* 5、命名导出和默认导出可以同时出现,默认导出只能一个 */
        export const A=111export const B=111export default function C(){}//只能一个
    
    • 转移导出

        /* 1、模块导入的值可以直接通过管道转移到导出。此时,也可以将默认导出转换为命名导出 */
        // foo.js
        export const A=111export const B=111export default function C(){}//只能一个
    
        // bar.js
        /* 1-1、情况一:foo.js有默认导出,则该语法会忽略它【这种情况会把一个模块的所有命名导出集中在一块】 */
        export * from './foo.js';
    
        /* 1-2、情况二:明确要转移导出的,支持别名 */
        export { A, B as K } from './foo.js';
    
        /* 1-3、情况三:foo.js导出A,bar.js也导出A,则最终导出的是bar.js中的值。这个“重写”是静默发生的 */
        export * from './foo.js';
        export const A=999//导出的A以这个为准
    
        /* 1-4、情况四:将默认导出转移成默认导出 */
        export { default } from './foo.js'; //使用时:import E from './bar.js'【这个E指向foo中的C】
    
        /* 1-5、情况五:将命名导出指定为默认导出: */
        export { A as default } from './foo.js'; //使用时:import D from './bar.js'【这个D指向foo中的A】
    
        /* 1-6、情况六:将默认导出指定为命名导出: */
        export { default as p } from './foo.js'; //使用时:import {P} from './bar.js'【这个P指向foo中的C】
    
  • 几个要注意的点

    • 导入的是引用的导入,指向的是同一个源,原则上导入多次,指向的源一样,那么他们的值都是相等的,同时对他们任何一个操作之后,都会同时改变【ems系统的导入导出是单例模式

        import { A as A1 } from 'A';
        import { A as A2 } from 'A';
        import { A as A3 } from 'A';
        console.log(A1===A2===A3); //true
    
        A1.b=666;
    
        console.log(A1.b===A2.b===A3.b===666); //true
    
    • 接着上一点说的:导入对模块而言是只读的,实际上相当于const声明的变量。在使用*执行批量导入时,赋值给别名的命名导出就好像使用Object.freeze()冻结过一样。直接修改导出的值是不可能的,但可以修改导出对象的属性。同样,也不能给导出的集合添加或删除导出的属性。要修改导出的值,必须使用有内部变量和属性访问权限的导出方法。

        import foo, * as Foo from './foo.js';
    
        foo = 'foo';     // 错误【重新赋值,修改foo】
        Foo.foo = 'foo'; // 错误【假设原来的对象上没有foo属性】
        foo.bar = 'bar'; // 允许【假设原来的对象上有bar属性】
    
    • 模块可以通过使用import关键字使用其他模块导出的值

        /* 必须import时,才可在当前文件允许访问和使用 */
        import { foo } from './foo.js' 
        console.log(foo)//可使用
    
        export { foo } from './foo.js' 
        console.log(foo)//不可使用
    
    • es module 是静态的,必须要先加载,于是esm更多的是必须要在顶层。export和import必须出现在模块顶级,就算不在顶级也会提升到顶级

      • esm 运行时是不能被支持的
      • 由于预加载这个特性就决定了esm天生就是适合静态分析的
        /* 静态预加载,位置 */
        import foo from './foo.js'; //顶层允许
        if(xxx){
            import foo from './foo.js'; //非顶层,运行时根据条件执行,不被运行
        }
    
        /* 提升 */
        console.log(66666);
        import foo from './foo.js'; //会被提升到666之前的顶层;实现静态预加载【但是这样的写法不建议】
    
    • 导出就是单纯的导出,是一个值的导出,导入使用的时候不要掺杂任何运算和执行语句在内

        const foo={A:1,B:2,C:3}
        export { foo };
    
        // 正确
        import foo from './foo.js'; 
        console.log(foo.A)
    
        // 错误【不能执行解构语句】
        import {A,B,C} from './foo.js'; 
    
        // 错误【不能执行计算】
        import {A + B} from './foo.js'; 
    
        /* 经典错误 */
        // 默认导出,导出了一个对象,导入时,先取对象,
        const a=1,b=2,c=3;
        export default {
            A:a,
            B:b,
            C:c
        }
        import {A,B,C} from './foo.js'; //错误
    
        import foo from './foo.js';//正确
        console.log(foo.A,foo.B,foo.C)
    
    
    • ESM模块系统会识别作为别名提供的default关键字。此时,虽然对应的值是使用命名语法导出的,实际上则会成为默认导出

        const foo={A:1,B:2,C:3}
      
        // 两种等效的默认导出
        export default foo
        export {foo as default}
    
    • ESM模块系统命名导出可以多个,且多种类型,默认导出只能一个

        const foo={A:1,B:2,C:3}
    
        export const A=666;
        export const B=999;
        export { foo }
        export default function fnd(){}
       
        //导入时
        import fnd,{A,B,foo} from 'xxx'
    
    • 导出常见错误

        // 行内默认导出中不能出现变量声明
        export default const foo = 'bar';
    
        // 只有标识符可以出现在export子句中
        export { 123 as foo }
    
        // 别名只能在export子句中出现
        export const foo = 'foo' as myFoo;
    
    • 默认导出居然和位置有关【看来 export default不是完全提升,只是执行语句提升到一级,但是并不会执行语句下沉到页面最后,而export则看上去会提升下沉到页面最下方】

      -【这个可以理解因为:export default命令的本质是将后面的值,赋给default变量,一旦赋值就执行导出,所以一旦执行了default,就是将变量赋值给了default,这时在export default后面再对变量进行赋值就会失败】
        //  export default 场景一:
        // a.js
        let e1='export 1';
        export default e1;
        e1='export 1 modified';
        
        import e1 from "./a.js"//export 1
    
        //  export default 场景二:
        // a.js
        let e1='export 1';
        e1='export 1 modified';
        export default e1;
    
        import e1 from "./a.js"//export 1 modified
    
        // export 场景:
        // a.js
        let e1='export 1';
        export {e1};
        e1='export 1 modified';
        
        import {e1} from "./a.js"//export 1 modified
    
    • 导入即执行

      • import是导入即执行的,即导入之后,对应导入的模块的代码和副作用就会执行【有的人甚至直白简单的翻译为:import导入就是copy过来,虽然有出入但是很形象】
      • 这点很重要要谨记
  • 说明

    • import 和 export 是ESM模块系统的基础,更是工程化的基础,对于静态分析、tree-Shaking等来说更是必要的基础,因此需要认真掌握
    • 后续需要以此为起点,继续拓展静态分析、tree-Shaking等知识
    • export default命令的本质是将后面的值,赋给default变量
  • 与cmj的转化

        // a.js
        const foo = 'bar';
        export default foo;
    
        // b.js
        const c = require('./a.js').default;
    
  • 参考文献