~$ 存档

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

闭包是一个比较难理解的问题,下面就自已的理解写写

如果说闭包,首先还是牵涉到函数。函数在JS中是一个非常特殊的存在。原因在于,它经常在表面上迷惑,好像是一段代码,其实它也是对象。
对于这个问题,我们可以先从直观上了解一下,比如:

function f(){
}
f.show=function(){
    console.log('对象f的show方法')
}
f.show()

----#2020-10-25 23:02:16补充----:

对于上面这个方法,我们用另一种思路来理解,
f.show=function(){...},可以等价于f.show = new Function(....),这么写可以更直观的认识到函数是一种对象,这个思路很重要!

如果f不是对象,它怎么能挂载show属性呢?因此,在下面分析之前,首先达成一个认识:函数就是对象,当我们看到函数时,脑子中就把它当成let o={}这种普通对象来看待。

一旦得到这个结论,我们就会得到另一个令人惊奇的结论,既然是对象,那么对象就可以多份,就好比通过一个构造函数得到多个对象一样,这有点颠覆日常的认知,为什么会这样是因为
在我们的概念里,函数就是一段代码,被编译之后内存中只有一份。比如像C这种编译型的语言。但是,我们的推导从逻辑上来说也没有问题对吗?

我们推导的理由是:函数就是对象,而对象是可以多个的,所以函数可以在内存中多份。我们不管对不对,只是纯粹从逻辑上推导。

因为函数是对象,所以在一个函数里面返回一个函数,就好比返回一个‘普通对象’一样,这么说也是合乎逻辑的
这种基础的认识一直在重复,因为我觉得基础的认识要在脑子中形成本能,而且这是所有推导的核心依据

代码如下:

function f(){
    //等价:return new Function('console.log("内部返回的对象")')
    return function(){
        console.log('内部返回的对象')
    }
}
let a=f()
let b=f()
let c=f()
console.log(a==b)    //false
console.log(a==c)    //false
console.log(b==c)    //false

上面这段代码,调用f()就返回一个函数,但是前面说过,函数是对象,所以我们说这个函数f()就像工厂机器一样,调用一次就生产一个对象出来。代码中生产出来三个对象a,b,c,我们有理由相信这三个对象都是不同的。
于是,下面的判断都是false。这从侧面也证明了,函数就像对象一样,可以多个。

下面,在函数f()里面放置一个数据,代码如下:

function f(){
    let data = 10
    return function(){
        console.log(data)
    }
}
let a=f()
let b=f()
let c=f()
a()
b()
c()

因为a,b,c三个对象都不一样,那么从常理上来想,它引用的data也不一样,就像中秋节发月饼,每个人各领自己的一份,东西是独占的。这就是闭包的基本概念,把[对象]+[引用的数据]打包成一份,进行各自分发

有了这个思路,我们很快就想到,这是一个好东西,一个机器可以生产很多份样式相同的东西,但是每个东西又是独立的,不是正好可以隔离吗?
这一点像类构造对象的模式,同一个构造函数可以创建很多不同的对像

那么data为什么会像各种资料上所说的不会消失呢?这个道理也很容易理解,data是被对象所引用的,至少目前为止,对象还是活蹦乱跳的。比如说let a,这个a虽然不是全局的(参考es6),但是在当前这块大环境里面还是存活的,对象还是活着的,那么它引用的数据也会一直存活。比如说,A引用B,B引用C,C引用D,只要源头的引用还存在,后续的都必须存在,这个思路是完全合理的

有了这几个核心的认识,而且还是简单的认识,我们就可以写出各种变化的代码,比如下面一段:

function f(){
    let data = 0
    return {
        get:function(){
            return data
        },
        set:function(v){
            data = v
        }        
    }
}
let a=f()    //机器生产一份
let b=f()    //机器再生产一份

a.set(10)
console.log(a.get())

b.set(20)
console.log(b.get())

我们让机器f生产两份对象出来,每个对象包裹了两个函数属性,其实在我们眼里,所有都是对象,这一点反复在说明,因为把函数看成对象就会变的清晰

两份东西都不一样,如果非要在计算机上认识,我们可以想像的认为它们就在内存中的不同地址处。而且data是两份对象各自拥有的,因此a调用set()进行设置和b一点关系都没有,毫无影响

下面,我们改造一下,因为上面说过,用闭包可以形成隔离。如果要说到隔离,就会想到面向对象,我们沉浸这个概念很久了。面向对象的一个重要特征之一就是把数据封装。
首先,我们先用正常的思路构造对象

function Person(name,age){
    this.name=name
    this.age=age
}
let o = new Person('tianya',20)
console.log(o.name)

如代码所示,这是常用的构造方式,但是好像违背了面向对象的初衷,里面的name和age都展示在外面。于是联想到闭包,把name和age放在里面,外面只通过get/set进行操作,代码变成这种样式:

function Person(){
    let _name,_age
    this.getName=function(){
        return this._name
    }
    this.setName=function(name){
        this._name=name
    }
    //...
}
let o = new Person()
o.setName('tianya')
//o.name 未定义
console.log(o.getName()) //'tianya'

_name和_age已经封装到构造函数内部,外部只能通过接口进行访问,姑且不说这种方式实际是否合适,仅仅是理论上的分析

下面,分析一下闭包的引用

闭包的数据引用,其实非常符合我们的日常思维,内部方法能访问外部的数据,这不是非常符合常理吗,几乎都不需要特别的说明,就像上面的代码,内部函数可以访问自己上面的数据,但还是有几点需要说明

先看一份代码:

function f(){
    let data=10
    function g(){    //我能看到什么?
        console.log(data)
    }
}

内部函数g就像有一只眼睛,它能看到多大的范围呢?首先从直觉上,我们都知道,至少它头顶之上的都可以看到,而且函数f之外也能看到。对于人来说,是很快能反应出来的,但是语言没有那么聪明,它需要一套结构记录下来。

我们这样联想,g能看到data,而且还能看到f()之外的东西,这就像一根链条,是逐级往上查找。看到链条,我们马上就会联想原型链。这两者确实原理相似

如果我们是语言设计者会怎么考虑呢?从字面上来看,g比f的视野更宽,也就是越往内部眼睛能看到的数据就越多,而函数f是入口,所以我们可以先从f入手,把f能看到的记录下来记为F。g除了能看到F,还能看到里面的数据G,因此可以看到F+G,以此类推....一根链条就这样建立起来

 

关于这张图,我们不要像考古一样关注这张图是否准确,正确,仅仅是从思路上观察它

每个函数调用时都会形成一个闭包,为了容易理解,表述都是不严谨的,比如对于g来说,它需要收集下面几个信息:
1、自己的父闭包是谁?
2、自己内部定义了哪些变量?
3、自己内部定义了哪些方法

就像一张登记表,有了这张表就可以向上查询,又回到原型链的思路。比如说,g()内部引用了变量outer,查找路径如下:

1、自己内部有吗?
2、如果没有,在其父闭包中查找,本例中就是闭包f
3、如果再没有,就在闭包f的父闭包中查找....以此类推,直到最源头为止

这就是为什么g()能引用outer的原因

接下来,我们考虑一个问题,既然上面建了表,那么这个表是什么时候创建呢?按照正常理解,应该是调用进入时创建合适。这样就引起一个问题,函数还没有执行,表已经创建好了
这个给我们的启示是:函数还没有执行,内部结构已经登记好了,于是就出现这种代码:

let outer='outer data'

function f(){
    let data=10
    g()    //先执行
    function g(){
        let inner=20
        console.log(outer)
    }    
}
f()

这种现象就是常说的函数提升。不仅函数能提升,我们观察到,内部声明的变量也被提前登记了,至少说明可以在变量赋值之前就可以引用变量。这个问题也是一直诟病的,为什么要把var变成let的原因。比如:

console.log(msg)
var msg = 'hello js'

按照推导,执行打印之前,msg登记在册,仅仅没有赋值,因此并不报错只是显示为undefined
同时也解释了另一个问题,为什么使用变量赋值函数没有得到提升?比如:

g()
var g=function(){}

登记在册的仅仅是变量g,但是执行时并没有赋值,因此,变量声明的函数没有得到提升。

 

 

# 2020-10-26 21:00:24 追加

闭包的记忆功能

闭包的记忆功能可能是最奇妙的一种功能

首先,想象若干个桶去接水

 

每个桶都保存当时那个时间段的状态,这有点像游戏中所说的封印

封印这个词来形容闭包实在最合适不过,就是指闭包把当时那一刻的状态整个的封印起来,也可以简单化来说,是函数对象+数据进行封印,或者直接说 [对象+数据] 封印

应用一:对象的get/set设置

在js中,对象的属性可以指定get/set,这种模式是对封印非常好的理解,代码如下:

假如,有一个对象
let o={
    'name':'tianya'
}

我们要对name属性设置get/set,但是我们必须有一个中间量来保存name的值,否则会产生无限递归。考虑到这个中间量最好能封装起来,于是用闭包来保存最合适!
代码如下:

    let x = {
        'name': 'tianya'
    }

    function define(obj, key) {
        let value = obj[key]
        Object.defineProperty(obj, key, {
            get() {
                console.log('获取值')
                return value
            },
            set(v) {
                value = v
            }
        })
    }

    define(x, 'name')   //属性操作
    console.log(x.name) //取值
    x.name = 'xz'
    console.log(x.name)

 

应用2:数组遍历器

前面我们看到,创建一个闭包之后,并不是不管了,而是在程序整个运行期间,可能会发生不断的执行。每执行一次(可能)就会改变数据的状态,但这正是闭包的用途。

下面看一个数组遍历器的例子,也是利用闭包的记忆功能。

 

posted on 2018-07-07 22:27  LuoTian  阅读(166)  评论(0编辑  收藏  举报