Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

作用域和闭包 #30

Open
adodo0829 opened this issue Apr 14, 2020 · 0 comments
Open

作用域和闭包 #30

adodo0829 opened this issue Apr 14, 2020 · 0 comments

Comments

@adodo0829
Copy link
Owner

作用域和闭包

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

词法作用域
动态作用域 (动态作用域与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]
一条能访问上层执行上下文变量对象的链表
  • 闭包
能够访问自由变量(非参数和自己内部的变量)的函数;
理论上: 所有函数
实践上: 引用了上层上下文变量对象的函数
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant