你离高质量代码封装只差一个闭包,快来get吧!

2022/3/17 js

海阔凭鱼跃,天高任鸟飞。Hey 你好!我是猫力 Molly

闭包已经是一个老生常谈的问题了,不同的人对闭包有不同的理解。今天我来浅谈一下闭包,大家一起来就“闭包”这个话题,展开讨论,希望能擦出一些不一样的火花。

理解闭包之前,我们得先了解“上下文”和“作用域”两个知识点

# 上下文

浏览器引擎在解析 js 代码的时候,大致会经过两个阶段。解析阶段执行阶段

解析阶段:一段代码说白了只是一段有规则的代码文本而已,所以js引擎会拿到代码时会事先解析代码,初始化过程中会将变量,参数,函数,表达式,运算符等等提取并存起来,并且将变量默认赋值为undefined,函数默认为函数块,确定上下文关系等一系列准备工作

执行阶段:由上往下逐行执行代码,遇到对应的变量或函数,则去仓库里面匹配执行

console.log(str);
console.log(fun);
var str = "molly";
let str1;
console.log(str1);
function fun(a, b) {
  return a + b;
}
1
2
3
4
5
6
7
8

定义: 上下文可分为全局上下文局部上下文,上下文决定了变量或函数他们可以访问哪些数据,以及他们的行为(在初始化阶段就已经确定好),每一个上下文都有一个变量对象(环境记录) ,这个上下文中定义的所有变量和函数都会存储在这个变量对象当中,我们无法直接通过代码访问到这个变量对象,但是我们可以通过打断点的方式查看到。

全局上下文会在程序退出前(例如关闭网页或退出浏览器)被销毁,局部上下文会在其代码执行完毕后被销毁

那么这个变量对象长啥样呢?不着急,我们接着往下看

# 上下文执行栈

定义: 每个函数调用都有自己的上下文,当函数执行时,函数的上下文会被推入到一个上下文执行栈上,在函数执行完毕后,上下文执行栈会弹出该函数的上下文,将控制权返还给之前的执行上下文。

// 一个简单的例子,断点调试调用栈和上下文变量对象
let a_name = "猫力";
var a_sex = "男";
var a = "111";
function a_molly(age) {
  let a_like = "爱学习";
  var a_like2 = "爱运动";
  a_say(a_like);
  var a = "222";
  console.log(a);
  let test = "来啦?";
}
function a_say(a_like) {
  let code = "敲代码";
  console.log(a_like);
}
a_molly();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

执行如上示例代码,我们在控制台打上断点,来观察执行栈(call stack) 的过程

 执行栈.jpg

注意观察左边的call stack(执行栈)和右边的scope(环境记录)还有断点位置

通过观察断点调试结果,我们可以得到以下结论:

  • 当脚本程序初始化时,会往所有的上下文执行栈底部推入一个全局上下文,也就是Global属性
  • 每当执行到函数时,会往执行栈里面追加一个 “函数上下文”
  • 当函数执行完毕之后,会清除对应的 “函数上下文”
  • 每个上下文内部,确定了可以访问的数据

# 作用域和作用域链

上下文中的代码在执行的时候,会创建变量对象的一个作用域链。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。作用域是包含关系,全局包含局部。

总结:

上下文关联了变量对象决定了函数可以访问哪些数据,而作用域则是决定了数据访问的规则。

简单来说,作用域的访问规则可以总结为:由内向外查找访问,内部可以访问外部而外部无法访问内部,这样的访问方式可以称之为作用域链

函数参数被认为是当前上下文中的变量,因此也跟上下文中的其他变量遵循相同的访问规则。

# 闭包

现在我们已经简单了解了 “上下文”和“作用域”两个知识点,再来谈闭包就十分友好了

# 闭包的定义

红宝书: 闭包指的是那些引用了另一个函数作用域中变量的函数

MDN: 一个函数和对其周围状态(lexica environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

阮一峰: 闭包就是能够读取其他函数内部变量的函数。只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

总结一下: 函数的执行,可以触发另一个函数的定义(函数声明,函数表达式),并且支持引用另一个函数作用域中的变量。那么这个函数就是一个闭包

# 为什么会有闭包?

综上理论我们可以得知,局部作用域可以访问全局作用域,而全局无法访问到局部,两个不相干的局部也无法相互访问,那么,只要思想不滑坡,办法总比困难多,要解决此类问题,我们需要变通一下,结论就是:“闭包”

闭包就好像是一座桥梁,把多个不相干的作用域串联起来,实现互通。

闭包的原理正是利用了变量环境和作用域链访问规则

# 闭包的用途

闭包最大的用途有两个

  1. 可以读取函数体的内部变量
  2. 让闭包的变量始终保持在内存中

# 闭包的形式:

把函数视为一等公民,视为一个普通变量

# 1:返回一个函数

function fun(){
    var aaa = 111
    return function(b){
        return aaa+b
    }
}
fun()()
经典场景:防抖节流
1
2
3
4
5
6
7
8

# 2:返回一个函数变量

function fun(a) {
  let fn = function (b) {
    return a + b;
  };
  return fn;
}
fun()();
1
2
3
4
5
6
7

# 3:作为全局的闭包函数

var call;
function fun(a) {
  call = function (b) {
    return a + b;
  };
}
fun();
call();
1
2
3
4
5
6
7
8

# 4:作为函数参数传递

function fun1(fn) {
  fn(); //这个fn函数就是闭包
}
function fun2() {
  let str = "molly";
  function fun3() {
    console.log(str);
  }
  fun1(fun3);
}
fun2();
1
2
3
4
5
6
7
8
9
10
11

# 5:回调函数

function ajax(data) {
  console.log(data);
}
function sync() {
  const obj = { name: "molly", a: a };
  ajax(obj);
}
1
2
3
4
5
6
7

# 6:IIFE 立即执行函数

;!(function(){
   ...
});

经典场景:
for(var i = 1;i <= 5;i++){
  (function(j){
    setTimeout(function timer(){
      console.log(j)
    }, 0)
  })(i)
}
jquery自定义封装插件也是从立即执行函数开始的
1
2
3
4
5
6
7
8
9
10
11
12
13

# 闭包的优势:

  1. 可以访问到两个不相干的作用域变量
  2. 作为一个沙箱,存储变量
  3. 代码封装,工具函数
  4. 函数作为值,进行入参和返回

# 闭包的劣势:

  1. 内存泄露

# 如何规避内存泄露呢?

  1. 将函数指针指向null,纳入垃圾回收范围
  2. 将闭包执行完毕
function fun(a) {
  return function (b) {
    return a + b;
  };
}
let a = fun();
a = null;

// 再或者将闭包也执行完毕;

a();
1
2
3
4
5
6
7
8
9
10
11

这里简单提一嘴内存为什么闭包会导致内存泄露,js 的垃圾回收机制大致分为 “标记清理”“计数引用” 两种。当闭包内的变量在另一个函数中有使用时,这个变量则不会被识别为垃圾,而是常驻内存当中不会被清理。导致额外内存消耗

# 提问?

你知道有哪些巧用闭包的场景或代码么?欢迎评论区留言讨论!

# 感谢

欢迎关注我的个人公众号前端有猫腻 (opens new window)每天给你推送新鲜的优质好文。回复 “福利” 即可获得我精心准备的前端知识大礼包。愿你一路前行,眼里有光!

感兴趣的小伙伴还可以加我微信:猫力 molly (opens new window)前端交流群 (opens new window)和众多优秀的前端攻城狮一起交流技术,一起玩耍!

最后更新时间: 2022/3/17 上午9:36:36