前端基石:闭包

·  阅读 6033
前端基石:闭包

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

什么是闭包?

  • ✅ 官方说法:闭包就是指有权访问另一个函数作用域中的变量的函数。
  • ✅ MDN说法:闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。
  • ✅ 自我理解:把函数执行,形成私有上下文,并且保存和保护私有变量的机制,称之为“闭包” ,它是一种机制。

函数的执行看闭包

过程

  1. 形成私有上下文
  2. 进栈执行
  3. 一些列操作
    1. 初始化作用域链(两头<当前作用域,上级作用域>)
    2. 初始化this
    3. 初始化arguments
    4. 赋值形参
    5. 变量提升
    6. 代码执行
      1. 遇到变量就先看是否是自己私有的,不是自己私有的按照作用域链上查找,如果不是上级的就继续线上查找,,直到 EC(G),变量的查找其实就是一个作用域链的拼接过程,拼接查询的链式就是作用域链。
  4. 正常情况下,代码执行完成之后,私有上下文出栈被回收。但是遇到特殊情况,如果当前私有上下文执行完成之后中的某个东西被执行上下文以外的东西占用,则当前私有上下文就不会出栈释放,也就是形成了不被销毁的上下文,闭包。

三种情况

  • 当前上下文的某些东西被上下文以外的某些东西占用,那么当前上下文就不会被释放。
  • 如果没有被占用就是执行完成之后就被释放。
  • 除了这上面两种情况还有一种情况是,上下文没有被占用,但是要紧接着被用一次,这样没有用完之前是不能释放的,用完在释放,这样就形成了一个临时不被释放 )

函数每一次执行 都是从新形成一个全新的私有上下文,和之前执行产生的上下文没有必然的联系

闭包的作用

函数执行会形成全新的私有上下文,这个上下文可能被释放,也可能不被释放,不论是否被释放,它的作用是:

  1. 保护:划分一个独立的代码执行区域,在这个区域中有自己私有变量存储的空间,而用到的私有变量和其它区域中的变量不会有任何的冲突(防止全局变量污染)
  2. 保存:如果上下文不被销毁,那么存储的私有变量的值也不会被销毁,可以被其下级上下文中调取使用

市面上一般认为只有形成的私有上下文不被释放,才算是闭包(因为如果一但释放,之前的东西也就不存在了);还有人认为,只有一下级上下文用到了此上下文中的动西才算闭包;

练习题

let x = 5;
const fn = function fn(x) {
  return function(y) {
    console.log(y + (++x));
  }
}

let f = fn(6);
f(7); // 14
fn(8)(9); // 18
f(10); // 18
console.log(x); // 5
复制代码
  1. 变量提升(跳过,没有var、function...)
  2. 代码执行
    1. x -> 5

    2. fn -> 0x 001

    3. fn (6) 函数执行

      1. 创建私有上下文,创建活动对象 AO(fn1)
      2. 初始化作用域链,<<EC(fn1), EC(G)>>
      3. 初始化 this
      4. 初始化 arguments
      5. 形参赋值
      6. 变量提升
      7. 函数执行,返回一个小函数,为小函数创建新的堆内存存储函数内容(作用域,代码字符串,键值对)
    4. f = 返回的小函数,也就是新的函数执行,f -> 0x 002,由于0x 002 被 f 占用,所用0x 002 不能被释放,所以它的上下文(作用域)也不能被释放。

    5. f(7) 函数执行

      1. 创建私有上下文,创建活动对象 AO(f1)
      2. 初始化作用域链,<<EC(f1), EC(fn1)>>
      3. 初始化 this
      4. 初始化 arguments
      5. 形参赋值
      6. 变量提升
      7. 函数执行,y = 7,x 当前作用域没有,沿着作用域链往上找,所以在 EC(fn1) 中查找 x = 6, y +(++ x) = 14
    6. fn(8) 函数执行

      1. 创建私有上下文,创建活动对象 AO(fn2)
      2. 初始化作用域链,<<EC(fn2), EC(G)>>
      3. 初始化 this
      4. 初始化 arguments
      5. 形参赋值
      6. 变量提升
      7. 函数执行,返回一个小函数,为小函数创建新的堆内存存储函数内容(作用域,代码字符串,键值对)
    7. fn(8)(9) 函数执行

      1. 创建私有上下文,创建活动对象 AO(f2)
      2. 初始化作用域链,<<EC(f2), EC(fn2)>>
      3. 初始化 this
      4. 初始化 arguments
      5. 形参赋值
      6. 变量提升
      7. 函数执行,y = 9,x 当前作用域没有,沿着作用域链往上找,所以在 EC(fn2) 中查找 x = 8, y +(++ x) = 18
    8. f(10) 函数执行

      1. 创建私有上下文,创建活动对象 AO(f3)
      2. 初始化作用域链,<<EC(f3), EC(fn1)>>
      3. 初始化 this
      4. 初始化 arguments
      5. 形参赋值
      6. 变量提升
      7. 函数执行,y = 10,x 当前作用域没有,沿着作用域链往上找,所以在 EC(fn1) 中查找 x = 7, y +(++ x) = 18

下面子看一个例子,看大家能不能自己跟着上一个例子的思路来解出答案了。

let a = 0,
    b = 0;
let A = function(a) {
  A = function (b) {
    console.log(a + b++);
  }
  console.log(a); 
}
A(1); // 1
A(2); // 3
复制代码

面试中如何回答闭包问题

其实在面试中存在很多的闭包问题,笔试、问答都有可能,并且面试可能不会直接问闭包,问的是其他问题,但是可能会用到闭包。

在面试中如果是如上练习题的笔试或者看代码说结果的问题,那可参照上面的思路,这类题目不论多么复杂,万变不离其宗。

如果在面试中是问答,不要只会回答闭包的概念。还要学会从理论、底层运行机制、实践等角度来回答闭包,不仅能看出你基础能力的深度,也能看出你基础能力的广度。 在我之前的文章 “四说闭包” 惊艳面试官|8月更文挑战中有较为深入的理解

  1. 先说闭包是什么?
  2. 在说函数的创建和执行看闭包(引述:堆栈、EC、AO、VO、scope)
  3. 然后说闭包的作用以及在项目中的引用场景,以及带来的问题
  4. 最后说闭包引发的高级编程技巧,在框架源码中的使用,或者自己写类库的怎么使用

总结

闭包不管是日常的研发过程中还是面试中都是一个常见的场景,深入理解必将让你在日常开发和面试中如鱼得水。

分类:
前端
分类:
前端
收藏成功!
已添加到「」, 点击更改