带你深度了解作用域和闭包
了解作用域
一共有三种,函数作用域,全局作用域和块作用域(ES6的let,const会创建)
什么是块作用域呢?
使用{}包裹,就是一个块作用域,比如if条件判断。
函数和模块两者的不同之处呢?
都有各自的作用域,一个是函数内,一个是模块内,变量只在自己内部被访问。
外部访问不同,函数作用域不可以外部访问,模块可以导出让外部访问。
运行环境不同,函数作用域适用于所有JS运行环境,模块适用于ES module环境或者<script type=’module’>
这里我们可以了解一下模块
为什么要有模块?
早期js,所有变量和函数都在全局作用域里面共享,很容易造成变量名冲突,代码混乱。
所以ES6引入了模块系统ES Module,每个文件都是独立的模块作用域,可以导入导出内容,浏览器和nodejs环境都支持,在浏览器使用js注意,script添加type=‘module’,浏览器就知道这是一个模块。
不同的模块系统
1.ES Module现在最常用的方式
使用import和export 自动严格模式
最高级this是undefined 只会执行一次,缓存结果。
静态加载,编译的时候就确定依赖关系
拓展
有默认导出export default只能导出一个和具名导出export导出多个
重命名模块,可以在导入的时候使用as重命名模块
可以创建模块对象,通过exports * as module from ‘index.js’ 作为module对象成员使用。这样可以不用导出很多个。
可以合并模块,就是如果有很多不同的模块,都导出一个模块,那么我们可以创建一个文件夹用来进行所有模块的导入。
export { Square } from "./shapes/square.js";
export { Triangle } from "./shapes/triangle.js";
export { Circle } from "./shapes/circle.js";
导入声明有提升,也就是导入的值在模块声明前可以使用,但最好还是写在最上面,更容易分析依赖。
循环导入可能存在问题,比如A,导入B,B依赖A
编写同构模块,因为编写的模块代码不一定在每个环境中都可以运行(window在nodejs中没有),为了提高模块的可重用性,可以使用重构,即每个运行中表现相同。
比如在使用之前检测特定的全局变量是否存在。浏览器环境是全局对象是window,nodejs环境全局对象是global,process也是全局对象。process.env获取当前的环境变量,比如 process.env.NODE_ENV
// myModule.js
let password;
if (typeof process !== "undefined") {// 我们在 Node.js 中运行;从 `process.env` 中读取password = process.env.PASSWORD;
} else if (typeof window !== "undefined") {// 我们在浏览器中运行;从输入框中读取password = document.getElementById("password").value;
}
常见的故障:
.mjs 后缀的文件需要以 text/javascript MIME 类型来加载,否则,你会得到严格的 MIME 类型检查错误
如果你尝试用本地文件加载 HTML 文件(即,具有 file:// 的 URL),由于 JavaScript 模块的安全性要求,你会遇到 CORS 错误。你需要通过服务器来做你的测试。
2.CommonJS nodejs早期用法
使用require和module.exports
运行时加载,执行阶段读取。
同步执行,不适用于浏览器环境,只在nodejs中使用
main.js
const a=1
function b (){return 1+1
}
module.exports ={a,b}
引入使用
const {a,b}=require('./main.js')
3.AMD浏览器早期异步模块加载方式
使用define和require
为了解决浏览器中同步加载模块卡顿的问题
在前端打包工具普及之前很常见
静态加载VS运行时加载
静态加载 在代码编译阶段也就是执行前,知道需要哪些模块变量和函数,所以加载速度快,能做语法检测和优化
运行时加载 在代码运行时,加载到require时候才读取加载模块,灵活,但加载效率低,无法提前优化。
动态加载模块什么情况?
仅在需要的时候动态加载模块,而不是预先加载所有模块
import("/modules/mymodule.js").then((module) => {// 使用模块做一些事情。
});
返回一个promise,可以访问导出的模块对象。
什么是MIME?
模块引入的时候要确认文件类型,就可以通过MIME
MIME就是告诉浏览器或者服务器,一个文件的类型是什么。
text/javascript,现代标准,推荐使用 application/javascript,曾经被推荐,现在依然可用 一些旧规范或旧项目 module ,<script type="module"> 中的属性值,表明脚本是 ESM 模块,ES Module加载 text/plain 表示纯文本
了解闭包
闭包的理解就是,函数和函数周围状态的引用组合而成。
具体一点就是,一个函数,里面嵌套了一个函数,内部函数引用外部函数的变量,外部函数在外面被调用,这就形成了一个闭包。
作用: 1.封装私有化变量。 2.维持状态(比如函数实现一个累加功能,闭包的话,每次调用这个函数变量会一直累加,而不是重新创建一个新的变量。
function add (){let count=0return function (){count ++console.log(count)}
}
let t=add()
t() //1
t() //2
缺点: 1.内存占用增加,因为被引用的变量不会销毁,我们可以把变量不再使用时置为 null 2.意外引用错误变量,for 循环中用 var 定义变量,改用 let,或创建立即执行函数
for (var i = 0; i < 3; i++) {setTimeout(() => {console.log(i);}, 1000);
}
//实际输出 2 2 2
循环嵌套定时器,为什么会出现都是2呢?
第一点是var声明的变量是函数作用域,整个for循环使用同一个i 第二点是settimeout是异步的,在for完成之后执行,所以都是2
上面问题的解决办法是什么呢?
要么把var变为let,要么创建立即执行函数
for (var i = 0; i < 3; i++) {(function(i) {setTimeout(() => {console.log(i);}, 1000);})(i);
}
i的作用是什么呢?
函数立刻执行创建独立作用域,传入当前的i值
