ES6经常使用但被忽略的方法(第九弹Module)

写在开头

  • ES6经常使用但被忽略的方法 系列文章,整理做者认为一些平常开发可能会用到的一些方法、使用技巧和一些应用场景,细节深刻请查看相关内容链接,欢迎补充交流。

相关文章

Module

  • ES6-Module
  • CommonJSAMD 模块,都只能在运行时肯定这些东西。 ES6 能够在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高,这种加载称为“编译时加载”或者静态加载。
  • 优点
    1. 能进一步拓宽 JavaScript 的语法,好比引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
    2. 再也不须要UMD模块格式。
    3. 未来浏览器的新 API 就能用模块格式提供,再也不必须作成全局变量或者navigator对象的属性。
    4. 再也不须要对象做为命名空间(好比Math对象),将来这些功能能够经过模块提供。

严格模式

  • ES6 的模块自动采用严格模式,无论你有没有在模块头部加上"use strict"
  • 限制:
    1. 变量必须声明后再使用
    2. 函数的参数不能有同名属性,不然报错
    3. 不能使用with语句
    4. 不能对只读属性赋值,不然报错
    5. 不能使用前缀 0 表示八进制数,不然报错
    6. 不能删除不可删除的属性,不然报错
    7. 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
    8. eval不会在它的外层做用域引入变量
    9. evalarguments不能被从新赋值
    10. arguments不会自动反映函数参数的变化
    11. 不能使用arguments.calleearguments.caller
    12. 禁止this指向全局对象
    13. 不能使用fn.callerfn.arguments获取函数调用的堆栈
    14. 增长了保留字(好比protectedstaticinterface
  • 尤为须要注意this的限制。ES6 模块之中,顶层的this指向undefined,即不该该在顶层代码使用this

export 命令

  • export命令用于规定模块的对外接口。
  • 一个模块就是一个独立的文件。该文件内部的全部变量,外部没法获取。若是你但愿外部可以读取模块内部的某个变量,就必须使用export关键字输出该变量。除了输出变量,还能够输出函数或类(class)。
// index.js
export const name = 'detanx';
export const year = 1995;
export function multiply(x, y) {
  return x * y;
};

// 写法二
const name = 'detanx';
const year = 1995;
function multiply(x, y) {
  return x * y;
};
export { name, year, multiply }
复制代码
  • export输出的变量就是原本的名字,可是可使用as关键字重命名。重命名后,能够用不一样的名字输出屡次。
function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};
复制代码
  • export命令规定的是对外的接口,必须与模块内部的变量创建一一对应关系。
// 报错
export 1;

var m = 1;
export m;

// 正确
export var m = 1;

var m = 1;
export {m};

var n = 1;
export {n as m};
复制代码
  • export命令能够出如今模块的任何位置,只要处于模块顶层就能够。
  • export *命令会忽略模块的default方法。
// 总体输出
export * from 'my_module';
复制代码

import 命令

  • 使用export命令定义了模块的对外接口之后,其余 JS 文件就能够经过import命令加载这个模块。想为输入的变量从新取一个名字,import命令要使用as关键字,将输入的变量重命名。
import { name, year } from './index.js';
import { name as username } from './profile.js';
复制代码
  • import命令输入的变量都是只读的,由于它的本质是输入接口。 也就是说,不容许在加载模块的脚本里面,改写接口。若是a是一个对象,改写a的属性是容许的。
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
a.foo = 'hello'; // 合法操做
复制代码
  • import后面的from指定模块文件的位置,能够是相对路径,也能够是绝对路径,.js后缀能够省略。若是只是模块名,不带有路径,那么必须有配置文件(例如使用webpack配置路径),告诉 JavaScript 引擎该模块的位置。
import {myMethod} from 'util';
复制代码
  • import命令具备提高效果,会提高到整个模块的头部,首先执行。
foo(); // 不会报错
import { foo } from 'my_module';
复制代码
  • import是静态执行,因此不能使用表达式和变量,这些只有在运行时才能获得结果的语法结构。
// 报错
import { 'f' + 'oo' } from 'my_module';

// 报错
let module = 'my_module';
import { foo } from module;

// 报错
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}
复制代码
  • 屡次重复执行同一句import语句,那么只会执行一次,而不会执行屡次。
import 'lodash';
import 'lodash'; // 只会执行一次

import { foo } from 'my_module';
import { bar } from 'my_module';

// 等同于
import { foo, bar } from 'my_module';
复制代码

模块的总体加载

  • 除了指定加载某个输出值,还可使用总体加载,即用星号(*)指定一个对象,全部输出值都加载在这个对象上面。
import * as user from './index.js';
user.name; // 'detanx'
user.year; // 1995
复制代码

export default 命令

  • export default命令,为模块指定默认输出。其余模块加载该模块时,import命令(import命令后面,不使用大括号)能够为该匿名函数指定任意名字。
// export-default.js
export default function () {
  console.log('detanx');
}

// import-default.js
import customName from './export-default';
customName(); // 'detanx'
复制代码
  • 使用export default时,对应的import语句不须要使用大括号;使用export,对应的import语句须要使用大括号。 一个模块只能有一个默认输出,所以export default命令只能使用一次。
export default function crc32() {  ...}
import crc32 from 'crc32'; 

export function crc32() { ... };
import { crc32 } from 'crc32';
复制代码

export 与 import 的复合写法

  • 若是在一个模块之中,先输入后输出同一个模块, import语句能够与export语句写在一块儿。写成一行之后,foobar实际上并无被导入当前模块,只是至关于对外转发了这两个接口,致使当前模块不能直接使用foobar
export { foo, bar } from 'my_module';

// 能够简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
复制代码
  • 模块的接口更名和总体输出,也能够采用这种写法。
// 接口更名
export { foo as myFoo } from 'my_module';

// 总体输出
export * from 'my_module';
复制代码
  • 默认接口的写法以下。
export { default } from 'foo';
复制代码
  • 具名接口改成默认接口的写法以下。
export { es6 as default } from './someModule';

// 等同于
import { es6 } from './someModule';
export default es6;
复制代码
  • 一样地,默认接口也能够更名为具名接口。
export { default as es6 } from './someModule';
ES2020 以前,有一种import语句,没有对应的复合写法。

import * as someIdentifier from "someModule";
复制代码
  • ES2020补上了这个写法。
export * as ns from "mod";

// 等同于
import * as ns from "mod";
export {ns};
复制代码

应用

  1. 公共模块
    • 例如项目有不少的公共方法放到一个constant的文件,咱们须要什么就加载什么。
    // constants.js 模块
    export const A = 1;
    export const B = 3;
    export const C = 4;
    
    // use.js
    import {A, B} from './constants';
    复制代码
  2. import()
    • import命令会被 JavaScript 引擎静态分析,先于模块内的其余语句执行(import命令叫作“链接” binding 其实更合适)。因此咱们只能在最顶层去使用。ES2020引入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()函数与所加载的模块没有静态链接关系,这点也是与import语句不相同。import()相似于 Noderequire方法,区别主要是前者是异步加载,后者是同步加载。
    • 适用场景按需加载、条件加载、动态的模块路径。
  3. 注意点
    • import()加载模块成功之后,这个模块会做为一个对象,看成then方法的参数。所以,可使用对象解构赋值的语法,获取输出接口。
    import('./myModule.js')
    .then(({export1, export2}) => {
      // ...·
    });
    复制代码
    • 上面代码中,export1export2都是myModule.js的输出接口,能够解构得到。
    • 若是模块有default输出接口,能够用参数直接得到。
    import('./myModule.js')
    .then(myModule => {
      console.log(myModule.default);
    });
    复制代码
    • 上面的代码也可使用具名输入的形式。
    import('./myModule.js')
    .then(({default: theDefault}) => {
      console.log(theDefault);
    });
    复制代码
    • 若是想同时加载多个模块,能够采用下面的写法。
    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();
    复制代码

Module 加载实现

简介

  1. 传统加载
    • 默认状况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。为了解决<script>标签打开deferasync属性,脚本就会异步加载。
    • deferasync的区别是:defer要等到整个页面在内存中正常渲染结束(DOM 结构彻底生成,以及其余脚本执行完成),才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本之后,再继续渲染。一句话,defer是“渲染完再执行”,async是“下载完就执行”。另外,若是有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。
  2. 加载规则
    • 浏览器加载 ES6 模块,也使用<script>标签,可是要加入type="module"属性。等同于打开了<script>标签的defer属性。
    <script type="module" src="./foo.js"></script>
    
    <!-- 等同于 -->
    <script type="module" src="./foo.js" defer></script>
    复制代码
    • 对于外部的模块脚本,有几点须要注意。
      1. 代码是在模块做用域之中运行,而不是在全局做用域运行。模块内部的顶层变量,外部不可见。
      2. 模块脚本自动采用严格模式,无论有没有声明"use strict"
      3. 模块之中,可使用import命令加载其余模块(.js后缀不可省略,须要提供绝对 URL 或相对 URL),也可使用export命令输出对外接口。
      4. 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无心义的。
      5. 同一个模块若是加载屡次,将只执行一次。
      import utils from 'https://example.com/js/utils.js';
      const x = 1;
      
      console.log(x === window.x); //false
      console.log(this === undefined); // true
      复制代码
    • 利用顶层的this等于undefined这个语法点,能够侦测当前代码是否在 ES6 模块之中。
    const isNotModuleScript = this !== undefined;
    复制代码

ES6 模块与 CommonJS 模块的差别

  • 讨论 Node.js 加载 ES6 模块以前,必须了解 ES6 模块与 CommonJS 模块彻底不一样。
  • 它们有两个重大差别。
    1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
    2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。(由于 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。)
  • CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。除非写成一个函数,才能获得内部变更后的值。
// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

// 写成函数
// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};

$ node main.js
3
4
复制代码
  • ES6 模块是动态引用,而且不会缓存值,模块里面的变量绑定其所在的模块。
// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
复制代码
  • ES6 输入的模块变量,只是一个“符号链接”,因此这个变量是只读的,对它进行从新赋值会报错。
// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError
复制代码
  • export经过接口,输出的是同一个值。不一样的脚本加载这个接口,获得的都是一样的实例。
// mod.js
function C() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.show = function () {
    console.log(this.sum);
  };
}

export let c = new C();
复制代码
  • 上面的脚本mod.js,输出的是一个C的实例。不一样的脚本加载这个模块,获得的都是同一个实例。
// x.js
import {c} from './mod';
c.add();

// y.js
import {c} from './mod';
c.show();

// main.js
import './x';
import './y';
复制代码
  • 如今执行main.js,输出的是 1
$ babel-node main.js
1
复制代码
  • 证实了x.jsy.js加载的都是C的同一个实例。

Node.js 加载

  • Node.js 要求 ES6 模块采用.mjs后缀文件名。Node.js 遇到.mjs文件,就认为它是 ES6 模块,默认启用严格模式,没必要在每一个模块文件顶部指定"use strict"。 若是不但愿将后缀名改为.mjs,能够在项目的package.json文件中,指定type字段为module
{
   "type": "module"
}
复制代码
  • 这时还要使用 CommonJS 模块,那么须要将 CommonJS 脚本的后缀名都改为.cjs。若是没有type字段,或者type字段为commonjs,则.js脚本会被解释成 CommonJS 模块。node

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

  • 注意,ES6 模块与 CommonJS 模块尽可能不要混用。require命令不能加载.mjs文件,会报错,只有import命令才能够加载.mjs文件。反过来,.mjs文件里面也不能使用require命令,必须使用import。es6

  • Node.js 加载 主要是介绍ES6 模块和 CommonJS 相互之间的支持,有兴趣的能够本身去看看。web

循环加载

  • “循环加载”(circular dependency指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。“循环加载”表示存在强耦合,若是处理很差,还可能致使递归加载,使得程序没法执行,所以应该避免出现,但很难避免尤为是特别复杂的项目。
相关文章
相关标签/搜索