JavaScript中的执行环境、做用域、做用域链、闭包一直是一个很是有意思的话题,不少博主和大神都分享过相关的文章。这些知识点不只比较抽象,不易理解,更重要的是与这些知识点相关的问题在面试中高频出现。以前我也看过很多文章,依旧是似懂非懂,模模糊糊。最近,仔细捋了捋相关问题的思路,对这些问题的理解清晰深刻了很多,在这里和你们分享。
本文已同步至个人我的主页。欢迎访问查看更多内容!若有错误或遗漏,欢迎随时指正探讨!谢谢你们的关注与支持!前端
这篇文章,我会按照执行环境、做用域、做用域链、闭包的顺序,结合着JS中函数的运行机制来梳理相关知识。由于这样的顺序恰好也是这些知识点相互关联且递进的顺序,同时这些知识点都又与函数有着千丝万缕的联系。这样讲解,会更容易让你们完全理解,至少我就是这样理解清晰的。git
废话再也不多说,咱们开始。github
首先,咱们仍是要理解一下什么是执行环境,这也是理清后面问题的基础。web
执行环境是JavaScript中最为重要的一个概念。执行环境定义了变量或函数有权访问的其余数据,决定了它们各自的行为。——《JavaScript高级程序设计》
抽象!不理解!不要紧,我来解释:其实,执行环境就是JS中提出的一个概念,它是为了保证代码合理运行采用的一种机制。面试
一种概念...机制...更抽象,那它究竟是什么?实际上,执行环境在JS机制内部就是用一个对象来表示的
,称做执行环境对象
,简称环境对象
。数组
那么,这个执行环境对象到底又是什么时候、怎么产生的呢?有如下两种状况:浏览器
Window
对象。所以,执行环境就分为全局执行环境
和局部执行环境
两种,每一个执行环境都有一个属于本身的环境对象。闭包
既然执行环境是使用一个对象表示的,那么对象就有属性。咱们来看看环境对象的三个有意思的属性。变量对象
、[[scope]]
、this
。函数
《JS高程》中明确说明,执行环境定义了变量或函数有权访问的其余数据。那么这些数据到底被放(存储)在哪里呢?性能
其实,每一个执行环境都有一个与之关联的变量对象,在环境中定义的全部变量和函数都保存在这个对象中。咱们在代码没法访问这个对象,但解析器在处理数据时会在内部使用它。
通俗地说就是:一个执行环境中的全部变量和函数都保存在它对应的环境对象的变量对象(属性)中。
在讲[[scope]]
前,咱们就须要先弄清楚什么是做用域了。由于做用域与[[scope]]
之间存在着很是紧密的关系。
《JS高程》中没有明确给出做用域的定义和描述。其实,做用域就是变量或者函数能够被访问的代码范围,或者说做用域就是变量和函数所起做用的范围。
这样看来做用域也像是一个概念,它是用来描述一个区域(或者说范围)的。在JS中,做用域分为全局做用域
、局部做用域
两种。
咱们来看看这两种做用域的具体描述:
①在页面中的脚本开始执行时,就会产生一个“全局做用域”。它是最外围(范围最大,或者说层级最高)的一个做用域。全局做用域的变量、函数
能够在代码的任何地方访问到。
②当一个函数被建立的时候,会建立一个“局部做用域”。局部做用域中的函数、变量只能在某些局部代码中能够访问到。
看一个例子:
var g = 'Global'; function outer() { var out = 'outer'; function inner() { var inn = 'inner'; } }
上面这个例子,产生的做用域就以下图所示:
请注意上面①、②这两段话!!!是否是以为很熟悉,似曾相识?!没错,这两段话和介绍全局/局部执行环境(全局/局部环境对象)时候的描述几乎一摸同样!做用域是否是和环境对象有着千丝万缕的联系呢?与此同时,咱们再仔细回忆一下:一、做用域就是变量或者函数能够被访问的代码范围。二、一个执行环境中定义的全部变量和函数都保存在它对应的环境对象中。
结合上面所述,其实不可贵出:尽管做用域的描述更像是一个概念,但若是必定要将它具象化,问它究竟是什么东西,与执行环境有什么关系?其实,做用域所对应的(不是相等、等于)是环境对象中的变量对象。
明白了这些,咱们就能够来看看环境对象中的[[scope]]
属性。
首先,要明确的是,环境对象中的[[scope]]
属性值是一个指针,它指向该执行环境的做用域链。
到底什么是做用域链呢?做用域链本质上就是一个有序的列表
,而列表中的每一项都是一个指向不一样环境对象中的变量对象的指针
。
那么,这个做用域链究竟是怎么造成的呢?它里面指向变量对象的指针的顺序又是如何规定的呢?咱们用下面这个简单的例子说明。
var g = 'Hello'; function inner() { var inn = 'Inner'; var res = g + inn; return res; } inner();
当执行了inner();
这一行代码后,代码执行流进入inner
函数内部,此时,JS内部会先建立inner
函数的局部执行环境,而后建立该环境的做用域链。这个做用域链的最前端,就是inner
执行环境本身的环境对象中的变量对象
,做用域链第二项,就是全局环境的环境对象中的变量对象
。这条做用域链以下图所示:
造成了这样的做用域链以后,就能够有秩序地访问一个变量了。以这个例子为例:当执行inner();
进入函数体内后,执行g + inn;
一行,须要访问变量g、inn
,此时JS内部机制就会沿着这条做用域链查找所需变量。在当前inner
函数的做用域中找到了变量inn
,值为'Inner'
,查找终止。可是却没有找到变量g
,因而沿着做用域链向上查找,进入全局做用域,在全局变量对象中找到了变量g
,值为'Hello'
,查找终止。计算得出res
为'HelloInner'
,并在最后返回结果。
与上面所讲机制彻底相同,若是是多层执行环境嵌套,则做用域链是这么造成的:
当代码执行进入一个执行环境时,JS内部会开始建立该环境的做用域链。做用域链的 最前端,始终都是 当前执行环境的执行环境对象中的变量对象。若是这个环境是 局部执行环境(函数执行环境),则将其 活动对象做为 变量对象。做用域链中的下一个是来自 外层环境对象的变量对象,而再下一个则是来自 再外层环境对象的变量对象...... 这样 一直延续到全局环境对象的变量对象。因此,全局执行环境的变量对象始终都是做用域链中的最后一个对象。
讲到这里,可能你已经对执行环境、执行环境对象、变量对象、做用域、做用域链的理解已经他们之间的关系有了一个较清晰的认识。也有可能,对这么多的抽象问题仍是有些懵懵懂懂。不要紧,咱们用下面这一张图,将上面的全部内容串联起来,来直观感觉和理解他们。
var g = 'Global'; function outer() { var out = 'outer'; function inner() { var inn = 'inner'; } inner(); } outer();
对于这张图,有一些须要注意的地方:
outer
、inner
的变量对象实际上是该函数的活动对象。全局环境是没有活动对象的,只有在函数环境中,才会使用函数的活动对象来做为它的变量对象。arguments
类数组和其余命名参数来初始化的。因此实际上,函数的变量对象中应该还包含一个指向arguments
类数组的指针。有了对做用域、做用域链的理解,最后,咱们来讲一说闭包。
闭包就是有权访问另外一个函数做用域中的变量的函数。——《JavaScript高级程序设计》
对于闭包,最简单的大白话能够这么理解:
①外部函数声明内部函数,内部函数引用外部函数的局部变量,这些变量不会被释放!——这是我曾经看到的别人的说法
或者这么理解:
②当在一个函数中返回另外一个函数的时候(是返回一个函数,不是返回函数的调用或者函数的执行结果),就会造成闭包,被返回的这个函数就叫作闭包函数。——这是我本身的理解
上面两句话看似不一样,其实本质是同样的。来看一个最简单的闭包的例子:
function sum() { var num1 = 100; // 这里返回的是函数(体),不是函数的调用 return function(num2) { return num1 + num2; } } // 此时result指向sum返回的那个匿名函数 // 注意!此时该匿名函数并无被执行 let result = sum(); result(200);
那么,上面几行代码,为何就会造成闭包呢?咱们来分析一下,代码执行中JS内部到底作了什么?
首先,有一点必须明确,就是通常状况下,一个函数执行完内部的代码,函数调用时所建立的执行环境、环境对象(包括变量对象、[[scope]]等)都会被销毁,它们的生命周期就只有函数调用到函数执行结束这一段时间。
可是上面的例子,就会出现例外。
当执行sum()
时,调用该函数,建立它的环境对象,其中做用域链中第一项是本身环境的变量对象,第二项是全局环境的变量对象。当建立匿名函数的时候,也会建立匿名函数的环境对象,其中做用域链第一项是本身环境的变量对象,第二项是sum
环境的变量对象,第三项是全局变量对象。
这时,问题就来了。按说,当函数sum
执行完return
以后,他的执行环境、变量对象、做用域链都会被销毁。但是这时候却不能销毁他的变量对象,由于返回的匿名函数(此时由result
指向该函数)并无执行,这个匿名函数的做用域链中还引用着sum
函数的变量对象。换句话说,即便,sum
执行完了,其执行环境的做用域链会被销毁,可是它的变量对象还会保存在内存中,咱们在sum
函数外部,还能访问到它内部的变量num1
、num2
,这就是造成闭包的真正缘由。可是,当result()执行完后,这些变量对象、做用域链就会被销毁。
由于闭包造成后,会在函数执行完仍将他的变量对象保存在内存中,当引用时间过长或者引用对象不少的时候,会占用大量内存,严重影响性能。
来看下面的例子:(这个例子曾经是Tencent微众银行的笔试原题,出如今《JS高程》的7.2.3章节——P184)
function assignHandler() { var element = document.getElementById("someElement"); element.onclick = function(){ alert(element.id); }; }
assignHandler
函数中定义的匿名函数是做为element
元素的事件处理函数的,且内部使用了element
元素(访问元素的id
`),所以assignHandler
函数执行完,对于element
的引用也会一直存在,element
元素会一直保存在内存中。
将上面的例子改为下面这样,就能解决这个问题。
function assignHandler(){ var element = document.getElementById("someElement"); // 这里获取element的id,为其建立一个副本 // 这样是为了在下面事件处理函数中解除对element元素的引用 var id = element.id; element.onclick = function(){ alert(id); }; // 将element置为null,断开对element元素的引用 // 这样方便垃圾回收机制回收element所占的内存 element = null; }