闭包

之前在我执行上下文执行上下文栈这篇文章中,出现了这样一个题目

for (var i=0; i<10; i++){
setTimeout(
()=>{
console.log(i) // 猜猜结果
},
2000
)
}

题目答案是: 大约2s后输出10个10

引发这个问题的原因恰恰就是因为var关键字没有块级作用域,当定时器异步执行时,同步执行的for早已经执行完毕了,此时i早已经变成了10了,所以最终10个定时器输出的i就全部都是10了

我后来使用了一个方法解决了这个问题,这个方法就是本篇文章介绍的一个概念----闭包

for (var i = 0; i < 10; i++) {  // for循环的i由于是var申明没有块级作用域,是全局变量
(function (i) { // 形参i是IIFE的局部变量
setTimeout(
() => {
console.log(i)
},
2000
)
})(i) // 闭包产生
}

闭包如何产生

我们先不管闭包是什么,有什么用。而是先研究一下闭包是如何产生的

function func1 () {
var a = 1 // 断点打在这
function func2 () {
console.log(a)
}
func2() // 无需调用,闭包也已经产生了
} func1() // 执行函数的时候,就会落在断点处

到这,我们就能知道: 当两个函数互相嵌套,内部函数引用外部函数的变量时,就会产生闭包

需要注意的是:

  • 变量值可以是对象也可以是普通类型的值
  • 内部函数不需要调用,只要引用了外部函数的变量,闭包就已经产生

最后,我们总结一下闭包的产生, 总共需要两个条件:

  1. 函数嵌套
  2. 内部函数引用了外部函数的变量

闭包是什么

根据闭包是如何产生所需的两个条件,我们就能顺便说出闭包究竟是什么东西了---- 闭包就是那个包含被引用变量的对象

常见的闭包

常见的闭包有两种

  • 将内部函数作为外部函数的返回值
  • 将函数作为实参传递给另一个函数调用

先看第一种:

function func1() {
var a = 1
function func2() {
a++
console.log(a)
}
return func2
} var getInnerFunc = func1() // 执行外部函数得到其返回值 ---- func2函数
getInnerFunc() // 2
getInnerFunc() // 3
getInnerFunc() // 4

这里插个题外话,如何计算闭包产生的个数?

这就需要我们对前面闭包是如何产生的定义很清楚: 闭包是内部函数被定义的时候产生的(当然这个内部函数还要引用外部函数的变量)

所以,闭包产生的个数就是外部函数被调用的次数,上面例子产生闭包个数是1个。

在看第二种,这种方式可能对初学者比较劝退,python中装饰器概念对应着的就是这种方式:

function fn1 (fn) {
var MyName = 'Fitz'
function wrapper () { // 内部函数嵌套于外部函数
return fn(MyName) // 闭包产生
}
return wrapper
} // 这个函数作为参数
function fn2 (a) {
console.log(a)
} var decorator = fn1(fn2)
decorator()

闭包的作用

讲了这么多,那闭包究竟有什么作用?,既然都是要在最外层(全局)中调用的,为什么不定义在全局中,而是这样多此一举呢?

闭包能够使得函数内部的变量在函数执行完毕后,继续存活于内存中(延长局部变量的生命周期)

function func1() {
var a = 1
function func2() {
a++
console.log(a)
}
return func2
} var getInnerFunc = func1()
getInnerFunc() // 2
getInnerFunc() // 3

根据执行上下文的相关概念: 函数执行上下文在函数调用时产生,函数内的语句执行完(函数调用完毕)后函数内申明的局部变量/函数将会被销毁(回收)

但是上面这个例子很明显,在函数调用结束后,我们仍然能够访问函数内定义的局部变量(函数),这是为什么呢? 我来画图表示一下

究其原因: 还是因为全局中仍然有变量关联着局部变量func2对应那个函数对象,所以能够通过全局变量getInnerFunc访问到这个函数对象

由于func2这个变量对应的函数对象仍被引用着,所以当外部函数func1及其内部的局部变量被垃圾回收器进行回收时,这个被引用着的函数对象将不会被回收(注意:func1里面的局部变量a和func2都会被回收,但是func2变量指向的对象不会被回收),这将会导致下面介绍的内存泄露与内存溢出的问题

闭包除了能够延长局部变量的申明周期,还能在对外部隐藏实现的情况下,让外部安全的操作函数内部的数据,这也是众多编程语言中gettersetter寄存器的基本原理

function func1() {
var a = 1
function getter() {
console.log(a)
}
function setter(val) {
a = val
}
return {
get: getter,
set: setter
}
} var getInnerFunc = func1() // 执行外部函数得到其返回值 ---- func2函数
getInnerFunc.get() // 1
getInnerFunc.set(666)
getInnerFunc.get() //666

闭包的生命周期

产生: 闭包是在函数定义的时候就产生,跟作用域一样,是静态的

死亡: 当内部函数也成为垃圾对象的时候

function func1 () {
var a = 1
function func2 () {
console.log(a)
}
return func2
} var f = func1()
f()
f = null // 这一步释放操作,让内部函数也成为垃圾对象,释放闭包

闭包的应用

介绍一大堆闭包相关的知识,那这个闭包它在实际中有什么用呢?

闭包其中一个大的作用就是用于编写js模块,最典型的例子就是Jquery,看过Jquery源码的同学都会看到,Jquery是一个巨大的IIFE函数,这个函数里面向全局对象暴露$对象或者说JQuery对象

基于闭包,我们也来简单的做一个js模块

// 模仿jquery源码的方式
// 我们来自定义一个数学工具方法
(function myMath (globalObject) {
var initVal = 1
function add (val) {
return initVal += val
}
function pow (val) {
initVal = initVal ** val
return initVal
} globalObject.$ = globalObject.fakeJquery = {
// es6的对象简写语法
add,
pow
}
})(window)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
</head> <body>
<script src="./myModule.js"></script>
<script>
console.log(window)
console.log($) console.log($.add(1)) // 2
console.log($.add(2)) // 4
console.log($.pow(2)) // 16
</script>
</body>
</html>

闭包的缺点

闭包在实际运用很广泛,但是它有一个显著的缺点就是: 如果不及时释放闭包,就会造成内存泄露,内存泄漏到了一定程度,最终导致内存溢出

内存泄露

内存在使用完后没用被及时释放,一直被没用的东西占用着

常见的内存泄露的情况

意外的全局变量

function test () {
a = 'Fitz' // 忘记使用关键字申明, 这里的a是全局变量
}
console.log(a) // 'Fitz'

没被及时清理的定时器

setTimeout(()=>{
console.log('hello')
},1000)

操作dom及其回调函数

const btn = document.getElementById('btn01')
btn.onclick = function () {
alert('my name is Fitz')
}
btm = null // 既要清空变量的应用
document.body.removeChild('btn') // 还要清空DOM的引用

最后一种就是我们本篇介绍的闭包,导致的内存泄露了

内存溢出

内存占用超过了可用内存的总大小时,就会产生内存溢出

闭包面试题

题目1

var name = 'the window'
var object = {
name: 'my object',
getNameFunc: function () {
return function () {
return this.name
}
}
}
console.log(object.getNameFunc()()) // 'the window'

解析: 这一题考查的是闭包中this的指向,闭包中的this可能令人有些迷惑,但是只要我们对this的知识比较扎实,就能不被表象所欺骗

这里object.getNameFunc()()其实真正可以写成

var innerFunc = object.getNameFunc()
innerFunc() // this自然指向window
/*
这其实属于this中的隐式绑定丢失的概念
*/

那对于这道题目,如果我们一定要访问object中的name怎么办呢? 那我们就需要防止this的隐式绑定丢失,我们将object的this保存下来

var name = 'the window'
var object = {
name: 'my object',
getNameFunc: function () {
var that = this
return function () {
return that.name
}
}
}
console.log(object.getNameFunc()()) // 'my object'

接着是一道终极无敌蛇皮怪怪锤面试题,这题就是玩弄心态的,各位年轻人耗子尾汁

上菜!

function fun(n, o) {
console.log(o)
return {
fun: function (m) {
return fun(m, n)
}
}
} var a = fun(0)
a.fun(1)
a.fun(2)
a.fun(3)
/*
输出结果是啥? */ var b = fun(0).fun(1).fun(2).fun(3)
/*
输出结果是啥? */ var c = fun(0).fun(1)
c.fun(2)
c.fun(3)
/*
输出结果是啥? */

答案:

function fun(n, o) {
console.log(o)
return {
fun: function (m) {
return fun(m, n)
}
}
} var a = fun(0)
a.fun(1)
a.fun(2)
a.fun(3)
/*
输出结果是啥?
- undefined
- 0
- 0
- 0
*/ var b = fun(0).fun(1).fun(2).fun(3)
/*
输出结果是啥?
- undefined
- 0
- 1
- 2
*/ var c = fun(0).fun(1)
c.fun(2)
c.fun(3)
/*
输出结果是啥?
- undefined
- 0
- 1
- 1
*/

解析:

function fun(n, o) {
console.log(o)
return {
fun: function (m) {
return fun(m, n)
}
}
} var a = fun(0) // 由于没有实参给予o,所以o为undefined,输出undefined // 然后得到的一个对象赋值给变量a
/*
a => {
fun: function (m) {
return fun(m, 0) // function(m){...}是闭包
}
}
*/ // 此时实参1赋值给形参m
a.fun(1) // 执行fun(n=1, o=0) 输出o=0
// 此时实参2赋值给形参m
a.fun(2) // 执行fun(n=2, o=0) 输出o=0
// 此时实参3赋值给形参m
a.fun(3) // 执行fun(n=3, o=0) 输出o=0
/*
输出结果是啥?
- undefined
- 0
- 0
- 0
*/

由于a.fun()是三次独立的调用,即产生的是不同的执行上下文,所以函数之间的变量是独立、没有记忆的

接着

function fun(n, o) {
console.log(o)
return {
fun: function (m) {
return fun(m, n)
}
}
}
var b = fun(0).fun(1).fun(2).fun(3) /*
fun(0): n=0 o=undefined 输出undefined
fun(0).fun(1): m=1 n=上次的n=0 输出0
fun(0).fun(1).fun(2): m=2 n=上次的m=1 输出1
fun(0).fun(1).fun(2).fun(3): m=3 n=上次的m=2 输出2
*/ /*
输出结果是啥?
- undefined
- 0
- 1
- 2
*/

由于是连续的调用,执行上下文对象始终是同一个,所以前一次调用后的变量/参数,会影响后一次的结果,是有记忆的

最后

function fun(n, o) {
console.log(o)
return {
fun: function (m) {
return fun(m, n)
}
}
} var c = fun(0).fun(1)
/*
fun(0): n=0 o=undefined 输出undefined
fun(0).fun(1): m=1 n=上次的n=0 输出0 此时c是一个对象:
c = {
fun: function (m) {
return fun(m, 1)
}
}
*/ c.fun(2)
/*
相当于执行:
function (2) {
return fun(2, 1)
}
输出1
*/ c.fun(3)
/*
相当于执行:
function (3) {
return fun(3, 1)
}
输出1
*/ /*
输出结果是啥?
- undefined
- 0
- 1
- 1
*/

这个例子是上面两种的结合,连续调用得到c对象,然后在对c对象进行独立的调用,考查的是执行上下文对象以及显而易见的闭包

让你弄懂js中的闭包的更多相关文章

  1. JavaScript中的this详解(彻底弄懂js中的this用法)!

    要想学好js,那么其中那些特别令人混淆迷惑的知识点,就一定要弄清楚.this关键字就是其中让初学者比较迷惑的知识点之一,不过灵活运用this可以提升代码的性能和复用性,那么今天我就和大家一起来了解th ...

  2. 详解js中的闭包

    前言 在js中,闭包是一个很重要又相当不容易完全理解的要点,网上关于讲解闭包的文章非常多,但是并不是非常容易读懂,在这里以<javascript高级程序设计>里面的理论为基础.用拆分的方式 ...

  3. JS中的闭包(closure)

    JS中的闭包(closure) 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现.下面就是我的学习笔记,对于Javascript初学者应该是很有用 ...

  4. js进阶 12-2 彻底弄懂JS的事件冒泡和事件捕获

    js进阶 12-2 彻底弄懂JS的事件冒泡和事件捕获 一.总结 一句话总结:他们是描述事件触发时序问题的术语.事件捕获指的是从document到触发事件的那个节点,即自上而下的去触发事件.相反的,事件 ...

  5. js中的闭包之我理解

    闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的 ...

  6. 浅谈JS中的闭包

    浅谈JS中的闭包 在介绍闭包之前,我先介绍点JS的基础知识,下面的基础知识会充分的帮助你理解闭包.那么接下来先看下变量的作用域. 变量的作用域 变量共有两种,一种为全局变量,一种为局部变量.那么全局变 ...

  7. js中的“闭包”

    js中的“闭包” 姓名:闭包 官方概念:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. ( ⊙o⊙ )!!!这个也太尼玛官方了撒,作为菜鸟的 ...

  8. 彻底弄懂AngularJS中的transclusion

    点击查看AngularJS系列目录 彻底弄懂AngularJS中的transclusion AngularJS中指令的重要性是不言而喻的,指令让我们可以创建自己的HTML标记,它将自定义元素变成了一个 ...

  9. Js中的闭包原理

    要了解清楚js中的闭包制机,那么得先了解全局执行环境.块级执行环境.函数执行环境.变量对象.环境栈.作用域链.摧毁执行环境. 全局执行环境 全局执行环境指的是最外层的执行环境.在web中全局执行环境被 ...

随机推荐

  1. android 获取时间

    首先,先说下java下可以正常使用的方法: import java.text.DateFormat; import java.text.SimpleDateFormat; import java.ut ...

  2. 获取Spring容器中的Bean

    摘要 SpringMVC框架开发中可能会在Filter或Servlet中用到spring容器中注册的java bean 对象,获得容器中的java bean对象有如下方法 Spring中的Applic ...

  3. ASp.net 注册

    <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default2.aspx.cs ...

  4. 转储oracle的redo文件

    1.确定当前使用的redo文件 SQL> select member from v$logfile where group# = ( select group# from v$log where ...

  5. 异常处理与调试4 - 零基础入门学习Delphi53

    调试(Debug) 让编程改变世界 Change the world by program 调试(Debug) 在应用程序开发中检测.处理程序中的错误是一个非常重要的环节.在Delphi的集成开发环境 ...

  6. C++_基础_类和对象2

    内容: (1)构造函数 (2)初始化列表及其必要性 (3)支持自定义类型转换的构造函数 (4)this指针 (5)const对象和成员函数 (6)析构函数 1.构造函数1.1 格式: class 类名 ...

  7. 关于request、response转发与重定向的简述

    在做页面的请求与响应的时候我们多用request与response进行操作,而我们大家也知道,request是表示用户发向服务器的请求,而response是对用户请求的一个响应. 关于转发和重定向,通 ...

  8. mysql 5.7~默认sql_mode解读

    当5.6升级到5.7时,首先要注意的就是sql_mode对业务的影响 大概可以分为几类1 共同支持,如果你的5.6和5.7sql_mode配置支持的交集一样,那么不用考虑2 5.7细说  1 ONLY ...

  9. yii 动态增加路由规则

    使用 UrlManager类的addRules方法. 如 $url->addRules([ 'zzz/<id:\d+>' => '/test/hi', ]); 这个规则必须在开 ...

  10. java实现sftp客户端上传文件以及文件夹的功能

    1.依赖的jar文件 jsch-0.1.53.jar 2.登录方式有密码登录,和密匙登录 代码: 主函数: import java.util.Properties; import com.cloudp ...