hzzzzzzzq / Blog

这是我记录博客的地方,希望能多写一些技术博客,JS、ES、React、Vue等
14 stars 0 forks source link

深入理解JavaScript系列(7):闭包 #48

Open hzzzzzzzq opened 2 years ago

hzzzzzzzq commented 2 years ago

闭包

介绍

闭包,在外面介绍了前面的之后,就可以了解我们的闭包了。

概念

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域

我们先来简单回顾一下执行上下文的第二个例子吧。

var name = 'hzq';
function checkName() {
  var name = 'hzzzzzzzq';
  return function fn() {
    return name;
  };
}

checkName()(); // hzzzzzzzq

我们这上面这段代码来看一下执行上下文栈和执行上下文的情况。

  1. 进入全局代码创建全局上下文,globalContext 入栈
  2. 全局代码执行
  3. 执行 checkName 函数,创建 checkName 函数执行上下文,checkNameContext 入栈
  4. checkName 执行上下文初始化,创建变量对象、作用域链、this
  5. checkName 函数执行完毕,出栈
  6. 执行 fn 函数,创建 fn 函数执行上下文,fn 函数上下文入栈
  7. fn 执行上下文初始化,创建变量对象、作用域链、this
  8. fn 函数执行完毕,fn 函数上下文弹出
  9. 执行其他全局代码,代码执行完毕,全局上下文弹出

我们看这个过程时,发现 checkName 函数已经不在执行上下文栈中,为什么还可以取到 name 的值呢?

《JavaScript 深入之作用域链》 中,我们了解到,fn 函数,其实维护了一个作用域链,可以看看。

fnContext = {
  Scope: [AO, fnContext.AO, globalContext.VO],
};

所有,其实 fn1 取到的就是上层的函数。就是因为作用域链,实现了父级函数即使被销毁,也可以取到值。

我们先举一个简单的闭包使用方式。

function fn() {
  var count = 1;
  return function f() {
    return ++count;
  };
}

var fn1 = fn();
console.log(fn1()); // 2
console.log(fn1()); // 3
console.log(fn1()); // 4
console.log(fn1()); // 5
console.log(fn1()); // 6

我们的执行过程类似,但是在作用域链中保存着值,可以进行修改,也导致了一个问题,也就是内存删除不干净,内存泄露。

必刷面试题

我们来看一道经典的闭包面试题。

const data = [];
for (var i = 0; i < 5; i++) {
  data[i] = function () {
    console.log(i);
  };
}
data[2](); // 5
data[3](); // 5
data[4](); // 5

这题是我们经常遇到的题目,为什么打印的全是 5 呢?

我们来看一下全局上下文中,执行函数之前的 globalContext,如果对此有问题,可以去看一下前面的几篇文章。《JavaScript 深入之执行上下文栈》《JavaScript 深入之变量对象》《JavaScript 深入之作用域链》《JavaScript 深入之 this》

我们来看一下上面代码的全局上下文(globalContext) 的内容是什么。

globalContext = {
  VO: {
    data: [...], // item => function
    i: 5
  },
  Scope: [globalContext.VO],
}

这样就一定都打印 5 了吗? 不是的,我们还需要看看 data 中函数的上下文。

data[0]Context = {
  AO: {
    arguments: {
      length: 0,
    },
  },
  Scope: [AO, globalContext.VO],
}

所以,我们在 data[0] 函数中没有找到结果 i,于是从 global 中寻找,于是找到结果为 5

const data = [];
for (var i = 0; i < 5; i++) {
  data[i] = (function () {
    return function () {
      console.log(i);
    };
  })();
}
data[2](); // 2
data[3](); // 3
data[4](); // 4

这变成了一个立即执行函数。

我们在全局上下文的 VO 是没有变化的。

但是我们来看看 data[0] 函数执行时,其中有一个是匿名函数,我们假设为 NoName 函数,我们来看一下 data[0] 的上下文

data[0]Context = {
  AO: {
    arguments: {
        length: 0,
    },
  },
  Scope: [AO, NoNameContext.AO, globalContext.VO]
}

在来看看匿名函数的上下文

NoNameContext = {
  AO: {
    arguments: {
      length: 0,
    },
    i: 0,
  },
  Scope: [AO, globalContext.VO],
};

所以,这时候执行时,找到了 i = 0,返回结果。