⭐️ 本书所有知识点均可在《你不知道的JavaScript(上)》中找到出入点,如果想要进阶原生js的同学,推荐你去把《你不知道的JavaScript》上中两册精读一遍,我相信你能收获很多东西。
🔑:以下的内容是我结合上中两册总结出来的笔记,文笔有限,随缘更新 。
一,闭包和作用域
0. 作用域
什么是作用域:用来存储变量,存储变量的值,变量的增删改查对接的地方【也就是变量能访问到的范围较作用域】
栈内存和堆内存
-
函数执行形成栈内存,所有的变量都是在栈内存中存储的,也就是所谓的作用域【基本数据类型存储在栈内存】【复杂数据类型存储在堆内存】
-
作用域中的变量的查找方式是向上级作用域查找,不能向同级作用域查找
PS:
1. 编译原理
在浏览器中共有三个角色负责程序的执行,分别是:编译器,执行引擎,作用域
我们来看下 var a = 20
这段代码的编译过程
- 首先,遇到
var a
,编译器会询问作用域是否有同名a
这个变量存在,若存在,则跳过这一步;若不存在,编译器会要求当前作用域生成一个变量a
【在Es6中,使用let,const
关键字进行重复声明会报错,而var具有预解析,不会报错】 - 接下来编译器会为执行引擎生成代码,这些代码会用来处理
a = 20
这一步。 - 引擎运行,首先会询问当前作用域有没有一个叫
a
的变量,若有,则把这个变量的值赋值为20;
2. LHS和RHS查询
执行引擎在进行变量操作的时候,会对不同的情况对作用域中的变量进行 LHS和RHS查询
LHS:查找变量的存储地址【赋值的时候,把值存储在这个变量地址中】
RHS:查找变量的值【获取一个变量的值的时候】
观察下面的代码
a = 20; // LHS查询,获取a变量,把a变量的值修改成20【基于作用域】
console.log(a) // RHS查询,获取a的值并打印【基于作用域】
LHS和RHS的区别
两者都是基于:当前作用域 -> 上级作用域 -> 上级作用域…
- LHS:若在所有作用域作用域链上都没找到变量,则在顶层作用域(global)中创建变量并赋值等等
- RHS:若在所有作用域作用域链上都没找到变量,则抛出错误
let aaa = 0 // 全局作用域创建aaa,赋值成0
;(function() {
let aaa = 1 // 创建aaa变量,赋值成1
;(function() {
console.log(aaa) // RHS查询,在上级作用域中找到aaa, 值为1
aaa = 1000 // LHS查询,在上级作用域中找到aaa, 修改值为1000
console.log(aaa) // RHS查询,在上级作用域中找到aaa, 值为1000
})()
console.log(aaa) // RHS查询,在当前作用域中找到aaa, 值为1000
})()
console.log(aaa) // RHS查询,在当前作用域中找到aaa, 值为0
3. 词法作用域
词法作用域完全由编写代码期间函数的位置来顶底决定
词法作用域:函数在被定义后,不管这个函数之后在哪个别的作用域里被调用,他的作用域都是从被定义的位置开始查找的【比如:函数在哪里定义,他的作用域就在哪,无论实在哪里被调用】
举个例子:
这里形成了三个作用域:
- 全局window作用域
- fn1函数作用域
- fn2函数作用域
虽然 fn1
函数是在 fn2
中被调用的,但是他的作用域在代码被编写的的时候就天注定了:fn1
的上级作用域是window
,而不是fn2
,所以在寻找 value 变量的时候会向 window 作用域找【词法作用域决定函数的作用域】
所以叫做词法作用域,因为他的作用域是编写时就天注定了,不管是在哪里被调用
4. 函数作用域
函数作用域定义:
⭐️ 类似于一个封闭的小气泡,外部不能访问这个气泡里边的变量和函数,只能里面函数或者变量访问外面的变量;【变量访问遵循作用域链原则,遵循词法作用域原则】
函数作用域的优点:
-
隐藏函数内部实现,只暴露一个接口。类似黑盒,把处理数据的代码封装到一个黑盒中,只暴露入口(参数)和出口(return)
-
避免变量冲突。一个函数作用域中可各种变量和函数,并且把这些变量和全局作用域隔绝,不会污染全局作用域
-
闭包。用过的都说好,函数执行后对外 return 一个对象或者函数,用来突破函数作用域不能被外部作用域访问的限制
-
保护函数作用域中的变量(外部无法访问内部)
函数作用域的缺点:
需要定义一个函数(污染一个变量),再执行该函数(可以使用IIFE立即执行函数优化)
5. 块级作用域
首先我们来看下Es6之前的 var 关键字,这个关键字是没有块级作用域的
var
⭐️ var没有块级作用域的(try…catch的catch中有),使用一个for循环都会污染到全局作用域
let,const
接着就是 let,const
这两个关键字,在遇到 {}
会产生块级作用域,类似函数作用域
⭐️ 块级作用域最大的作用就是,它会加入到作用域查找链中【超重要!】,并且块级作用域和闭包进行配合可以替代函数作用域
块级作用域的优点:
- 可以替代部分IIFE的工作(具体看需求)
- 变量污染程度大量减少,比函数作用域更加容易创建(for循环有块级作用域)
- 优化垃圾回收机制(若产生闭包,函数私有作用域不会删除,但是如果内部有大量的数据残留,可以使用块级作用域 P33)
6. 提升
其实,在JS代码执行前,编译器会对我们写好的代码进行一个预编译,这个就叫提升。
⭐️ 这个过程就是把代码中的var a
var b
function a() {...}
这样的变量申明提升到作用域的最前面【提升只针对 var 和 function关键字】
-
首先看 var 的变量提升【只提升申明,不赋值】
其中的原因就是,编译器首先会把所有的 var 申明提升到作用域最前面,再执行代码
真实运行代码的状况其实是这样的:
-
再来看 function 关键字的提升【又提升申明,又赋值】
用function声明的函数,也会像var一样提升到最前面,但是这个关键字是
提升+申明
代码真实运行情况:
-
两者提升的优先级
function 的提升优先级 > var关键字优先级
上面代码的真实运行情况
-
匿名函数没有提升
当我们使用function关键字作为匿名函数时,没有变量提升
但是!有名函数就会有提升
7.{}中的提升
😋 在ES6之前是没有块级作用域这个说法的,所以在 {} 中使用 var 和 function 关键字定义的变量和函数,本质上是属于全局作用域的【函数中的变量和函数除外,因为函数被编写时就有词法作用域】,所以会被提升,具体的提升规则:
- var 变关键字的提升规则不变
- function 关键字不再提升函数整体,只提升函数名,和 var 一样的形式
⭐️ 请记住一点:在块级作用域中的匿名有名函数没有提升
8. 闭包
闭包可以说是 javaScript中最重要的技术之一,在 javaScript 中,闭包遍地可见:定时器,DOM事件监听器,Ajax请求等等以及任何传递回调函数的地方,都是用了闭包
⭐️ 专业点说就是:闭包就是 基于词法作用域+函数 来突破 作用域限制 的一种工具
⭐️ 用我自己的话说:函数执行后形成不销毁的作用域,且该作用域中的变量能通过一个函数被任何其作用域访问,这就是闭包【不销毁的关键就是该作用域中的对象或者函数被别的作用域指向,垃圾回收机制不会回收】
💥 PS:块级作用域和闭包结合也更香哦~
-
来看一个闭包突破作用域限制的例子:
🅰️ 我们知道,函数具有词法作用域,即被编写时就具有天注定的作用域,无论这个函数是在哪里调用,执行的时候,函数内部的变量的查找规则始终是从被编写的地方开始查找。
🅱️ 而在JavaScript中,函数是一等公民,可以作为函数参数或者函数返回值四处传递。
⭐️ 结合上面两点,我们就可以把一个在 fn 作用域中的函数作为参数传递到别的作用域中【函数是一等公民】,在别的作用域中执行的时候,基于词法作用域的特性,该函数【返回的匿名函数的词法作用域】是能够访问到fn 内的作用域的!!!
综上所述:我们实现突破了函数作用域的限制,使得函数外能访问函数内的这种方式
-
闭包产生的原理
- 函数执行会创建自己的私有作用域,该作用域的生命周期是函数执行结束
- 若函数作用域中有一个或者多个变量被外部作用域指向时,作用域就不会回收销毁
- 利用上面的两个特性就能够形成不销毁的函数作用域,这也是闭包能产生的原因
附上丑图一张:
-
闭包的运用:
-
各种回调函数
-
自执行函数
-
柯理化函数
-
模块模式
-
…还有一大堆…
-