【前端开发核心知识进阶-笔记】2.闭包的理解

2.闭包的理解

1. 基本知识

1. 作用域

在JavaScript中执行某个函数时,如果遇见变量且需要读取其值,就会“就近”先在函数内部查找该变量的声明或赋值情况。如果在函数内无法找到该变量,就要跳出函数作用域,到更上层作用域中查找。更上层作用域也可以顺着作用域范围向外扩散,一直到全局作用域。

2. 作用域链

变量作用域的查找是一个扩散的过程,就像各个环节相扣的链条,逐次递进,这就是“作用域链”的由来。

3. 块级作用域和暂时性死区

使用let或const声明变量时会针对这个变量形成一个封闭的块级作用域,在这个块级作用域中,如果在声明变量前访问该变量,就会报referenceError错误;如果在声明变量后访问该变量,则可以正常获取变量。

function foo() {
  console.log(bar)
  var bar = 3
}
undefined

执行以上代码会输出undefined,原因是在 JavaScript 中,当一个变量被声明但未被赋值时,它的默认值是 undefined。因此,当 console.log(bar) 执行时,bar 的值为 undefined。然后,在下一行代码中,bar 被赋值为 3。但这个赋值语句不会影响之前的 console.log() 语句,因为它已经在变量被赋值之前执行了。

使用var声明的变量会被提升至其作用域的顶部,但它的赋值不会,因此这里是undefined。

function foo() {
  console.log(bar)
  let bar = 3
}
报错:referenceError

在函数 foo 中,使用 let 声明了变量 bar。与 var 不同,let 声明的变量不会被提升到其作用域的顶部。所以当 console.log(bar) 执行时,bar 还没有被初始化,因此会抛出 ReferenceError 错误。这个错误是因为 let 声明的变量有一个称为“暂时性死区(Temporal Dead Zone,简称 TDZ)”的概念。在 TDZ 中,访问变量会抛出一个错误,直到该变量被声明和初始化。

在JavaScript中,变量的声明、初始化和赋值是不同的概念,尽管它们通常在同一行代码中完成。
变量声明是指使用关键字(如 varletconst)声明一个新变量。声明一个变量只是告诉JavaScript,我们有一个新的变量,并为它分配一个内存空间。
变量初始化是指在声明变量时为其赋一个初始值。如果在声明时没有赋值,变量的初始值将为undefined。
变量赋值是指将一个新值赋给变量。这可以在变量声明后的任何时候进行,而不是在声明时。赋值将覆盖变量的当前值,使其指向新的值。

4. 执行上下文和调用栈

执行上下文是指当前代码的执行环境/作用域

执行JavaScript代码主要分为以下两个阶段:

  • 代码预编译阶段:虽然JS是解释型语言,编译一行,执行一行。但在执行前,JS引擎确实会做一些预先工作
    • 在预编译阶段进行变量声明
    • 在预编译阶段对变量声明进行提升,但是值为undefined
    • 在预编译阶段对所有非表达式函数声明进行提升
  • 代码执行阶段

例题1

function bar(){
  console.log('bar1')
}

var bar = function(){
  console.log('bar2')
}
bar2

例题2

var bar = function(){
  console.log('bar2')
}

function bar(){
  console.log('bar1')
}
bar2

以上代码的输出结果都是bar2,因为在预编译阶段虽然对变量bar进行了声明,但不会对其赋值;函数bar则被创建和提升。在代码的执行阶段,变量bar才会通过表达式被赋值,赋值的内容是函数体为console.log('bar2')的函数,输出结果为bar2

例题3

foo(10)
function foo (num){
  console.log(foo)
  foo = num
  console.log(foo)
  var foo
}

console.log(foo)
foo = 1
console.log(foo)
undefined
10
function foo (num){
  console.log(foo)
  foo = num
  console.log(foo)
  var foo
}
1

在foo(10)执行时,会在函数体内进行变量提升,此时执行函数体内的第一行会输出undefined,执行函数内的第三行会输出foo。接着运行代码,运行到函数体外的console.log时,会输出foo函数的内容。(foo函数内的foo=num,num被赋值给函数作用域内的foo变量)

作用域在预编译阶段确定,但是作用域连是在执行上下文的创建阶段完全生成的,因为函数在调用时才会开始创建对应的执行上下文。执行上下文包括变量对象、作用域连以及this的指向。

**JavaScript引擎执行机制最基本的原理:**在预编译阶段创建变量对象(Variable Object,VO)此时只是创建,而未进行赋值。在代码执行阶段,变量对象转为激活对象(Active Object,AO),即完成VO向AO的转换。此时,作用域也被确定,它由当前执行环境的变量对象和所有外层已经完成的激活对象组成。

调用栈:在执行一个函数时,如果这个函数又调用了另外一个函数,而这“另外一个函数”又调用了另外一个函数,这样便形成了一系列的调用栈,类似以下代码:

function foo1(){
  foo2()
}
function foo2(){
  foo3()
}
function foo3(){
  foo4()
}
function foo4(){
  console.log('foo4')
}
foo1()
foo4

扩展:正常来说,在函数执行完毕并出栈时,函数内的局部变量在下一个垃圾回收(GC)节点会被回收,该函数对应的执行上下文将会被销毁,这也正是我们在外界无法访问函数内定义的变量的原因。

5.闭包

函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局环境下可访问,进而形成闭包。

例题

function numGenerator(){
  let num = 1
  num++
  return ()=>{
    console.log(num)
  }
}

var getNum = numGenerator()
genNum()
2

这是个简单的闭包示例,numGenerator创建了一个变量num,接着返回了 一个匿名函数,这个函数引用了变量num,使得外部可以通过调用getNum方法访问变量num。即相关调用栈出栈后,变量num不会消失,仍然有机会被外界访问。

在JavaScript引擎分析中,num值被标记为Closure,即闭包变量

我们知道在正常情况下外界是无法访问函数内变量的,函数执行之后,上下文即被销毁。但是在函数(外层)中,如果我们返回了另外一个函数,且这个返回的函数使用了函数(外层)内的变量,那么外界便能通过这个返回的函数获取原函数(外层)内部的变量值,这就是闭包的基本原理。

可以利用闭包实现“模块化”;Redux源码的中间件实现机制也大量运用了闭包(函数式理念)。

6. 内层管理

内存管理基本概念

内层空间分为栈空间和堆空间,具体如下:

  • 栈空间:由操作系统自动分配释放,存放函数的参数值、局部变量的值等,类似于数据结构中的栈
  • 堆空间:一般由开发者分配释放,关于这部分空间要考虑垃圾回收的问题

在JavaScript中,数据类型包括基本数据类型和引用类型,具体如下:

  • 基本数据类型:undefined、null、number、boolean、string等
  • 引用类型:object、array、function

一般情况下,基本数据类型按照值大小保存在栈空间中,占有固定大小的内存空间;引用类型保存在堆空间中,内存空间大小并不固定,需按引用情况来进行访问。

内存泄漏场景举例

var element = document.getElementById("element")
element.mark = "marked"

//移除element节点
function remove(){
  element.parentNode.removeChild(element)
}

这里只是把element节点移除了,但变量element依然存在,该节点占用的内存无法被释放。

为了解决该问题,需要在remove方法中添加element=null,这样更为稳妥

var element = document.getElementById('element')
element.innerHTML = '<button id="button">click</button>'

var button = document.getElementById('button')
button.addEventListener('click',function (){
  ...
})

element.innerHTML = ''

因为存在element.innerHTML = '',所以button元素已经从DOM中移除了,但是由于其事件处理句柄还在,所以该节点变量依然无法被回收。因此还需要添加removeEventListener函数,以防止内存泄漏。

function foo(){
  var name = 'lucas'
  window.setInterval(function(){
    console.log(namme)
  },1000)
}
foo()

这段代码中,由于存在window.setInterval,所以name内存空间始终无法释放,如果不是业务要求的话,一定要记得在合适的时机使用clearInterval对其进行清理。

浏览器垃圾回收

大部分场景下浏览器都会依靠标记清除、引用计数两种算法进行回收

2. 例题分析

实战例题1

const foo = (function(){
  var v = 0
  return ()=>{
    return v++
  }
}()
)

for (let i = 0; i < 10; i++){
  foo()
}
//for之后v=9
console.log(foo())
10

foo是一个立即执行函数,为()=>{return v++},在循环执行foo时,v自增10次,最后执行foo时,得到10。变量v是闭包变量

实战例题2

const foo = ()=>{
  var arr = []
  var i 
  for (i = 0; i < 10; i++){
    arr[i] = function () {
      console.log(i)
    }
  }
  return arr[0]
}
foo()()
10

i是自由变量,arr[0]=function(){console.log(i)},由于arr[0]是个函数,未执行时并没有获取i的值,执行时才去查找i的指向,由于i是自由变量,因此会一直保留循环后的值。

实战例题3

var fn = null
const foo = () => {
  var a = 2
  function innerFoo() {
    console.log(a)
  }
  fn = innerFoo
}

const bar = () =>{
  fn()
}

foo()
bar()
2

通过将innerFoo函数赋值给全局变量fn,foo的变量对象a也会被保留下来。所以,函数fn在函数bar内部执行时,依然可以访问这个被保留下来的变量对象,输出结果为2

实战例题4

var fn = null
const foo = () => {
  var a = 2
  function innerFoo() {
    console.log(c)
    console.log(a)
  }
  fn = innerFoo
}

const bar = () =>{
  var c = 100
  fn()
}

foo()
bar()
报错:ReferenceError: c is not defined

在bar中执行fn时,fn已经被复制为innerFoo,但变量c不在其作用域链上,c只是bar函数的内部变量,因此会报错

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值