ECMAScript 6(十二)

Module

Posted by     "Jordon Li" on Thursday, December 26, 2019

TOC

ECMAScript 6 简介

ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

Module 的语法

概述

  • 基本用法: ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJSAMD 模块,都只能在运行时确定这些东西。
    // CommonJS模块
    let { stat, exists, readFile } = require('fs');
    
    // ES6模块
    import { stat, exists, readFile } from 'fs';
    

严格模式

  • 基本用法: ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";
  • 严格模式限制:
    • 变量必须声明后再使用
    • 函数的参数不能有同名属性
    • 不能使用with语句
    • 不能对只读属性赋值
    • 不能使用前缀 0 表示八进制数
    • 不能删除不可删除的属性
    • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
    • eval不会在它的外层作用域引入变量
    • evalarguments不能被重新赋值
    • arguments不会自动反映函数参数的变化
    • 不能使用arguments.calleearguments.caller
    • 禁止this指向全局对象
    • 不能使用fn.callerfn.arguments获取函数调用的堆栈
    • 增加了保留字(比如protectedstaticinterface

export 命令

  • 基本用法: 模块功能主要由两个命令构成:exportimport
    • export命令用于规定模块的对外接口,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
    //输出一个变量
    export var firstName = 'Michael';
    export var lastName = 'Jackson';
    export var year = 1958;
    
    //输出一组变量
    var firstName = 'Michael';
    var lastName = 'Jackson';
    var year = 1958;
    
    export { firstName, lastName, year as date };
    
    //export命令除了输出变量,还可以输出函数或类(class)。
    //输出一个函数
    export function multiply(x, y) {
      return x * y;
    };
    
    //输出一组函数
    function v1() { ... }
    function v2() { ... }
    
    export {
      v1 as streamV1,
      v2 as streamV2,
      v2 as streamLatestVersion
    };
    

import 命令

  • 基本用法: import命令用于输入其他模块提供的功能,import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。
    import { firstName, lastName as surname, year } from './profile.js';
    
    function setName(element) {
      element.textContent = firstName + ' ' + surname;
    }
    

模块的整体加载

  • 基本用法: 除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
      //a.js
      function v1() { ... }
      function v2() { ... }
      export { v1, v2};
    
      //b.js
      import * as circle from './a.js';
      circle.v1();
      circle.v2();
    

export default 命令

  • 基本用法: export default命令,为模块指定默认输出。其他模块加载该模块时,import命令可以为该输出指定任意名字。
    // 默认输出
    export default function crc32() { ... } // 输出
    import crc32 from 'crc32'; // 输入
    
    // 正常输出
    export function crc32() { ... };// 输出
    import {crc32} from 'crc32'; // 输入
    

export 与 import 的复合写法

  • 基本用法: 如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。
    export { foo, bar } from 'my_module';
    
    // 可以简单理解为
    import { foo, bar } from 'my_module';
    export { foo, bar };
    

    写成一行以后,foo和bar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo和bar。

跨模块常量

  • 基本用法: const声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法。
    // constants.js 模块
    export const A = 1;
    export const B = 3;
    
    // test1.js 模块
    import * as constants from './constants';
    console.log(constants.A); // 1
    console.log(constants.B); // 3
    

    如果要使用的常量非常多,可以建一个专门的constants目录,将各种常量写在不同的文件里面,保存在该目录下。然后,将这些文件输出的常量,合并在index.js里面。使用的时候,直接加载index.js就可以了。

    // 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'];
    
    // constants/index.js
    export {db} from './db';
    export {users} from './users';
    
    // script.js
    import {db, users} from './constants/index';
    

Module 的加载实现

浏览器加载

  • 传统方法:HTML 网页中,浏览器通过<script>标签加载 JavaScript 脚本。默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。
    <!-- 页面内嵌的脚本 -->
    
    <script type="application/javascript">
    
      // module code
    </script>
    
    
    <!-- 外部脚本 -->
    
    <script type="application/javascript" src="path/to/myModule.js">
    
    </script>
    
    

    浏览器允许脚本异步加载:

    • defer: 要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行
    • async: 一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染
    <script src="path/to/myModule.js" defer></script>
    
    <script src="path/to/myModule.js" async></script>
    
    
  • 加载规则: 浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性。浏览器对于带有type="module"<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。有多个<script type="module">,它们会按照在页面出现的顺序依次执行。
    <script type="module" src="./foo.js"></script>
    
    <!-- 等同于 -->
    
    <script type="module" src="./foo.js" defer></script>
    
    

ES6 模块与 CommonJS 模块

  • 差异:
    1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用:
      • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
    2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口:
      • 因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。
      • ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

Node.js 加载

  • 基本用法: Node.jsES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。
    • Node.js 要求 ES6 模块采用.mjs后缀文件名。
    • 如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module
    • 如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs
    • 如果没有type字段,或者type字段为commonjs,则.js脚本会被解释成 CommonJS 模块。
      {
        "type": "module"
      }
      

    .mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置。

main 字段

  • 基本用法: package.json文件有两个字段可以指定模块的入口文件:mainexports

    // ./node_modules/es-module-package/package.json
    {
      "type": "module",
      "main": "./src/index.js"
    }
    
    // ./my-app.mjs
    import { something } from 'es-module-package';
    // 实际加载的是 ./node_modules/es-module-package/src/index.js
    

exports字段

  • 基本用法: exports字段的优先级高于main字段。
    • 子目录别名:package.json文件的exports字段可以指定脚本或子目录的别名。
      // ./node_modules/es-module-package/package.json
      {
        "exports": {
          "./features/": "./src/features/"
        }
      }
      
      import feature from 'es-module-package/features/x.js';
      // 加载 ./node_modules/es-module-package/src/features/x.js
      
    • main 的别名: exports字段的别名如果是.,就代表模块的主入口,优先级高于main字段,并且可以直接简写成exports字段的值。
      {
        "exports": {
          ".": "./main.js"
        }
      }
      
      // 等同于
      {
        "exports": "./main.js"
      }
      

      由于exports字段只有支持 ES6Node.js 才认识,所以可以用来兼容旧版本的 Node.js

      {
        "main": "./main-legacy.cjs",
        "exports": {
          ".": "./main-modern.cjs"
        }
      }
      
    • 条件加载: 利用.这个别名,可以为 ES6 模块和 CommonJS 指定不同的入口。目前,这个功能需要在 Node.js 运行的时候,打开--experimental-conditional-exports标志。
      //别名.的require条件指定require()命令的入口文件(即 CommonJS 的入口),default条件指定其他情况的入口(即 ES6 的入口)。
      {
        "type": "module",
        "exports": {
          ".": {
            "require": "./main.cjs", //CommonJS 的入口
            "default": "./main.js" //ES6 的入口
          }
        }
      }
      
      // 等同于
      {
        "exports": {
          "require": "./main.cjs", //CommonJS 的入口
          "default": "./main.js" //ES6 的入口
        }
      }
      

「下次一定」

下次一定

使用微信扫描二维码完成支付