闭包全面详解

砾滩超类
• 阅读 3478

什么是闭包

补充:闭包形成的本质是内层作用域中堆地址暴露,原因是局部作用域中访问了其他局部变量?

最原始定义

闭包(closure),是指函数变量可以保存在函数作用域内,因此看起来是函数将变量“包裹”了起来。

//根据定义,包含变量的函数就是闭包
function foo() {
    var a = 0;
}
cosole.log(a) 
// Uncaught ReferenceError: a is not defined

《JavaScript高级程序设计》对闭包定义

闭包是指有权访问另一个函数作用域中的变量的函数

 //访问上层函数的作用域的内层函数就是闭包
function foo() {
    var a = 2;
    function bar() {
        console.log(a);
    }
    bar();
}
foo();

《JavaScript权威指南》对闭包定义

函数对象可以通过作用域链相互关联起来,函数体内部变量可以保存在函数作用域内,这就是闭包。

 var global = "global scope"; //全局变量
function checkscope() {
    var scope = "local scope"; //局部变量
    function f() {
        return scope; //在作用域中返回这个值
    };
    return f();
}
checkscope(); // 返回 "local scope"

《你不知道的JavaScript》这样描述

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

//fn3就是fn2函数本身。执行fn3能正常输出name
//这不就是fn2能记住并访问它所在的词法作用域,而且fn2函数的运行还是在当前词法作用域之外了。
function fn1() {
    var name = 'iceman';
    function fn2() {
        console.log(name);
    }
    return fn2;
}
var fn3 = fn1();
fn3();

MDN 上面这么说:

闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。
简单说就是指那些能够访问自由变量的函数。

严格来说,闭包需要满足三个条件:

【1】访问所在作用域;
【2】函数嵌套;
【3】在所在作用域外被调用

闭包的形成原理

先了解JavaScript的垃圾回收机制

Javascript 会找出不再使用的变量,不再使用意味着这个变量生命周期的结束。
Javascript 中存在两种变量——全局变量和局部变量,全部变量的声明周期会一直持续,直到页面卸载而局部变量声明在函数中,它的声明周期从执行函数开始,直到函数执行结束。在这个过程中,局部变量会在堆或栈上被分配相应的空间以存储它们的值,函数执行结束,这些局部变量也不再被使用,它们所占用的空间也就被释放。
但是有一种情况的局部变量不会随着函数的结束而被回收,那就是局部变量被函数外部的变量所使用,其中一种情况就是闭包,因为在函数执行结束后,函数外部的变量依然指向函数内的局部变量,此时的局部变量依然在被使用,所以也就不能够被回收

var scope = 'global scope';
function checkScope() {
    var scope = 'local scope';
    return function() {
        console.log(scope);
    }
}

var result = checkScope(); 
result();   // local scope checkScope变量对象中的scope,非全局变量scope

此匿名函数的作用域链包括checkScope的活动对象和全局变量对象, 当checkScope函数执行完毕后,checkScope的活动对象并不会被销毁,因为匿名函数的作用域链还在引用checkScope的活动对象,也就是checkScope的执行环境被销毁,但是其活动对象没有被销毁,留存在堆内存中,直到匿名函数销毁后,checkScope的活动对象才会销毁

从作用域链理解闭包的形成

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  2. 从实践角度:以下函数才算是闭包:

i. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
ii. 在代码中引用了自由变量

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo();

fContext = {//f函数的执行上下文
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

对的,就是因为这个作用域链,f函数在声明的时候压入了上层的变量对象,f 函数依然可以读取到 checkscopeContext.AO 的值,并且如果当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中(和垃圾回收机制有关下文会说),f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

闭包的作用-模仿块级作用域,封装私有变量

任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。
私有变量包括函数的参数、局部变量和函数内定义的其他函数。
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));//外部是无法直接拿到arr的只能通过get来拿

闭包的作用-使变量保存在内存中不被销毁

实例1-计数器

我们来实现一个计数器,每调用一次计数器返回值加一:

var counter = 0;
function add() {
   return counter += 1;
}
add();
add();
add();// 计数器现在为 3 

问题:

  • 全局变量容易被其他代码改变
  • 如果我需要同时用两个计数器,但这种写法只能满足一个使用,另一个还想用的话就要再写个counter2函数,再定义一个counter2的全局变量。

那我们把counter放在add函数里面不就好了么?

function add() {
    var counter = 0;
    return counter += 1;
} 
add();
add();
add();// 本意是想输出 3, 但输出的都是 1

所以这样做的话,每次调用add函数,counter的值都要被初始化为0,还是达不到我们的目的。

使用闭包来写就会解决这些问题

function add() {
    var index = 1;
    function counter() {
        return index ++;
    }
    return counter;
}

// test
var addA = add() ;
var addB = add() ;
addA();        // 1
addA();        // 2
addB();        // 1
addB();        // 2

实例2-延时打印

这样打印出来的全部都是10,原因是for循环是同步的会在延时1000毫秒的过程中一直执行
等function执行的时候变量i指向的是同一个内存地址,且值已经变成的10

for (var i = 1; i <= 10; i++) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

改进,用自执行的函数创建简单的闭包,让每一次for循环的i都在不同的内存地址中且不被销毁

for (var i = 1; i <= 10; i++) {
    (function () {
        var j = i;
        setTimeout(function () {
            console.log(j);
        }, 1000);
    })();
}

错误写法:

    for (var i = 1; i <= 10; i++) {
    (function () {
        setTimeout(function () {
        // 这样虽然也是闭包,但没有缓存i的值到作用域
        // 导致1000ms到时,然后还是去外层作用域链找i的值,已经变成了11
            console.log(i);
        }, 1000);
    })();
}

优化写法:

for (var i = 1; i <= 10; i++) {
    (function (j) {
        setTimeout(function () {
            console.log(j);
        }, 1000);
    })(i);
}

ES6写法:

for (let i = 1; i <= 10; i++) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

for循环里面用let,()会生成一个隐藏作用域,产生多个不同的块级作用域,类似于闭包,如下:

// 伪代码
(let i = 0) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

(let i = 1) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

(let i = 2) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
};
......
......

联系Static静态变量

闭包的作用主要就是让变量的值始终保持在内存中。
C++或C语言还有Java中都有static静态变量也是让变量始终保存在内存中。
这样来看好像闭包好像有点static静态变量的意思。

总结

闭包就是子函数可以有权访问父函数的变量、父函数的父函数的变量、一直到全局变量。
归根结底,就是利用js得词法(静态)作用域,即作用域链在函数创建的时候就确定了。
子函数如果不被销毁,整条作用域链上的变量仍然保存在内存中,这样就形成了闭包

点赞
收藏
评论区
推荐文章
Souleigh ✨ Souleigh ✨
4年前
JS - 从执行上下文的角度来理解闭包
今天看到一篇关于闭包的文章,里面有这样一句话“就我而言对于闭包的理解仅止步于一些概念,看到相关代码知道这是个闭包,但闭包能解决哪些问题场景我了解的并不多”,这说的不就是我么,每每在面试中被问及什么是闭包,大部分情况下得到的答复是(至少我以前是)A函数嵌套B函数,B函数使用了A函数的内部变量,且A函数返回B函数,这就是闭包。而往往面试官想要听到的并不是这样的
Dax Dax
4年前
JS核心原理理解闭包
前置概念在正式看闭包之前,我们先来学习一下前置知识,那就是JS中的作用域,我们知道,在ES5之中,作用域分为两种:全局作用域和函数作用域,随着ES6的到来,新增了块级作用域,想更好的理解闭包,那么搞清楚作用域是首要条件全局作用域我们知道,对于变量而言,我们一般会分成两类:全局变量和局部变量,一般定义在最外围环境的为全局变量,定义在函数当中的为局部变量,在we
Jacquelyn38 Jacquelyn38
4年前
你不可不知的JS面试题(第三期)
1、什么是闭包?如图所示,闭包就是一个定义在函数内部的函数,其作用是将函数内部和函数外部连接起来。大家知道,作用域的问题,就是在函数内部定义的变量称为局部变量,外部取不到值。下面我们通过代码来更加详细地看一下:function A()       let x  1;       return function B()           c
菜园前端 菜园前端
2年前
一篇文章教会你什么是闭包
原文链接:什么是闭包?闭包的概念并不复杂,但是它的定义比较绕(就像平时经常用到它,却又说不出来是什么)。可以在一个作用域中调用函数的内部函数并访问到该函数中的作用域的成员,这就是闭包。给一个建议,网上闭包的概念可以搜出来一大堆,但是你真的了解它吗?你有去调
Bill78 Bill78
4年前
Python 中的闭包
闭包定义:如果在一个内部函数里,对在外部作用于(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包Python中的闭包原文出处:田小计划(http://www.cnblogs.com/wilber2013/p/4658894.html)闭包(closure)是函数式编程的重要的语法结构
Wesley13 Wesley13
3年前
JS 闭包(内存溢出与内存泄漏)(垃圾回收机制)
1.有关闭包定义闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量闭包的特性:函数内再嵌套函数内部函数可以引用外层的参数和变量参数和变量不会被垃圾回收机制回收
Easter79 Easter79
3年前
Swift讲解专题八——闭包
Swift讲解专题八——闭包一、引言      Swift中的闭包是有一定功能的代码块,这十分类似于ObjectiveC中的block语法。Swift中的闭包语法风格十分简洁,其作用和函数的作用相似。二、从一个系统函数看闭包      Swift标准函数库中提供了一个sort排序函数,对
Stella981 Stella981
3年前
JavaScript函数——闭包
闭包概念只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁例子functionouter(){varlocalVal30;returnlocalVal;}
Wesley13 Wesley13
3年前
go 学习笔记之10 分钟简要理解 go 语言闭包技术
闭包是主流编程语言中的一种通用技术,常常和函数式编程进行强强联合,本文主要是介绍Go语言中什么是闭包以及怎么理解闭包.如果读者对于Go语言的闭包还不是特别清楚的话,可以参考上一篇文章go学习笔记之仅仅需要一个示例就能讲清楚什么闭包(https://www.oschina.net/ac
Easter79 Easter79
3年前
Swift3.0 闭包(blcok)的全面介绍及使用
闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift中的闭包与C和ObjectiveC中的代码块(blocks)以及其他一些编程语言中的匿名函数比较相似。闭包可以捕获和存储其所在上下文中任意常量和变量的引用。被称为包裹常量和变量。Swift会为你管理在捕获过程中涉及到的所有内存操作。闭包表达式语法有如下的一般形式:
Stella981 Stella981
3年前
Golang闭包案例分析与普通函数对比
闭包案例packagemainimport("fmt""strings"//记住一定引入strings包)//①编写一个函数makeSuffix,可以接收一个文件后缀名(比如.jpg),并返回一个闭包//②调用闭包,可以传入一个文件名,如果该文件名没有指