前言
这期简单讲一下 var、let、const,由于内容很少,多加了一个闭包,与他们之间的一些故事,也更方便我们理解他们,在实战中尽量避免一些错误
var、let、const
var
:
这个声明原意是 variable
可变的意思,也就是我们声明一个变量
是用到的,后续我们可以用来修改变量内容
但 var 出现了变量提升,也就是 var 声明的变量会提升到整个函数作用域、一些场景会成为全局作用域,因此会出现一些副作用
副作用就是作用域范围内,声明位置之前,声明之后,不同块都会使用到同一个对象,尤其是在循环、闭包使用过程中,会出现与预期不相符的情况,这也算是不小的问题了
ps:但其设计时不知道是为了解决一些特殊场景,还是设计缺陷(其中有人说是为了一些场景下的通信),但这类情况不应该出现,因为其他编程语言设计时,根本没有按照这个特性设计,仍然可以轻松代替 var 变量提升带来的效果,且没有那么多弊端,更容易带来预期相符的效果,因此后面也出现了 let
let
let
的出现绝非偶然,个人感觉其完全可以代替 var 出现的,但是由于遗留代码需要兼容,不能大刀阔斧的改动,因此出现了let
let 也是声明一个变量的意思,与 var 有所不同:
let 舍弃了 var 带来的变量提升效果
- 其作用域也是在自己的
块作用域
,且声明前的位置会出现暂时性死区
,提前访问会报错, - 在自己作用域,自己的优先级最高,作用域外的同名变量优先级会小一些
- 符合声明方可用的情况,也达到了早期其他编程语言声明变量的效果
ps: 个人感觉唯一的缺陷是没有代替 var,var 的语义就很明确,相比较之下 swift看着更好一些,但早期 swift的强硬改动,使使用者项目大范围报错这种情况也是一个痛,但后续编程语言更加规范健壮(但swift没js开发应用范围广,不能算swift不行😂),两者都算是福祸相依
const
:
const
其相比较 let
,是一个常量
,也就是声明后,其指向的内容指针不可再更改了的意思,如果强行更改会直接提示错误
当然 const
也只是约束到了其指向的内容地址不被更换,但如果是一个对象,对象内部修改则就没有办法了,这点是需要注意的
闭包
闭包(Closure)是编程语言中的一个重要概念,是指一个函数及其相关的引用环境组合而成的实体,能够访问其外部作用域的变量,即使在外部函数已经返回的情况下。
闭包的关键特性
- 访问外部变量:闭包允许内部函数访问其外部函数的局部变量,即使外部函数已结束执行。
- 封装与持久化:通过闭包,可以封装私有变量或实现数据的持久化存储,避免全局变量的污染。
- 作用域链:闭包通过作用域链机制,使得函数能够"记住"其定义时的环境状态。
通过上面定义和概念,我们大概了解了,闭包实际上就是一个内部函数引用了外部作用域的变量,使用时他们整体形成了一个封闭的场景,这也是为了保证内部函数在执行时,仍然能够正常执行里面的内容
假设不形成闭包
疑问
:有人可能会问,即使这样,为何要打包环境,内部函数的参数正常访问参数也行吧?
答
:
- 那样问题就来了,
js垃圾回收机制
会直接回收用不到的对象 - 当一个函数执行完毕后,栈内的临时变量就成了无主之物,会被
js垃圾回收机制
回收,当我们内部函数返回给外部使用的时候,此时内部函数场景执行完毕,其临时变量都都可能会被回收 - 外部执行到返回的内部函数时,此时内部临时变量指向的其场景内临时变量,全部变成了 undefined 未定义了(其他语言可能直接崩溃了),因为其会成为危险的野指针
- 正是因为此特性,需要形成闭包,他会把内部函数使用到的变量全部打包到一起(临时变量也会因为被持有无法释放),因此形成一个新的结构,这个结构就是闭包
js
function getName() {
let name = 'dog'
return () => {
console.log(name)
}
}
//为了保证 printName 正常执行,返回的内部函数会形成闭包,以保证正常执行
const printName = getName()
printName()
形成闭包后的新问题(内存泄漏)
经过场面步骤,闭包形成后,临时变量会无法被垃圾回收机制正常回收(因此会被认为出现了内存泄漏),如果想提前回收的话,那么可以和返回的内部函数一样,多返回一个清理临时变量的一个方法,这样可以提前处理掉临时变量,就可以保证其能被正常回收了
js
function getName() {
let name = 'dog'
return {
printName() {
console.log(name)
},
clearName() {
name = null
}
}
}
//为了保证 printName 正常执行,返回的内部函数会形成闭包,以保证正常执行
const printObj = getName()
printObj.printName()
printObj.clearName()
内存泄露与垃圾回收
当然上面场景不是说真的内存泄露了,是否内存泄露要从所在的场景角度看,不是说使用了闭包就会产生内存泄露
- 如果使用的闭包场景是在一个全局区内执行,那么如果不主动清理,里面的 name 垃圾回收是真的不会回收
- 如果是在一个页面,此时该页面引用了此闭包的话,那么随着页面的释放,里面的闭包也会随之释放
- 如果闭包是指一个函数内执行,那么这个函数执行完毕后,其也会被随之释放,因为没人持有闭包了
垃圾回收
是回收没有被根节点直接或者间接持有的对象,这点需要注意,一旦切断联系则成了无根之萍,就是垃圾回收机制回收的对象
所以也不要别人忽悠,闭包该怎么使用就怎么使用,不需要提前释放,那就不需要管(当然,如果临时变量太多,等不到页面销毁,那么就需要手动切断联系了)
闭包与临时变量的关系
前面说了,闭包是内部函数引用了临时变量形成的结构,那么闭包是怎么持有临时变量的呢,实际上就是持有了临时变量的引用
,注意不是直接深拷贝了该对象,否则后续的案例则会陷入泥潭
闭包与 let、var 循环的一个案例
可以分别看下面的三个循环执行情况,这也是对闭包引用临时变量的一个考验
1.正常使用 let 声明 index
js
for (let index = 0; index < 5; index++) {
setTimeout(() => {
console.log(index)
}, 1000);
}
2.使用 var 声明 index
js
for (var index = 0; index < 5; index++) {
setTimeout(() => {
console.log(index)
}, 1000);
}
3.使用 let 在外部声明 index
js
let index = 0
for (; index < 5; index++) {
setTimeout(() => {
console.log(index)
}, 1000);
}
答案如下:
- 1s 之后一口气执行5个 setTimeout的回调,换行打印 1 2 3 4 5
- 1s 之后一口气执行5个 setTimeout的回调,换行打印 5 5 5 5 5
- 1s 之后一口气执行5个 setTimeout的回调,换行打印 5 5 5 5 5
上面就是考察了闭包持有的临时变量引用,以及for声明的变量所在作用域
1.for中使用 let 声明的变量,使用时相当于每个index变量都是在{}单独的一个作用域,并不是同一个引用,所以每个闭包引用的都是不同的 index
2.var 声明的变量获得变量提升,里面所有的 index使用的都是同一个引用index,所以当闭包执行时会显示同一个值
3.let 放到 for 外面,里面所有的 index 使用的都是外部同一个引用index,闭包引用的自然是同一个 index,所以当闭包执行时会显示同一个值
最后
这里除了了解 var、let、const,还了解了一些垃圾回收、闭包,与for 循环下的闭包延迟执行的表现,加深一下理解
- 同时也能理解到 for 循环早期使用 var 出现的变量提升,在闭包延迟执行的情况下与预期不符的情况
- 同时也了解到 es6 引出的 let,能让 for循环在每个新的作用域中声明一个新的索引,避免循环中使用同一个索引,出现的与预期不符的情况