函数作用域的缺点
1、函数无法访问在其他函数中定义的变量
不用担心别的函数会影响到自己作用域里的变量,但是不方便在不同的函数中共享变量,可以使用更上一层的作用域来共享变量,比如去全局作用域。
这种方式得保证脚本的执行顺序,全局作用域和子函数的依赖关系是不明显的,不利于代码维护,任何子函数都可随意更改全局作用域的变量,这是很危险的。
模块也是一种作用域
模块作用域可用于在模块中的函数之间共享变量。但是与函数作用域不同,模块可以使用 export 明确的说明模块中的变量或函数可用。其他模块可以通过 import 明确的声明他们依赖那些变量和函数。
因为这是一个明确的关系,所以您可以判断如果删除另一个模块,哪些模块会损坏。
举个例子
// main.js
export let main = 'main'
import { index } from './index.js'
console.log('main', index)
// index.js
import { main } from 'main'
export let index = 'index'
console.log('index', main)
ES 模块如何工作
构建
1. 找出从哪里下载包含模块的文件(又名模块解析)
// 在 html 中
<script src="main.js" type="module">
// 在 js 中
import { count } from './count.js'
2. 获取文件(通过从 URL 下载或从文件系统加载),并添加到模块映射中。
异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>
标签的defer
属性。
3. 生成模块记录,并添加到模块映射中。
将获取的文件,解析成一个模块记录,包含了导入和导出的结构信息。
创建模块记录后,将其放置在模块映射中。这意味着无论何时从现在开始请求它,加载器都可以从该映射中获取它。
整体流程如下:
- 模块会构成一个树
实例化
执行深度优先后序遍历,从模块依赖图的最底部开始实例化。
1、创建作用域
2、创建模块命名空间对象
key 值为 export 导出的变量名,值为指向该变量分配的内存空间。 其属性可更改,值可变量,不可配置。
3、创建模块环境记录
管理模块中最顶层的变量,包块模块自己定义的变量,和通过 import 导入的存在于另一个模块记录中的变量。
将 import 进来的变量 N 指向为另一个模块导出的变量 N2 分配的内存空间(这样访问 N 会间接的访问 N2 的值),注意在该阶段,N 和 N2 都没有实际的值,但是会先初始化任何导出的函数声明。
获取 this 会得到 undefined。
如果获取的是一个间接绑定的值,如果该值所在的模块还未初始化,则抛出 ReferenceError 错误
执行代码
这一步主要是执行代码,填充内存空间。
由于模块映射,模块执行也是单例的。与实例化相同,也是深度优先后序遍历执行的。
EsModule 和 CommonJs 的区别
本质区别,EsModule 是语法层,Js引擎做的事,而 CommonJs 是 node 自己实现的模块机制。
CommonJs 从文件系统加载文件,加载时可阻塞主流程,在加载时便进行模块的实例化和执行,意味着在返回模块实例之前,会遍历整个树,加载、实例化和执行任何依赖项。
CommonJs 可以在模块说明符中使用变量,EsModule 需要在执行前构建整个模块图,不可在模块说明符中使用变量,因为这些变量还没有值。
在 CommonJS 中,整个导出对象在导出时被复制。这意味着导出的任何值(如数字)都是副本。
关于循环引用,在 CommonJs 中
最终会输出 undefined,如果是 Esmodule 会获取到改变后的值。
一些题
普通的导入导出
// module.js
export let thing = 'initial';
setTimeout(() => {
thing = 'changed';
}, 500);
// main.js
import { thing as importedThing } from './module.js';
const module = await import('./module.js');
let { thing } = await import('./module.js');
setTimeout(() => {
console.log(importedThing); // changed
console.log(module.thing); // changed
console.log(thing); // initial
}, 1000);
默认导入导出表达式
// module.js
let thing = 'initial';
export { thing };
export default thing;
setTimeout(() => {
thing = 'changed';
}, 500);
// main.js
import { thing, default as defaultThing } from './module.js';
import anotherDefaultThing from './module.js';
setTimeout(() => {
console.log(thing); // changed
console.log(defaultThing); // initial
console.log(anotherDefaultThing); // initial
}, 1000);
let thing = 'initial';
export { thing, thing as default };
setTimeout(() => {
thing = 'changed';
}, 500);
// main.js
import { thing, default as defaultThing } from './module.js';
import anotherDefaultThing from './module.js';
setTimeout(() => {
console.log(thing); // changed
console.log(defaultThing); // changed
console.log(anotherDefaultThing); // changed
}, 1000);
默认导出函数
// module.js
export default function thing() {}
setTimeout(() => {
thing = 'changed';
}, 500);
// main.js
import thing from './module.js';
setTimeout(() => {
console.log(thing); // changed
}, 1000);
// module.js
function thing() {}
export default thing;
setTimeout(() => {
thing = 'changed';
}, 500);
// function thing() {}
循环引用
// main.js
import { foo } from './module.js';
foo();
export function hello() {
console.log('hello');
}
// module.js
import { hello } from './main.js';
hello();
export function foo() {
console.log('foo');
}
// hello foo
// main.js
import { foo } from './module.js';
foo();
export const hello = () => console.log('hello');
// module.js
import { hello } from './main.js';
hello();
export const foo = () => console.log('foo');
export { hello as default }
// 报错