闭包详解-小白也能看懂的闭包理解

182 阅读6分钟

了解闭包前先来了解一下上级作用域和堆栈内存释放问题。(要是想直接看闭包跳过也行)

上级作用域

函数是在哪个作用域下声明的,上级作用域就是谁。

var a = 10
//函数 foo() 是在全局下定义的,故上级作用域是全局
function foo(){
	var a = 20;
	//函数 fo() 是在foo内创建的,故上级作用域是foo的私有作用域
	function fo(){
		console.log(a);
	}
    return fo;
}

堆栈内存释放

JS中的内存分为堆内存和栈内存

  • 堆内存:存储引用类型值(对象:键值对 函数:代码字符串)

    堆内存释放:当没有变量占用这个堆内存了浏览器会在空闲时候把它释放掉(即让所有引用堆内存空间地址的变量赋值未null即可)。

  • 栈内存:提供JS代码执行的环境和存储基本类型值

    栈内存释放:一般情况下,当函数执行之前,栈内存会存储基本函数中的基本变量,当函数执行完之后,栈内存中存的这些东西都会自动释放掉(存储变量名和值也都会释放掉)。

    但是也有特殊不销毁的情况: 1. 函数执行完成,但是函数的私有作用域内有内容被栈外的变量还在使用的(比如形成闭包时),栈内存就不能释放里面的基本值也就不会被释放 2. 全局栈内存只有在页面关闭的时候才会被释放掉

一上来就搞几个概念可能让人有一点懵,没事,上面的看不懂没有关系下面还会带着讲

首先,

什么是闭包?

在 JS 忍者秘籍(P90)中对闭包的定义:闭包允许函数访问并操作函数外部的变量。

红宝书上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数。(我觉得这个最好记)

MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。这里的自由变量是外部函数作用域中的变量。

你不知道的js中说:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

所以我们可以概述为:闭包是指有权访问另一个函数作用域中变量的函数

举个例子:

function test(){
	var temp = 100;//temp是一个test创建的局部变量
	function a(){  //a 是一个内部函数,也是一个闭包
		console.log(temp);// 使用了上级作用域中声明的变量
	}
	return a;
}

var demo = test();//注意!这里的test是执行,是带括号的,所以是将test的返回值a函					  数,给了demo而不是test函数
demo();//这里执行的是a函数
//控制台输出:100

解读:我们声明了一个test函数其中嵌套了一个a函数,然后我们将内部的a函数作为test函数的返回值传了出去。这时我们又声明了demo,并把test返回值a函数,赋值给了demo。

在这样一个例子中,由于作用域的原因,我们的demo函数本是不可以访问到temp参数的(因为demo是一个在全局声明的函数,无法访问test的作用域)但是由于这里的a是一个闭包,使得demo函数(即a函数),不管在哪都可以去自由的访问temp的值。

正常情况下,foo()执行后其整个内部作用域被销毁,栈内存被释放,但是现在的 foo的私有作用域中的 a还在使用,所以不会对其进行回收。a() 依然持有对该作用域的引用,这个引用就叫做闭包。这个a()函数在定义的词法作用域以外的地方(因为被赋值给了全局声明的demo)被调用,而闭包使得函数可以继续访问定义时的词法作用域。

闭包的作用

  • 保护函数的私有变量不受外部的干扰。形成不销毁的栈内存。
  • 保存,把一些函数内的值保存下来。闭包可以实现方法和属性的私有化

闭包是如何所产生的?

内部的函数存在外部作用域的引用就会产生闭包。创建闭包最常见的方式就是:在一个函数内部创建另一个函数。

常见的闭包举例

如果对于刚才的那个例子还不是很理解,可以看看下面这些例子。

function foo(a) {
    setTimeout(function timer(){
        console.log(a)
    }, 1000)
}
foo(2);

foo执行1000ms 后,它的内部作用域不会消失,timer函数依然保有 foo 作用域的引用。timer函数就是一个闭包。

定时器,事件监听器,Ajax请求,跨窗口通信,Web Workers或者其他异步或同步任务中,只要使用回调函数,实际上就是闭包。

for(var i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    }, i * 1000);
}
//输出: 5, 5, 5, 5, 5

首先我们需要知道,延迟函数的回调会在循环结束时才执行。循环和函数的执行事实上是分开的,当定时器运行时即使每个迭代中执行的都是 setTimeout(.., 0),根本就没有去访问i的值,所有的回调函数依然是在循环结束后才会被执行。循环完后,i的值已经变成5了,所以此时每次访问的i值都是同一个i。所以输出的都是5。

若想要分别输出 0, 1, 2, 3, 4, 只能用let去声明i或是将内部改为立即执行函数。

/* 法一 */
for(let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    }, i * 1000);
}
/* 法二 */
for(var i = 0; i < 5; i++) {
    (function(j){
        setTimeout(() => {
            console.log(j);
        }, j * 1000);
    })(i)
}

闭包的应用场景

回调函数

回调函数是指将其他函数作为参数传递给另一个函数。

function forEach(arr, callback) {
  for (var i = 0; i < arr.length; i++) {
    var item = arr[i];
    callback(item);
  }
}
//这里面的function (item) {...}就是回调函数
forEach(arr, function (item) {
  console.log(item);
});

当我们要使用forEach()时,这个回调函数具有了forEach函数内部作用域的引用,就形成了闭包。

定义模块

闭包的应用比较典型是定义模块,我们将操作函数暴露给外部,而细节隐藏在模块内部:

function module() {
	var arr = [];
	function add(val) {
		if (typeof val == 'number') {
			arr.push(val);
		}
	}
	function get(index) {
		if (index < arr.length) {
			return arr[index]
		} else {
			return null;
		}
	}
	return {
		add: add,
		get: get
	}
}
var mod1 = module();
mod1.add(1);
mod1.add(2);
mod1.add('xxx');
console.log(mod1.get(2));

[参考文章]

深入理解JavaScript闭包之什么是闭包
面试 | JS 闭包经典使用场景和含闭包必刷题
闭包详解二:JavaScript中的高阶函数