闭包和let提升的一些理解
首先我们要知道一点是:
在一个作用域中,函数提升优先级比变量提升要高,且不会被变量声明覆盖,但是会被变量赋值覆盖。
闭包
-
什么是闭包?
- 闭包是指有权访问另一个函数作用域中的变量的函数。
-
如何产生闭包?
- 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包
function fn1 () { var a = 2 var b = 'abc' function fn2 () { //执行函数定义就会产生闭包(不用调用内部函数) console.log(a) } } fn1()
-
闭包的作用?
- 可以在函数的外部访问到函数内部的局部变量。
- 让这些变量始终保存在内存中,不会随着函数的结束而自动销毁(一般情况下函数执行完后, 函数内部声明的局部变量不存在, 存在于闭中的变量才可能存在)
-
闭包的缺点?
-
函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长,容易造成内存泄露,所以我们要及时释放
function fn1() { var arr = new Array[100000] function fn2() { console.log(arr.length) } return fn2 } var f = fn1() f() f = null //让内部函数成为垃圾对象-->回收闭包
-
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。
考虑以下示例:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }
在上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype.getName = function() { return this.name; }; MyObject.prototype.getMessage = function() { return this.message; };
-
闭包的应用--节流和防抖
由于函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长,我们可以利用这个特性实现节流和防抖
防抖
当持续触发事件,一定时间内没有再触发事件,事件处理函数才会执行一次
如果设定的事件到来之前,又一次触发了事件,就重新开始延时
典型案例就是输入搜索:输入结束后n秒才进行搜索请求
//利用闭包特性,每次清除上次的定时器
function debounce(fun, delay) {
let timer
return function(args) {
clearInterval(timer)
timer = setTimeout(function() {
fun(args)
}, delay)
}
}
节流
当持续触发事件的时候,保证一段时间内,只调用一次事件处理函数
典型案例就是鼠标不断点击触发,规定在n秒内多次点击只有一次生效
function throttle(func, wait) {
let timerOut
return function() {
if(!timerOut) {
timerOut = setTimeout(function() {
timerOut = null
func()
}, wait)
}
}
}
let 的特性
- let 声明的变量的作用域是块级的;
- let 不能重复声明已存在的变量;
- let 有暂时死区,不会被提升。
当程序的控制流程在新的作用域(module, function或block作用域)进行实例化时,在此作用域中的用let/const声明的变量会先在作用域中被创建出来,但因此时还未进行词法绑定,也就是对声明语句进行求值运算,所以是不能被访问的,访问就会抛出错误。所以在这运行流程一进入作用域创建变量,到变量开始可被访问之间的一段时间,就称之为TDZ(暂时死区)。
执行一段闭包代码如下:
// 代码段1
var liList = document.querySelectorAll('li') // 共5个li
for( var i=0; i<liList.length; i++){
liList[i].onclick = function(){
console.log(i)
}
}
大家都知道依次点击 li[会打印出 5 个 5,如果把 var i 改成 let i,就会分别打印出 0、1、2、3、4;
又来看一段代码:
// 代码段2
var liList = document.querySelectorAll('li') // 共5个li
for( let i=0; i<liList.length; i++){
liList[i].onclick = function(){
console.log(i)
}
}
MDN上也有这个代码,它的例子是这样的:
MDN 的例子,在每次循环的时候用 let j 保留的 i 的值,所以在 i 变化的时候,j 并不会变化。而console.log 的是 j,所以不会出现 5 个 5。
为什么 MDN 要故意声明一个 j 呢,为什么不直接用 i 呢?
在ES 文档中的理解:
- for( let i = 0; i< 5; i++) 这句话的圆括号之间,有一个隐藏的作用域
- for( let i = 0; i< 5; i++) { 循环体 } 在每次执行循环体之前,JS 引擎会把 i 在循环体的上下文中重新声明及初始化一次。
也就是说上面的代码段2可以近似近似近似地理解为
// 代码段3
var liList = document.querySelectorAll('li') // 共5个li
for( let i=0; i<liList.length; i++){
let i = 隐藏作用域中的i // 看这里看这里看这里
liList[i].onclick = function(){
console.log(i)
}
}
那样的话,5 次循环,就会有 5 个不同的 i,console.log 出来的 i 当然也是不同的值。
再加上隐藏作用域里的 i,一共有 6 个 i。
这就是 MDN 加那句 let j = i 的原因:方便新人理解。
总得来说就是 let/const 在与 for 一起用时,会有一个 perIterationBindings 的概念(一种语法糖)。
let有没有提升
首先明确一点:提升不是一个技术名词。
要搞清楚提升的本质,需要理解 JS 变量的「创建create、初始化initialize 和赋值assign」
1. 我们来看看 var 声明的「创建、初始化和赋值」过程
假设有如下代码:
function fn(){
var x = 1
var y = 2
}
fn()
在执行 fn 时,会有以下过程(不完全):
- 进入 fn,为 fn 创建一个环境。
- 找到 fn 中所有用 var 声明的变量,在这个环境中「创建」这些变量(即 x 和 y)。
- 将这些变量「初始化」为 undefined。
- 开始执行代码
- x = 1 将 x 变量「赋值」为 1
- y = 2 将 y 变量「赋值」为 2
也就是说 var 声明会在代码执行之前就将「创建变量,并将其初始化为 undefined」。
这就解释了为什么在 var x = 1 之前 console.log(x) 会得到 undefined。
2. 接下来来看 function 声明的「创建、初始化和赋值」过程
假设代码如下:
fn2()
function fn2(){
console.log(2)
}
JS 引擎会有一下过程:
- 找到所有用 function 声明的变量,在环境中「创建」这些变量。
- 将这些变量「初始化」并「赋值」为 function(){ console.log(2) }。
- 开始执行代码 fn2()
也就是说 function 声明会在代码执行之前就「创建、初始化并赋值」。
3. 接下来看 let 声明的「创建、初始化和赋值」过程
假设代码如下:
{
let x = 1
x = 2
}
我们只看 {} 里面的过程:
- 找到所有用 let 声明的变量,在环境中「创建」这些变量
- 开始执行代码(注意现在还没有初始化)
- 执行 x = 1,将 x 「初始化」为 1(这并不是一次赋值,如果代码是 let x,就将 x 初始化为 undefined)
- 执行 x = 2,对 x 进行「赋值」
这就解释了为什么在 let x 之前使用 x 会报错:
let x = 'global'
{
console.log(x) // Uncaught ReferenceError: x is not defined
let x = 1
}
原因有两个
- console.log(x) 中的 x 指的是下面的 x,而不是全局的 x
- 执行 log 时 x 还没「初始化」,所以不能使用(也就是所谓的暂时死区)
看到这里,你应该明白了 let 到底有没有提升:
- let 的「创建」过程被提升了,但是初始化没有提升。
- var 的「创建」和「初始化」都被提升了。
- function 的「创建」「初始化」和「赋值」都被提升了。
所谓暂时死区,就是不能在初始化之前,使用变量。