从JS的运行机制的角度谈谈闭包

news/2024/7/20 13:32:59 标签: javascript, 内存管理, java

JS中的作用域、闭包、this机制和原型往往是最难理解的概念之一。笔者将通过几篇文章和大家谈谈自己的理解,希望对大家的学习有一些帮助。

上一篇中我们说了作用域,这一篇我们谈一谈闭包。笔者避免了使用JS中一些复杂的概念,仅仅阐述一些必要的概念和原理,避免因为复杂的概念使得闭包让大家望而生畏。

闭包是什么?看似一个晦涩难懂的名词。MDN上给对闭包的解释是:

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

这个说法实在是太不明确了,组合?只要组合了就是闭包么?如果不是的话?那怎么组合才能形成闭包呢?

我们再来看看百度百科上对于闭包的定义。

闭包就是能够读取其他函数内部变量的函数。

仿佛各不相同的说法,那么,我们究竟应该如何理解闭包呢?

美丽的意外

首先抛出一个概念,自由变量,就是下文代码中的foo1()函数内部的变量a,为什么叫它自由变量呢, 因为它既不是它所在函数内的参数,也不是在所在函数的内部创建的。 作为人我们可能很好理解a代表着什么,但是JS引擎怎么理解这个a呢,肯定有一套相应的规则帮助JS引擎理解这个a。JS中的词法作用域,就是帮助JS引擎理解这个a的一套规则。

javascript>javascript copyable">function foo() {
  var a = 1;
  function foo1() {
    console.log(a);
  }
  foo1();
}

foo();	// 1
复制代码

我们通过我们掌握的词法作用域的知识分析一下上面的代码,全局作用域中定义了foo,foo的作用域中定义了a和foo1,foo1作用域中什么都没定义,但是有一个输出语句,当foo1执行时console.log(a);语句时,会按照作用域从里到外查找,找到它上层的a的值并打印出来。理论上来说,这个时候,其实已经创建了一个闭包(实际上,在JS中,任何一个拥有了自由变量的函数都是闭包)。所以我们现在再来看看MDN和百度百科上的定义,其实两个说的都对,MDN的说法相对抽象一些,百度百科说的也对,因为在JavaScript中,由于词法作用域的规则存在,能访问函数内部的局部变量的只有定义在该函数内部的子函数,所以一些人理解JS中的闭包一定是存在嵌套的函数的,这样的理解也没有什么错误。

一个值得注意的地方是,闭包是在代码创建时候产生的,而不是在代码运行时产生的, 很多人会在这个地方产生误解。不过毫无疑问的是,由于闭包的存在导致了JS代码在运行时可以产生一些独特的表现。

JS中的垃圾回收机制

用过Vue和React等框架的同学,对于组件生命周期肯定不陌生,其实在代码的执行过程中,一段段的代码也和组件一样,存在的属于自己的生命周期。JS代码的生命周期大致分为三个阶段,内存分配阶段、内存使用阶段、内存回收阶段。其中,明白内存回收机制(垃圾回收机制)对于透彻的理解和使用闭包具有重要的作用。

垃圾回收机制是编程语言中必备的一个机制,代码运行在内存中,定义的变量毫无疑问的会占用一定的内存。学习JS的同学应该可以直观的感受到,JS相较于C/JAVA系列的语言而言,自由了太多(例如在C/JAVA中,如果定义数组的话,初始化的时候需要指定为这个数组分配的内存大小,它们对于内存的控制相对严格的多)。

如此宽松的内存分配,如果还没有垃圾回收机制的话,占用的内存会随着代码的增多而越多越多,最后耗尽系统的内存,造成系统的崩溃。JS引擎对于不再需要的变量占用的内存资源进行回收,可以尽可能的减少代码运行时候的内存占用。

不同语言中实现垃圾回收机制的做法各不相同,大体上的实现策略有两种,一种是标记清除,一种是引用计数。但是对于理解闭包而言,理解两种垃圾清除策略十分的重要。

标记清除的策略是这样的,对于进入执行环境的变量,标记为进入执行环境的状态,当离开当前执行环境的时候,标记为离开的状态,下次垃圾回收器来的时候对离开的状态的变量释放内存,即垃圾回收。

引用计数的策略则不同,MDN中对于引用计数的描述:

The main notion garbage collection algorithms rely on is the notion of reference. Within the context of memory management, an object is said to reference another object if the former has an access to the latter (either implicitly or explicitly). For instance, a JavaScript object has a reference to its prototype (implicit reference) and to its properties values (explicit reference). In this context, the notion of "object" is extended to something broader than regular JavaScript objects and also contains function scopes (or the global lexical scope).

例如一段代码中定义了var a={c:1};,然后定义var b=a;,最后令b=1;a=1;。这个时候{c:1}的引用为0,可以吧{c:1}进行回收了。事实上,只要一个对象不论是a还是b ,只要任何一个有访问另一个对象{c:1}的权限,就始终不能回收{c:1}对象。这里的对象不仅仅是javascript>javascript对象,也包括了作用域。 明白这一点至关重要。

JS中的运行时机制

JS中代码的执行一般分成两个环境,一个是创建时环境,即词法作用域;还有一个就是运行时环境,我们通常叫它执行环境(执行上下文和执行上下文栈)。JS是单线程的,每进行一个函数调用时就会创建一个执行上下文对象, 将这个对象压入执行上下文栈中,函数调用结束时则将该执行上下文对象从执行上下文栈中弹出。 这样确保了单线程的JS在同一时间只在同一个执行上下文中。

首先JS代码进入全局,创建全局执行上下文对象并压入栈中。
每当发生一个函数调用,就创建一个执行上下文对象并将这个对象压栈。
当函数调用完成时,将该函数调用时创建的执行上下文对象出栈。
最后整个代码执行完毕,弹出全局上下文执行对象。

执行上下文是一个相对抽象的概念,用于标记函数调用的过程。

特殊的闭包

明白了JS中的垃圾回收机制和执行上下文机制,我们再来分析一下第一段代码:

javascript>javascript copyable">function foo() {
  var a = 1;
  function foo1() {
    console.log(a);
  }
  foo1();
}

foo();	// 1
复制代码

词法作用域:
全局(global)=> foo => foo1
执行上下文栈的操作顺序:
Global Execution Context(push) => foo Execution Context(push) => foo1 Exection Context(push)
=> foo1 Execution Context(pop) => foo Execution Context(pop) => Global Execution Context
内存管理
给foo分配内存 => foo 函数调用 => 给foo1,a分配内存 => foo1 函数调用 => foo1 调用结束 => 释放foo1的内存 => foo调用结束   => 释放a的内存 => 全局调用结束 => 释放foo内存

怎么样,没什么特别的吧,我们再来看看第二段代码:

javascript>javascript copyable">function foo() {
    var a = 1;
    function foo1() {
        console.log(a);
    }
    return foo1;
}

var b = foo();
b();
复制代码

第二段代码的词法作用域和执行上下文栈的操作顺序都是一样的,但是在内存管理上略有不同。

内存管理
给foo,b分配内存 => foo 函数调用 => 给foo1,a分配内存 => 将foo1的引用返回给b => foo 函数调用结束 => b(foo1) 函数调用 => foo1调用结束 => 释放foo1, a的内存 => 全局调用结束 => 释放foo, b的内存

这里说明一下,JS中的函数使用是引用方式传递的,return foo1; 是将指向foo1函数的指针返回给b,所以b()实际上是foo1();

看出来区别了么,因为foo 中的return将foo1返回到全局变量b中,所以b始终通过foo1的作用域保持着对局部变量a引用(参考上面垃圾回收引用计数策略的定义),在b执行结束前都不会释放a的内存。

除了return之外,还有一种常见的写法会导致局部变量的内存无法释放,那就是将函数作为参数,这种写法常常在回调函数中见到。

javascript>javascript copyable">function foo () {
    var a = 1;
    setTimeout(function foo1 () {
        console.log(a);
    },1000);
    setTimeout(function () {	
        console.log(a);
    },1000);
}

foo();
复制代码

说明一下,具名函数和匿名函数的差别仅仅在是否可以调用自身上已经是否方便定位错误,除此之外并没什么区别。这里foo1内部存在自由变量a,所以是一个闭包。setTimeout是一个全局的方法,所以实际上,foo1被作为参数传到了全局方法中,在setTimeout方法执行完成前,始终保持着对foo内部作用域的引用,a的内存也不会被释放。

第二段和第三段写法的闭包都具共同的特征,就是局部函数的变量和参数不会被垃圾回收,是常驻内存的!我们暂且称呼它们为特殊的闭包

如何正确使用特殊的闭包

普通的闭包的存在看似毫无用处,特殊的闭包看似有百害无一益,实际上,只要运用得到,特殊的闭包也可以很强大。特殊的闭包可以让局部变量常驻内存,同时避免局部变量污染了全局变量,使得设计私有的方法和变量成为可能!
特殊闭包的主要用法一:函数工厂

javascript>javascript copyable">function foo(value1) {
    return function v2(value2) {
        console.log(value1 * value2);
    }
}

var a = foo(2);
var b = foo(3);

a(2); // 4
b(2); // 6
复制代码

特殊闭包的主要用法二:设计私有变量和方法

javascript>javascript copyable">function foo() {
    var value = 0;
    function addOne() {
        value += 1; 
    }
    return {
        addOne: addOne,
        getValue: function getValue() {
            console.log(value);
        }
    }
}

var b = foo();
var c = foo();
b.addOne();
b.getValue();	// 1
c.getValue(); // 0
复制代码

特殊闭包的主要用法三:函数柯里化
函数柯里化和闭包的结合并不简单,涉及到求值策略和编程思想的转换,笔者将在后续的文章中单独介绍这一部分的用法。

特殊的闭包还有很多其他的用法,这里就不一一列举了,有兴趣的读者可以自行百度。

坑外话

闭包与立即执行函数(IIFE)

在ES6出现之前,闭包的作用就是模拟块级作用域,说到模拟块级作用域,则和立即执行函数分不开。
什么是立即执行函数?

javascript>javascript copyable">(function foo() {
	...
})()
  
!function foo() {
	...
}()
  
+function foo() {
	...
}
复制代码

首先把函数声明或者匿名函数加上一些特殊运算符,如加上()、+、!等,将其变成函数表达式,再在后面加上()表示立即执行,就创建了一个立即执行函数。立即执行函数内部定义的方法和变量不会污染全局变量。

javascript>javascript copyable">for(var i=0;i<10;i++) {
    setTimeout(function(){
      console.log(i);
    },1000);
}
复制代码

上面是很常见的一道面试题,延时函数中的方法在循环结束之后才会执行,由于没有块级作用域,所有的setTimeout中的回调函数在闭包的作用下共享一个全局变量i,这个i的值是10。所以打印出来的结果是10个10。
这个时候就需要用一个东西捕获每个循环中i的值,放在自己的作用域中。立即执行函数刚好派上了用场。

javascript>javascript copyable">for(var i=0;i<10;i++) {
    (function (i) {
      setTimeout(function() {
        console.log(i)
      },1000);
    })(i);
}
复制代码

上面代码中回调函数打印的i实际上是每一次循环中立即执行函数捕获的i的副本。

有同学会说,我把setTimeout的延时改成0是不是就可以了?

javascript>javascript copyable">for(var i=0;i<10;i++) {
    setTimeout(function(){
      console.log(i);
    },0);
}
复制代码

事实上并不行,仍然打印出来的还是10个10,为什么呢?因为JS是单线程的,只能为setTimeout维护了一个单独的队列,当前任务处理完了才会处理setTimeout队列中的内容,所以setTimeout的时间参数并不是相对于调用该函数的时间差,还是相对于开始执行setTimeout队列时的时间差。

码字不易,如果喜欢就点个赞吧~

转载于:https://juejin.im/post/5c8beda6f265da2de04b02c9


http://www.niftyadmin.cn/n/1474506.html

相关文章

解析一个Java对象占用多少内存空间

说明: alignment, 对齐, 比如8字节的数据类型long, 在内存中的起始地址必须是8的整数倍。 padding, 补齐; 在对象所占据空间的末尾,如果有空白, 需要使用padding来补齐, 因为下一个对象的起始位置必须是4/8字节(32bit/64bit)的整数倍(这又是一种对齐)。 问题描述 一个对象具有1…

省选集训日记

一个蒟蒻的被包菜日记 内容过于残暴,慎入!!! ------------- 前言: 这是我参加省选停课集训的日记,主要是用来记录这段时间的考试,每天的学习内容. 3.18 停课第一天. 没有什么事情发生. 考试 考试第一题先打sb搜索,18pts. 然后是链,sb10pts. 然后是环,sb递推式(然而并不sb),15pt…

Code First开发系列实战之使用EF搭建小型博客平台

返回《8天掌握EF的Code First开发》总目录 本篇目录 理解应用需求数据库设计创建实体数据模型 创建实体类创建关系和导航属性实现DbContext类执行数据访问 理解仓储模式理解工作单元管理分类管理博客管理评论总结本系列的源码本人已托管于Coding上&#xff1a;点击查看&#xf…

删除MySQL表的SQL语句-DROP-TABLE-简介

辨析: 在SQL中, DROP 用于删除表结构等信息, 属于数据格式定义 DDL的范畴&#xff1b; 而 DELETE 用于删除数据&#xff0c;属于数据内容操作 DML 的范畴。 官方给出的 DROP 关键字删除表结构的语法如下: DROP [TEMPORARY] TABLE [IF EXISTS]tbl_name [, tbl_name] ...[RESTRI…

HDU3032 Nim or not Nim?

解&#xff1a;使用sg函数打表发现规律&#xff0c;然后暴力异或起来即可。 1 #include <bits/stdc.h>2 3 typedef long long LL;4 const int N 1000010;5 6 int a[N];7 8 inline LL sg(LL x) {9 int t x & 3; 10 if(t 1 || t 2) return x; 11 if(t …

JVM技术细节: HotSpot的内存模型

1、对象无句柄 在早期的JVM版本&#xff0c;例如 Classic VM 实现中&#xff0c;使用了 间接句柄(indirect handle) 来表示对象引用。 虽然使用这种方式使得垃圾收集器在重定位对象&#xff08;relocating&#xff09;时非常方便&#xff0c;但却导致了严重的性能瓶颈&#xf…

Java坑人面试题系列: 包装类(中级难度)

Java Magazine上面有一个专门坑人的面试题系列: https://blogs.oracle.com/javamagazine/quiz-2。 这些问题的设计宗旨&#xff0c;主要是测试面试者对Java语言的了解程度&#xff0c;而不是为了用弯弯绕绕的手段把面试者搞蒙。 如果你看过往期的问题&#xff0c;就会发现每一…

Linux环境下 RabbitMQ 的下载与安装

0 环境 CentOS7 RabbitMQ 3.6.5 erlang 18.3 socat rabbitmq是使用erlang语言编写的&#xff0c;所以需要先安装erlang&#xff0c;其次rabbitmq安装依赖于socat&#xff0c;所以三个安装包都需要下载。另外rabbitmq对于erlang的版本是有要求的&#xff0c;这个可以去官网查看。…