用模块封装代码
javascript用“共享一切”的方法加载代码,这是该语言中最容易出错且另人感到困惑的地方。其他语言使用诸如包这样的概念来定义代码作用域。在es6以前,在应用程序的每一个js中定义的一切都共享一个全局作用域。随着web应用程序变得更加复杂,js代码的使用量也开始增长,这一做法会引起问题,如命名冲突和安全问题。es6的一个目标是解决作用域问题,也为了使用js应用程序显得有序,于是引进了模块
什么是模块
模块是自动运行在严格模式下并且没有办法退出运行的js代码。与共享一切架构相反的是,在模块顶部创建的变量不会自动被添加到全局共享作用域,这个变量仅在模块的顶级作用域存在,而且模块必须导出一些外部代码可以访问的元素,如变量或函数。模块也可以从其他模块导入绑定。
另外两个模块的特性与作用域关系不大,但也很重要。首先,在模块的顶部,this的值是undefined;其次,模块不支持html风格的代码注释,这是从早期浏览器残余下来的javascript的特性。
脚本,也就是任何不是模块的javascript代码,则缺少这些特性。模块和其他javascript代码之间的差异可能乍一看不起眼(好像一样)。但是它们代表了javascript代码加载和求值的一个重要变化。模块真正魔力所在是仅导出和导入你需要的绑定,而不是将所有的东西都放到一个文件。只有很好的理解了导出和导入才能理解模块与脚本的区别。
导出的基本语法
可以使用export
关键字将一部分已发布的代码暴露给其他模块,在最简单的用例中,可以将export放在任何变量、函数或类声明的前面,以将它们从模块中导出。如:
// 导出数据
export var color = "red"
export let name = "Nicholas";
export const magicNumber = 7;
// 导出函数
export funciton sum(num1,num2){
return num1+num2
}
// 导出类
export class Rectangle {
constructor(length,width) {
this.length = length;
this.width = width
}
}
// 定义一个函数
function multiply(num1,num2){
return num1 * num2;
}
// ...之后将它导出
export multiply
注意到,除中了
export
关键字外,每一个声明与脚本中的一模一样,因为导出的函数和类声明需要一个名称,所以代码中的每一个函数或类也确实有这个名称。除非用default
关键字, 所以不能用export直接导入一个匿名函数或类,multiply
在定义它时没有马上导出它,由于不必总是导出声明,可以导出引用。
任何未显示导出的变量、函数或类都是模块私有的,无法从模块外部访问的
导入语法
import { identifier1, identifier2 } from './example.js';
导入绑定的列表看起来与解构对象很相似,但它不是,从模块中导入一个绑定时,它就好像使用了const定义的一样。结果是你无法定义另一个同名变量(包括导入另一个同名绑定)也无法在import
语句前使用标识符或改变绑定的值。
// 导入一个
import { sum } from './example.js'
console.log(sum(1,2)) //3
sum = 1 //抛出一个错误
// 不能给导入的绑定重新赋值
// 导入多个绑定
import { sum, multiply, magicNumber } from './example.js'
console.log(sum(1, magicNumber)); //8
console.log(multiply(1,2)); // 2
// 导入整个模块作为一个单一的对象。然后所有导出都可以作为对象的属性使用
import * as exapmle from './example.js'
console.log(exapmle.sum(1, magicNumber)); //8
console.log(exapmle.multiply(1,2)); // 2
import * as exapmle from './example.js'
这种引入格式被称作命名空间导入(namespace import)。 因为example.js文件中不存在example
对象,故而它作为example.js中所有导出成员的命名空间对象而被创建。但是,请注意,不管在import语句中把一个模块写了多少次,该模块将只执行一次。,导入模块的代码执行后,实例化过的模块被保存在内存中,只要另一个import
语句引用它就可以重复使用它。
import { smu } from './example.js';
import { multiply } from './example.js';
import { magicNumber } from './example.js';
尽管模块中有3个import语句,但example.js将只执行一次,如果同一个应用程序中的其它模块也从example.js中导入绑定,那么那些模块与此代码使用的是相同的模块。
import
和export
的一个重要限制是,它们必须在其他语句和函数之外使用(if else funciton中)。导入绑定的一个微妙怪异之外
es6的 import
语句为变量、函数和类创建的是只读绑定,而不是像正常变量一样简单地引用原始绑定。标签符只有在被导出的模块中可以修改,即便是导入绑定的模块也无法更改绑定的值,如:
// example.js
export var name = "Nicholas";
export funtion setName(newName){
name = newName
}
// 使用模块
import { name, setName } from './example.js'
console.log(name); // Nicholas
setName("Greg");
// 会回到导出setName()的模块中去执行,并将name设置为Greg
// 然后name的更改会体现在(自动)导入name模块中,
// 导入的name是导出的name标识的本地名称。
// 两个name不是同一回事儿
console.log(name); // Greg
name = "hehe" // 抛出错误
导入和导出的
as
的使用
function sum(num1, num2) {
return num1 + num2;
}
export { sum as add}
// sum 是以add名导出的,引入时只能引用add
import { add } from './sample.js';
// 导入也可以使用as给它重命个名
import { add as sum } from './sample.js'
// 当前上下文中只有sum 没有add
默认值 ,由于诸如commonjs(浏览器外的另一个js使用规范)的其它模块系统中,从模块中导出和导入默认值是一个常见的做法,该语法被进行优化。模块的默认值指的是通过default关键字指定的单个变量、函数或类,只能为每个模块设置一个默认的导出值,导出时多次使用default关键字是一个语法错误。
default
是一个关键字,不能出现在变量中
下面导出默认的3种方式
// 1
export default funciton(num1, num2) {
return num1 + num2
}
// 2
function sum(num1, num2) {
return num1 + num2
}
export default sum
// 3
function sum(num1, num2) {
return num1 + num2
}
export {sum as default}
导入默认的使用
import sum from './sample.js'
console.log(sum(1,2)) //3
// 导入默念值及非默认值的混合
// example.js
export let color = 'red';
export default function(num1, num2) {
return num1 + num2
}
// 使用时
import sum,{ color } from './sample.js'
// 默认导出的被重全名为sum,并且导入了color
重新导出已经导入的值
// 1
import { sum } from './example.js'
export { sum }
// 也可以通过一条语句来完成这个功能
export { sum } from './example.js'
// 或者给它起个别的名字导出
export { sum as add } from './example.js'
无绑定导入
某些模块可能不导出任何东西,相反它们可能只修改全局作用域中的对象。尽管模块中的顶层变量、函数和类不自动地出现在全局作用域中,但这并不意味着模块无法方法全局作用域。内建对象(如Array和Object)的共享定义可以在模块中访问,对这些对象所做的更改将反映在其它模块中。
如我们给Array添加一个pushAll方法,无绑定导入最有可能被应用于创建Polyfill和Shim
// sample.js
// 没有导出也没有导入
Array.prototype.pushAll = function(items){
if(!Array.isArray(items)){
throw new TypeError('参数必须是一个数组')
}
return this.push(...items)
}
// 使用
import './example.js' //没导入任何,仅执行了一下
let colors = ["red", "green", "blue"];
let items = []
items.pushAll(colors)
在web应用中使用模块
es6以前,web浏览器也有多种方式可以将javascript包含在web应用程序中,这些脚本加载的方式:
- 在
<script>
元素中通过src
属性指定一个加载代码的地址来加载javascript代码文件 - 将javascript代码内嵌到没有src属性的
<script>
元素中 - 通过web worker或service worker的方法加载并执行javascript代码文件
为了完全支持模块功能,web浏览器必须更新这些机制,具体说明总结如下:
在
<script>
中使用模块
非模块加载时,当type属性缺失或包含一个javascript内容类型"text/javascript"
时是作为脚本加载,<script>
元素可以执行内联代码工加载src指定的文件,当type属性值为type="module"
时支持加载模块,这种模式下可以让浏览器将所有内联代码或包含在src指定的文件中的代码按照模块而非脚本的方式加载。区分是模块还是脚本就是看这个type="module"
与否
<!-- 加载一个javascript 模块文件 -->
<script type="module" src="module.js"></script>
<!-- 内联引入一个模块 -->
<script type="module">
import { sum } from './example.js'
let result = sum(1,2);
</script>
web浏览器中模块的加载顺序
模块加载时,defer
这个可选属性是必须的(默认就是这种,不一定要写在那),因为模块和脚本不同它是独一无二的,可以通过import关键字来指明其所依赖的其他文件,并且这些文件被加载进该模块才能正确执行。
模块是按照它们出现在html文件中的顺序执行,也就是说,无论模块中包含的是内联代码还是指定的src属性,第一个<script type="module">
总是在第二个之前执行,如下
<!-- 先执行这个 -->
<script type="module" src="module1.js"></script>
<!--第二执行这个 -->
<script type="module">
import { sum } from './example.js'
let result = sum(1,2);
</script>
<!-- 最后执行这个 -->
<script type="module" src="module2.js"></script>
因为每个模块都可以从一个或多个其它的模块导入,这会使问题复杂化。因此,首先解析模块以识别所有导入语句,然后,每个导入语句都触发一次获取过程(从网络或缓存),并且在所有导入资源被加载和执行后才会执行当前模块。
用<script type="module">
显示引入和用import隐式导入的所有模块都是按需加载并执行的。 执行过程描述如下
- 下载并解析module1.js
- 递归下载并解析module1.js中导入的资源
- 解析内联模块
- 递归下载并解析内联模块中导入的资源
- 下载并解析module2.js
- 递归下载并解析module2.js中导入的资源
加载完成后,只有当文档完全被解析之后才会执行其它操作,文档解析后,会发生以下操作
- 递归执行module1.js中导入的资源
- 执行module1.js
- 递归执行内联模块中导入的资源
- 执行内联模块
- 递归执行module2.js中导入的资源
- 执行module2.js
web浏览器中的异步模块加载
script
中的asnyc
属性,当其应用于脚本时,脚本文件将在文件完全下载并解析后执行。但是,文档中asnyc脚本的顺序不会影响脚本的执行顺序,脚本在下载完成后立即执行,而不必等待包含的文档完成解析。这个属性应用于module时,情况类似,唯一区别是,在模块执行前,模块中所有的导入资源都必须下载下来。这可以大确保只有当模块执行所需的所有资源都下载完成后才执行模块,但不能保证模块的执行时机。
<!-- 不能保证哪个先执行,哪个先把相关资源下载完,哪个先执行 -->
<script type="module" src="module1.js" asnyc></script>
<script type="module" src="module2
交模块作为Worker加载
// 第二个参数用来指定类型
let worker = new Worker('module.js',{type:'module'})
浏览器模块说明符解析
浏览器要求模块说明符具有以下几个格式之一:
- 以
/
开头的解析为从根目录开始 - 以
./
开头的解析为从当前目录开始 - 以
../
开头的解析为从父目录开始 - URL格式
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://joyjs.cn/archives/4339