这里有一份简洁的前端知识体系等待你查收,看看吧,会有惊喜哦~若是以为不错,恳求star哈~javascript
一段 JS 代码可能会包含函数调用的相关内容,你可能据说过不少概念,诸如闭包、做用域链、执行上下文、this值。前端
实际上,尽管它们是表示不一样的意思的术语,所指向的几乎是同一部分知识,那就是函数执行过程相关的知识。咱们能够简单看一下图。java
咱们先来说讲这个有点复杂的概念:闭包。git
在编程语言领域,闭包表示一种函数。github
在上世纪60年代,主流的编程语言是基于lambda演算的函数式编程语言,最初的闭包定义,是“带有一系列信息的λ表达式”。对函数式语言而言,λ表达式其实就是函数。编程
因此,闭包其实只是一个绑定了执行环境的函数,闭包与普通函数的区别是,它携带了执行的环境。浏览器
咱们来看下古典的闭包定义跟 JS 中的闭包定义,观察他们的区别。闭包
古典的闭包定义中,闭包包含两个部分:app
JS 中闭包组成部分:编程语言
有些人会把 JS 执行上下文,或者做用域(Scope,ES3中规定的执行上下文的一部分)这个概念看成闭包。实际上JS 中跟闭包对应的概念就是“函数”。
这里给闭包作个简单的定义:函数 A 内部有一个函数 B,函数 B 访问到函数 A 中的变量,那么函数 B 就是闭包。
咱们能够这样理解:
相比普通函数,JS 闭包的主要复杂性来自于它携带的“环境部分”。固然,发展到今天的 JS ,它所定义的环境部分,已经比当初经典的定义复杂了不少。
JS 中与闭包“环境部分”相对应的术语是“词法环境”,可是 JS 函数比λ函数要复杂得多,咱们还要处理this、变量声明、with等等一系列的复杂语法,λ函数中可没有这些东西,因此,在 JS 的设计中,词法环境只是 JS 执行上下文的一部分。
JS 标准把一段代码(包括函数),执行所需的全部信息定义为:“执行上下文”。
由于这部分术语经历了比较多的版本和社区的演绎,因此定义比较混乱,这里咱们先来理一下 JS 中的概念。
ES3
执行上下文在ES3中,包含三个部分。
注意:网上流传甚广的,用global object,和active object 来解释闭包、做用域、执行上下文,这是ES3里的解释法,如今已经解释不了不少语法了。
ES5
在ES5中,咱们改进了命名方式,把执行上下文最初的三个部分改成下面这个样子。
ES2018
在ES2018中,执行上下文又变成了这个样子,this值被纳入lexical environment,可是增长了很多内容。
咱们在这里介绍执行上下文的各个版本定义,是考虑到你可能会从各类网上的文章中接触这些概念,若是不把它们理清楚,咱们就很难分辨对错。若是是咱们本身使用,建议统一使用最新的ES2018中规定的术语定义。
接下来,咱们从代码实例出发,推导函数执行过程当中须要哪些信息,它们又对应着执行上下文中的哪些部分。
好比,咱们看如下的这段 JS 代码:
var b = {}
let c = 1
this.a = 2;
复制代码
要想正确执行它,咱们须要知道如下信息:
这些信息就须要执行上下文来给出了,这段代码出如今不一样的位置,甚至在每次执行中,会关联到不一样的执行上下文,因此,一样的代码会产生不同的行为。
这里咱们先讲var声明与赋值,let,realm三个特性来分析执行上下文中提供的信息。
咱们来分析一段代码:var b = 1;
一般咱们认为它声明了b,而且为它赋值为1,var声明做用域是函数执行的做用域。也就是说,var会穿透for 、if等语句。
在只有var,没有let的旧 JS 时代,诞生了一个技巧,叫作:当即执行的函数表达式(IIFE),经过建立一个函数,而且当即执行,来构造一个新的域,从而控制var的范围。
因为语法规定了function关键字开头是函数声明,因此要想让函数变成函数表达式,咱们必须得加点东西,最多见的作法是加括号。
(function(){
var a;
//code
}());
(function(){
var a;
//code
})();
复制代码
值得特别注意的是,有时候var的特性会致使声明的变量和被赋值的变量是两个b,JS 中有特例,那就是使用with的时候:
var b;
void function(){
var env = {b:1};
b = 2;
console.log("In function b:", b);
with(env) {
var b = 3;
console.log("In with b:", b);
}
}();
console.log("Global b:", b);
复制代码
在这个例子中,咱们利用当即执行的函数表达式(IIFE)构造了一个函数的执行环境,而且在里面使用了咱们一开头的代码。
能够看到,在Global、function、with三个环境中,b的值都不同,而在function环境中,并无出现var b
,这说明with内的var b
做用到了function这个环境当中。
var b = {}
这样一句对两个域产生了做用,从语言的角度是个很是糟糕的设计,这也是一些人坚决地反对在任何场景下使用with的缘由之一。
let是 ES6开始引入的新的变量声明模式,比起var的诸多弊病,let作了很是明确的梳理和规定。
为了实现let,JS 在运行时引入了块级做用域。也就是说,在let出现以前,JS 的 if 、for 等语句皆不产生做用域。
简单统计了下,如下语句会产生let使用的做用域:
在最新的标准(9.0)中,JS 引入了一个新概念Realm,它的中文意思是“国度”“领域”“范围”。这个英文的用法就有点比喻的意思,几个翻译都不太适合 JS 语境,因此这里就不翻译啦。
咱们继续来看这段代码:var b = {}
在 ES2016 以前的版本中,标准中甚少说起{}的原型问题。但在实际的前端开发中,经过iframe等方式建立多window环境并不是罕见的操做,因此,这才促成了新概念Realm的引入。
Realm中包含一组完整的内置对象,并且是复制关系。
对不一样Realm中的对象操做,会有一些须要格外注意的问题,好比 instanceOf 几乎是失效的。
如下代码展现了在浏览器环境中获取来自两个Realm的对象,它们跟本土的Object作instanceOf时会产生差别:
var iframe = document.createElement('iframe')
document.documentElement.appendChild(iframe)
iframe.src="javascript:var b = {};"
var b1 = iframe.contentWindow.b;
var b2 = {};
console.log(typeof b1, typeof b2); //object object
console.log(b1 instanceof Object, b2 instanceof Object); //false true
复制代码
能够看到,因为b一、 b2由一样的代码“ {} ”在不一样的Realm中执行,因此表现出了不一样的行为。
咱们再来讲下做用域,简单来讲做用域就是一个区域,包含了其中变量,常量,函数等等定义信息和赋值信息,以及这个区域内代码书写的结构信息。做用域能够嵌套。咱们一般知道 js 中函数的定义能够产生做用域,下面咱们用具体代码来示例下:
全局做用域(global scope)里面定义了两个变量,一个函数。walk 函数生成的做用域里面定义了一个变量,两个函数。innerFunc 和 anotherInnerFunc 这两个函数生成的做用域里面分别定义了一个变量。
在规范中做用域更官方的叫法是词法环境,没错,就是上文提到的词法环境,包含在执行上下文中。
做用域其实由两部分组成:
规范中定义了查找一个变量的过程:先查看当前做用域里面的 Environment Record 是否有此变量的信息,若是找到了,则返回当前做用域内的这个变量。若是没有查找到,则顺着 outer 到父做用域里面的 Environment Record 查找,以此递归。
因此咱们一般所说的函数内同名变量遮蔽全局变量就是这么回事。不过若是你在变量查找的时候指定某个做用域中的 Environment Record,那么也是能够的,譬如:window.name 【其实 window 对象就是全局做用域的 Environment Record 对象,可是普通函数做用域的 Environment Record 对象是获取不到的】。
执行上下文是用于跟踪代码的运行状况,而做用域用于获取变量或者this值。从职责上看,他们几乎是没有啥交集的。那么为啥一般二者会被同时提到呢?由于在一个函数被执行时,建立的执行上下文对象除了保存了些代码执行的信息,还会把当前的做用域保存在执行上下文中。因此它们的关系只是存储关系。
结合做用域和执行上下文,咱们再来看下变量查找的过程。其实第一步不是到做用域里面找 Environment Record,而是先从当前的执行上下文中找保存的做用域(对象),而后再是经过做用域链向上查找变量。
在这篇文章中,咱们梳理了一些概念:有编程语言的概念闭包,也有各个版本中的 JS 标准中的概念:执行上下文、做用域、this值等等。
以后咱们又从代码的角度,分析了一些执行上下文中所须要的信息,并从var、let、对象字面量等语法中,推导出了词法做用域、变量做用域、Realm的设计。
最后,咱们对比了执行上下文跟做用域的关系。