乐趣区

关于javascript:前端开发核心知识进阶-第一章第二节-老手也会在闭包上翻车


闭包是 JS 中最根本、最重要的概念之一,闭包绝不是一个繁多的概念,它波及作用域、作用域链、执行上下文、内存治理等多重知识点。

1. 作用域

首先是作用域,在 ES6 之前只有函数作用域和全局作用域之分,ES6 中通过 let 和 const 申明变量的块级作用域,使得 JS 的作用域更加丰盛。上面说说变量晋升和暂时性死区。

上面看个 var 申明变量的例子

function f1(){console.log(b);//undefined
  var b = 2
}
f1()

var 申明的变量会提前申明,然而不赋值,所以输入 undefined,而这里的 var 换成 let、const 的话,会报 referenceError 谬误。起因是 let、const 申明的变量,在未声明之前的区域会造成一个“死区”,业余名为 TDZ(Temporal Dead Zone)它始于函数结尾,终于变量申明的语句,在这个“死区”中调用变量都会报错, 在死区外就能够失常拜访变量。暂时性死区有种极其的状况就是,函数的参数默认值设置也会受它的影响

例子如下

function fan(a1 = a2, a2){console.log(a1,a2);
}
fan(1,2)
fan(null,2)
fan(undefined,2)

调用了三次 fan,别离输入什么呢,第一次是 1,2,第一次是 null,2,第三次报错了。第三次因为传的是 undefined,默认没传,a1 赋值为 a2, 但此时 a2 没有申明,所以报错。

2. 执行上下文和调用栈

执行上下文就是以后代码的执行环境 / 作用域,和作用域相辅相成,但又是俩个齐全不同的概念。

2.1 代码执行的俩个阶段

JS 代码执行分俩个阶段,一个是代码预编译阶段,一个是代码执行阶段

在预编译阶段,会进行变量的申明,对变量进行晋升,但值为 undefined,对所有非表达式的函数申明进行晋升

在执行阶段,会执行代码逻辑,执行上下文会在这个阶段全副创立实现

上面看个例子

function fbn(){console.log('a');
}

var fbn = function(){console.log('b');
}

fbn()

这里会输入 b, 如果调换程序呢

var fbn = function(){console.log('b');
}

function fbn(){console.log('a');
}

fbn()

这里输入还是 b

为什么呢?因为预编译阶段对 fbn 进行变量晋升,并未赋值,而后对函数 fbn 进行创立申明,接下来才轮到 fbn 的赋值,fbn 被赋值的内容是函数体为 console.log(‘b’) 的函数,所以输入 b

作用域在预编译阶段确定,,但作用域链是在执行上下文的创立实现生成的,因为函数调用才会创立相应的执行上下文。执行上下文包含变量对象,作用域链及 this 的指向

2.2 调用栈

在 a 函数中调用了 b 函数,在 b 函数中调用了 c 函数,这样便造成了一系列的调用栈,a、b、c 顺次入栈,c 执行完出栈,b 执行完出栈,最初 a 执行完出栈,造成调用栈。

失常来讲,在函数执行结束并出栈时,函数内的局部变量在下一个垃圾回收(GC)节点被回收,该函数的执行上下文会被销毁,这也是外界无法访问函数内定义的变量的起因,也就是说,只有在函数执行时,相干函数才能够拜访函数内的变量,变量会在预编译阶段被创立,在执行阶段被激活,在函数执行完结被销毁

3. 闭包

讲了这么多前置概念,置信你大略了解闭包的工作原理了吧。

在前述内容中,咱们晓得失常状况下外界无法访问函数内的变量,函数执行后,其变量会被销毁。但如果在 a 函数内,返回一个 b 函数,且这个返回的 b 函数应用 a 函数内的变量,那么外界能够通过 b 函数拜访 a 函数的变量了,这就是闭包的原理。

3.1 内存治理

内存空间分为堆空间和栈空间

undefined,string,null,number,boolean 等等根本数据类型寄存在栈空间,有固定的内存大小

object、array、function 等等援用类型寄存在堆空间,内存大小并不固定

3.2 内存透露

内存透露是指内存空间明明曾经不再被应用,但因为某种原因没有被开释的景象。

内存透露的危害十分直观,它会导致程序运行迟缓,甚至解体

看个例子

var ele = document.querySelector('div')
ele.index = '0'
ele.parentNode.removeChild(ele)

这里只是把节点移除了,但变量 ele 仍然存在,该变量占用的内存无奈开释

所以应该增加 ele = null 更加稳当

再来一个例子

var ele = document.querySelector('div')
ele.innerHTML = '<button id="btn"> 点击 </button>'
var btn = document.querySelector('#btn')
btn.addEventListener('click',function(){//...})
ele.innerHTML = ''

这里 button 曾经在 DOM 中移除了,然而事件处理句柄还在,该节点变量无奈回收,所以还须要增加 removeEventListener 函数,避免内存透露

除了开发者被动保障回收外,大部分场景下浏览器都会依附标记革除和援用计数俩种算法回收

一个闭包的例子

function fcn(){
  let value = 1

  function fdn(){console.log(value);
  }

  return fdn
}
var fdn = fcn()
fdn()

在谷歌浏览器的 V8 引擎中执行,在 fdn 函数中设置断点,会发现存在闭包变量 value

最初,来个例题实战

var fn = null
const fun = () =>{
   var a = 1
  function ffn(){console.log(a);
    console.log(b);
  }
  fn = ffn


}
let bar = () =>{
  var b = 2
  fn()}
fun()
bar()

运行后果是什么呢,运行后果是 1 和 ReferenceError:b is not defined,fn 被赋值为 ffn,变量 b 并不在它的作用域链上,如果在 fun 里或全局申明 b =2,那便会输入 2

退出移动版