搜索
首页web前端js教程前端基础进阶(四):详细图解作用域链与闭包
前端基础进阶(四):详细图解作用域链与闭包Feb 16, 2017 pm 01:28 PM
作用域链前端基础进阶闭包

1.png

初学JavaScript的时候,我在学习闭包上,走了很多弯路。而这次重新回过头来对基础知识进行梳理,要讲清楚闭包,也是一个非常大的挑战。

闭包有多重要?如果你是初入前端的朋友,我没有办法直观的告诉你闭包在实际开发中的无处不在,但是我可以告诉你,前端面试,必问闭包。面试官们常常用对闭包的了解程度来判定面试者的基础水平,保守估计,10个前端面试者,至少5个都死在闭包上。

可是为什么,闭包如此重要,还是有那么多人没有搞清楚呢?是因为大家不愿意学习吗?还真不是,而是我们通过搜索找到的大部分讲解闭包的中文文章,都没有清晰明了的把闭包讲解清楚。要么浅尝辄止,要么高深莫测,要么干脆就直接乱说一通。包括我自己曾经也写过一篇关于闭包的总结,回头一看,不忍直视[捂脸]。

因此本文的目的就在于,能够清晰明了得把闭包说清楚,让读者老爷们看了之后,就把闭包给彻底学会了,而不是似懂非懂。


一、作用域与作用域链

在详细讲解作用域链之前,我默认你已经大概明白了JavaScript中的下面这些重要概念。这些概念将会非常有帮助。

1.基础数据类型与引用数据类型

2.内存空间

3.垃圾回收机制

4.执行上下文

5.变量对象与活动对象

如果你暂时还没有明白,可以去看本系列的前三篇文章,本文文末有目录链接。为了讲解闭包,我已经为大家做好了基础知识的铺垫。哈哈,真是好大一出戏。

作用域

1.在JavaScript中,我们可以将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。

这里的标识符,指的是变量名或者函数名

2.JavaScript中只有全局作用域与函数作用域(因为eval我们平时开发中几乎不会用到它,这里不讨论)。

3.作用域与执行上下文是完全不同的两个概念。我知道很多人会混淆他们,但是一定要仔细区分。

JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。

2.png

作用域链

回顾一下上一篇文章我们分析的执行上下文的生命周期,如下图。

3.png

我们发现,作用域链是在执行上下文的创建阶段生成的。这个就奇怪了。上面我们刚刚说作用域在编译阶段确定规则,可是为什么作用域链却在执行阶段确定呢?

之所有有这个疑问,是因为大家对作用域和作用域链有一个误解。我们上面说了,作用域是一套规则,那么作用域链是什么呢?是这套规则的具体实现。所以这就是作用域与作用域链的关系,相信大家都应该明白了吧。

我们知道函数在调用激活时,会开始创建对应的执行上下文,在执行上下文生成的过程中,变量对象,作用域链,以及this的值会分别被确定。之前一篇文章我们详细说明了变量对象,而这里,我们将详细说明作用域链。

作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。

为了帮助大家理解作用域链,我我们先结合一个例子,以及相应的图示来说明。

var a = 20;

function test() {
    var b = a + 10;

    function innerTest() {
        var c = 10;
        return b + c;
    }

    return innerTest();
}

test();

在上面的例子中,全局,函数test,函数innerTest的执行上下文先后创建。我们设定他们的变量对象分别为VO(global),VO(test), VO(innerTest)。而innerTest的作用域链,则同时包含了这三个变量对象,所以innerTest的执行上下文可如下表示。

innerTestEC = {
    VO: {...},  // 变量对象
    scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链
    this: {}
}

是的,你没有看错,我们可以直接用一个数组来表示作用域链,数组的第一项scopeChain[0]为作用域链的最前端,而数组的最后一项,为作用域链的最末端,所有的最末端都为全局变量对象。

很多人会误解为当前作用域与上层作用域为包含关系,但其实并不是。以最前端为起点,最末端为终点的单方向通道我认为是更加贴切的形容。如图。

4.png

注意,因为变量对象在执行上下文进入执行阶段时,就变成了活动对象,这一点在上一篇文章中已经讲过,因此图中使用了AO来表示。Active Object

是的,作用域链是由一系列变量对象组成,我们可以在这个单向通道中,查询变量对象中的标识符,这样就可以访问到上一层作用域中的变量了。


二、闭包

对于那些有一点 JavaScript 使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生,突破闭包的瓶颈可以使你功力大增。

先直截了当的抛出闭包的定义:当一个函数可以记住并访问所在的作用域(全局作用域除外),并在定义该函数的作用域之外执行时,该函数就可以称之为一个闭包。

简单来说,假设函数A在函数B的内部进行定义了,并在函数B的作用域之外执行(不管是上层作用域,下层作用域,还有其他作用域),那么A就是一个闭包。记住这个定义,你在其他地方很难看到了。

在基础进阶(一)中,我总结了JavaScript的垃圾回收机制。JavaScript拥有自动的垃圾回收机制,关于垃圾回收机制,有一个重要的行为,那就是,当一个值,在内存中失去引用时,垃圾回收机制会根据特殊的算法找到它,并将其回收,释放内存。

而我们知道,函数的执行上下文,在执行完毕之后,生命周期结束,那么该函数的执行上下文就会失去引用。其占用的内存空间很快就会被垃圾回收器释放。可是闭包的存在,会阻止这一过程。

先来一个简单的例子。

var fn = null;
function foo() {
    var a = 2;
    function innnerFoo() { 
        console.log(a);
    }
    fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}

function bar() {
    fn(); // 此处的保留的innerFoo的引用
}

foo();
bar(); // 2

在上面的例子中,foo()执行完毕之后,按照常理,其执行环境生命周期会结束,所占内存被垃圾收集器释放。但是通过fn = innerFoo,函数innerFoo的引用被保留了下来,复制给了全局变量fn。这个行为,导致了foo的变量对象,也被保留了下来。于是,函数fn在函数bar内部执行时,依然可以访问这个被保留下来的变量对象。所以此刻仍然能够访问到变量a的值。

这样,我们就可以称fn为闭包。

下图展示了闭包fn的作用域链。

5.png

所以,通过闭包,我们可以在其他的执行上下文中,访问到函数的内部变量。比如在上面的例子中,我们在函数bar的执行环境中访问到了函数foo的a变量。个人认为,从应用层面,这是闭包最重要的特性。利用这个特性,我们可以实现很多有意思的东西。

通过闭包,我们可以访问到函数的内部变量。这是闭包的一种特性,但是由于在其他很多地方,被用来当成闭包的定义,这其实是不准确的。

不过读者老爷们需要注意的是,虽然例子中的闭包被保存在了全局变量中,但是闭包的作用域链并不会发生任何改变。在闭包中,能访问到的变量,仍然是作用域链上能够查询到的变量。

对上面的例子稍作修改,如果我们在函数bar中声明一个变量c,并在闭包fn中试图访问该变量,运行结果会抛出错误。

var fn = null;
function foo() {
    var a = 2;
    function innnerFoo() { 
        console.log(c); // 在这里,试图访问函数bar中的c变量,会抛出错误
        console.log(a);
    }
    fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}

function bar() {
    var c = 100;
    fn(); // 此处的保留的innerFoo的引用
}

foo();
bar();

上面的例子,可以很直观的感受到闭包的存在,但是还有一种情况的闭包,则更加隐蔽难以感受。我们来看一个例子。

function test() {
    function bar (str) {
        console.log(str);
    }

    function foo (fn, string) {
        fn(string);
    }

    foo(bar, 'this is closure');
}


test();

这个例子中,函数bar在函数test的作用域中定义,然后被作为参数传入了函数foo中并在foo的作用域中被执行。根据定义,我们很容易知道函数bar就是一个闭包。因为其隐蔽性,很多人并没有意识到这就是一个闭包。这种情况,就是我们常常说的回调函数。在实际开发中,我们遇到的大多数回调函数都是闭包。

很多时候,回调函数都是匿名函数,但是要注意的是,在其他一些语言中,闭包与匿名函数是有区别的,但是JavaScript在实现匿名函数的时候允许形成闭包,当匿名函数作为参数传入函数中时,匿名函数的引用会保存在改函数变量对象的arguments对象中。因此在JavaScript中,我们可以不用那么严格的区别闭包与匿名函数。

闭包的应用场景

接下来,我们来总结下,闭包的常用场景。

延迟函数setTimeout

我们知道setTimeout的第一个参数是一个函数,第二个参数则是延迟的时间。在下面例子中,

function fn() {
    console.log('this is test.')
}
var timer =  setTimeout(fn, 1000);
console.log(timer);

执行上面的代码,变量timer的值,会立即输出出来,表示setTimeout这个函数本身已经执行完毕了。但是一秒钟之后,fn才会被执行。这是为什么?

按道理来说,既然fn被作为参数传入了setTimeout中,那么fn将会被保存在setTimeout变量对象中,setTimeout执行完毕之后,它的变量对象也就不存在了。可是事实上并不是这样。至少在这一秒钟的事件里,它仍然是存在的。这正是因为闭包。

很显然,这是在函数的内部实现中,setTimeout通过特殊的方式,保留了fn的引用,让setTimeout的变量对象,并没有在其执行完毕后被垃圾收集器回收。因此setTimeout执行结束后一秒,我们任然能够执行fn函数。

回调函数

在上面的例子中,我们已经解释过了回调函数。所以就不再多说。

柯里化

在函数式编程中,利用闭包能够实现很多炫酷的功能,柯里化算是其中一种。关于柯里化,我会在以后详解函数式编程的时候仔细总结。

模块

在我看来,模块是闭包最强大的一个应用场景。如果你是初学者,对于模块的了解可以暂时不用放在心上,因为理解模块需要更多的基础知识。但是如果你已经有了很多JavaScript的使用经验,在彻底了解了闭包之后,不妨借助本文介绍的作用域链与闭包的思路,重新理一理关于模块的知识。这对于我们理解各种各样的设计模式具有莫大的帮助。

(function () {
    var a = 10;
    var b = 20;

    function add(num1, num2) {
        var num1 = !!num1 ? num1 : a;
        var num2 = !!num2 ? num2 : b;

        return num1 + num2;
    }

    window.add = add;
})();

在上面的例子中,我使用函数自执行的方式,创建了一个模块。方法add被作为一个闭包,对外暴露了一个公共方法。而变量a,b被作为私有变量。在面向对象的开发中,我们常常需要考虑是将变量作为私有变量,还是放在构造函数中的this中,因此理解闭包,以及原型链是一个非常重要的事情。模块十分重要,因此我会在以后的文章专门介绍,这里就暂时不多说啦。

为了验证自己有没有搞懂作用域链与闭包,这里留下一个经典的思考题,常常也会在面试中被问到。

利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5

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

关于作用域链的与闭包我就总结完了,虽然我自认为我是说得非常清晰了,但是我知道理解闭包并不是一件简单的事情,所以如果你有什么问题,可以在评论中问我,留言必回。你也可以带着从别的地方没有看懂的例子在评论中留言。大家一起学习进步。

声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
5个常见的JavaScript内存错误5个常见的JavaScript内存错误Aug 25, 2022 am 10:27 AM

JavaScript 不提供任何内存管理操作。相反,内存由 JavaScript VM 通过内存回收过程管理,该过程称为垃圾收集。

巧用CSS实现各种奇形怪状按钮(附代码)巧用CSS实现各种奇形怪状按钮(附代码)Jul 19, 2022 am 11:28 AM

本篇文章带大家看看怎么使用 CSS 轻松实现高频出现的各类奇形怪状按钮,希望对大家有所帮助!

Node.js 19正式发布,聊聊它的 6 大特性!Node.js 19正式发布,聊聊它的 6 大特性!Nov 16, 2022 pm 08:34 PM

Node 19已正式发布,下面本篇文章就来带大家详解了解一下Node.js 19的 6 大特性,希望对大家有所帮助!

实战:vscode中开发一个支持vue文件跳转到定义的插件实战:vscode中开发一个支持vue文件跳转到定义的插件Nov 16, 2022 pm 08:43 PM

vscode自身是支持vue文件组件跳转到定义的,但是支持的力度是非常弱的。我们在vue-cli的配置的下,可以写很多灵活的用法,这样可以提升我们的生产效率。但是正是这些灵活的写法,导致了vscode自身提供的功能无法支持跳转到文件定义。为了兼容这些灵活的写法,提高工作效率,所以写了一个vscode支持vue文件跳转到定义的插件。

浅析Vue3动态组件怎么进行异常处理浅析Vue3动态组件怎么进行异常处理Dec 02, 2022 pm 09:11 PM

Vue3动态组件怎么进行异常处理?下面本篇文章带大家聊聊Vue3 动态组件异常处理的方法,希望对大家有所帮助!

聊聊如何选择一个最好的Node.js Docker镜像?聊聊如何选择一个最好的Node.js Docker镜像?Dec 13, 2022 pm 08:00 PM

选择一个Node​的Docker镜像看起来像是一件小事,但是镜像的大小和潜在漏洞可能会对你的CI/CD流程和安全造成重大的影响。那我们如何选择一个最好Node.js Docker镜像呢?

聊聊Node.js中的 GC (垃圾回收)机制聊聊Node.js中的 GC (垃圾回收)机制Nov 29, 2022 pm 08:44 PM

Node.js 是如何做 GC (垃圾回收)的?下面本篇文章就来带大家了解一下。

【6大类】实用的前端处理文件的工具库,快来收藏吧!【6大类】实用的前端处理文件的工具库,快来收藏吧!Jul 15, 2022 pm 02:58 PM

本篇文章给大家整理和分享几个前端文件处理相关的实用工具库,共分成6大类一一介绍给大家,希望对大家有所帮助。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
2 周前By尊渡假赌尊渡假赌尊渡假赌
仓库:如何复兴队友
4 周前By尊渡假赌尊渡假赌尊渡假赌
Hello Kitty Island冒险:如何获得巨型种子
3 周前By尊渡假赌尊渡假赌尊渡假赌

热工具

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

功能强大的PHP集成开发环境

适用于 Eclipse 的 SAP NetWeaver 服务器适配器

适用于 Eclipse 的 SAP NetWeaver 服务器适配器

将Eclipse与SAP NetWeaver应用服务器集成。

EditPlus 中文破解版

EditPlus 中文破解版

体积小,语法高亮,不支持代码提示功能

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一个PHP/MySQL的Web应用程序,非常容易受到攻击。它的主要目标是成为安全专业人员在合法环境中测试自己的技能和工具的辅助工具,帮助Web开发人员更好地理解保护Web应用程序的过程,并帮助教师/学生在课堂环境中教授/学习Web应用程序安全。DVWA的目标是通过简单直接的界面练习一些最常见的Web漏洞,难度各不相同。请注意,该软件中

Atom编辑器mac版下载

Atom编辑器mac版下载

最流行的的开源编辑器