细读闭包

简介: 对于闭包,可以说是老生常谈的话题,但是真正理解闭包的,又有哪些核心点呢?

网络异常,图片无法展示
|

对于闭包,可以说是老生常谈的话题,但是真正理解闭包的,又有哪些核心点呢?


静态作用域


先理解一下静态作用域的定义:函数作用域在定义时就已确定。

可以看出闭包就是一个静态作用域。内部函数在定义的地方向上查找所需变量。


例1:


let a = 1
function fn() {
    let a = 0
 function fn2() {
    console.log(a);
 }
  fn2()
}
fn() // 0


例2:


let a = 1
function fn() {
   let a = 0
   fn2()
 }
function fn2() {
   console.log(a);
}
fn() // 1


根据上面两个案例,可以很好的展示静态作用域的特点。


例1:

在代码定义时,确定其作用域范围。在运行时,根据作用域创建相应的执行环境。

在fn2函数中需要输出a时,会先查看当前fn2作用域是否存在,有就会使用;没有则会通过作用域链向上一级fn函数作用域中查找,找到第一次与之匹配的变量停止。

所以,a输出的时fn中的值0。

网络异常,图片无法展示
|
例2中,依旧同理,fn2会找它上一级的作用域,即全局作用域中a变量的值1。


闭包定义


闭包:首先闭包是一个对象,是内部嵌套函数引用了外部函数变量(函数)的对象,是内部函数与周围状态的引用捆绑在一起的一个组合。


查看闭包


chrome浏览器控制台进行debug,执行外部函数,可以发现内部函数需要的变量存在于内部函数的[[scopes]]内。

网络异常,图片无法展示
|

如上图,闭包就是存在于内部嵌套函数中,包含被引用的a变量。


常见的闭包


常见的闭包有两种:将子函数作为父函数的返回值、将父函数中实参传递给子函数。


子函数作为父函数的返回值


let a = 1
function fn() {
  let a = 0
  function fn2() {
    a++
    console.log(a);
  }
  return fn2
}
let f = fn()
f() // 1
f() // 2


执行let f = fn();f()调用的是fn函数内部的fn2函数。

执行fn2:fn2通过作用域链找到要fn中引用的a变量,执行a++,输出1。

再次执行f()语句,再次调用fn2。fn函数内部的变量依旧存在,不过经过上一次自加,a变量值为1。此时fn2再次a++,输出值为2。


注意: 闭包调用次数,取决于调用几次外部函数。


父函数中实参传递给子函数


function fn(a) {
 function fn2() {
    a++
    console.log(a);
 }
 fn2()
 fn2()
}
fn(0) // 1  2


fn2()内部函数引用fn外部函数参数。第一次执行fn2(),a自加后输出1;第二次执行fn2(),此时a值已经为1,a自加后输出2。


闭包作用


  • 使用父函数内部的变量在函数执行完成后,仍然存活在内存中(延长局部变量的生命周期
  • 外部可以调用子函数,操作到父函数内部数据(变量/函数)

function fn() {
  let a = 0
  function fn2() {
    a++
    console.log(a);
  }
  return fn2
}
let f = fn()
f()


1、fn()执行完成后,变量a仍然存活在内存中。

网络异常,图片无法展示
|

解释:在执行let f = fn()时,变量f指向了fn2指向的函数对象。该行语句执行完成后,fn2函数变量会释放。但是f指向的函数对象(闭包)仍然存在,闭包内部a变量仍然存活在内存中。再执行f(),就相当于调用的上图的函数对象。


如果未定义变量f指向闭包,即只执行fn()。那么,内部函数在fn执行完成后释放回收。外部也无法调用fn内部函数。


所以,父函数执行完成后,父函数内部声明的局部变量一般不存在,在闭包中的变量才可能存在


2、在外部可以通过操作变量f,来对fn函数内部变量a进行自加。


闭包的生命周期


产生:在子函数定义时就产生了(不是调用)

死亡:在子函数的内部对象成为垃圾对象时



function fn() {
  let a = 0
  function fn2() {
    a++
    console.log(a);
  }
  return fn2
}
let f = fn()
f()
f()


网络异常,图片无法展示
|


在fn内部,执行到39行时,fn2定义时就存在了闭包。


网络异常,图片无法展示
|


代码执行完成,闭包仍然存在!

此时,我们可以将f变量置为空,将fn2指向的函数对象变为垃圾对象。添加f = null

网络异常,图片无法展示
|


闭包应用


闭包最常见的应用应该就是在定义js模块了。定义一个js模块,对外暴露多个方法或变量。如果对外暴露的方法中,使用到模块内部的变量就属于闭包的应用。

可以这样定义简单的js模块:


;(function (window) {
  let a = 0
  function f1() {
    console.log('f1', a)
  }
  function f2() {
    console.log('f2', a)
  }
  window.myModule = { f1, f2 }
})(window)


直接在所需使用的地方引入文件,并调用myModule的可用方法。如:myModule.f1()就可以执行模块中f1的方法。


js模块中使用匿名函数,对外暴露的是myModule全局对象。在使用时不需要执行myModule,直接调用myModule的方法即可。传入window参数,通常是为了在代码压缩时,进行简写window参数。


闭包缺点及解决方法


缺点:容易引起逻辑问题;函数执行完,函数内部变量未被释放占用内存时间变长,容易造成内存泄漏。


循环与闭包


闭包容易引起逻辑问题。先来看个典型的案例:

for (var i = 0; i <= 5; i++) {
  setTimeout(function timer() {
     console.log(i);
  }, 1000)
} // 6 6 6 6 6 6


for语句在js中不属于块作用域,定义的i属于的全局变量。6次循环都被封闭在共享的全局作用域中,因此i只有一个。


再者,setTimeout属于异步回调。6次循环结束后,才执行setTimeout。

上段代码等价于将setTimeout函数回调重复定义6次,完全不使用循环。

如果想要输出0、1、2、3、4、5怎么办呢?


解决:

核心:使用封闭循环的各自作用域。

  • 使用IIFE(立即执行函数表达式)在每次迭代中创建的作用域封闭起来。
  • 使用let声明,可以用来劫持块作用域,并且在块作用域中声明这个变量。


内存泄漏


内存泄漏:一个被分配的内存既不能使用,也不能回收到浏览器,直到进程结束。 关于内存泄漏,可以参考[闭包的生命周期]中,闭包手动释放回收。

解决:

  • 能不用闭包就不用闭包
  • 让内部函数称为垃圾对象,回收闭包(及时释放)。


补充:内存溢出与内存泄漏


内存溢出与内存泄漏关系:内存泄漏积累到一定程度,会造成内存溢出。


内存泄漏


内存泄漏的情况:

  • 闭包
  • 意外的全局变量


function fn() {
   a = 0
   console.log(a);
}
fn()


网络异常,图片无法展示
|


变量a未正常定义,变成全局变量。在代码执行完成之后,全局变量中依旧存在a变量。

  • setInterval
setInterval(() => {
  console.log('--');
}, 1000);

启动循环定时器时,未及时清理。需要使用clearInterval手动清理。

let time = setInterval(() => {
  console.log('--');
}, 1000);
clearInterval(time)


内存溢出


当程序运行所需内存超过剩余内存时,抛出内存溢出的错误。


网络异常,图片无法展示
|

执行上述代码,浏览器一直卡在运行状态,内存不能满足程序所需内存。

目录
相关文章
|
JavaScript
【深入理解 —— js闭包】
【深入理解 —— js闭包】
|
JavaScript 前端开发
JS闭包之灵魂7问
Q1:慧眼认“包” Q2: 参数为何凭空消失 Q3:作用域嵌套惹的祸 Q4:迟到的兑现,错误的值 Q5:变量的从一而终 Q6:公私分明 Q7:柯里化
|
存储 自然语言处理 JavaScript
兄台:JS闭包了解一下
函数即对象 闭包
105 0
|
自然语言处理 JavaScript
再谈JS闭包
作用域 作用域嵌套 词法作用域(lexicsl scope) 闭包 闭包示例
209 0
|
JavaScript 前端开发
js闭包
js闭包
96 0
|
自然语言处理 JavaScript 前端开发
每日一题:说说你对闭包的理解?闭包使用场景
每日一题:说说你对闭包的理解?闭包使用场景
116 0
|
自然语言处理 JavaScript 前端开发
【重温基础】19.闭包
【重温基础】19.闭包
151 0
|
缓存 JavaScript
这一次,彻底搞懂闭包
这一次,彻底搞懂闭包
124 0
|
JavaScript
关于js闭包的简易理解
      function fn(){             var i = 1;             return function(n){                   console.
933 0
|
C#
C#温故而知新系列 -- 闭包
闭包的由来    要说闭包的由来就不得不先说下函数式编程了。近几年函数式编程也是比较火热,我们先来看看函数式编程的一些基本的特性这个有助于我们理解闭包的由来。    函数式编程      函数式编程是一种编程模型,他将计算机运算看做是数学中函数的计算,并且避免了状态以及变量的概念。
1434 0