JavaScript闭包的认识/应用/原理

闭包认识

只用三句话,通俗解释什么是闭包:

  1. JS变量的查找规则是由内到外,JS函数是可以嵌套声明的,所以子函数可以访问父函数的作用域;
  2. 当外层函数把子函数return出去,又赋值给外部变量时,那么这个变量就相当于拥有访问封闭数据包的能力;
  3. 最终的效果就是:外层函数执行完毕后,局部变量数据由于被引用了而未被销毁;
javascript 复制代码
function useFoo(){
    var a = 2;
    function bar(){
        console.log(a); //2
    }
    return bar;
}

// useFoo函数执行后,其作用域不会立即销毁,称之为闭包函数
var baz = useFoo();

闭包应用

闭包怎么用

  • 利用闭包函数能留存数据的特点,可以更加便捷的创建数据模块化
  • 而所谓模块化,就是指封装隔离的代码环境,在模块内部的数据和方法不会影响外部代码,就比如像传统面向对象的Class封装、ES6的Module封装、指令和组件封装等,都是不同粒度的代码模块化的封装方式。
  • 闭包会造成内存泄露吗?闭包只是延迟变量生命周期的一种方式,正常情况不会造成内存泄露,而只有在DOM事件引用、定时器未清除,或者闭包变量长期持有大数据未释放,才会造成内存泄漏。解决的方式是手动把引用设置为null,切断引用;

闭包的实践案例

  • React的hooks 逻辑封装
  • Vue computed 计算属性和 watch 监听回调
  • 实现防抖截流函数
  • 实现函数柯里化
  • 实现链式调用
  • 实现高阶函数

闭包(Closure)与Object、ESModule的区别与选择

模块化是对逻辑代码封装和避免JS变量冲突的主要方式

Object对象

  • 先说object对象,虽然经常使用字面量的声明方式,把对象当做map集合来存数据,但对象的本质其实是继承自Object基类的
  • 而类,或者说Class,是源自传统的面向对象的设计范式,是指先定义Class,然后通过实例化得到实例对象
  • 相同点就是,Class类也是模块化的一种方式。用来组织和管理对象相同的属性和方法,类的成员可以是private私有的、protect派生类共享的以及public公有的
  • 通常用来抽象封装设计时使用,比如抽象一个Axios请求类,通过参数化间隔时间和响应结构来实例化不同的请求对象;
  • 而闭包相较于对象,定义灵活,不需要提前定义类,并且像hooks封装一样,可以随着组件的生命周期很方便的被回收管理。对象就像是结构化的闭包,但如果闭包结构变得复杂时,就应该使用对象封装更合适;

Module的模块

  • ES6 Module模块则通常是独立的js文件,是基于文件作用域的
  • 相同点是模块文件内部定义的变量/常量/方法默认都是私有的,需要通过export导出,而导出的内容是单例模式的,也就是说,哪怕其他不同的文件通过import引入多次,都指向同一个实例;
  • 区别是Module是ES6新特性,老旧浏览器可能不支持,而闭包是JS的基础特性。且ESM的加载和解析是代码运行前完成的,通常用来定义一些全局的工具方法或者全局变量;

闭包原理

三个问题

所有的JS函数都是闭包函数吗?

《JavaScript权威指南》犀牛书以及《现代JavaScript教程》都提到过一句话:

"从技术的角度来讲,所有的JavaScript函数都是闭包函数",如何理解这句话?

因为:

  • ECMAScript标准中,所有JavaScript函数(哪怕是空函数),函数的隐藏属性[[enviroment]]词法环境都包含outer外部环境的引用
  • 也就是说,任何函数总能访问外部环境的变量,哪怕这个函数在当前自己所处的词法环境之外被执行

JS执行时怎么就能区分普通函数和闭包函数,实现数据留存的?

JS代码分为"预编译"和"执行"两个阶段:

  • 预编译阶段:创建VO(变量对象),并为变量分配内存空间,进行变量提升,生成AST;
  • 执行阶段:把VO转成AO(活动对象),创建执行上下文(包含作用域链),执行代码逻辑;

所以在预编译阶段时,普通函数的局部变量只在当前函数的作用域,执行后就被销毁;而闭包函数中的局部变量由于跨作用域引用,所以在预编译时就被分配在了堆空间,函数执行后实现了生命周期延长;

V8引擎的内存管理机制怎么理解闭包的实现?

上面闭包的特性,只是ECMAScript定义的标准,和JS引擎的具体实现又有些许不同,主要包含以下内容:

惰性解析(lazy parsing)
  • 虽然JS分为预编译和执行两个阶段,但V8引擎反而采用的是编译执行+解释执行混合策略(JIT-Just in Time),它不会一次性把所有的JavaScript代码进行编译,是这考虑到一次性编译会影响JS代码启动速度,以及内存的大量占用。
  • 这就是V8引擎的"惰性解析/延迟解析"特性:就是说在解析代码时,仅仅编译顶层代码,遇到函数声明,会跳过函数内部的代码,等函数执行时再编译函数内部的代码并执行
预编译器(preparser)
  • 但惰性解析不了解函数内部的逻辑,使得在预编译阶段,无法区分普通函数还是闭包函数了,所以v8引擎引入了预编译器模块来解决
  • 当解析代码遇到函数时,并不是真正的跳过函数,而是通过预编译器对函数内部进行快速的预解析,检查语法有没有错误,以及有没有引用外部作用域的变量,从而进行判断该函数是否为闭包函数
逃逸分析(escape analysis)
  • 首先回顾内存管理分为"栈内存"和"堆内存":
    • 栈内存由引擎的GC来管理分配和释放,内存大小固定且空间连续,后进先出策略,像JS基础数据类型都会被分配在栈内存中;
    • 堆内存由开发者管理,内存大小不固定,树形存储;
  • 通过预编译器模块确定了如果是普通函数的变量,则说明未逃逸,在当前作用域使用,则保留在栈空间,快速访问,自动释放;如果是闭包引用变量,则分配到堆内存中,长生命周期,需手动GC管理;
优化词法环境(不重要)
  • 当我们在嵌套的子函数中debugger,watch查看外层函数的局部变量时,发现本应该可以访问的,却显示not available
  • 这是v8引擎对函数的词法环境进行了优化,outer(外部作用域)实际上是一个删减版本的对象。只是对调试有影响,但不影响最终结果

最后总结

  • 闭包并不是JavaScript语言独有的,是Go、PHP、swift、Java、Dart等函数式编程语言/动态语言的核心特性,甚至是计算机科学重要的编程概念;
  • 闭包的本质就是词法作用域规则的副产品,在预编译时通过外部词法环境引用来获得局部变量的访问权限,在执行时延迟了变量的生命周期。简言之,就是一句话:定义时有了引用,执行时就能使用
  • JS引擎是JIT策略且惰性解析的,通过预解析器模块进行逃逸分析,将闭包数据存储到堆内存中,从而实现延迟变量的生命周期;
相关推荐
孩子 你要相信光1 小时前
安卓edge://inspect 和 chrome://inspect调试移动设备上的网页
前端
乐闻x2 小时前
提升 React 应用性能:使用 React Profiler 进行性能调优
前端·javascript·react.js·性能优化
gotoc丶4 小时前
堆排序:力扣215.数组中的第K个大元素
javascript·数据结构·算法·leetcode·排序算法
NaZiMeKiY4 小时前
HTML5前端第二章节
前端·html·html5
天若有情6734 小时前
深入浅出:HTML 中 <a> 标签嵌入链接教程
前端·html
烂蜻蜓4 小时前
HTML 样式之 CSS 全面解析
前端·css·html
冬冬小圆帽4 小时前
Webpack 优化深度解析:从构建性能到输出优化的全面指南
前端·webpack·node.js
字节源流6 小时前
【SpringMVC】常用注解:@SessionAttributes
java·服务器·前端
肥肠可耐的西西公主6 小时前
前端(vue)学习笔记(CLASS 4):组件组成部分与通信
前端·vue.js·学习