闭包随笔

随笔 · 浏览次数 : 78

小编点评

首先,我们先了解一下作用域链的概念,以及闭包的概念。 作用域链是一个对象列表,它定义了函数的执行环境。闭包是指一个函数和它被声明的词法作用域环境的组合。 在分析代码时,我们可以发现,每次我们调用一个函数的时候都会创建一个新的对象来保存其局部变量。这个对象会存储在作用域链中,并当函数返回之后,就从作用域链中将这个对象删除。 闭包可以用来避免就生闭包的现象,因为当我们创建了一个闭包之后,我们无法访问外部函数定义的变量,因为它们已经不在作用域链中。 闭包的例子: ```javascript function fun(n,o) { console.log(o) return { fun:function(m){ return fun(m,n); } }; } ``` 在这个例子中,`fun`函数是一个闭包,因为它保存了`n`和`o`在函数中声明的值。即使我们删除了`fun`函数,`n`和`o`的值仍然可以访问到。

正文

开始正式介绍之前先看一个比较有难度的关于闭包的面试题:

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);        //问:三行a,b,c的输出分别是什么?

(原文链接:http://www.cnblogs.com/xxcanghai/p/4991870.html)

这道题是个人拿到手都会感到棘手,尤其对于一些基础不扎实的新手,看着看着就看晕了,我大体解释一些这个fun函数干了一些什么事(文章最后再分析一下这个):

首先,打印了一下他的第二个参数,然后return了一个对象,这个对象有一个方法fun,然后这个方法return了fun(m,n)--最外层函数的执行结果;大体弄懂基本的流程,就可以大概分析一下了,不过讲闭包之前,还是讲一下js的作用域链:

(来自四年后的补充: 这个题看着有点难逐次递归也是一个方面,哈哈)

每一段js代码(全局代码或函数)(注意一下这里,敲黑板!!!)都有一个与之关联的作用域链(scope chain)。这个作用域链是一个对象列表或者链表,这组对象这组对象定义了这段代码作用域中的变量,当js需要查找变量x的值得时候,(这个过程叫做‘变量解析(varaible resolution)’),他会从链中的第一个对象查找,如果在第一个对象中找不到就会查找下一个,以此类推。 在js顶层代码中,作用域链由一个全局对象组成。在不包含嵌套的函数体内,作用域链上有两个对象第一个是该函数定义参数和局部变量的对象,第二个是全局对象。

--以上两段的内容来自js权威指南,就是那本犀牛书 ,当定义一个函数时,它实际上保存一个作用域链。当调用这个函数时,它创建一个新的对象来存储他的局部变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的链。对于嵌套函数来讲每次调用外部函数时,作用域链都是不同的,内部函数又会重新定义一遍,每次调用外部函数时,内部函数的代码都是相同的,而关联这段代码的作用域链也不相同。

--以上内容也是来自js权威指南,就是那本犀牛书 大概概括一下就是每个函数会形成一条作用域链,作用域链不同,能访问到的内容也不尽相同,由于作用域链上对象的排序关系访问变量时,可以遵循一个原则--就是就近原则。

说完作用域之后,就来谈谈闭包吧。 简言之,混乱的(2022.1.22的补充,此处混乱的用词可能并不准确)作用域链就是形成闭包的元凶。 来一个极为简单的例子:

var d;      
function outter(){          
    var c = 0;          
    console.log(c);          
    d = function(){ 
        c++; 
    }      
 }     
 outter();      
 d();//0        
 d();//1

为什么会这样呢?

一般而言,每次调用js函数时,都会创建一个新的对象用来保存局部变量,并把这个对象添加至作用域链中,当函数返回之后,就从作用域链中将这个对象删除,如果不存在嵌套函数也没有其他引用指向这个绑定对象,他就会被当做garbage回收。如果定义了嵌套函数,每个嵌套函数都会形成自己的作用域链,并且这个作用域链指向一个变量绑定对象,如果这些嵌套的函数对象在外部函数中保存下来,那么他们也会和所指向的变量绑定对象一样被当做garbage回收。但是如果将这些嵌套函数作为返回值返回,或存储在某处的属性里,就会有一个外部的引用指向这个嵌套的函数。它就不会被当做garbage回收,并且所指向的变量绑定对象,也不会被当做garbage回收!

所以在上边的代码里当调用outter之后,由于外部变量d引用了嵌套的函数,故而在outter执行完毕之后,d指向的嵌套函数何其所指向的变量绑定对象是没有被回收的,所以c也没有回收,所以有了往后的故事。。。 来一个闭包的英文解释(以下摘自MDN的解释):

A closure is the combination of a function and the lexical environment within which that function was declared.

什么意思?上边话直接翻译过来就是:闭包是一个函数和这个函数被声明的词法作用域环境的组合。 再说一下开头说到的这道题:

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函数调用之后都会产生一个新对象,这个对象会将传进来的第一个参数,用闭包的方式保存下来,接下来逐条解释一下这条语句执行时会发生的情况:

fun(0):   
    fun(0) => n = 0,o = undefined;   
    console.log(o); => o = undefined;打印undefined   
    return { fun: function(m){ return fun(m,n ) } }; 
     => 定义嵌套函数,形成作用域链,
      并由return的对象的属性 fun引用,m = undefined,n =0 ,这条作用域链被保留   
      
fun(0).fun(1):       
    实际上是调用返回对象的fun方法:       
        function(m)  => m = 1;       
            return fun(m,n) => m=1,n=0;       
     再执行fun函数:       
         fun(n,o) => n=1,o=0;       
         console.log(o) => 打印0       
             return { fun: ...return fun(m,n) } 
                  => m = undefined,n =1; 同上,作用域链被保存 
                  
fun(0).fun(1).fun(2):           
       还是调用了返回对象的fun方法:        
           function(m) => m=2        
               return fun(m,n) m=2,n=1        
                   再调用fun(n,o) => n=2,o=1        
                       然后再打印o就是打印1啦。。。        
                       
最后    fun(0).fun(1).fun(2).fun(3);            
        还是调用了返回对象的fun方法:            
            function(m) => m=3            
                return fun(m,n) m=3,n=2            
                    再调用fun(n,o) => n=3,o=2            
                        然后再打印o就是打印2啦。。。        ...    
                            最后的b是调用了返回对象的fun方法,fun执行的时候又返回了一个对象,
                                所以b是一个对象,里边有一个键为fun值函数的属性,这种属性我们一般叫他方法。

其实最后才是我要讲的重点,如果为了炫技或者条件必须得情况下,使用闭包外,闭包能避免就避免,因为只要你使用了闭包就意味着你在内存里划出了一块地方,你的程序可能不一定会用,而电脑在运行时其他的程序却一点不能利用的空间,对程序的性能无疑有影响。

20180917补充

最近在整理项目的时候,无意中发现以前技术用with写的语句,为了避免坑,决定研究一下with,其实with是被禁止的,并且是推荐不使用的,所以也没怎么上心。 这次机缘巧合之下,搞了一下with语句,感觉对作用域链的研究更深了。

with是这样的,with(obj) 会将obj对象添加至当前作用域链的最前头,这时候 with(obj){ console.log(key) } , 变量查找时会首先看obj上是否有key,然后才会查找上一级作用域链上的东西。

当然with这一特性,有时候虽然可以很骚,但是是非常不利于后期维护的,所以还是不会用的。

与闭包随笔相似的内容:

闭包随笔

开始正式介绍之前先看一个比较有难度的关于闭包的面试题: 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

React闭包陷阱

# React闭包陷阱 `React Hooks`是`React 16.8`引入的一个新特性,其出现让`React`的函数组件也能够拥有状态和生命周期方法,其优势在于可以让我们在不编写类组件的情况下,更细粒度地复用状态逻辑和副作用代码,但是同时也带来了额外的心智负担,闭包陷阱就是其中之一。 ## 闭

彻底理解闭包实现原理

前言 闭包对于一个长期写 Java 的开发者来说估计鲜有耳闻,我在写 Python 和 Go 之前也是没怎么了解,光这名字感觉就有点"神秘莫测",这篇文章的主要目的就是从编译器的角度来分析闭包,彻底搞懂闭包的实现原理。 函数一等公民 一门语言在实现闭包之前首先要具有的特性就是:First class

Rust函数与闭包

1. 常规函数 函数都拥有显示的类型签名,其本身也是一种类型。 1.1 函数类型 自由函数 // 自由函数 fn sum(a: i32, b: i32) -> i32 { a+b } fn main() { assert_eq!(3, sum(1, 2)) } 关联函数与方法 struct A(i3

深入理解 python 虚拟机:原来虚拟机是这么实现闭包的

在本篇文章当中主要从虚拟机层面讨论函数闭包是如何实现的,所谓闭包就是将函数和环境存储在一起的记录。这里有三个重点一个是函数,一个是环境(简单说来就是程序当中变量),最后一个需要将两者组合在一起所形成的东西,才叫做闭包。

函数的作用域和匿名函数、闭包、回调

一、匿名函数: 1、filter函数,可以过滤出列表中大于3的数据,但是使用都需要提前定义一个函数,有没有更加简便的方式呢? def f(o): # 过滤器 if o>3: print(o) list(filter(f,[3,1,5,9,7,10])) 运行截图: 2、匿名函数(lambda后面是空

[转帖]记录一次前端内存泄漏排查经历

https://juejin.cn/post/6844904019983335438 对于前端的“内存泄漏”这个东西,说实话我只在书上看到过: 闭包、匿名函数和事件绑定尤其容易造成内存泄漏。 然而这些操作造成的“内存泄漏”究竟是什么样子的?如何排查?虽然很好奇,却不得而知。直到这次公司应用频繁出现浏

Python常见面试题016. 请实现如下功能|谈谈你对闭包的理解

016. 请实现如下功能|谈谈你对闭包的理解 摘自<流畅的python> 第七章 函数装饰器和闭包 实现一个函数(可以不是函数)avg,计算不断增加的系列值的平均值,效果如下 def avg(...): pass avg(10) =>返回10 avg(20) =>返回10+20的平均值15 avg(

你认识的C# foreach语法糖,真的是全部吗?

> 本文的知识点其实由golang知名的for循环陷阱发散而来, 对应到我的主力语言C#, 其实牵涉到闭包、foreach。为了便于理解,我重新组织了语言,以倒叙结构行文。 先给大家提炼出一个C#题:观察for、foreach闭包的差异 ![](https://files.mdnice.com/us

Linux软件包管理

软件包管理 【1】、Linux软件类型 开源软件 软件源代码开放,供用户免费学习,允许用户二次开发,用户使用放心,后期如果开发者不再进行维护,会有其他人进行维护 闭源软件 软件代码不公开发布,无法二次开发,后期开发者如果不进行维护损失很大 【2】、开源软件包类型 源码包 优点: 可以看到软件源代码,