zoukankan      html  css  js  c++  java
  • ECMA Script 6_模块加载方案 ES6 Module 模块语法_import_export

    1. 模块加载方案 commonJS

    背景:

    历史上,JavaScript 一直没有模块(module)体系,

    无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。

    其他语言都有这项功能: 

    Ruby 的require

    Python 的import

    甚至就连 CSS 都有@import

    但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍

    在 ES6 之前,社区制定了一些模块加载方案,最主要的有:

    CommonJS     用于服务器

    AMD    用于浏览器

    ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规

    范,成为浏览器和服务器通用的模块解决方案

    ES6 模块的设计思想: 尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入输出的变量

    CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

    比如,CommonJS 模块就是对象,输入时必须查找对象属性。

    运行时加载:实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法

    • let { stat, exists, readFile } = require('fs');    // CommonJS模块
      
      
      // 等同于
      let _fs = require('fs');
      let stat = _fs.stat;
      let exists = _fs.exists;
      let readfile = _fs.readfile;

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

    编译时加载: 实质是从fs模块加载 3 个方法,其他方法不加载。

    • import { stat, exists, readFile } from 'fs';    // ES6模块

    ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。

    当然,这也导致了没法引用 ES6 模块本身,因为它不是对象

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

    2. 模块功能主要由两个命令构成:export 和 import

    export、import 命令 可以出现在模块的任何位置,

    只要处于模块顶层就可以,

    不能处于块级作用域内,否则就会报错

    export

    用于输出模块的对外接口

    一个模块就是一个独立的文件。

     

    注意1. export语句输出的接口,与其对应的值是动态绑定关系,

    即通过该接口,可以取到模块内部实时的值

    • export var foo = 'bar';
      setTimeout(() => foo = 'baz', 500);
      // 输出变量 foo,值为bar,500 毫秒之后变成baz

    不同于CommonJS 模块输出的是值的缓存,不存在动态更新

    注意2. export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

    • // 报错
      export 1;
      
      // 报错
      var m = 1;
      export m;

        // 报错
        function f() {}
        export f;

      /**** 正确写法 ****/
      // 写法一
      export var m = 1;
      
      // 写法二
      var m = 1;
      export {m};
      
      // 写法三
      var n = 1;
      export {n as m};

        // 正确
        export function f() {};

      
      

        // 正确
        function f() {}
        export {f};

    • export 命令输出变量

    模块文件内部的所有变量,外部无法获取。

    如果你希望外部能够读取模块内部的某个变量,就必须使用 export 关键字输出该变量

    • // profile.js
      export var firstName = 'Michael';
      export var lastName = 'Jackson';
      export var year = 1958;

    优先考虑以下写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。

    • // profile.js
      var firstName = 'Michael';
      var lastName = 'Jackson';
      var year = 1958;
      
      export {firstName, lastName, year};
    • export 命令输出函数或类(class)
    • export function multiply(x, y) {
          return x * y;
      };
    • 可以使用 export { ...as...} 关键字重命名
    • function v1() { ... }
      function v2() { ... }
      
      export {
          v1 as streamV1,
          v2 as streamV2,
          v2 as streamLatestVersion    // v2 可以用不同的名字输出两次。
      }; 

     

    import

    用于输入其他模块提供的功能

    其他 JS 文件就可以通过 import 命令加载这个模块

    • // main.js
      import {firstName, lastName, year} from './profile.js';
      
      function setName(element) {
          element.textContent = firstName + ' ' + lastName;
      }
    • import 命令要使用 as 关键字,将输入的变量重命名
    • import { lastName as surname } from './profile.js';
    • import 命令输入的变量都是只读的

    因为它的本质是输入接口。

    也就是说,不允许在加载模块的脚本里面,改写接口

    • import {a} from './xxx.js'
      
      a = {};     // Syntax Error : 'a' is read-only;

       

       // 如果a是一个对象,改写a的属性是允许的
       a.foo = 'hello'; // 合法操作

    • import 命令具有提升效果,会提升到整个模块的头部,首先执行

    本质是,import命令是编译阶段执行的,在代码运行之前就输入完成了。

    • 由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构
    • 仅仅执行模块,但是不输入任何值
    • import 'lodash';
      import 'lodash';    // 多次重复执行同一句 import 语句,那么只会执行一次,而不会执行多次
    • CommonJS 模块的require命令  和  ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做
    • 因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。
    • require('core-js/modules/es6.symbol');
      require('core-js/modules/es6.promise');
      import React from 'React';

    模块的整体加载

    • 现有模块 circle.js
    • // circle.js
      export function area(radius) { return Math.PI * radius * radius; }; export function circumference(radius) { return 2 * Math.PI * radius; };
    • index.js 整体加载
    • // index.js
      import * as circle from './circle';
      
      console.log('圆面积:' + circle.area(4));
      console.log('圆周长:' + circle.circumference(14));

    export default 模块指定默认输出

    使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。

    但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法

    为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到 export default 命令,为模块指定默认输出

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

    使用 export default 时,对应的 import 语句不需要使用大括号

    • // export-default.js
      export default function foo() {
          console.log('foo');
      };
      
      // 或者写成
      function foo() {
          console.log('foo');
      };
      
      export default foo;
    • 如果想在一条 import 语句中,同时输入默认方法和其他接口,可以写成下面这样
    • export default function (obj) {
        // ···
      }
      
      export function each(obj, iterator, context) {
        // ···
      }
      
      export { each as forEach };
      
      
      /**** 导入 ****/
      import _, { each, forEach } from 'lodash';

    跨模块常量 const

    引入import()函数,完成动态加载

    import函数的参数specifier,指定所要加载的模块的位置。

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

    import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载

    import()返回一个 Promise 对象

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

     

    3. 浏览器加载

    默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下

    来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间

     

    如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”

    了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载,

    下面就是两种异步加载的语法

    • <script src="path/to/myModule.js" defer></script>
      <script src="path/to/myModule.js" async></script>
    • <script> 标签打开 defer 或 async 属性,脚本就会异步加载。

    渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。

    defer 与 async 的区别是:

    defer 要等到整个页面在内存中正常渲染结束

    (DOM 结构完全生成,以及其他脚本执行完成),才会执行

    async 一旦下载完,渲染引擎就会中断渲染,

    执行这个脚本以后,再继续渲染

    • 浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性
    • <script type="module" src="./foo.js"></script>

    浏览器对于带有 type="module"的 <script>,都是异步加载,不会造成堵塞浏览器,

    即等到整个页面渲染完,再执行模块脚本,等同于打开了 <script> 标签的 defer 属性。

    ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致

    • <script type="module">
          import utils from "./utils.js";
      
          // other code
      </script>

    注意:

              • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
              • 模块脚本自动采用严格模式,不管有没有声明 use strict
              • 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。
              • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。
              • 同一个模块如果加载多次,将只执行一次。
    • 利用顶层的 this 等于 undefined 这个语法点,可以侦测当前代码是否在 ES6 模块之中。
    • const isNotModuleScript = this !== undefined

    4. ES6 模块与 CommonJS 模块完全不同。

    • CommonJS 模块输出的是值的拷贝            ES6 模块输出的是值的引用。
    • 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
    • ES6 模块的运行机制与 CommonJS 不一样。

    JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。

    等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值

    • // 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 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错

    5. Node 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。

    目前的解决方案是,将两者分开,ES6 模块 和 CommonJS 采用各自的加载方案

    • 为了与浏览器的 import 加载规则相同,Node 的.mjs文件支持 URL 路径。
    • import './foo?query=1';    // 加载 ./foo 传入参数 ?query=1
    • 只要文件名中含有:%#?等特殊字符,最好对这些字符进行转义。

    因为 Node 会按 URL 规则解读

    • Node 的 import 命令只支持加载本地模块(file:协议),不支持加载远程模块
    • 如果模块名不含路径,那么 import 命令会去 node_modules 目录寻找这个模块
    • 如果脚本文件省略了后缀名,

    比如import './foo',Node 会依次尝试四个后缀名

    ./foo.mjs

    ./foo.js

    ./foo.json

    ./foo.node

    如果这些脚本文件都不存在,Node 就会去加载 ./foo/package.json 的 main 字段指定的脚本。

    如果 ./foo/package.json 不存在 或者 没有 main 字段,那么就会抛出错误。

    6. ES6 模块加载 CommonJS 模块 

    CommonJS 模块的输出 都定义在 module.exports 这个属性上面

    • // a.js
      module.exports = {
          foo: 'hello',
          bar: 'world'
      };
      
      
      // 等同于
      export default {
          foo: 'hello',
          bar: 'world'
      };
      
      /**** 
          export 指向 modeule.exports,
          即 exports 变量 是对 module 的 exports 属性的引用
          因此
       ****/
      module.exports = func;    // 正确
      export = func;    // 错误

      module.exports会被视为默认输出,即import命令实际上输入的是这样一个对象{ default: module.exports }

    • 通过 import 一共有三种写法,可以拿到 CommonJS 模块的module.exports
    • // 写法一
      import baz from './a';
      // baz = {foo: 'hello', bar: 'world'};
      
      // 写法二
      import {default as baz} from './a';
      // baz = {foo: 'hello', bar: 'world'};
      
      // 写法三
      import * as baz from './a';
      // baz = {
      //   get default() {return module.exports;},
      //   get foo() {return this.default.foo}.bind(baz),
      //   get bar() {return this.default.bar}.bind(baz)
      // }
    • CommonJS 的一个文件,就是一个模块
    • 每个模块文件都默认包裹一层函数:console.log(arguments.callee.toString());
    • 可以通过将变量和函数设置为  module.exports / exports 的属性来暴露模块内容(变量和函数)
    • require 命令第一次导入加载模块内容,就会执行整个脚本,然后在内存生成一个对象
    • function(exports, require, module, __filename, __dirname){}

    正是因为有了这层看不见的函数,所以一个模块就是一个函数作用域,与其他模块作用域互相独立

     

    --------小尾巴 ________一个人欣赏-最后一朵颜色的消逝-忠诚于我的是·一颗叫做野的心.决不受人奴役.怒火中生的那一刻·终将结束...
  • 相关阅读:
    算法14-----位运算操作(1)
    算法13------集合所有的子集或者字符串所有子串
    21、缓存设计
    20、内存溢出(Out of Memory)
    19、内容共享
    14、数据传输更省电
    15、自定义Content Provider
    16、编写适应多个API Level的APK
    17、屏幕适配,多语言支持,手机类型适配
    18、不同平台版本
  • 原文地址:https://www.cnblogs.com/tianxiaxuange/p/10133809.html
Copyright © 2011-2022 走看看