ES6学习系列——Module

ES6 模块(Module)总览:

在ES6 之前,JS中并没有模块体系,但是程序猿还是搞出了替代的模块加载方案:客户端用AMD,而服务端用commonJS。在ES6 中正式提出了模块(Module),相较于前面的替代方案,它的静态优化更好,所以效率更加高,前展也更为可观,为JS 的语法拓展可以提供条件。
为什么说它的静态优化更好呢?因为ES6 的模块在编译的过程中就可以确定模块间的依赖关系以及要输出和输入的变量。更直观地说,就是ES6 模块在编译的时候就可以完成模块的加载。
反观AMD 和 commonJS,在这类模块加载方案中,只有在运行当中用到相关的模块,才会对这个模块进行整体的加载,然后生成一个对象,在这个对象中去查找相关属性,效率自然就低一点。

再来说说ES6 模块的一些特点:
  • 自动采用严格模式,不需要再加上”use strict”;
  • 尤其需要注意的是,在模块的顶层代码(也就是模块中非块级作用域{ }内的代码)中,不要使用this,因为在严格模式中禁止this 指向全局对象(此时this 为 undefined);

1、export 命令

export 命令作用是向外输出变量,而关键就在于提供对外接口,规定要与模块内部的变量对应起来;而且export 命令必须要在模块的顶层中,不能写在块级作用域中,否则就给你报错;
所以往往是这样输出的 export { };

let a = 123;
let b = function () { console.log("hello world");};

//可以使用as 来重命名多次, 用不同的名字来调用
export {
    a,
    b as renamedB1,
    b as renamedB2
};

//函数和类还可以这样输出
export function c () {
    console.log("welcome to NewYork");
}

//接口和对应的值是动态绑定的,输出的foo的值是test1,在一秒后悔变成test2
export let foo = 'test1';
setTimeoUt( ()=>{foo = 'test2'}, 1000);

2、import 命令

使用import 命令可以加载某个已经输出的模块;
语法一般像这样: import { } from url ;
放在module文件夹中,要输出的模快profile.js

let name = "Troyes";
let age = 39;
function printMsg (name, age) {
    console.log('name is:'+ name+ ' ,and age is:'+ age);
}
export { name, age, printMsg };
  • { }中可以写从其他模块导入的对应变量名(也就是说这些变量要与export 的对外接口同名);
import {name, age, printMsg as outPut} from 'module/profile';
outPut(); //name is:Troyes ,and age is:39
  • 可以在{} 使用 as来为输入的变量重新命名;
  • import 命令具有提升效果,写在文件末尾也会提升到头部首先执行;
  • url 是输出模块所在的位置,可以是相对或者绝对路径,倘若url 只是模块的名字而非路径,那就要另外写配置文件来给 JS 引擎指明该模块的位置;
  • 由于import 是静态执行,所以在 import 中,不能使用只有在运行中才能得到结果的语法结构,譬如表达式和变量;
  • 同一个import 语句如果同时出现,那么只会执行一次;
  • 加载同一个模块中的多个变量,import 也是只执行一次(在编译的时候将这些要输入的变量都搜集起来,然后一次性都加载进来)。
  • 现阶段最好不要混用ModuleimportcommomJSrequire,容易出bug;
如何加载整个模块?

整体加载某个模块,只需要用 * 来指定一个新的对象便可,只不过这个新对象在加载完之后是不允许在运行的时候再去改变(譬如添加属性之类的操作)。要输出的模块还是上面的proflie.js,具体操作如下:

import * as person from 'module/profile';

person.printMsg(); //name is:Troyes ,and age is:39

3、export 和 import 的配合使用

  • 倘若在一个模块当中,要先后输入输出同一个模块,有简写的方式,操作如下:
import {name, age} from 'module/profile';
export {name, age};

//可以简写为:
export {name, age} from 'module/profile';
  • 整个模块输出:
export * from 'module/profile';

4、往前看:import() 函数

import 是静态分析,也就是在编译阶段就处理完成了,所以无法在运行的时候动态加载;就有提案建议搞个import() 函数,来支持动态加载;

下面来两个链接(对应中英文版):
Native ECMAScript modules: dynamic import()
原生 ECMAScript 模块:动态import()

5、ES6 Module 加载实现探讨

这里先讨论在浏览器中Module的用法,客户端的node 我还没动手去学习。
在ES6 之前,浏览器加载脚本都是 用的<script>标签, 而浏览器对js 脚本的加载采取的是同步方式,也就是说页面渲染的时候遇到js 脚本,必须要先把脚本执行完(如果是外部脚本还要先下载),才会继续渲染。

//内部脚本
<script type='application/javascript'>
    let a = 123;
</script>

//外部脚本(也就加上src)
//注意:如果是外部脚本,标签里面写的内容将会被忽略
<script type='application/javascript' src=url></script>

//浏览器的默认脚本语言就是js,所以type可以省略
<script>
    let a = 123; 
</script>

//js脚本异步加载处理:添加defer 或者 async 属性
//二者有何区别:虽然都是异步加载,defer 是先下载,等整个页面渲染完才会去执行;
//而async 就比较厉害了,下载完之后页面渲染就要停下来,等此脚本执行完才继续渲染;
//另外,加defer 才能保证加载顺序,加async 是不能保证的;
<script src=url defer> </script>
<script src=url async> </script>
(1)浏览器如何加载ES6 模块?

加载也是利用<script> 标签,只是要加上 type="module"
再者它默认加上defer 属性,所以如果想要改变它的执行时机,就要另外加上async 属性。
另外一个需要注意的点是:模块中 import的时候,url中 其模块的文件名后缀 .js 不能省略;而且模块中,顶层的this返回的是 undefined,而不是window

(2)循环加载:两个脚本相互依赖,循环加载就会出现

说到循环加载,就必须探讨加载原理:

对于ES6 模块的加载原理,上面已经探讨过啦:在编译阶段就已经将模块的依赖关系和要输出输入的变量确定下来;

上实例:

// a.js
import {bar} from './b';
console.log('a.js');
console.log(bar);
export let foo = 'foo';

// b.js
import {foo} from './a';
console.log('b.js');
console.log(foo);
export let bar = 'bar';

a 和 b 相互依赖,构成循环加载。我们来看a 的执行结果:

b.js
undefined
a.js
bar

再来看具体的执行过程:
a.js 第一行就加载 b.js,所以先执行 b.js,而 b.js 的第一行又是执行 a.js , 而 a.js 已经在执行了,所以就继续执行 b.js , 所以第一行就是输出 “b.js” ;
接下来到 console.log(foo);,而这时候 在 a.js 中foo 还没有定义,所以第二行输出的是”undefined”;
b.js 接下来正常执行完,就会继续执行 a.js ,接下来输出的自然就是 “a.js”, “bar”;

接下来就看看 commonJS 模块是如何加载的:

在commonJS 中,一个模块就是单独的一个脚本文件,一旦用 reqiure 命令去加载脚本,这个脚本就会整个执行,然后生成一个对象,放在内存中;接下来如果再一次用reqiure 加载这个模块,引擎不会再去执行整个脚本,而是去内存中取第一次加载生成的那个对象;更重要的是,commonJS 遇上循环加载,返回的是已经执行部分的值,后面还没执行的部分的值影响不到它。
上实例:

//a.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 执行完毕');

直接来看执行过程:
a.js 先执行,输出 done 变量 ,然后加载 b.js ,去执行 b.js,a.js 就停在了第二行,等待b.js 执行完;
b.js 执行到第二行的时候,去加载 a.js ,这时就出现循环加载;b.js 就去内存中查找 a.js 已经执行部分所对应的对象的属性,得到了 a.js 输出的done 变量,所以第三行打印:在 b.js 之中,a.done = false。知道b.js 执行完毕,a.js 才会继续执行;

(3)ES6 转码:

本渣喜欢用babel 来将ES6 代码 编译成 ES5;
babel 基于nodeJS,安装和使用如下(用的是windows的 cmd命令行):

//创建初始化文件
npm init 

//安装babel-cli, 加-D 是为了误删node_module文件夹之后,可以通过'npm install' 命令行再下回来
npm install babel-cli -D

//修改package.json 文件,往"script" 添加启动和构建命令简写
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    //新添run 和 build 命令
    "run": "node server.js --port=80 -s -O=a.log",
    //src是待编译文件所在文件夹的名字, 而lib(默认帮你创建)则是编译后的文件所在文件夹的名字
    "build": "babel src -d lib"
  }

//安装 babel-preset-env,下面的 --save-dev 可以简写为 -D
npm install --save-dev babel-preset-env

//安装babel-polyfill,这东西用来转码ES6 新增的API,Babel默认只转码语法。用的时候只要在脚本的头部加上 import 'babel-polyfill';
npm i babel-polyfill -D

//给babel-preset-env 写配置文件".babelrc",内容如下(更深入的的配置自己去翻文档):
{
    "presets": ["env"]
}
//启动babel 编译
npm run start

另外一个工具是 ES6 module transpiler (没用过),npm 文档建议这样安装和使用:
The transpiler can be used directly from the command line:

$ npm install -g es6-module-transpiler
$ compile-modules convert foo.js

Here is the basic usage:

compile-modules convert -I lib -o out FILE [FILE…]

猜你喜欢

转载自blog.csdn.net/qq_39798135/article/details/82430439