es6常见知识点

打印 上一主题 下一主题

主题 1033|帖子 1033|积分 3099

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
官方文档:[https://es6.ruanyifeng.com/](https://es6.ruanyifeng.com/)

一、Class

1、Class

Class只是一个语法糖,其功能用es5也能实现,但是比es5更符合类的等待 界说:
constructor代表构造方法,而this指向new 天生的实例
界说类方法时,可以不使用function
注意:类的内部全部界说的方法,都是不可枚举的(non-enumerable)。
  1. //定义类
  2. class Point {
  3.   constructor(x, y) {
  4.     this.x = x;
  5.     this.y = y;
  6.   }
  7.   toString() {
  8.     return '(' + this.x + ', ' + this.y + ')';
  9.   }
  10. }
复制代码
使用
  1. new Point(x,y)
复制代码
2、constructor

类的默认方法,new天生对象实例的时间实验的就是这个方法 一个类必须有constructor方法
constructor默认返回实例对象
3、Class不存在变量提拔

```plain new Foo(); // ReferenceError class Foo {} ``` 4、Class的继承

extends关键字
  1. class ColorPoint extends Point {}
复制代码
注意:
1.子类必须调用super,子类本身没有this,super是父类的构造函数,调用super,子类才有this
这是由于子类实例的构建,是基于对父类实例加工,只有super方法才华返回父类实例。
5、类的prototype属性和__proto__属性

1.子类的__proto__指向父类 2.子类的prototype的__proto__指向父类的.prototype
  1. class A {
  2. }
  3. class B extends A {
  4. }
  5. B.__proto__ === A // true
  6. B.prototype.__proto__ === A.prototype // true
复制代码
super关键字
1.调用super方法时,super代表父类的constructor方法
2.作为属性super调用时,super代表父类本身
ES6改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种情势调用,ES6规定Object构造函数会忽略参数。
6、getter和setter

7、Generate方法

直接在方法属性前加* 8、静态方法static

一个方法前加static,则该方法不会被继承,而是通过类来直接调用 父类的静态方法,可以被子类继承。
静态方法也可以从super上调用,由于super指向父类本身
注意:Class内部只有静态方法,没有静态属性。
9、new.target

(在构造函数中)返回new命令作用于的那个构造函数。 二、Module

1、严酷模式

ES6 的模块自动接纳严酷模式,不管你有没有在模块头部加上`"use strict";` 。 严酷模式重要有以下限定。


  • 变量必须声明后再使用
  • 克制this指向全局对象
  • 不能使用fn.caller和fn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protected、static和interface)
其中,尤其必要注意this的限定。ES6 模块之中,顶层的this指向undefined,即不应该在顶层代码使用this。
2、export命令

模块功能重要由两个命令构成:`export` 和`import` 。`export` 命令用于规定模块的对外接口,`import` 命令用于输入其他模块提供的功能。 一个模块就是一个独立的文件。该文件内部的全部变量,外部无法获取。假如你希望外部可以或许读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,内里使用export命令输出变量。
  1. // profile.js
  2. export var firstName = 'Michael';
  3. export var lastName = 'Jackson';
  4. export var year = 1958;
复制代码
上面代码是profile.js文件,生存了用户信息。ES6 将其视为一个模块,内里用export命令对外部输出了三个变量。
export的写法,除了像上面这样,还有另外一种。
  1. // profile.js
  2. var firstName = 'Michael';
  3. var lastName = 'Jackson';
  4. var year = 1958;
  5. export { firstName, lastName, year };
复制代码
上面代码在export命令后面,使用大括号指定所要输出的一组变量。它与前一种写法是等价的,但是应该优先考虑使用这种写法。由于这样就可以在脚本尾部,一眼看清晰输出了哪些变量。
export命令除了输出变量,还可以输出函数或类(class)。
  1. export function multiply(x, y) {
  2.   return x * y;
  3. };
复制代码
上面代码对外输出一个函数multiply。
通常环境下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。
  1. function v1() { ... }
  2. function v2() { ... }
  3. export {
  4.   v1 as streamV1,
  5.   v2 as streamV2,
  6.   v2 as streamLatestVersion
  7. };
复制代码
上面代码使用as关键字,重命名了函数v1和v2的对外接口。重命名后,v2可以用不同的名字输出两次。
3、import命令

使用`export`命令界说了模块的对外接口以后,其他 JS 文件就可以通过`import`命令加载这个模块。
  1. // main.js
  2. import { firstName, lastName, year } from './profile.js';
  3. function setName(element) {
  4.   element.textContent = firstName + ' ' + lastName;
  5. }
复制代码
import命令输入的变量都是只读的,由于它的本质是输入接口。也就是说,不允许在加载模块的脚本内里,改写接口。
  1. import {a} from './xxx.js'
  2. a = {}; // Syntax Error : 'a' is read-only;
复制代码
上面代码中,脚本加载了变量a,对其重新赋值就会报错,由于a是一个只读的接口。但是,假如a是一个对象,改写a的属性是允许的。
  1. import {a} from './xxx.js'
  2. a.foo = 'hello'; // 合法操作
复制代码
上面代码中,a的属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,发起凡是输入的变量,都看成完全只读,不要轻易改变它的属性。
import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径。假如不带有路径,只是一个模块名,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
  1. import { myMethod } from 'util';
复制代码
上面代码中,util是模块文件名,由于不带有路径,必须通过配置,告诉引擎怎么取到这个模块。
注意,import命令具有提拔结果,会提拔到整个模块的头部,起首实验。
  1. foo();
  2. import { foo } from 'my_module';
复制代码
上面的代码不会报错,由于import的实验早于foo的调用。这种行为的本质是,import命令是编译阶段实验的,在代码运行之前。
import()类似于 Node 的require方法,区别重要是前者是异步加载,后者是同步加载
3、模块的整体加载

除了指定加载某个输出值,还可以使用整体加载,即用星号(`*`)指定一个对象,全部输出值都加载在这个对象上面。 下面是一个circle.js文件,它输出两个方法area和circumference。
  1. // circle.js
  2. export function area(radius) {
  3.   return Math.PI * radius * radius;
  4. }
  5. export function circumference(radius) {
  6.   return 2 * Math.PI * radius;
  7. }
复制代码
现在,加载这个模块。
  1. // main.js
  2. import { area, circumference } from './circle';
  3. console.log('圆面积:' + area(4));
  4. console.log('圆周长:' + circumference(14));
复制代码
上面写法是逐一指定要加载的方法,整体加载的写法如下。
  1. import * as circle from './circle';
  2. console.log('圆面积:' + circle.area(4));
  3. console.log('圆周长:' + circle.circumference(14));
复制代码
4、export default命令

从前面的例子可以看出,使用`import`命令的时间,用户必要知道所要加载的变量名或函数名,否则无法加载。 为了给用户提供方便,让他们不消阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。
  1. // export-default.js
  2. export default function () {
  3.   console.log('foo');
  4. }
复制代码
上面代码是一个模块文件export-default.js,它的默认输出是一个函数。
其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
  1. // import-default.js
  2. import customName from './export-default';
  3. customName(); // 'foo'
复制代码
上面代码的import命令,可以用任意名称指向export-default.js输出的方法,这时就不必要知道原模块输出的函数名。必要注意的是,这时import命令后面,不使用大括号。
5、export、import、export default几种指令的用法总结

一,import(模块、文件)引入方式

1 引入第三方插件

```plain import echarts from 'echarts' ``` 2.导入 css文件

```plain import 'iview/dist/styles/iview.css'; ``` 假如是在.vue文件中那么在表面套个style
  1. 1. <style>
  2. 2. @import './test.css';
  3. 3. </style>
复制代码
3.导入组件

```plain 1. import name1 from './name1' 2. import name2 from './name2' 3. components:{ 4. name1, 5. name2, 6. }, ``` 4.import '@…'的语句

@ 等价于 /src 这个目录,避免写贫苦又易错的相对路径 5.引入工具类

```plain 1. 第一种是引入单个方法 2. 3. import {axiosfetch} from './util'; 4. 5. 下面是写法,必要export导出 6. export function axiosfetch(options) { 7. 8. } ```
  1. 1. 第二种  导入成组的方法
  2. 2.
  3. 3. import * as tools from './libs/tools'
  4. 4.
  5. 5. 其中tools.js中有多个export方法,把tools里所有export的方法导入
  6. 6.
  7. 7. vue中怎么用呢?
  8. 8. Vue.prototype.$tools = tools
  9. 9. 直接用 this.$tools.method调用就可以了
复制代码
说到这 export 和 export default 又有什么区别呢?
下面看下区别

  1. 1.  先是 export
  2. 2. import {axiosfetch} from './util';  //需要加花括号  可以一次导入多个也可以一次导入一个,但都要加括号
  3. 3. 如果是两个方法
  4. 4. import {axiosfetch,post} from './util';
  5. 5. 再是 export default
  6. 6. import axiosfetch from './util';  //不需要加花括号  只能一个一个导入
复制代码


二,export,import和export default的关系

export 与import是es6中新增模块功能最重要的两个命令。 1.export与export default均可用于导出常量、函数、文件、模块等
2.在一个文件或模块中,export、import可以有多个,export default仅有一个
3.通过export方式导出,在导入时要加{ },export default则不必要{ }
一、import引入文件路径

` import` 引入一个依赖包,不必要相对路径。如:**import  app from ‘app’**; <font style="color:#C7254E;">import</font> 引入一个本身写的js文件,是必要相对路径的。如:import app from ‘./app.js’;
二、import引入文件变量名

1 、使用` export` 抛出的变量必要用{}进行` import`
  1. 1. //a.js
  2. 2. export const str = "blablabla~";
  3. 3. export function log(sth) {
  4. 4.           return sth;
  5. 5.         }
  6. 6.
  7. 7. 对应的导入方式:
  8. 8.
  9. 9. //b.js
  10. 10. import { str, log as _log } from 'a'; //也可以分开写两次,导入的时候带花括号。还可以用as重命名
复制代码
2、使用<font style="color:#C7254E;">export default</font>抛出的变量,只必要本身起一个名字就行:
  1. 1. //a.js :
  2. 2. var obj = { name: ‘example’ };
  3. 3. export default obj;
  4. 4.
  5. 5. //b.js:
  6. 6. import newNname from ‘./a.js’;   //newNname 是自己随便取的名字,这里可以随便命名
  7. 7. console.log(newNname .name);       // example;
复制代码
总结
其中export和export default最大的区别就是export不限变量数 可以不停写,而export default  只输出一次 而且 export出的变量想要使用必须使用{}来盛放,而export default 不必要 只要import任意一个名字来接收对象即可。
三,部分导入和部分导出,全部导入和全部导出

一、部分导出和部分导入

部分导出和部分导入的优势,当资源比较大时建使用部分导出,这样一来使用者可以使用部分导入来减少资源体积,比如element-ui官方的就保举使用部分导入来减少项目体积,由于element-ui是一个十分巨大的框架,假如我们只用到其中的一部分组件, 那么只将用到的组件导入就可以了。
  1. 1. //部分导出
  2. 2. //A.js
  3. 3. export function helloWorld(){
  4. 4.  conselo.log("Hello World");
  5. 5. }
  6. 6. export function test(){
  7. 7.  conselo.log("this's test function");
  8. 8. }
  9. 9.
  10. 10. //部分导入
  11. 11. //B.js
  12. 12. import {helloWorld} from "./A.js" //只导入A.js中的helloWorld方法
  13. 13. helloWorld(); //执行utils.js中的helloWorld方法
复制代码
假如我们必要A.js中的全部资源,则可以全部导入,如下:
  1. 1. import * as _A from "./A.js" //导入全部的资源,_A为别名,在调用时使用
  2. 2. _A.helloWorld(); //执行A.js中的helloWorld方法
  3. 3. _A.test(); //执行A.js中的test方法
复制代码
二、全部导出和全部导入

假如使用全部导出,那么使用者在导入时则必须全部导入,保举在写方法库时使用部分导出,从而将全部导入或者部分导入的权力留给使用者。 必要注意的是:一个js文件中可以有多个export,但只能有一个export default
  1. 1. //全部导出  A.js
  2. 2. var helloWorld=function(){
  3. 3.  conselo.log("Hello World");
  4. 4. }
  5. 5. var test=function(){
  6. 6.  conselo.log("this's test function");
  7. 7. }
  8. 8. export default{
  9. 9.  helloWorld,
  10. 10.  test
  11. 11. }
  12. 12.
  13. 13. //全部导入  B.js
  14. 14. import A from "./A.js"
  15. 15. A.helloWorld();
  16. 16. A.test();
复制代码
6、Module 的加载实现

欣赏器加载

传统方法

HTML 网页中,欣赏器通过`
  1. <!-- 页面内嵌的脚本 -->
  2. <script type="application/javascript">
  3.   // module code
  4. </script>
  5. <!-- 外部脚本 -->
  6. <script type="application/javascript" src="path/to/myModule.js">
  7. </script>
复制代码
默认环境下,欣赏器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,比及实验完脚本,再继续向下渲染。假如是外部脚本,还必须参加脚本下载的时间。假如脚本体积很大,下载和实验的时间就会很长,因此造成欣赏器堵塞,用户会感觉到欣赏器“卡死”了,没有任何相应。这显然是很欠好的体验,以是欣赏器允许脚本异步加载,下面就是两种异步加载的语法。
  1. <script src="path/to/myModule.js" defer></script>
  2. <script src="path/to/myModule.js" async></script>
复制代码
上面代码中,<script>标签打开defer或async属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和实验,而是直接实验后面的命令。
defer与async的区别是:
1、defer要比及整个页面在内存中正常渲染结束(DOM 结构完全天生,以及其他脚本实验完成),才会实验;async一旦下载完,渲染引擎就会中断渲染,实验这个脚本以后,再继续渲染。一句话,<font style="color:#F5222D;">defer</font>是“渲染完再实验”,<font style="color:#F5222D;">async</font>是“下载完就实验”。
2、假如有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能包管加载顺序的。
加载规则

欣赏器加载 ES6 模块,也使用`
  1. <script type="module" src="./foo.js"></script>
复制代码
上面代码在网页中插入一个模块foo.js,由于type属性设为module,以是欣赏器知道这是一个 ES6 模块。
欣赏器对于带有type="module"的<script>,都是异步加载,不会造成堵塞欣赏器,即比及整个页面渲染完,再实验模块脚本,等同于打开了<script>标签的defer属性。
  1. <script type="module" src="./foo.js"></script>
  2. <!-- 等同于 --><script type="module" src="./foo.js" defer></script>
复制代码
假如网页有多个<script type="module">,它们会按照在页面出现的顺序依次实验。
<script>标签的async属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即实验。实验完成后,再规复渲染。
  1. <script type="module" src="./foo.js" async></script>
复制代码
一旦使用了async属性,<script type="module">就不会按照在页面出现的顺序实验,而是只要该模块加载完成,就实验该模块。
ES6 模块与 CommonJS 模块的差别

讨论 Node.js 加载 ES6 模块之前,必须了解 ES6 模块与 CommonJS 模块完全不同。 它们有三个巨大差别。


  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
第二个差别是由于 CommonJS 加载的是一个对象(即<font style="color:#F5222D;">module.exports</font>属性),该对象只有在脚本运行完才会天生。而 ES6 模块不是对象,它的对外接口只是一种静态界说,在代码静态解析阶段就会天生。
下面重点表明第一个差别。
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变革就影响不到这个值。请看下面这个模块文件lib.js的例子。
  1. // lib.js
  2. var counter = 3;
  3. function incCounter() {
  4.   counter++;
  5. }
  6. module.exports = {
  7.   counter: counter,
  8.   incCounter: incCounter,
  9. };
复制代码
上面代码输出内部变量counter和改写这个变量的内部方法incCounter。然后,在main.js内里加载这个模块。
  1. // main.js
  2. var mod = require('./lib');
  3. console.log(mod.counter);  // 3
  4. mod.incCounter();
  5. console.log(mod.counter); // 3
复制代码
上面代码阐明,lib.js模块加载以后,它的内部变革就影响不到输出的mod.counter了。这是由于mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才华得到内部变动后的值。
  1. // lib.js
  2. var counter = 3;
  3. function incCounter() {
  4.   counter++;
  5. }
  6. module.exports = {
  7.   get counter() {
  8.     return counter
  9.   },
  10.   incCounter: incCounter,
  11. };
复制代码
上面代码中,输出的counter属性实际上是一个取值器函数。现在再实验main.js,就可以正确读取内部变量counter的变动了。
  1. $ node main.js
  2. 3
  3. 4
复制代码
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时间,遇到模块加载命令import,就会天生一个只读引用。比及脚本真正实验时,再根据这个只读引用,到被加载的那个模块内里去取值。换句话说,ES6 的import有点像 Unix 系统的“符号毗连”,原始值变了,<font style="color:#F5222D;">import</font>加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块内里的变量绑定其所在的模块。
还是举上面的例子。
  1. // lib.js
  2. export let counter = 3;
  3. export function incCounter() {
  4.   counter++;
  5. }
  6. // main.js
  7. import { counter, incCounter } from './lib';
  8. console.log(counter); // 3
  9. incCounter();
  10. console.log(counter); // 4
复制代码
上面代码阐明,ES6 模块输入的变量counter是活的,完全反应其所在模块lib.js内部的变革。
由于 ES6 输入的模块变量,只是一个“符号毗连”,以是这个变量是只读的,对它进行重新赋值会报错。
  1. // lib.js
  2. export let obj = {};
  3. // main.js
  4. import { obj } from './lib';
  5. obj.prop = 123; // OK
  6. obj = {}; // TypeError
复制代码
上面代码中,main.js从lib.js输入变量obj,可以对<font style="color:#F5222D;">obj</font>添加属性,但是重新赋值就会报错。由于变量<font style="color:#F5222D;">obj</font>指向的地址是只读的,不能重新赋值,这就比如main.js创造了一个名为obj的const变量。
末了,<font style="color:#F5222D;">export</font>通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。
  1. // mod.js
  2. function C() {
  3.   this.sum = 0;
  4.   this.add = function () {
  5.     this.sum += 1;
  6.   };
  7.   this.show = function () {
  8.     console.log(this.sum);
  9.   };
  10. }
  11. export let c = new C();
复制代码
上面的脚本mod.js,输出的是一个C的实例。不同的脚本加载这个模块,得到的都是同一个实例。
  1. // x.js
  2. import {c} from './mod';
  3. c.add();
  4. // y.js
  5. import {c} from './mod';
  6. c.show();
  7. // main.js
  8. import './x';
  9. import './y';
复制代码
现在实验main.js,输出的是1。
  1. $ babel-node main.js
  2. 1
复制代码
这就证明白x.js和y.js加载的都是C的同一个实例。
Node.js 的模块加载方法

概述

JavaScript 现在有两种模块。一种是 ES6模块,一种是 CommonJS 模块。 CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容。语法上面,两者最显着的差别是,CommonJS 模块使用require()和module.exports,ES6 模块使用import和export。
它们接纳不同的加载方案。从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。
Node.js 要求 ES6 模块接纳.mjs后缀文件名。也就是说,只要脚本文件内里使用import或者export命令,那么就必须接纳.mjs后缀名。Node.js 遇到.mjs文件,就认为它是 ES6 模块,默认启用严酷模式,不必在每个模块文件顶部指定"use strict"。
假如不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。
  1. {
  2.    "type": "module"
  3. }
复制代码
一旦设置了以后,该目录内里的 JS 脚本,就被表明用 ES6 模块。
  1. # 解释成 ES6 模块
  2. $ node my-app.js
复制代码
假如这时还要使用 CommonJS 模块,那么必要将 CommonJS 脚本的后缀名都改成.cjs。假如没有type字段,或者type字段为commonjs,则.js脚本会被表明成 CommonJS 模块。
总结为一句话:.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json内里type字段的设置。
注意,ES6 模块与 CommonJS 模块只管不要混用。require命令不能加载.mjs文件,会报错,只有import命令才可以加载.mjs文件。反过来,.mjs文件内里也不能使用require命令,必须使用import。
package.json 的 main 字段

`package.json`文件有两个字段可以指定模块的入口文件:`main`和`exports`。比较简单的模块,可以只使用`main`字段,指定模块加载的入口文件。
  1. // ./node_modules/es-module-package/package.json
  2. {
  3.   "type": "module",
  4.   "main": "./src/index.js"
  5. }
复制代码
上面代码指定项目的入口脚本为./src/index.js,它的格式为 ES6 模块。假如没有type字段,index.js就会被表明为 CommonJS 模块。
然后,import命令就可以加载这个模块。
  1. // ./my-app.mjs
  2. import { something } from 'es-module-package';
  3. // 实际加载的是 ./node_modules/es-module-package/src/index.js
复制代码
上面代码中,运行该脚本以后,Node.js 就会到./node_modules目录下面,寻找es-module-package模块,然后根据该模块package.json的main字段去实验入口文件。
这时,假如用 CommonJS 模块的require()命令去加载es-module-package模块会报错,由于 CommonJS 模块不能处理export命令。
package.json 的 exports 字段

`exports`字段的优先级高于`main`字段。它有多种用法。 (1)子目录别名
package.json文件的exports字段可以指定脚本或子目录的别名。
  1. // ./node_modules/es-module-package/package.json
  2. {
  3.   "exports": {
  4.     "./submodule": "./src/submodule.js"
  5.   }
  6. }
复制代码
上面的代码指定src/submodule.js别名为submodule,然后就可以从别名加载这个文件。
  1. import submodule from 'es-module-package/submodule';
  2. // 加载 ./node_modules/es-module-package/src/submodule.js
复制代码
下面是子目录别名的例子。
  1. // ./node_modules/es-module-package/package.json
  2. {
  3.   "exports": {
  4.     "./features/": "./src/features/"
  5.   }
  6. }
  7. import feature from 'es-module-package/features/x.js';
  8. // 加载 ./node_modules/es-module-package/src/features/x.js
复制代码
假如没有指定别名,就不能用“模块+脚本名”这种情势加载脚本。
  1. // 报错
  2. import submodule from 'es-module-package/private-module.js';
  3. // 不报错
  4. import submodule from './node_modules/es-module-package/private-module.js';
复制代码
(2)main 的别名
exports字段的别名假如是.,就代表模块的主入口,优先级高于main字段,并且可以直接简写成exports字段的值。
  1. {
  2.   "exports": {
  3.     ".": "./main.js"
  4.   }
  5. }
  6. // 等同于
  7. {
  8.   "exports": "./main.js"
  9. }
复制代码
由于exports字段只有支持 ES6 的 Node.js 才认识,以是可以用来兼容旧版本的 Node.js。
  1. {
  2.   "main": "./main-legacy.cjs",
  3.   "exports": {
  4.     ".": "./main-modern.cjs"
  5.   }
  6. }
复制代码
上面代码中,老版本的 Node.js (不支持 ES6 模块)的入口文件是main-legacy.cjs,新版本的 Node.js 的入口文件是main-modern.cjs。
(3)条件加载
利用.这个别名,可以为 ES6 模块和 CommonJS 指定不同的入口。目前,这个功能必要在 Node.js 运行的时间,打开--experimental-conditional-exports标志。
  1. {
  2.   "type": "module",
  3.   "exports": {
  4.     ".": {
  5.       "require": "./main.cjs",
  6.       "default": "./main.js"
  7.     }
  8.   }
  9. }
复制代码
上面代码中,别名.的require条件指定require()命令的入口文件(即 CommonJS 的入口),default条件指定其他环境的入口(即 ES6 的入口)。
上面的写法可以简写如下。
  1. {
  2.   "exports": {
  3.     "require": "./main.cjs",
  4.     "default": "./main.js"
  5.   }
  6. }
复制代码
注意,假犹如时还有其他别名,就不能接纳简写,否则或报错。
  1. {
  2.   // 报错
  3.   "exports": {
  4.     "./feature": "./lib/feature.js",
  5.     "require": "./main.cjs",
  6.     "default": "./main.js"
  7.   }
  8. }
复制代码
CommonJS 模块加载 ES6 模块

CommonJS 的`require()`命令不能加载 ES6 模块,会报错,只能使用`import()`这个方法加载。
  1. (async () => {
  2.   await import('./my-app.mjs');
  3. })();
复制代码
上面代码可以在 CommonJS 模块中运行。
require()不支持 ES6 模块的一个原因是,它是同步加载,而 ES6 模块内部可以使用顶层await命令,导致无法被同步加载。
ES6 模块加载 CommonJS 模块

ES6 模块的`import`命令可以加载 CommonJS 模块,但是只能整体加载,不能只加载单一的输出项。 CommonJS模块输出的是一个值的拷贝,而ES6模块输出的是值的引用。
CommonJS一旦输出一个值,模块内部的变革就影响不到这个值。
ES6模块原始值变了,import输入的值也会跟着变。
  1. // 正确
  2. import packageMain from 'commonjs-package';
  3. // 报错
  4. import { method } from 'commonjs-package';
复制代码
这是由于 ES6 模块必要支持静态代码分析,而 CommonJS 模块的输出接口是module.exports,是一个对象,无法被静态分析,以是只能整体加载。
加载单一的输出项,可以写成下面这样。
  1. import packageMain from 'commonjs-package';
  2. const { method } = packageMain;
复制代码
还有一种变通的加载方法,就是使用 Node.js 内置的module.createRequire()方法。
  1. // cjs.cjs
  2. module.exports = 'cjs';
  3. // esm.mjs
  4. import { createRequire } from 'module';
  5. const require = createRequire(import.meta.url);
  6. const cjs = require('./cjs.cjs');
  7. cjs === 'cjs'; // true
复制代码
上面代码中,ES6 模块通过module.createRequire()方法可以加载 CommonJS 模块。但是,这种写法等于将 ES6 和 CommonJS 混在一起了,以是不发起使用。
同时支持两种格式的模块

一个模块同时要支持 CommonJS 和 ES6 两种格式,也很容易。 假如原始模块是 ES6 格式,那么必要给出一个整体输出接口,比如export default obj,使得 CommonJS 可以用import()进行加载。
假如原始模块是 CommonJS 格式,那么可以加一个包装层。
  1. import cjsModule from '../index.js';
  2. export const foo = cjsModule.foo;
复制代码
上面代码先整体输入 CommonJS 模块,然后再根据必要输出具名接口。
你可以把这个文件的后缀名改为.mjs,或者将它放在一个子目录,再在这个子目录内里放一个单独的package.json文件,指明{ type: "module" }。
另一种做法是在package.json文件的exports字段,指明两种格式模块各自的加载入口。
  1. "exports":{
  2.   "require": "./index.js",
  3.   "import": "./esm/wrapper.js"
  4. }
复制代码
上面代码指定require()和import,加载该模块会自动切换到不一样的入口文件。
Node.js 的内置模块

Node.js 的内置模块可以整体加载,也可以加载指定的输出项。
  1. // 整体加载
  2. import EventEmitter from 'events';
  3. const e = new EventEmitter();
  4. // 加载指定的输出项
  5. import { readFile } from 'fs';
  6. readFile('./foo.txt', (err, source) => {
  7.   if (err) {
  8.     console.error(err);
  9.   } else {
  10.     console.log(source);
  11.   }
  12. });
复制代码
加载路径

ES6 模块的加载路径必须给出脚本的完整路径,不能省略脚本的后缀名。`import`命令和`package.json`文件的`main`字段假如省略脚本的后缀名,会报错。
  1. // ES6 模块中将报错
  2. import { something } from './index';
复制代码
为了与欣赏器的import加载规则雷同,Node.js 的.mjs文件支持 URL 路径。
  1. import './foo.mjs?query=1'; // 加载 ./foo 传入参数 ?query=1
复制代码
上面代码中,脚本路径带有参数?query=1,Node 会按 URL 规则解读。同一个脚本只要参数不同,就会被加载多次,并且生存成不同的缓存。由于这个原因,只要文件名中含有:、%、#、?等特殊字符,最好对这些字符进行转义。
目前,Node.js 的import命令只支持加载当地模块(file:协议)和data:协议,不支持加载长途模块。另外,脚本路径只支持相对路径,不支持绝对路径(即以/或//开头的路径)。
内部变量

ES6 模块应该是通用的,同一个模块不消修改,就可以用在欣赏器环境和服务器环境。为了达到这个目标,Node.js 规定 ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量。 起首,就是this关键字。ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块,这是两者的一个巨大差别。
其次,以下这些顶层变量在 ES6 模块之中都是不存在的。


  • arguments
  • require
  • module
  • exports
  • __filename
  • __dirname
循环加载

“循环加载”(circular dependency)指的是,`a`脚本的实验依赖`b`脚本,而`b`脚本的实验又依赖`a`脚本。
  1. // a.js
  2. var b = require('b');
  3. // b.js
  4. var a = require('a');
复制代码
通常,“循环加载”表示存在强耦合,假如处理欠好,还可能导致递归加载,使得步伐无法实验,因此应该避免出现。
但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现a依赖b,b依赖c,c又依赖a这样的环境。这意味着,模块加载机制必须考虑“循环加载”的环境。
对于 JavaScript 语言来说,目前最常见的两种模块格式 CommonJS 和 ES6,处理“循环加载”的方法是不一样的,返回的结果也不一样。
CommonJS 模块的加载原理

先容 ES6 怎样处理“循环加载”之前,先先容目前最盛行的 CommonJS 模块格式的加载原理。 CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会实验整个脚本,然后在内存天生一个对象。
  1. {
  2.   id: '...',
  3.   exports: { ... },
  4.   loaded: true,
  5.   ...
  6. }
复制代码
上面代码就是 Node 内部加载模块后天生的一个对象。该对象的id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,表示该模块的脚本是否实验完毕。其他还有许多属性,这里都省略了。
以后必要用到这个模块的时间,就会到exports属性上面取值。即使再次实验require命令,也不会再次实验该模块,而是到缓存之中取值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动扫除系统缓存。
CommonJS 模块的循环加载

CommonJS 模块的重要特性是加载时实验,即脚本代码在`require`的时间,就会全部实验。一旦出现某个模块被"循环加载",就只输出已经实验的部分,还未实验的部分不会输出。 让我们来看,Node 官方文档内里的例子。脚本文件a.js代码如下。
  1. exports.done = false;
  2. var b = require('./b.js');
  3. console.log('在 a.js 之中,b.done = %j', b.done);
  4. exports.done = true;
  5. console.log('a.js 执行完毕');
复制代码
上面代码之中,a.js脚本先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,期待b.js实验完毕,再往下实验。
再看b.js的代码。
  1. exports.done = false;
  2. var a = require('./a.js');
  3. console.log('在 b.js 之中,a.done = %j', a.done);
  4. exports.done = true;
  5. console.log('b.js 执行完毕');
复制代码
上面代码之中,b.js实验到第二行,就会去加载a.js,这时,就发生了“循环加载”。系统会去a.js模块对应对象的exports属性取值,可是由于a.js还没有实验完,从exports属性只能取回已经实验的部分,而不是末了的值。
a.js已经实验的部分,只有一行。
  1. exports.done = false;
复制代码
因此,对于b.js来说,它从a.js只输入一个变量done,值为false。
然后,b.js接着往下实验,比及全部实验完毕,再把实验权交还给a.js。于是,a.js接着往下实验,直到实验完毕。我们写一个脚本main.js,验证这个过程。
  1. var a = require('./a.js');
  2. var b = require('./b.js');
  3. console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
复制代码
实验main.js,运行结果如下。
  1. $ node main.js
  2. 在 b.js 之中,a.done = false
  3. b.js 执行完毕
  4. 在 a.js 之中,b.done = true
  5. a.js 执行完毕
  6. 在 main.js 之中, a.done=true, b.done=true
复制代码
上面的代码证明白两件事。一是,在b.js之中,a.js没有实验完毕,只实验了第一行。二是,main.js实验到第二行时,不会再次实验b.js,而是输出缓存的b.js的实验结果,即它的第四行。
  1. exports.done = true;
复制代码
总之,CommonJS 输入的是被输出值的拷贝,不是引用。
另外,由于 CommonJS 模块遇到循环加载时,返回的是当前已经实验的部分的值,而不是代码全部实验后的值,两者可能会有差别。以是,输入变量的时间,必须非常鉴戒。
  1. var a = require('a'); // 安全的写法
  2. var foo = require('a').foo; // 危险的写法
  3. exports.good = function (arg) {
  4.   return a.foo('good', arg); // 使用的是 a.foo 的最新值
  5. };
  6. exports.bad = function (arg) {
  7.   return foo('bad', arg); // 使用的是一个部分加载时的值
  8. };
复制代码
上面代码中,假如发生循环加载,require('a').foo的值很可能后面会被改写,改用require('a')会更保险一点。
ES6 模块的循环加载

ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用,假如使用`import`从一个模块加载变量(即`import foo from 'foo'`),那些变量不会被缓存,而是成为一个指向被加载模块的引用,必要开发者本身包管,真正取值的时间可以或许取到值。 请看下面这个例子。
  1. // a.mjs
  2. import {bar} from './b';
  3. console.log('a.mjs');
  4. console.log(bar);
  5. export let foo = 'foo';
  6. // b.mjs
  7. import {foo} from './a';
  8. console.log('b.mjs');
  9. console.log(foo);
  10. export let bar = 'bar';
复制代码
上面代码中,a.mjs加载b.mjs,b.mjs又加载a.mjs,构成循环加载。实验a.mjs,结果如下。
  1. $ node --experimental-modules a.mjs
  2. b.mjs
  3. ReferenceError: foo is not defined
复制代码
上面代码中,实验a.mjs以后会报错,foo变量未界说,这是为什么?
让我们一行行来看,ES6 循环加载是怎么处理的。起首,实验a.mjs以后,引擎发现它加载了b.mjs,因此会优先实验b.mjs,然后再实验a.mjs。接着,实验b.mjs的时间,已知它从a.mjs输入了foo接口,这时不会去实验a.mjs,而是认为这个接口已经存在了,继续往下实验。实验到第三行console.log(foo)的时间,才发现这个接口根本没界说,因此报错。
解决这个问题的方法,就是让b.mjs运行的时间,foo已经有界说了。这可以通过将foo写成函数来解决。
  1. // a.mjs
  2. import {bar} from './b';
  3. console.log('a.mjs');
  4. console.log(bar());
  5. function foo() { return 'foo' }
  6. export {foo};
  7. // b.mjs
  8. import {foo} from './a';
  9. console.log('b.mjs');
  10. console.log(foo());
  11. function bar() { return 'bar' }
  12. export {bar};
复制代码
这时再实验a.mjs就可以得到预期结果。
  1. $ node --experimental-modules a.mjs
  2. b.mjs
  3. foo
  4. a.mjs
  5. bar
复制代码
这是由于函数具有提拔作用,在实验import {bar} from './b'时,函数foo就已经有界说了,以是b.mjs加载的时间不会报错。这也意味着,假如把函数foo改写成函数表达式,也会报错。
  1. // a.mjs
  2. import {bar} from './b';
  3. console.log('a.mjs');
  4. console.log(bar());
  5. const foo = () => 'foo';
  6. export {foo};
复制代码
上面代码的第四行,改成了函数表达式,就不具有提拔作用,实验就会报错。
我们再来看 ES6 模块加载器SystemJS给出的一个例子。
  1. // even.js
  2. import { odd } from './odd'
  3. export var counter = 0;
  4. export function even(n) {
  5.   counter++;
  6.   return n === 0 || odd(n - 1);
  7. }
  8. // odd.js
  9. import { even } from './even';
  10. export function odd(n) {
  11.   return n !== 0 && even(n - 1);
  12. }
复制代码
上面代码中,even.js内里的函数even有一个参数n,只要不等于 0,就会减去 1,传入加载的odd()。odd.js也会做类似操作。
运行上面这段代码,结果如下。
  1. $ babel-node
  2. > import * as m from './even.js';
  3. > m.even(10);
  4. true
  5. > m.counter
  6. 6
  7. > m.even(20)
  8. true
  9. > m.counter
  10. 17
复制代码
上面代码中,参数n从 10 变为 0 的过程中,even()一共会实验 6 次,以是变量counter等于 6。第二次调用even()时,参数n从 20 变为 0,even()一共会实验 11 次,加上前面的 6 次,以是变量counter等于 17。
这个例子要是改写成 CommonJS,就根本无法实验,会报错。
  1. // even.js
  2. var odd = require('./odd');
  3. var counter = 0;
  4. exports.counter = counter;
  5. exports.even = function (n) {
  6.   counter++;
  7.   return n == 0 || odd(n - 1);
  8. }
  9. // odd.js
  10. var even = require('./even').even;
  11. module.exports = function (n) {
  12.   return n != 0 && even(n - 1);
  13. }
复制代码
上面代码中,even.js加载odd.js,而odd.js又去加载even.js,形成“循环加载”。这时,实验引擎就会输出even.js已经实验的部分(不存在任何结果),以是在odd.js之中,变量even等于undefined,比及后面调用even(n - 1)就会报错。
  1. $ node
  2. > var m = require('./even');
  3. > m.even(10)
  4. TypeError: even is not a function
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

河曲智叟

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表