[JavaScript]闭包与执行上下文

348 阅读9分钟

好久没写了,55,失踪人口回归

前言

两年前我在面试的时候,现在这家公司的面试官问了我一个基础的问题,什么是闭包

我:回答一个函数内部中定义的函数,可以对其包裹的外层函数所在作用域进行变量访问。

面试官:函数出栈后其函数和它下面申请的临时变量不是销毁了吗,为什么还可以访问。

我:因为闭包对其作用域下的变量有持续的引用,所以虽然出栈了但是垃圾回收的引用还没清零还没有找到。

面试官:这个闭包函数怎么去寻找那些外部函数作用域下的变量呢?

我:通过作用域链

面试官:那你来讲讲作用域链吧。

我(内心OS):完了,这块我可能讲不好

我:额,通过额,我忘了

面试官:外部函数出栈后其作用域还在吗?

我:额,应该是在的

面试官:为什么?

我:额,我这里讲不好,我下去复习下(内心OS:捂脸,下去一定得好好看清楚这里)

最后我过了,,但是一直也没有好好复习下这块,业务线上也对闭包缓存的场景用的相对少(捂脸,业务线的节奏和组成都比较不标准,就不细嗦了,dddd),那最近呢,我们这边的项目终于要封装一个地图的API了,我也刚好小小复习了下闭包,接下来就给大家唠嗑唠嗑面试里经常问的这几个基础中的基础,js的核心:闭包,函数作用域,函数作用域链,以及经常红红火火恍恍惚惚分不清楚的执行上下文,执行上下文栈,词法环境,变量环境到底是啥,在哪里。

闭包

What is it

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。

Why Use it

优点:

1. 让外部访问函数内部变量成为可能;

2. 局部变量会常驻在内存中,可以用来做缓存;

3. 可以避免使用全局变量,防止全局变量污染;

缺点:

以下代码copy自 什么是闭包?闭包的优缺点?(侵删)

1. 引用的变量可能已经发生变化

function outer() {
      var result = [];
      for (var i = 0i<10; i++){
        result.[i] = function () {
            console.info(i)
        }
     }
     return result
}

解决:使用自执行或者let

function outer() {
      var result = [];
      for (var i = 0; i<10; i++){
        result[i] = function (num) {
             return function() {
                   console.info(num);    // 此时访问的num,是上层函数执行环境的num,数组有10个函数对象,每个对象的执行环境下的number都不一样
             }
        }(i)
     }
     return result
}
// let
function outer() {
      var result = [];
      forlet i = 0; i<10; i++){
        result[i] = function () {            
            console.info(i)
        }
     }
     return result
}

2. this指向问题

var object = {
     name: ''object",
     getName: function() {
        return function() {
             console.info(this.name)
        }
    }
}
object.getName()()    // underfined

解决: 别这样玩,使用函数模块化或者class

function SomeObject() {
    var name = "object";
    this.getName = function () {
        console.log(name);
    }
}
new SomeObject().getName()

3. 内存泄漏

function  showId() {
    var el = document.getElementById("app")
    el.onclick = function(){
      aler(el.id)   // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
    }
}

// 改成下面
function  showId() {
    var el = document.getElementById("app")
    var id  = el.id
    el.onclick = function(){
      aler(id)   // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
    }
    el = null    // 主动释放el
}

从JavaScript执行上下文说起

以下内容部分搬运自 (ES5版)深入理解 JavaScript 执行上下文和执行栈 (侵删)

你已经看到了前言和上一节中很多的关键词,什么词法环境,执行栈,全局上下文,函数上下文,函数作用域,函数作用域链,what,开始懵逼? 搞什么,怎么这么多,没事,让我们慢慢的从零开始,逐步吃掉这些概念。

规范的区分说明

在ES3中,也就是经典中的经典,红皮书第三版中,执行上下文包含:scope(作用域)、variable object(变量对象)、this value(this 值)。

而在官方ES5中,执行上下文包含:lexical environment(词法环境)、variable environment(变量环境)、this value(this 值)。

那么,以后看到变量对象、作用域知道是 ES3 里面的内容,而如果是词法环境、变量环境这种词就是 ES5 以后的内容。

执行上下文

执行上下文是计算和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。

分为三种:

1. 全局执行上下文

2. 函数执行上下文

3. Eval函数执行上下文

执行栈

What is it

一种拥有 LIFO(后进先出)的数据结构,被用来存储代码运行时创建的所有执行上下文

JS引擎在执行代码时,会先创建一个全局执行上下文压入执行栈,每当遇到一个函数的执行,就会为该函数创建一个函数上下文并进栈。

创建执行上下文

1. 创建

  1. this 值的决定,即我们所熟知的 this 绑定

  2. 创建**词法环境(作用域)**组件。

  3. 创建**变量环境(变量对象)**组件。

所以执行上下文在概念上表示如下:

GlobalExectionContext = {  LexicalEnvironment: {    EnvironmentRecord: {      Type: "Object",      // 在这里绑定标识符    }    outer: <null>  }}FunctionExectionContext = {  LexicalEnvironment: {    EnvironmentRecord: {      Type: "Declarative",      // 在这里绑定标识符    }    outer: <Global or outer function environment reference>  }}

词法环境

它是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用outer词法环境的空值组成。

词法环境有两种类型:

  • 全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 this的值指向全局对象。

  • 函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。

在词法环境的内部有两个组件:(1) 环境记录器和 (2) 一个外部环境的引用

  1. 环境记录器是存储变量和函数声明的实际位置。

  2. 外部环境的引用意味着它可以访问其父级词法环境(作用域)。

注意 — 对于函数环境声明式环境记录器还包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length

变量环境

它同样是一个特殊的词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

如上所述,变量环境也是一个词法环境,所以它有着上面定义的词法环境的所有属性。

在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(letconst)绑定,而后者只用来存储 var 变量绑定。

看点样例代码来理解上面的概念:

let a = 20;const b = 30;var c;function multiply(e, f) { var g = 20; return e * f * g;}c = multiply(20, 30);

执行上下文看起来像这样:

GlobalExectionContext = {  ThisBinding: <Global Object>,  LexicalEnvironment: {    EnvironmentRecord: {      Type: "Object",      // 在这里绑定标识符      a: < uninitialized >,      b: < uninitialized >,      multiply: < func >    }    outer: <null>  },  VariableEnvironment: {    EnvironmentRecord: {      Type: "Object",      // 在这里绑定标识符      c: undefined,    }    outer: <null>  }}FunctionExectionContext = {  ThisBinding: <Global Object>,  LexicalEnvironment: {    EnvironmentRecord: {      Type: "Declarative",      // 在这里绑定标识符      Arguments: {0: 20, 1: 30, length: 2},    },    outer: <GlobalLexicalEnvironment>  },VariableEnvironment: {    EnvironmentRecord: {      Type: "Declarative",      // 在这里绑定标识符      g: undefined    },    outer: <GlobalLexicalEnvironment>  }}

注意 — 只有遇到调用函数 multiply 时,函数执行上下文才会被创建。

可能你已经注意到 letconst 定义的变量并没有关联任何值,但 var 定义的变量被设成了 undefined

这是因为在创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为 undefinedvar 情况下),或者未初始化(letconst 情况下)。

这就是为什么你可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 letconst 的变量会得到一个引用错误。

这就是我们说的变量声明提升

2. 执行

**在此阶段,完成对所有这些变量的分配,最后执行代码。
**

作用域链

上面的内容挺多,但是也很清晰,慢点吃。

那么作用域链在这里就很简单了,

在定义函数的时候,作用域链就已经诞生了,而我们可以打开控制台通过执行以下代码来在实际的js引擎下一探究竟。

function foo() {
    var a = 1;
    return function() {
        console.log(a)
    }
}
var a = foo()
a()
console.dir(a)

打印出来的a如下

ƒ anonymous()length: 0name: ""arguments: nullcaller: nullprototype: {constructor: ƒ}__proto__: ƒ ()[[FunctionLocation]]: VM328:3[[Scopes]]: Scopes[2]0: Closure (foo) {a: 1}1: Global {parent: Window, opener: null, top: Window, length: 0, frame

你已经发现了一个神奇的物品:[[Scopes]]

没错,它就是神秘的什么? 相信看了前面这么多,大家可以自己说出来(老谜语人了)

作用域链?函数上下文?

点开Global你可以看到很多,具体不在这里扩展。(其实是我也没看完hh)

相信你也看到了CLosure这个东东了,下面就放着我们心心念念的a,也就是出栈后还活的好好的一直打印都会在都能取到值的a,这个时候,就是闭包让他活下来辣,实际的原理,是不是就是从作用域链找到作用域再取到其下面仍然存活(被引用到)的变量呢。(因为闭包可以让内部返回的匿名访问a,所以a没有被从内存中清除,所以a被缓存了!)

总之,引擎转动之时,一条条链条将它实际的作用域跟踪出来,然后去拿取下面对应的变量。完成了闭包。

——————————————————————————————————————

相信看到这里,各位小伙伴对于js中闭包,作用域,作用域链有了一系列的串联的理解了,那么在这个基础上,相信对其应用,我们是不是可以继续来点干货呢,比如在实际工作中常常用到的数据缓存场景,数据模块化(MVVM 中的 Model 封装)场景,好,我们继续:

数据缓存的实现...

数据Model块到底怎么搞(MVVM 中的 Model 层抽象)

如何通过闭包写一个自己的 MapAPI

如何继续深入

参考木易杨的调用堆栈

。。。嗯,没了!看到这里脑壳还不麻吗!这几个就先放一个烟雾弹,后面我再更新(大概*可能*迫真)出来了哈哈哈哈哈哈

参考

(ES5版)深入理解 JavaScript 执行上下文和执行栈 

什么是闭包?闭包的优缺点?