ES6中的模块化

  JS用"共享一切"的方法加载代码,这是该语言中最易出错且容易令人感到困惑的地方。在ES6以前,在应用程序的每一个JS中定义的一切都共享一个全局作用域。随着web应用程序变得更加复杂,JS代码的使用量也开始增长,这一做法会引起问题,如命名冲突和安全问题。ES6的一个目标是解决作用域问题,也为了使JS应用程序显得有序,于是引进了模块。

一、概述

  模块是自动运行在严格模式下并且没有办法退出运行的JS代码。与共享一切架构相反的是,在模块顶部创建的变量不会自动被添加到全局共享作用域,这个变量仅在模块的顶级作用域中存在,而且模块必须导出一些外部代码可以访问的元素,如变量或函数。模块也可以从其他模块导入绑定

  另外两个模块的特性与作用域关系不大,但也很重要。首先,在模块的顶部,this的值是undefined;其次,模块不支持HTML风格的代码注释,这是从早期浏览器残余下来的JS特性

  脚本,也就是任何不是模块的JS代码,则缺少这些特性。模块和其他JS代码之间的差异可能乍一看不起眼,但是它们代表了JS代码加载和求值的一个重要变化。模块真正的魔力所在是仅导出和导入需要的绑定,而不是将所用东西都放到一个文件。只有很好地理解了导出和导入才能理解模块与脚本的区别

二、导出

  可以用export关键字将一部分己发布的代码暴露给其他模块,在最简单的用例中,可以将export放在任何变量、函数或类声明的前面,以将它们从模块导出

// 导出数据
export var color = "red";
export let name = "Nicholas";
export const magicNumber = 7;
// 导出函数
export function sum(num1, num2) {
    return num1 + num1;
}
// 导出类
export class Rectangle {
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }
}
// 此函数为模块私有
function subtract(num1, num2) {
    return num1 - num2;
}
// 定义一个函数……
function multiply(num1, num2) {
    return num1 * num2;
}
// ……稍后将其导出
export { multiply };

  在这个示例中需要注意几个细节,除了export关键字外,每一个声明与脚本中的一模一样。因为导出的函数和类声明需要有一个名称,所以代码中的每一个函数或类也确实有这个名称。除非用default关键字,否则不能用这个语法导出匿名函数或类

  另外,在定义multiply()函数时没有马上导出它。由于不必总是导出声明,可以导出引用,因此这段代码可以运行。此外,这个示例并未导出subtract()函数,任何未显式导出的变量、函数或类都是模块私有的,无法从模块外部访问

三、导入

  从模块中导出的功能可以通过import关键字在另一个模块中访问,import语句的两个部分分别是要导入的标识符和标识符应当从哪个模块导入

  这是该语句的基本形式

import { identifier1, identifier2 } from "./example.js";

  import后面的大括号表示从给定模块导入的绑定(binding),关键字from表示从哪个模块导入给定的绑定,该模块由表示模块路径的字符串指定(被称作模块说明符)。浏览器使用的路径格式与传给<script>元素的相同,也就是说,必须把文件扩展名也加上。另一方面,Nodejs则遵循基于文件系统前缀区分本地文件和包的惯例。例如,example是一个包而./example.js是一个本地文件

  当从模块中导入一个绑定时,它就好像使用const定义的一样。无法定义另一个同名变量(包括导入另一个同名绑定),也无法在import语句前使用标识符或改变绑定的值

1、导入单个绑定

  假设前面的示例在一个名为"example.js"的模块中,我们可以导入并以多种方式使用这个模块中的绑定

// 单个导入
import { sum } from "./example.js";
console.log(sum(1, 2)); // 3
sum = 1; // 出错

  尽管example.js导出的函数不止一个,但这个示例导入的却只有sum()函数。如果尝试给sum赋新值,结果是抛出一个错误,因为不能给导入的绑定重新赋值

  为了最好地兼容多个浏览器和Node.js环境,一定要在字符串之前包含/、./或../来表示要导入的文件

2、导入多个绑定

  如果想从示例模块导入多个绑定,则可以明确地将它们列出如下

// 多个导入
import { sum, multiply, magicNumber } from "./example.js";
console.log(sum(1, magicNumber)); // 8
console.log(multiply(1, 2)); // 2

  在这段代码中,从example模块导入3个绑定sum、multiply和magicNumber。之后使用它们,就像它们在本地定义的一样

3、导入整个模块

  特殊情况下,可以导入整个模块作为一个单一的对象。然后所有的导出都可以作为对象的属性使用

// 完全导入
import * as example from "./example.js";
console.log(example.sum(1,example.magicNumber)); // 8
console.log(example.multiply(1, 2)); // 2

  在这段代码中,从example.js中导出的所有绑定被加载到一个被称作example的对象中。指定的导出(sum()函数、mutiply()函数和magicNumber)之后会作为example的属性被访问。这种导入格式被称作命名空间导入(namespaceimport)。因为example.js文件中不存在example对象,故而它作为example.js中所有导出成员的命名空间对象而被创建

  但是,不管在import语句中把一个模块写了多少次,该模块将只执行一次。导入模块的代码执行后,实例化过的模块被保存在内存中,只要另一个import语句引用它就可以重复使用它

import { sum } from "./example.js";
import { multiply } from "./example.js";
import { magicNumber } from "./example.js";

  尽管在这个模块中有3个import语句,但example加载只执行一次。如果同一个应用程序中的其他模块也从example.js导入绑定,那么那些模块与此代码将使用相同的模块实例

4、导入绑定的一个微妙怪异之处

  ES6的import语句为变量、函数和类创建的是只读绑定,而不是像正常变量一样简单地引用原始绑定。标识符只有在被导出的模块中可以修改,即便是导入绑定的模块也无法更改绑定的值

export var name = "huochai";
export function setName(newName) {
    name = newName;
}

  当导入这两个绑定后,setName()函数可以改变name的值

import { name, setName } from "./example.js";
console.log(name); // "huochai"
setName("match");
console.log(name); // "match"
name = "huochai"; // error

  调用setName("match")时会回到导出setName()的模块中去执行,并将name设置为"match"。此更改会自动在导入的name绑定上体现。其原因是,name是导出的name标识符的本地名称。本段代码中所使用的name和模块中导入的name不是同一个。也就是说可以调用导入模块里的方法去更改导入模块里的值,但是不能直接在模块里去更改导入的值

四、重命名

  有时候,从一个模块导入变量、函数或者类时,可能不希望使用它们的原始名称。幸运的是,可以在导出过程和导入过程中改变导出元素的名称

  假设要使用不同的名称导出一个函数,则可以用as关键字来指定函数在模块外的名称(类似于sql的语法了)

function sum(num1, num2) {
    return num1 + num2;
}
export { sum as add };

  在这里,函数sum()是本地名称,add()是导出时使用的名称。也就是说,当另一个模块要导入这个函数时,必须使用add这个名称

import { add } from "./example.js";

  如果模块想使用不同的名称来导入函数,也可以使用as关键字

import { add as sum } from "./example.js";
console.log(typeof add); // "undefined"
console.log(sum(1, 2)); // 3

  这段代码导入add()函数时使用了一个导入名称来重命名sum()函数(当前上下文中的本地名称)。导入时改变函数的本地名称意味着即使模块导入了add()函数,在当前模块中也没有add()标识符,只能用重命名的标识符

五、默认值

  由于在诸如CommonJS的其他模块系统中,从模块中导出和导入默认值是一个常见的做法,该语法被进行了优化。模块的默认值指的是通过default关键字指定的单个变量、函数或类,只能为每个模块设置一个默认的导出值,导出时多次使用default关键字是一个语法错误

1、导出默认值

  下面是一个使用default关键字的简单示例

export default function(num1, num2) {
    return num1 + num2;
}

  这个模块导出了一个函数作为它的默认值,default关键字表示这是一个默认的导出,由于函数被模块所代表,因而它不需要一个名称

  也可以在export default之后添加默认导出值的标识符,就像这样

function sum(num1, num2) {
    return num1 + num2;
}
export default sum;

  先定义sum()函数,然后再将其导出为默认值,如果需要计算默认值,则可以使用这个方法。为默认导出值指定标识符的第三种方法是使用重命名语法,如下所示

function sum(num1, num2) {
    return num1 + num2;
}
export { sum as default };

  在重命名导出时标识符default具有特殊含义,用来指示模块的默认值。由于default是JS中的默认关键字,因此不能将其用于变量、函数或类的名称;但是,可以将其用作属性名称。所以用default来重命名模块是为了尽可能与非默认导出的定义一致。如果想在一条导出语句中同时指定多个导出(包括默认导出),这个语法非常有用

2、导入默认值

  可以使用以下语法从一个模块导入一个默认值

// 导入默认值
import sum from "./example.js";
console.log(sum(1, 2)); // 3

  这条import语句从模块example.js中导入了默认值,请注意,这里没有使用大括号,与非默认导入的情况不同。本地名称sum用于表示模块导出的任何默认函数,这种语法是最纯净的,ES6的创建者希望它能够成为web上主流的模块导入形式,并且可以使用已有的对象

  对于导出默认值和一或多个非默认绑定的模块,可以用一条语句导入所有导出的绑定

export let color = "red";
export default function(num1, num2) {
    return num1 + num2;
}

  可以用以下这条import语句导入color和默认函数

import sum, { color } from "./example.js";
console.log(sum(1, 2)); // 3
console.log(color); // "red"

  用逗号将默认的本地名称与大括号包裹的非默认值分隔开

  注意:在import语句中,默认值必须排在非默认值之前

  与导出默认值一样,也可以在导入默认值时使用重命名语法

// 等价于上个例子
import { default as sum, color } from "example";
console.log(sum(1, 2)); // 3
console.log(color); // "red"

  在这段代码中,默认导出(export)值被重命名为sum,并且还导入了color

六、静态加载模式

  ES6中的模块与node.js中的模块加载不同,nodeJS中的require语句是运行时加载,而ES6中的import是静态加载,所以有一些语法限制

1、不能使用表达式和变量等这些只有在运行时才能得到结果的语法结构

// 报错
import { ‘f‘ + ‘oo‘ } from ‘my_module‘;

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

2、importexport命令只能在模块的顶层,不能在代码块之中,如不能在if语句和函数内使用

if (flag) {
    export flag; // 语法错误
}

// 报错
if (x === 1) {
  import { foo } from ‘module1‘;
} else {
  import { foo } from ‘module2‘;
}
function tryImport() {
    import flag from "./example.js"; // 语法错误
}

  以上的写法会报错,是因为在静态分析阶段,这些语法都是没法得到值的

  这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。如果import命令要取代 Node 的require方法,这就形成了一个障碍。因为require是运行时加载模块,import命令无法取代require的动态加载功能

const path = ‘./‘ + fileName;
const myModual = require(path);

  上面的语句就是动态加载,require到底加载哪一个模块,只有运行时才知道。import语句做不到这一点

七、重新导出

  可能需要重新导出模块已经导入的内容

import { sum } from "./example.js";
export { sum }

  虽然这样可以运行,但只通过一条语句也可以完成同样的任务

export { sum } from "./example.js";

  这种形式的export在指定的模块中查找sum声明,然后将其导出。当然,对于同样的值也可以不同的名称导出

export { sum as add } from "./example.js";

  这里的sum是从example.js导入的,然后再用add这个名字将其导出

  如果想导出另一个模块中的所有值,则可以使用 * 模式

export * from "./example.js";

  导出一切是指导出默认值及所有命名导出值,这可能会影响可以从模块导出的内容。例如,如果example.js有默认的导出值,则使用此语法时将无法定义一个新的默认导出

八、无绑定导入

  某些模块可能不导出任何东西,相反,它们可能只修改全局作用域中的对象。尽管模块中的顶层变量、函数和类不会自动地出现在全局作用域中,但这并不意味着模块无法访问全局作用域。内建对象(如Array和Object)的共享定义可以在模块中访问,对这些对象所做的更改将反映在其他模块中

  例如,要向所有数组添加pushAll()方法,则可以定义如下所示的模块

// 没有导出与导入的模块
Array.prototype.pushAll = function(items) {
    // items 必须是一个数组
    if (!Array.isArray(items)) {
        throw new TypeError("Argument must be an array.");
    }
    // 使用内置的 push() 与扩展运算符
    return this.push(...items);
};

  即使没有任何导出或导入的操作,这也是一个有效的模块。这段代码既可以用作模块也可以用作脚本。由于它不导出任何东西,因而可以使用简化的导入操作来执行模块代码,而且不导入任何的绑定

import "./example.js";
let colors = ["red", "green", "blue"];
let items = [];
items.pushAll(colors);

  这段代码导入并执行了模块中包含的pushAll()方法,所以pushAll()被添加到数组的原型,也就是说现在模块中的所有数组都可以使用pushAll()方法了

  注意:无绑定导入最有可能被应用于创建polyfill和Shim

时间: 2024-10-25 17:50:14

ES6中的模块化的相关文章

从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 十六 ║ Vue前篇:ES6初体验 &amp; 模块化

缘起 昨天说到了<从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 十五 ║ Vue前篇:JS对象&字面量&this>,通过总体来看,好像大家对这一块不是很感兴趣,嗯~~这一块确实挺枯燥的,不能直接拿来代码跑一下那种,不过还是得说下去,继续加油吧!如果大家对昨天的小demo练习的话,相信现在已经对JS的面向对象写法很熟悉了,如果嵌套字面量定义函数,如何使用this关键字指向.今天呢,主要说一下ES6中的一些特性技巧,然后简单说一下模块化的问题,好啦,开始今天的讲

好程序员web前端教程分享js中的模块化一

好程序员web前端教程分享js中的模块化一:我们知道最常见的模块化方案有CommonJS.AMD.CMD.ES6,AMD规范一般用于浏览器,异步的,因为模块加载是异步的,js解释是同步的,所以有时候导致依赖还没加载完毕,同步的代码运行结束:CommonJS规范一般用于服务端,同步的,因为在服务器端所有文件都存储在本地的硬盘上,传输速率快而且稳定. 1.script标签引入 最开始的时候,多个script标签引入js文件.但是,这种弊端也很明显,很多个js文件合并起来,也是相当于一个script,

ES6 中 let 和 const 总结

目录 let const 1. let要好好用 1. 基本用法 2. let声明的变量不存在变量提升 3. TDZ(temporal dead zone)暂时性死区 4. 不允许重复声明 2. 块级作用域 1. 为什么需要块级作用域 2. ES6中实现了块级作用域 3. 块级作用域与函数声明 4. do表达式(仅仅是提案) 3. const(不要忘记立即初始化哦) 1. 基本用法 2. const的本质 4. ES6中声明变量的六种方法 5. ES6对顶层对象属性的改变 1. ES5中顶层对象的

Node.js 中使用 ES6 中的 import / export 的方法大全

转自原文 Node.js 中使用 ES6 中的 import / export 的方法大全, 2018.11 如何在 Node.js 中使用 import / export 的三种方法, 2018.8 nodejs_es6_tutorials 因为一些历史原因,虽然 Node.js 已经实现了 99% 的 ES6 新特性,不过截止 2018.8.10,How To Enable ES6 Imports in Node.JS 仍然是老大难问题 下面我来介绍三种方法可以让我们在 Node.js 中使

Es6中如何使用splic,delete等数组删除方法

Es6中如何使用splic,delete等数组删除方法 1:js中的splice方法 splice(index,len,[item])    注释:该方法会改变原始数组. splice有3个参数,它也可以用来替换/删除/添加数组内某一个或者几个值 index:数组开始下标        len: 替换/删除的长度       item:替换的值,删除操作的话 item为空 如:arr = ['a','b','c','d'] 删除 ----  item不设置 arr.splice(1,1)   /

关于ES6中的结构

ES6中的解构赋值主要遵循的规则是,先看等号右边,右边有值走赋值,右边无值走左边默认.下面列出几个小栗子介绍它的主要运用. 1 function fn(){ 2 return 3; 3 } 4 let [x=fn()]=[1]; 5 console.log(x); //x=1 上面的列子看出,尽管x=一个立即执行的函数,但还是先走右边的赋值.如果把等号右边变成一个空数组,那么x=3,走左边的默认赋值. 1 let [a=2,[b=1]]=[1,[2]]; 2 console.log(a,b) 解

ES6中的迭代器(Iterator)和生成器(Generator)

前面的话 用循环语句迭代数据时,必须要初始化一个变量来记录每一次迭代在数据集合中的位置,而在许多编程语言中,已经开始通过程序化的方式用迭代器对象返回迭代过程中集合的每一个元素 迭代器的使用可以极大地简化数据操作,于是ES6也向JS中添加了这个迭代器特性.新的数组方法和新的集合类型(如Set集合与Map集合)都依赖迭代器的实现,这个新特性对于高效的数据处理而言是不可或缺的,在语言的其他特性中也都有迭代器的身影:新的for-of循环.展开运算符(...),甚至连异步编程都可以使用迭代器 本文将详细介

ES6中Arguments和Parameters用法解析

原文链接 译文 ECMAScript 6 (也称 ECMAScript 2015) 是ECMAScript 标准的最新版本,显著地完善了JS中参数的处理方式.除了其它新特性外,我们还可以使用rest参数.默认值.解构赋值等. 本教程中,我们将详细探索arguments和parameters,看看ES6是如果改善升级它们的. 对比 Arguments 和 Parameters 通常情况下提到 Arguments 和 Parameters, 都认为是可以互换使用的.然而,基于本教程的目的,我们做了明

iOS中的模块化开发思路一

我想不光是在iOS开发中,在其他各种软件产业开发中,模块化开发是必不可少的一种开发模式. 那么,在iOS中如何做到模块化开发.小伙伴们之间并行的有规矩的开发.集成.代码了?答案是,设计模式出.依赖抽象,不依赖具体实现. 第一步,拿到每一个模块的开发任务的小伙伴们,首先不要马上去写各种viewController.subViews.networkApi.dataBaseApi.entity.... .我想的是,我们应该定义一下接口,告诉其他调用这个模块的小伙伴们,我们这个模块能够干一些什么...