ES6 |Module语法

在这里插入图片描述

前言

背景:在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

引入:下面代码实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取3个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”**

// CommonJS模块
let { stat, exists, readFile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

下面代码实质是从fs模块加载3个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载。

import { stat, exists, readFile } from 'fs';

由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

严格模式

ES6 的模块自动采用严格模式:

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀0表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protectedstaticinterface

export 命令

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。export命令用于规定模块的对外接口,如果你希望外部能够读取模块内部的某个东西,就必须使用export关键字输出:

  • export可以输出变量、函数和类

    //输出变量
    export var firstName = 'Michael';
    //输出函数
    export function myfun() {};
    //输出类
    export class myclass {};
    
  • export的第二种写法

    var firstName = 'Michael';
    function myfun() {};
    class myclass {};
    export {firstName, myfun, myclass};
    
  • export输出的接口名称可以使用as关键字重命名

    function v1() { ... }
    function v2() { ... }
    
    export {
      v1 as streamV1,
      v2 as streamV2,
    };
    
  • export命令规定的是对外的接口,而不是直接输出的数据或立即执行的命令

    // 报错
    export 1;
    
    // 报错
    var m = 1;
    export m;
    
    // 报错
    function f() {}
    export f;
    
    // 正确
    export var m = 1;
    
    // 正确
    var m = 1;
    export {m};
    
    // 正确
    export function f() {};
    
    // 正确
    function f() {}
    export {f};
    
  • export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值

  • export命令可以出现在模块的任何位置,只要处于模块顶层就可以

    function foo() {
      export default 'bar' // 非顶层,报错
    }
    foo()
    

import 命令

import命令用于输入其他模块提供的功能。使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

  • import命令从其他文件(或模块)中引入变量(函数或类)

  • import命令接受一对大括号,里面指定要从其他文件(或模块)导入的变量名(函数名或类名),名称必须与模块对外接口的名称相同

    import {firstName, lastName, year} from './profile';
    
  • import命令可以使用as关键字,给输入的变量(函数或类)重命名

    import { lastName as surname } from './profile';
    
  • import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js路径可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。

    import {myMethod} from 'util'; //util是个模块,引入前提是有配置文件
    import 'lodash'; ////lodash是个模块,引入前提是有配置文件
    
  • import命令具有提升效果,会提升到整个模块的头部,首先执行

    foo();
    import { foo } from 'my_module'; //会提升到foo前面,不会报错
    

模块的整体加载

除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面

//circle.js:输出两个方法area和circumference

export function area(radius) {
  return Math.PI * radius * radius;
}
export function circumference(radius) {
  return 2 * Math.PI * radius;
}
//main.js:加载circle.js模块

//1. 逐一加载
import { area, circumference } from './circle';
console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));

//2. 整体加载
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

export default 命令

  • export default命令为模块指定默认输出

    // export-default.js:默认输出一个匿名函数
    export default function () {}
    
  • import加载该模块时,可以为该匿名函数指定任意名字。这时import命令不使用大括号

    // import-default.js:加载模块
    import customName from './export-default';
    
  • export default命令也可以输出非匿名函数

    export default function foo() {}
    //or
    function foo() {}
    export default foo;
    

    虽然有名称,但在模块外部是无效的。加载的时候,视同匿名函数加载

  • 一个模块只能有一个默认输出,因此export default命令只能使用一次

  • 本质上,export default输出的是一个叫做default的变量或方法,所以它后面不能跟变量声明语句

    // 正确
    export var a = 1;
    
    // 正确
    var a = 1;
    export default a; //意为将变量a的值赋给变量default
    
    // 错误
    export default var a = 1;
    
    // 正确
    export default 42;
    
    // 报错
    export 42;
    
  • import可以同时输入默认方法和其他变量

    import _, { each } from 'lodash'; //前面的_表示默认输出的方法
    

    与上面对应的export语句如下

    export default function (obj) {}
    export function each(obj, iterator, context) {}
    //暴露出forEach接口,默认指向each接口,即forEach和each指向同一个方法
    export { each as forEach };
    
  • export default可以用来输出类

    export default class { ... }
    

export 与 import 的复合写法

如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起

import { foo, bar } from 'my_module';
export { foo, bar };
//可以写成:
export { foo, bar } from 'my_module';
// 整体输出
export * from 'my_module';

跨模块常量

const声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法:

// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;

// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3

// test2.js 模块
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3

如果要使用的常量非常多,可以建一个专门的constants目录,将各种常量写在不同的文件里面,保存在该目录下

// constants/db.js
export const db = {
  url: 'http://my.couchdbserver.local:5984',
  admin_username: 'admin',
  admin_password: 'admin password'
};

// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];

然后,将这些文件输出的常量,合并在index.js里面

// constants/index.js
export {db} from './db';
export {users} from './users';

使用的时候,直接加载index.js就可以了

// script.js
import {db, users} from './constants';

import()

引入问题

import命令会被 JavaScript 引擎静态分析,先于模块内的其他模块执行。所以下面代码会报错:

if (x === 2) {
  import MyModual from './myModual';
}
//引擎处理import语句是在编译时,这时不会去分析或执行if语句。所以import语句放在if代码块之中毫无意义

importexport命令只能在模块的顶层,不能在代码块之中。这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。从语法上,条件加载就不可能实现。

解决

import()函数,实现运行时加载模块,即动态加载

import(specifier) //参数specifier,指定所要加载的模块的位置

import命令能够接受什么参数,import()函数就能接受什么参数,两者区别主要是后者为动态加载

import()返回一个 Promise 对象的例子

const main = document.querySelector('main');

import(`./section-modules/${someVariable}.js`)
  .then(module => {
    module.loadPageInto(main);
  })
  .catch(err => {
    main.textContent = err.message;
  });

import()是运行时执行,也就是说,什么时候运行到这一句,也会加载指定的模块。import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。

适用场合

  • 按需加载

    button.addEventListener('click', event => {
      import('./dialogBox.js')
      .then(dialogBox => {
        dialogBox.open();
      })
      .catch(error => {
        /* Error handling */
      })
    });
    //import()方法放在click事件的监听函数之中,只有用户点击了按钮,才会加载这个模块
    
  • 条件加载

    if (condition) {
      import('moduleA').then(...);
    } else {
      import('moduleB').then(...);
    }
    
  • 动态的模块路径

    import(f())
    .then(...);
    //根据函数f的返回结果,加载不同的模块
    

注意

import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。

import('./myModule.js')
.then(({export1, export2}) => {
  // ...·
});
//export1和export2都是myModule.js的输出接口,可以解构获得

如果模块有default输出接口,可以用参数直接获得。

import('./myModule.js')
.then(myModule => {
  console.log(myModule.default);
});

如果想同时加载多个模块,可以采用下面的写法

Promise.all([
  import('./module1.js'),
  import('./module2.js'),
  import('./module3.js'),
])
.then(([module1, module2, module3]) => {
   ···
});

import()也可以用在 async 函数之中

async function main() {
  const myModule = await import('./myModule.js');
  const {export1, export2} = await import('./myModule.js');
  const [module1, module2, module3] =
    await Promise.all([
      import('./module1.js'),
      import('./module2.js'),
      import('./module3.js'),
    ]);
}
main();
posted @ 2020-09-01 14:00  sanhuamao  阅读(110)  评论(0编辑  收藏  举报