美文网首页
【ES6脚丫系列】模块Module

【ES6脚丫系列】模块Module

作者: 吃码小妖 | 来源:发表于2019-12-17 23:02 被阅读0次
    图片.png

    【ES6脚丫系列】模块Module

    第一节:Module基本概念

    【01】过去使用CommonJS和AMD,前者用于服务器,后者用于浏览器。

    Module可以取代CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。

    【02】运行时加载和编译时加载

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

    CommonJS和AMD模块,都只能在运行时确定这些东西。比如,CommonJS模块就是对象,输入时必须查找对象属性。

    这种加载称为“运行时加载”。

    整体加载fs模块(即加载fs的所有方法),然后使用时用到3个方法。

    let { stat, exists, readFile } = require('fs');
    

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

    这种加载称为“编译时加载”,即ES6可以在编译时就完成模块编译,效率要比CommonJS模块的加载方式高。

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

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

    【03】(+仅了解)好处:

    01、不再需要UMD模块格式了,将来服务器和浏览器都会支持ES6模块格式。目前,通过各种工具库,其实已经做到了这一点。

    02、将来浏览器的新API就能用模块格式提供,不再必要做成全局变量或者navigator对象的属性。

    不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。

    【04】ES6的模块自动采用严格模式,不管有没有在模块头部加上"use strict"。

    第二节:export命令

    【01】模块功能由两个命令构成:export和import。

    export命令用于规定本模块的对外接口。

    import命令用于引入其他模块的功能。

    【02】一个模块就是一个的文件。

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

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

    【03】export可以输出变量、函数、类。

    可以使用多个export。

    可以export+变量声明赋值一起输出。

    可以输出用逗号分隔的变量集合,用花括号括起来。

    可以export+函数声明一起输出。

    写法1:

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

    写法2:(推荐使用)

    // profile.js
    var firstName = 'Michael';var lastName = 'Jackson';var year = 1958;
    
    export {firstName, lastName, year};
    

    写法3:

    export function multiply (x, y) {return x * y;};
    

    【03】可以使用as关键字重命名输出的变量。甚至可以给一个变量取多个名字。

    v1 as newName,v1 as newName2

    function v1() { ... }function v2() { ... }
    
    export {
      v1 as streamV1,
      v2 as streamV2,
      v2 as streamLatestVersion
    };
    

    【04】export命令可以出现在模块的任何位置,只要处于模块最外层(非某个块级作用域中)就可以。

    如果处于块级作用域内,就会报错。

    import命令也是如此。

    function foo () {
      export default 'bar' // SyntaxError
    }
    foo()
    

    【05】export语句输出的值是动态绑定,绑定其所在的模块。

    代码输出变量foo,值为bar,500毫秒之后变成baz。

    export var foo = 'bar';
    setTimeout(() => foo = 'baz', 500);
    

    【06】模块之间也可以继承。

    export * from "fileName"

    假设有一个circleplus块,继承了circle模块。

    export *命令会忽略circle模块的default方法。

    // circleplus.js
    
    export * from 'circle';
    export var e = 2.71828182846;
    export default function(x) {return Math.exp(x);}
    

    第三节:import命令

    【01】通过import命令加载模块(文件)。

    【02】import命令接受用逗号分隔的要从其他模块导入的变量列表,用花括号括起来。

    变量名必须与被导入模块的输出的名称相同。

    import {item1,item2} from "fileUrl";

    // main.js
    
    import {firstName, lastName, year} from './profile';function setName(element) {
      element.textContent = firstName + ' ' + lastName;}
    

    【03】使用as关键字,将引用变量重命名。

    import { lastName as surname } from './profile';
    

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

    foo();
    
    import { foo } from 'my_module';//不会报错。
    

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

    但是从可读性考虑,不建议采用这种写法,而应该采用标准写法。

    export { es6 as default } from './someModule';
    // 等同于
    import { es6 } from './someModule';
    export default es6;
    

    【06】另外,ES7有一个提案,简化先输入后输出的写法,拿掉输出时的大括号。

    // 提案的写法
    export v from "mod";
    // 现行的写法
    export {v} from "mod";
    

    【07】import语句会执行加载的模块。

    仅仅执行lodash模块,但是不输入任何值。

    import+空格+模块名字符串。

    import 'lodash'
    

    【08】整体加载

    用星号(*)指代为一个对象,所有的引用值都加载在这个对象上面。

    import+*+as+新变量名+from+模块地址字符串。

    // circle.js
    
    export function area(radius) {return Math.PI * radius * radius;}
    export function circumference(radius) {return 2 * Math.PI * radius;}
    

    单一加载:

    // main.js
    
    import { area, circumference } from './circle';
    
    console.log("圆面积:" + area(4));
    console.log("圆周长:" + circumference(14));
    

    整体加载:

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

    【09】module命令

    module命令可以取代import命令,达到整体引用模块的作用。

    module命令后面跟一个变量,表示输入的模块定义在该变量上。

    module +变量名+from+模块地址字符串

    // main.js
    
    module circle from './circle';
    
    console.log("圆面积:" + circle.area(4));
    console.log("圆周长:" + circle.circumference(14));
    

    第四节:export default命令

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

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

    (吃码小妖:好像并不需要,直接整体加载就是了。)

    【02】使用export default命令,为模块指定默认输出。

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

    所以,import命令后面才不用加大括号,因为只可能对应一个方法。

    写法1:

    export default 匿名函数。

    其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

    这时就不需要知道原模块输出的函数名。

    这时import命令后面,不使用大括号。

    // export-default.js
    export default function () {  console.log('foo');}
    // import-default.js
    import customName from './export-default';customName(); // 'foo' 
    

    写法2:

    export default 函数声明

    export default命令用在非匿名函数前,也是可以的。函数的函数名,在模块外部是无效的。加载的时候,视同匿名函数加载。

    // export-default.js
    export default function foo() {  console.log('foo');}
    // 或者写成
    function foo() {  console.log('foo');}
    
    export default foo;
    

    写法3:

    export default value

    如果要输出默认的值,只需将值跟在export default之后即可。

    export default 42; 写法4:
    

    export default也可以用来输出类。

    // MyClass.js
    export default class { ... }
    // main.js
    import MyClass from 'MyClass'let o = new MyClass();
    

    【04】下面比较一下默认输出和正常输出。

    // 输出
    export default function crc32() { // ...}
    // 输入
    import crc32 from 'crc32';
    // 输出
    export function crc32() { // ...};
    // 输入
    import {crc32} from 'crc32';
    

    【05】本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。

    所以,下面的写法是有效的。

    // modules.js
    function add(x, y) {return x * y;};
    export {add as default};
    // app.js
    import { default as xxx } from 'modules';
    

    【06】如果想在一条import语句中,同时输入默认方法和其他变量。

    import customName, { otherMethod } from './export-default';
    

    【】例子:

    import $ from 'jquery';
    

    第五节:ES6模块加载的实质

    【01】Moduel模块加载的机制,与CommonJS模块完全不同。

    CommonJS模块输出的是一个值的拷贝,而Module模块输出的是值的引用。

    CommonJS模块输入的是被输出值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

    例子。

    下面是一个模块文件lib.js。

    // lib.js
    var counter = 3;function incCounter() {  counter++;}
    module.exports = {
      counter: counter,
      incCounter: incCounter,};
    

    加载上面的模块。

    counter输出以后,lib.js模块内部的变化就影响不到counter了。

    // main.js
    var counter = require('./lib').counter;var incCounter = require('./lib').incCounter;
    
    console.log(counter);  // 3
    incCounter();
    console.log(counter); // 3
    

    ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个动态的只读引用。

    等到真的需要用到时,再到模块里面去取值,换句话说,ES6的输入有点像Unix系统的”符号连接“,原始值变了,输入值也会跟着变。

    因此,ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

    还是举上面的例子。

    // lib.js
    export let counter = 3;
    export function incCounter() {  counter++;}
    // main1.js
    import { counter, incCounter } from './lib';
    console.log(counter); // 3
    incCounter();
    console.log(counter); // 4
    

    【】由于ES6输入的模块变量,只是一个”符号连接“,所以这个变量是只读的,对它重新赋值会报错。

    因为变量obj指向的地址是只读的,不能重新赋值,这就好比main.js创造了一个名为obj的const变量。

    // lib.js
    export let obj = {};
    // main.js
    import { obj } from './lib';
    
    obj.prop = 123; // OK
    obj = {}; // TypeError
    

    第六节:循环加载

    【01】“循环加载”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。

    通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。

    但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现a依赖b,b依赖c,c又依赖a这样的情况。这意味着,模块加载机制必须考虑“循环加载”的情况。

    // a.js
    var b = require('b');
    // b.js
    var a = require('a');
    

    【02】对于JavaScript语言来说,目前最常见的两种模块格式CommonJS和ES6,处理“循环加载”的方法是不一样的,返回的结果也不一样。

    【03】CommonJS模块的加载原理

    CommonJS的一个模块,就是一个脚本文件。

    require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。

    {
      id: '...',
      exports: { ... },
      loaded: true,...}
    

    上面代码中,该对象的id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。

    以后需要用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。

    CommonJS模块的循环加载

    CommonJS模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。

    CommonJS的做法是,一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

    让我们来看,Node官方文档里面的例子。

    脚本文件a.js代码如下。

    a.js脚本先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,等待b.js执行完毕,再往下执行。

    exports.done = false;var b = require('./b.js');
    console.log('在 a.js 之中,b.done = %j', b.done);
    exports.done = true;
    console.log('a.js 执行完毕');
    

    再看b.js的代码。

    exports.done = false;var a = require('./a.js');
    console.log('在 b.js 之中,a.done = %j', a.done);
    exports.done = true;
    console.log('b.js 执行完毕');
    

    b.js执行到第二行,就会去加载a.js,这时,就发生了“循环加载”。系统会去a.js模块对应对象的exports属性取值,可是因为a.js还没有执行完,从exports属性只能取回已经执行的部分,而不是最后的值。

    a.js已经执行的部分,只有一行。

    exports.done = false;
    

    因此,对于b.js来说,它从a.js只输入一个变量done,值为false。

    然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js。

    于是,a.js接着往下执行,直到执行完毕。

    我们写一个脚本main.js,验证这个过程。

    var a = require('./a.js');var b = require('./b.js');
    console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
    

    执行main.js,运行结果如下。

    $ node main.js
    
    在 b.js 之中,a.done = false
    b.js 执行完毕
    在 a.js 之中,b.done = true
    a.js 执行完毕
    在 main.js 之中, a.done=true, b.done=true
    

    上面的代码证明了两件事。一是,在b.js之中,a.js没有执行完毕,只执行了第一行。

    二是,main.js执行到第二行时,不会再次执行b.js,而是输出缓存的b.js的执行结果,即它的第四行。

    exports.done = true;
    

    总之,CommonJS输入的是被输出值的拷贝,不是引用。

    ES6模块的循环加载

    ES6处理“循环加载”与CommonJS有本质的不同。

    ES6模块是动态引用,遇到模块加载命令import时,不会去执行模块,只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

    ES6模块中的值属于【动态只读引用】。

    对于只读来说,即不允许修改引入变量的值,import的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

    对于动态来说,原始值发生变化,import加载的值也会发生变化。不论是基本数据类型还是复杂数据类型。

    循环加载时,ES6模块是动态引用。只要两个模块之间存在某个引用,代码就能够执行。

    吃码小妖:也就是说2个文件本身都是可以加载的,然后在运行时去找需要用的值,这时也是可以找到的。

    不需要加载依赖关系。

    例子

    // a.js
    import {bar} from './b.js';
    
    export function foo() {
        bar();
        console.log('执行完毕');
    }
    
    foo();
    
    // b.js
    import {foo} from './a.js';
    
    export function bar() {
        if (Math.random() > 0.5) {
            foo();
        }
    }
    

    按照CommonJS规范,上面的代码是没法执行的。a先加载b,然后b又加载a,这时a还没有任何执行结果,所以输出结果为null,即对于b.js来说,变量foo的值等于null,后面的foo()就会报错。

    但是,ES6可以执行上面的代码。

    a.js之所以能够执行,原因就在于ES6加载的变量,都是动态引用其所在的模块。只要引用是存在的,代码就能执行。

    相关文章

      网友评论

          本文标题:【ES6脚丫系列】模块Module

          本文链接:https://www.haomeiwen.com/subject/sbtynctx.html