闭包和let提升的一些理解

首先我们要知道一点是:

在一个作用域中,函数提升优先级比变量提升要高,且不会被变量声明覆盖,但是会被变量赋值覆盖。

闭包

  1. 什么是闭包?

    • 闭包是指有权访问另一个函数作用域中的变量的函数。
  2. 如何产生闭包?

    • 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包
    function fn1 () {
        var a = 2
        var b = 'abc'
        function fn2 () { //执行函数定义就会产生闭包(不用调用内部函数)
          console.log(a)
        }
      }
      fn1()
    
  3. 闭包的作用?

    • 可以在函数的外部访问到函数内部的局部变量。
    • 让这些变量始终保存在内存中,不会随着函数的结束而自动销毁(一般情况下函数执行完后, 函数内部声明的局部变量不存在, 存在于闭中的变量才可能存在)
  4. 闭包的缺点?

    • 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长,容易造成内存泄露,所以我们要及时释放

      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 文档中的理解:

  1. for( let i = 0; i< 5; i++) 这句话的圆括号之间,有一个隐藏的作用域
  2. 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 时,会有以下过程(不完全):

  1. 进入 fn,为 fn 创建一个环境。
  2. 找到 fn 中所有用 var 声明的变量,在这个环境中「创建」这些变量(即 x 和 y)。
  3. 将这些变量「初始化」为 undefined。
  4. 开始执行代码
  5. x = 1 将 x 变量「赋值」为 1
  6. y = 2 将 y 变量「赋值」为 2

也就是说 var 声明会在代码执行之前就将「创建变量,并将其初始化为 undefined」。

这就解释了为什么在 var x = 1 之前 console.log(x) 会得到 undefined。

2. 接下来来看 function 声明的「创建、初始化和赋值」过程

假设代码如下:

fn2()

function fn2(){
  console.log(2)
}

JS 引擎会有一下过程:

  1. 找到所有用 function 声明的变量,在环境中「创建」这些变量。
  2. 将这些变量「初始化」「赋值」为 function(){ console.log(2) }。
  3. 开始执行代码 fn2()

也就是说 function 声明会在代码执行之前就「创建、初始化并赋值」。

3. 接下来看 let 声明的「创建、初始化和赋值」过程

假设代码如下:

{
  let x = 1
  x = 2
}

我们只看 {} 里面的过程:

  1. 找到所有用 let 声明的变量,在环境中「创建」这些变量
  2. 开始执行代码(注意现在还没有初始化)
  3. 执行 x = 1,将 x 「初始化」为 1(这并不是一次赋值,如果代码是 let x,就将 x 初始化为 undefined)
  4. 执行 x = 2,对 x 进行「赋值」

这就解释了为什么在 let x 之前使用 x 会报错:

let x = 'global'
{
  console.log(x) // Uncaught ReferenceError: x is not defined
  let x = 1
}

原因有两个

  1. console.log(x) 中的 x 指的是下面的 x,而不是全局的 x
  2. 执行 log 时 x 还没「初始化」,所以不能使用(也就是所谓的暂时死区)

看到这里,你应该明白了 let 到底有没有提升:

  1. let 的「创建」过程被提升了,但是初始化没有提升。
  2. var 的「创建」和「初始化」都被提升了。
  3. function 的「创建」「初始化」和「赋值」都被提升了。

所谓暂时死区,就是不能在初始化之前,使用变量。

posted @ 2021-03-03 15:58  Hhhighway  阅读(229)  评论(0编辑  收藏  举报