1

JS作用域和闭包

总的来说可以分为以下几块内容区理解:

词法作用域
动态作用域 (动态作用域与this机制挂钩)
全局作用域
函数作用域
块级作用域 (es6+)

前言基础

我们先了解下js引擎(解释器/编译器结构), js引擎可以理解为根据ECMAScript定义的语言标准来动态执行JavaScript字符串...
js 引擎基础
js 执行环境
整个解析过程可分为: 语法检查阶段 -> 运行时阶段

语法检查阶段(一)

语法检查阶段: 分为 词法分析和语法分析

JS解释器先把JavaScript代码(字符串)的字符流按照ECMAScript标准转换为记号流,
分解为词法单元块. 
// 比如 var a = 2, 词法分析后的结果
[
  "var": "keyword",
  "a": "identifier",
  "=": "assignment",
  "2": "integer",
  ";": "eos" (end of statement)
]
// 然后根据解析的词法结构结合标准与法生成 AST
{
  operation: "=",
  left: {
    keyword: "var",
    right: "a"
  }
  right: "2"
}
// 遍历这颗抽象语法树(其中的一些操作还没去深入研究, 反正可以做很多事情, 什么eslint, babel 转化等),
最后直接会转化为机器指令(分配内存空间)

# 当语法检查出现错误时, 会抛出错误...
运行时阶段(二)

运行时阶段: 预解析 和 代码执行

  • 先预解析(一)
# 1.创建执行上下文环境: 先是全局, 然后是函数,会被依次push到执行栈(一块内存来管理上下文)
// 上下文环境包括: 
变量对象VO: 优先级依次是 arguments声明, function声明, var声明
作用域链Scope(词法环境): 由当前变量对象以及上层父级作用域构成的一条链表结构, 便于变量查询
this值:    表示当前上下文对象, 程序进入上下文就会确定下来

# 2.为上下文中的变量对象(VO)赋值
函数形参: undefined
函数声明: 如果变量对象已经包含了相同名字的属性,则会替换它的值, 
         此时函数的标识符在环境中已存在(变量提升的原因)
变量声明: undefined
// 例子就不举了...QAQ
  • 再代码执行(二)
进入执行代码阶段
预解析阶段的初始化属性:
  `变量对象`的undefined值可能会被覆盖 => 变为活动对象(AO)
  `作用域链`可能会改变
  `this值`也可能会改变

作用域

作用域[[scope]]: 可以理解为变量的生命周期,有效范围; 也可以理解为js中用来访问变量的一套规则...

词法作用域

上面为甚么提到 js引擎解析代码的过程, 因为语法检查阶段可以看做是理解词法作用域的基础;
词法作用域就是定义在词法阶段的作用域, JavaScript采用的是词法作用域, 变量,函数的作用域在函数定义的时候就决定

词法作用域只由变量,函数被声明时所处的位置决定
// 第一层
var a = 1
function foo(a) {
  // 第二层
  var b = a * 2;
  function bar(c) {
    // 第三层
    console.log(a, b, c);
  }
  bar(b * 3);
}
foo(2) // 2, 4, 12
分析一下: 这里有个嵌套的作用域
第一层: 全局作用域, 两个声明, foo, a
第二层: foo的函数作用域, 三个声明, a, b, bar
第三层: bar的函数作用域, 一个声明, c

作用域的这种嵌套结构和互相之间的位置关系给 js 引擎提供了的位置信息;
js引擎可以用这些信息来查找标识符声明的位置;

特性: 作用域查找从运行时所处的最内部作用域开始,逐级向上进行,直到遇见第一个匹配的标识符为止
动态作用域

动态作用域主要是js程序在运行时决定的, 与 this 机制相关; 动态作用域并不关心函数和作用域是如何声明以及在任何处声明的,只关心它从何处调用...

函数的作用域是在函数调用的时候才决定, 与上下文挂钩

作用域链

当查找变量标识符的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
作用域链在函数创建运行时是会发生改变的...

函数创建时

函数有一个内部属性 [[scope]],当函数创建的时候, 就会保存所有父变量对象到其中;

function foo() {
    function bar() {
      // ...
    }
}

// scope:
foo.[[scope]] = [
  globalContext.VO
];

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];
函数运行时

函数运行时, 进入函数上下文,创建 VO/AO, 就会将活动对象添加到作用链的顶端

Scope = [AO].concat([[Scope]])
总结一下作用域链创建过程(ES3规范下, ES5的规范会在后续ES6的内容中讲到)
var v1 = 'global'
function fn() {
  var v2 = 'local'
  return v2
}
fn()
语法检查阶段
  • 1.函数 fn 被 声明创建,保存作用域链到内部属性[[scope]]
fn.[[scope]] = [
  globalContext.VO
]
  • 2.函数 fn 被执行,创建 fn的函数上下文, 上下文被压入执行栈
ECStack = [
  fnContext,
  globalContext
]
  • 3.创建作用域链Scope...复制函数的 scope 属性
fnContext = {
  Scope: fn.[[scope]]
}
  • 4.添加AO, 即初始化VO(arguments,func声明, 变量声明)
fnContext = {
  AO: {
    arguments: {
      length: 0
    },
    v2: undefined
  },
  Scope: fn.[[scope]]
}
  • 5.AO 添加到 Scope 的顶端
fnContext = {
  AO: {
    arguments: {
      length: 0
    },
    v2: undefined
  },
  Scope: [AO, fn.[[scope]]]
}
运行时阶段
  • 6.准备工作完毕, 开始执行代码
fnContext = {
  AO: {
    arguments: {
      length: 0
    },
    v2: 'local'  // 为 AO 属性赋值
  },
  Scope: [AO, fn.[[scope]]]
}
  • 7.查找 v2 的值并返回, 函数执行完毕, 弹出调用栈
ECStack = [
  globalContext
]

闭包

MDN 闭包解释
ECMAScript中,闭包指的是:
理论角度:
所有的函数都是闭包.
因为它们都在创建的时候就将上层上下文的数据保存起来了.即使是全局变量也是如此,
因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
实践角度:(以下函数才算是闭包)
1.即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
2.在代码中引用了自由变量

闭包实例
var v = "global";
function outer(){
  var v = "local";
  function inner(){
      return v;
  }
  return inner;
}
var foo = outer();
foo();

# 代码执行过程分析:
1.进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
2.全局执行上下文初始化
3.执行outer函数,创建 outer 函数执行上下文,,outer的执行上下文入栈
4.outer 执行上下文初始化,创建变量对象、作用域链、this等
5.outer 函数执行完毕,outer 执行上下文出栈
6.执行 inner 函数,创建 inner 函数执行上下文,inner 执行上下文被压栈
7.inner 执行上下文初始化,创建变量对象、作用域链、this等
8.inner函数执行完毕,inner函数上下文出栈

# 当 inner函数 执行时, outer已经出栈, 如何获取 outer 作用域内的值呢?
答案是: 通过作用域链 Scope

其实 inner 在执行的时候会它的上下文维护这样一个属性
innerContext = {
  Scope: [AO, outerContext.AO, globalContext.VO]
}
所以, 即时 outer 销毁了, 内存中依然会存一份outerContext.AO
闭包解题思路

分析函数调用
分析上下文语义

globalContext = {
  VO: {
    arguments: {}
    变量: '',
  },
  Scope: []
}

funcContext = {
  AO: {
    arguments: {}
    变量: '',
  },
  Scope: [AO, ..., globalContext.VO]
}

总结

  • 作用域: 变量对象所能访问的区域
执行上下文 = {
  变量对象 = {
    arguments = {},
    func 声明,
    var 声明
  },
  Scope: [],
  this
}
词法作用域(声明时)
动态作用域(运行时)
  • 作用域链
Scope: [AO, fnContext.AO, ..., globalContext.VO]
一条能访问上层执行上下文变量对象的链表
  • 闭包
能够访问自由变量(非参数和自己内部的变量)的函数;
理论上: 所有函数
实践上: 引用了上层上下文变量对象的函数

a_dodo
2.4k 声望1k 粉丝

天下事有难易乎?