闭包和执行上下文

这里有一份简洁的前端知识体系等待你查收,看看吧,会有惊喜哦~若是以为不错,恳求star哈~javascript


概述

一段 JS 代码可能会包含函数调用的相关内容,你可能据说过不少概念,诸如闭包、做用域链、执行上下文、this值。前端

实际上,尽管它们是表示不一样的意思的术语,所指向的几乎是同一部分知识,那就是函数执行过程相关的知识。咱们能够简单看一下图。java



咱们先来说讲这个有点复杂的概念:闭包。git


闭包

在编程语言领域,闭包表示一种函数。github

在上世纪60年代,主流的编程语言是基于lambda演算的函数式编程语言,最初的闭包定义,是“带有一系列信息的λ表达式”。对函数式语言而言,λ表达式其实就是函数。编程

因此,闭包其实只是一个绑定了执行环境的函数,闭包与普通函数的区别是,它携带了执行的环境。浏览器

咱们来看下古典的闭包定义跟 JS 中的闭包定义,观察他们的区别。闭包

古典的闭包定义中,闭包包含两个部分:app

  1. 环境部分
    • 环境
    • 标识符列表
  2. 表达式部分

JS 中闭包组成部分:编程语言

  1. 环境部分
    • 环境:函数的词法环境(执行上下文的一部分)
    • 标识符列表:函数中用到的未声明的变量(也就是函数里不带var/let/const的变量)
  2. 表达式部分:函数体

有些人会把 JS 执行上下文,或者做用域(Scope,ES3中规定的执行上下文的一部分)这个概念看成闭包。实际上JS 中跟闭包对应的概念就是“函数”。

这里给闭包作个简单的定义:函数 A 内部有一个函数 B,函数 B 访问到函数 A 中的变量,那么函数 B 就是闭包。

咱们能够这样理解:

  1. 首先,函数B绑定了函数A的语法环境,该闭包无论在何处声明,函数B绑定的环境都不会改变。
  2. 其次,函数B用到了未声明的变量,这些变量来自函数A。

执行上下文:执行的基础设施

相比普通函数,JS 闭包的主要复杂性来自于它携带的“环境部分”。固然,发展到今天的 JS ,它所定义的环境部分,已经比当初经典的定义复杂了不少。

JS 中与闭包“环境部分”相对应的术语是“词法环境”,可是 JS 函数比λ函数要复杂得多,咱们还要处理this、变量声明、with等等一系列的复杂语法,λ函数中可没有这些东西,因此,在 JS 的设计中,词法环境只是 JS 执行上下文的一部分。

JS 标准把一段代码(包括函数),执行所需的全部信息定义为:“执行上下文”。

由于这部分术语经历了比较多的版本和社区的演绎,因此定义比较混乱,这里咱们先来理一下 JS 中的概念。


ES3

执行上下文在ES3中,包含三个部分。

  1. scope:做用域,也经常被叫作做用域链。
  2. variable object:变量对象,用于存储变量的对象。
  3. this value:this值。

注意:网上流传甚广的,用global object,和active object 来解释闭包、做用域、执行上下文,这是ES3里的解释法,如今已经解释不了不少语法了。


ES5

在ES5中,咱们改进了命名方式,把执行上下文最初的三个部分改成下面这个样子。

  1. lexical environment:词法环境,当获取变量时使用。
  2. variable environment:变量环境,当声明变量时使用。
  3. this value:this值。

ES2018

在ES2018中,执行上下文又变成了这个样子,this值被纳入lexical environment,可是增长了很多内容。

  1. lexical environment:词法环境,当获取变量或者this值时使用。
  2. variable environment:变量环境,当声明变量时使用
  3. code evaluation state:用于恢复代码执行位置。
  4. Function:执行的任务是函数时使用,表示正在被执行的函数。
  5. ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。
  6. Realm:使用的基础库和内置对象实例。
  7. Generator:仅生成器上下文有这个属性,表示当前生成器。

咱们在这里介绍执行上下文的各个版本定义,是考虑到你可能会从各类网上的文章中接触这些概念,若是不把它们理清楚,咱们就很难分辨对错。若是是咱们本身使用,建议统一使用最新的ES2018中规定的术语定义。

接下来,咱们从代码实例出发,推导函数执行过程当中须要哪些信息,它们又对应着执行上下文中的哪些部分。

好比,咱们看如下的这段 JS 代码:

var b = {}
let c = 1
this.a = 2;
复制代码

要想正确执行它,咱们须要知道如下信息:

  1. var 把 b 声明到哪里;
  2. b 表示哪一个变量;
  3. b 的原型是哪一个对象;
  4. let 把 c 声明到哪里;
  5. this 指向哪一个对象。

这些信息就须要执行上下文来给出了,这段代码出如今不一样的位置,甚至在每次执行中,会关联到不一样的执行上下文,因此,一样的代码会产生不同的行为。

这里咱们先讲var声明与赋值,let,realm三个特性来分析执行上下文中提供的信息。


var

咱们来分析一段代码: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

let是 ES6开始引入的新的变量声明模式,比起var的诸多弊病,let作了很是明确的梳理和规定。

为了实现let,JS 在运行时引入了块级做用域。也就是说,在let出现以前,JS 的 if 、for 等语句皆不产生做用域。

简单统计了下,如下语句会产生let使用的做用域:

  1. for;
  2. if;
  3. switch;
  4. try/catch/finally。

Realm

在最新的标准(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 这两个函数生成的做用域里面分别定义了一个变量。

在规范中做用域更官方的叫法是词法环境,没错,就是上文提到的词法环境,包含在执行上下文中。

做用域其实由两部分组成:

  1. 记录做用域内变量信息(咱们假设变量,常量,函数等统称为变量)和代码结构信息的东西,称之为 Environment Record。
  2. 一个引用 outer,这个引用指向当前做用域的父做用域。拿上面代码为例。innerFunc 的函数做用域有一个引用指向 walk 函数做用域,walk 函数做用域有一个引用指向全局做用域。全局做用域的 outer 为 null。

规范中定义了查找一个变量的过程:先查看当前做用域里面的 Environment Record 是否有此变量的信息,若是找到了,则返回当前做用域内的这个变量。若是没有查找到,则顺着 outer 到父做用域里面的 Environment Record 查找,以此递归。

因此咱们一般所说的函数内同名变量遮蔽全局变量就是这么回事。不过若是你在变量查找的时候指定某个做用域中的 Environment Record,那么也是能够的,譬如:window.name 【其实 window 对象就是全局做用域的 Environment Record 对象,可是普通函数做用域的 Environment Record 对象是获取不到的】。


做用域和执行上下文的关系

执行上下文是用于跟踪代码的运行状况,而做用域用于获取变量或者this值。从职责上看,他们几乎是没有啥交集的。那么为啥一般二者会被同时提到呢?由于在一个函数被执行时,建立的执行上下文对象除了保存了些代码执行的信息,还会把当前的做用域保存在执行上下文中。因此它们的关系只是存储关系。

结合做用域和执行上下文,咱们再来看下变量查找的过程。其实第一步不是到做用域里面找 Environment Record,而是先从当前的执行上下文中找保存的做用域(对象),而后再是经过做用域链向上查找变量。


结语

在这篇文章中,咱们梳理了一些概念:有编程语言的概念闭包,也有各个版本中的 JS 标准中的概念:执行上下文、做用域、this值等等。

以后咱们又从代码的角度,分析了一些执行上下文中所须要的信息,并从var、let、对象字面量等语法中,推导出了词法做用域、变量做用域、Realm的设计。

最后,咱们对比了执行上下文跟做用域的关系。

相关文章
相关标签/搜索