文章目录
- JavaScript模块化与作用域
-
- [作用域和作用域链 - 静态](#作用域和作用域链 - 静态)
- [执行上下文 - 动态](#执行上下文 - 动态)
- 模块化
-
- [ES6 模块化](#ES6 模块化)
-
- export与import的使用
- [import()函数 - 实现动态引入](#import()函数 - 实现动态引入)
- Node模块化
- [commonJs和es module的区别](#commonJs和es module的区别)
JavaScript模块化与作用域
作用域和作用域链 - 静态
- 作用域:一个代码段所在区域
- 核心:作用域是静态的,编写代码时就确定
- 作用:绑定变量在这个作用域有效,隔离变量,不同作用域下同名变量不会有冲突
- 作用域链:多个作用域嵌套,就近选择,先在自己作用域找,然后去就近的作用域找。
作用域分类
1.全局作用域
2.函数作用域
3.块级作用域
全局作用域可以理解为window.变量
,函数作用域函数名.变量
,块级作用域块唯一标识符.变量
。(这里只是方便理解,并不是知识点)
案例
aaa执行时,现在当前函数作用域找a(l理解为aaa.a),没找到。去父级作用域window里面找,找到a=10
js
let a = 10;
function aaa() {
console.log(a);
}
function bbb() {
let a = 20;
aaa();
}
bbb();
执行上下文 - 动态
抽象当前JavaScript
的执行环境,包括变量、this指向等信息。每当JavaScript
开始执行时,它都在执行上下文中运行。
全局执行上下文:在执行全局代码前将window确定为全局执行上下文,在整个页面生存周期内存在。
- let定义的全局变量变量提升,添加为window的属性
- function声明的全局函数 --> 赋值(函数体),添加为window的方法
- this --> 赋值window
- 开始执行全局代码
- 函数执行上下文:在调用函数,准备执行函数体之前,创建对应的函数执行上下文对象
函数执行上下文,每当调用一个函数时,都会创建一个新的函数执行上下文,函数执行上下文在函数执行结束后被销毁。
- 形参变量 --> 赋值(实参)--> 添加到函数执行上下文的属性
- arguments(形参列表封装成的伪数组)-->赋值(实参列表),添加到函数执行上下文的属性
- let 变量提升,添加为函数执行上下文的属性
- function声明的函数-->赋值(函数体),添加为函数执行上下文的方法
- this-->赋值(调用函数的对象)
- 开始执行函数体代码
执行上下文的两个阶段: 创建阶段和执行阶段
1.在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象
2.在全局执行上下文(window)确定后,将其添加到栈中(压栈)
3.在函数执行上下文创建后,将其添加到栈中(压栈)
4.在当前函数执行完成后,将栈顶的对象移除(出栈)
5.当所有的代码执行完后,栈中只剩下window
作用域 | 执行上下文 |
---|---|
定义了几个函数 + 1 = 几个作用域 | 执行了几个函数 + 1 = 几个执行上下文 |
函数定义时就确定了,一直存在,不会再变化,是静态的 | 全局执行上下文环境实在全局作用域确定之后,js代码执行之前创建的 调用函数时创建,函数调用结束被释放,是动态的 |
1.创建阶段
1.创建变量对象:根据上下文类型创建一个空的对象
2.建立作用域链:作用域链是一个指向父级作用域的链,用于查找变量的值。
3.确定this的指向
4.初始化变量对象:将函数的参数(仅函数执行上下文?)、函数声明和变量添加到变量对象中。
2.执行阶段
1.执行代码:按照代码的顺序执行,对变量赋值等操作。
2.访问变量:通过作用域链查找变量的值。
3.执行函数:在函数上下文中,执行函数体内的代码。
模块化
script
标签直接引入的方式,没有模块化,script
内部的变量是可以相互渲染的。
模块的概念:一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取。
CommonJS
服务于服务器 => NodeJS、BrowserifyES6
模块化 服务于服务器和浏览器
ES6 模块化
ES6模块化的设计思想使尽量静态化,使得编译时就能确定模块的依赖关系,以及输入输出的变量。但是也支持动态引入的方式。
特点
-
ES6 module
的引入和导出是静态的,import
会自动提升到代码的顶层import
,export
不能放在块级作用域或条件语句中。import
关键字是静态导入,import()
函数可以实现动态导入。这种静态语法,在编译过程中确定了导入和导出的关系,所以更方便去查找依赖,更方便去 tree shaking 。
-
使用
import
导入的变量是只读的,可以理解默认为const
装饰,无法被赋值 -
使用
import
导入的变量是与原变量绑定/引用的,可以理解为 import 导入的变量无论是否为基本类型都是引用传递。 -
import
导入文件时或仅导入文件的部分变量时,都会执行该文件。 -
import
导入相对路径上的./
不能省略
export与import的使用
前提:script
标签加上一个属性type=module
,那么该script
就是一个es
模块,可以使用模块的导入import
与导出export
。
默认暴露export default
js
export default 表达式
import 给抛出的表达式命名 from '地址'
分别暴露于统一暴露export
js
// 分别暴露
export 变量
export const name = "ranan";
export const fn = function(){};
import { fn } from '地址'
// 统一暴露
let name = "ranan";
let fun = function(){};
//注意{}不是对象的意思是特定的语法,并给name重命名
export {name1:name,fun};
import { name } from '地址'
import引入
可以通过as
关键字给引入部分起别名。
js
//通用引入 将所有输出的变量放在m1对象中,采用m1.xx的方式使用
import * as m1 from './xxx'
/*
统一暴露或分别暴露,这里的{}也是特定语法
并且{ }内的变量名称需要和export导出的变量名称相同
*/
import {name,fun} from './xxx'
//默认暴露的写法,必须要写别名,因为default是关键字
import {default as data} from "path"
//简便形式 针对默认暴露
import data from "path"
说明
-
export不支持直接导出变量和值
变量只存在声明时,声明之后变量都会作为表达式使用。错误写法:
js// 错误案例 const name = 'aaa' export name // 导出变量 export 'aaa' // 导出值
-
在一个文件或模块中
export
、import
可以有多个,export default
仅有一个
import()函数 - 实现动态引入
语法:
返回值:promise对象
,pormise
返回的成功值就是暴露的对象。
作用:懒加载/按需加载 -> import()
可以很轻松的实现代码分割。避免一次性加载大量 js 文件,造成首次加载白屏时间过长的情况。
import()
动态加载一些内容,可以放在条件语句或者函数执行上下文中
js
if(isRequire){
const result = import('./b')
}
Node模块化
Node 是 CommonJS
在服务端一个具有代表性的实现
特点
- 在
commonJs
中每个js文件都是一个单独的模块module
- 使用
require()
方法加载其他模块时,会执行被加载模块中的代码 module.exports
指向暴露的对象,exports
是简写情况。默认情况下,exports
和module.exports
指向同一个对象exports==module.exports
。最终引入的是module.exports
。require()
可以在任意的位置,动态加载(运行时加载)模块,不会提升到最开头- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- CommonJS 同步加载并执行模块文件,CommonJS 模块在执行阶段分析模块依赖。采用深度优先算法。
module.exports 和 exports
在具体引擎的实现中
js
module.exports = {}
exports = module.exports
所以使用 exports.xx = 'xxx'
其实就是往 module.exports = {}
这个对象中添加属性
引入的是module.exports属性
情况1:exports
默认情况下是指向module.exports
,下面案例重新给exports
赋值了。由于引入的是module.exports
属性,所以导入的数据是{}
。这种方式无法暴露变量。
js
exports = {
name: '123'
}
情况2:module.exports
重新赋值了,由于引入的是module.exports
属性,所以导入的数据是{}
js
exports.name = "123"
module.exports = {}
模块执行的原理
1.在编译过程中,commonJS对js的代码块进行了收尾包装
每个模块文件上存在require
、module
、exports
方法,但是这三个变量在文件中是没有定义的。 commonJS会将我们写代码包装起来,形成包装函数,require
、module
、exports
本质是通过形参的方式传递到包装函数中的。
require
: 引入模块的方法
module
: 记录当前模块信息
exports
:当前模块导出的属性
2.在模块加载的时候,会传入require
、module
、exports
等参数
js
const sayName = require('./hello.js')
module.exports = function say(){
return {
name:sayName(),
author:'ranan'
}
}
//包装后
(function(exports,require,module,__filename,__dirname){
const sayName = require('./hello.js')
module.exports = function say(){
return {
name:sayName(),
author:'ranan'
}
}
})
作用域案例
执行node test1.js
时,输出alex
。每个js文件都是一个单独的模块,每个模块在编译包装后形成函数,所以每个模块内容都在一个函数作用域里。引入模块,相当于执行这个函数并取出module.exports
的内容。
在本案例中fn
、info
和test2.js
的module.exports
都指向同一个地址,所以fn
调用时,实际是调用getInfo
函数。name会先在getInfo
作用域中找,没有找到,再去其父级作用域test2.js
中寻找。
js
//test2.js
const name = "alex"
const getInfo = () =>(info={name: name})
module.exports = getInfo
//test3.js
const name = 'ranran'
const info = require('./test2');
module.exports = info
//test1.js
const fn = require('./test3');
console.log(fn())
模块加载过程
- 使用
require()
方法加载其他模块时,会执行被加载模块中的代码。 require()
可以在任意的位置,动态加载(运行时加载)模块,不会提升到最开头
每个module
上保存了一个 loaded
属性,该属性表示该模块是否被加载。没有加载过就先缓存后执行,已经加载过,就直接去缓存不用执行来避免循环引用问题。
js
/*
根据文件标识符,先查找有没有缓存,有缓存直接返回缓存的内容
没有缓存,创建module对象,将其缓存到module上,然后加载文件,将 loaded 属性设置为 true ,然后返回 module.exports 对象。
*/
// id 为路径标识符
function require(id) {
/* 查找 Module 上有没有已经加载的 js 对象*/
const cachedModule = Module._cache[id]
/* 如果已经加载了那么直接取走缓存的 exports 对象 */
if(cachedModule){
return cachedModule.exports
}
/* 创建当前模块的 module */
const module = { exports: {} ,loaded: false , ...}
/* 将 module 缓存到 Module 的缓存属性中,路径标识符作为 id */
//只会在第一次加载时运行一次,后面都会从缓存中读取,
Module._cache[id] = module
/* 加载文件 */
runInThisContext(wrapper('module.exports = "123"'))(module.exports, require, module, __filename, __dirname)
/* 加载完成 *//
module.loaded = true
/* 返回值 */
return module.exports
}
commonJs和es module的区别
- | commonJs | es module |
---|---|---|
导入方式 | require() 动态加载模块,可以在任意的位置,不会被提升到最前面 |
导入方式分为静态导入和动态导入 静态导入:不能放在块级作用域和条件中,会提升到最前面 动态导入import() 类似require() ,但他是异步加载的 |
构建模块依赖的时期 | require同步加载并执行模块文件,CommonJS 模块在执行阶段分析模块依赖。 | 在编译阶段就建立起了模块之间的依赖关系 ES6 模块在预处理阶段分析模块依赖,在执行阶段执行模块 |
输出的值 | 输出值的拷贝值,一旦输出了某个值,如果模块内部发生变化,不会影响外部的值 | 输出的是值的引用,JS 引擎对脚本静态分析的时候。遇到模块加载命令import ,就会生成一个只读引用。等到脚本真正执行的时候,再根据这个只读引用,到被加载的那个模块里去取值。所以内部发生变化会影响外部的值。 |