study icon indicating copy to clipboard operation
study copied to clipboard

作用域、闭包、执行上下文、变量对象、活动对象、this、函数

Open cfour-hi opened this issue 6 years ago • 0 comments

汤姆大叔的博客 - 深入理解JavaScript系列 js 中的活动对象 与 变量对象 什么区别?

作用域 (Scope)

JavaScript 采用 词法作用域 (Lexical Scoping),也就是静态作用域(与之相对的是动态作用域)。

函数的作用域在函数 创建 的时候就已确定,而不是 调用 的时候。

函数被调用时(进入上下文阶段),会复制创建函数时的作用域,并将当前函数局部作用域放到作用域链的顶层。

作用域链

Scope = AO + [[scope]]

函数上下文的作用域链在函数调用时创建,包含活动对象和此函数的内部 [[scope]] 属性(所有父级 变量/活动 对象)。

[[scope]]

所有父级 变量/活动 对象的层级链,处于当前函数上下文之上,在函数 创建 时存于其中。

也就是说,[[scope]] 在函数创建时被存储,并且不会被改变,直至函数销毁。

与作用域链对比,[[scope]] 是函数的一个属性,而不是上下文。

通过构造函数创建的函数的 [[scope]]

通过构造函数创建的函数的 [[scope]] 属性总是 唯一的全局对象

var a = 1

function foo () {
  var b = 2

  var bar = new Function('console.log(a); console.log(b);')

  bar() // Uncaught ReferenceError: b is not defined
}

foo()

二维作用域链查找

变量如果在当前 变量/活动 对象中找不到对应的值,则会继续在原型链中查找。

活动对象没有原型,所以只有在全局对象找不到变量值时才会继续在原型链中查找。

function foo () {
  console.log(a)
}

Object.prototype.a = 1

foo() // 1
function foo () {

  var a = 1

  function bar () {
    console.log(a)
  }

  bar()
}

Object.prototype.x = 2

foo() // 1

上例 Object 也可换成 EventTarget 有同样的效果

代码执行时对作用域链的影响

代码执行时 withcatch 语句能够修改修改作用域链

Scope = withObject|catchObject + AO|VO + [[Scope]]

var foo = {x: 10, y: 20}

with (foo) {
  alert(x) // 10
  alert(y) // 20
}

// Scope = foo + AO|VO + [[Scope]]

闭包 (Closure)

A closure is a combination of a code block (in ECMAScript this is a function) and statically/lexically saved all parent scopes. Thus, via these saved scopes a function may easily refer free variables. 闭包是一系列代码块(在ECMAScript中是函数),并且静态保存所有父级的作用域,通过这些保存的作用域来搜寻到函数中的自由变量。

  • 因为每一个普通函数在创建时保存了 [[Scope]],理论上 ECMAScript 中所有函数都是闭包。

  • 闭包是函数代码和其 [[scope]] 的结合

  • 同一个父上下文中创建的闭包共用一个 [[Scope]] 属性

    也就是说,某个闭包对其中 [[Scope]] 的变量做修改会影响到其他闭包对其变量的读取,即所有的内部函数都共享同一个父作用域。

  • 最后,我的理解是:

    函数内部自由变量通过作用域链找到对应的值或引用,保存在作用域内,即使创建它的上下文已经销毁,它也仍然存在,此为 闭包

    自由变量是指在函数中使用,但既不是函数参数也不是函数局部变量的变量

执行上下文

标准规范没有从技术实现的角度定义 EC 的准确类型和结构,这应该是具体实现 ECMAScript 引擎时要考虑的问题。

函数被调用时分为 解析执行 两个阶段

解析阶段

解析阶段也叫进入上下文阶段,函数被调用时会创建函数执行上下文,上下文状态包含 变量对象 (VO: Variable Object) 、this 指针 (this value) 和 作用域链(SC: Scope Chain)

执行上下文

图片来自汤姆大叔 - 深入理解JavaScript系列(10):JavaScript核心(晋级高手必读篇)

执行阶段

执行阶段变量对象中属性值为 undefined 的属性的值会被确定

执行上下文栈 (ECS: Execution Context Stack)

在 ECMASscript 中有 Global, Function 和 Eval 三种可执行代码类型。

执行上下文栈底层永远是全局执行上下文 (Global Execution Context)。

执行过程中,可能会创建无数个函数执行上下文,压入执行上下文栈,当函数执行完毕且不被其他函数依赖则弹出。

(function () {
  var stop = function () {
    console.log('stop')
  }

  var walk = function () {
    console.log('walk')
    setTimeout(function () {
      stop()
    }, 1000)
  }

  walk()
  console.log('over')

  setTimeout(function () {
    console.log('really over')
  }, 1000)
})()

// 伪代码模拟 ECS 在代码执行过程中的状态
ECS.push(Global Context) // ECS 压入全局上下文

ECS.push([anonymous] Function Context)  // ECS 压入匿名立即执行函数上下文
ECS.push([walk] Function Context)       // ECS 压入 walk 函数上下文

// 1000ms later

ECS.push([anonymous] Function Context)  // ECS 压入 walk 函数内定时器执行的匿名函数上下文
ECS.push([stop] Function Context)       // ECS 压入 stop 函数上下文

ECS.pop([stop] Function Context)        // ECS 弹出 stop 函数上下文
ECS.pop([anonymous] Function Context)   // ECS 弹出 walk 函数内定时器执行的匿名函数上下文
ECS.pop([walk] Function Context)        // ECS 弹出 walk 函数上下文

ECS.push([anonymous] Function Context)  // ECS 压入匿名立即执行函数内定时器执行的匿名函数上下文

ECS.pop([anonymous] Function Context)   // ECS 弹出匿名立即执行函数内定时器执行的匿名函数上下文
ECS.pop([anonymous] Function Context)   // ECS 弹出匿名立即执行函数上下文

// 全局上下文会一直保存到应用程序结束

变量对象 (VO: Variable Object)

如果变量与执行上下文相关,那变量自己应该知道它的数据存储在哪里,并且知道如何访问。这种机制称为变量对象(variable object)。

A variable object is a scope of data related with the execution context. It’s a special object associated with the context and which stores variables and function declarations are being defined within the context.

变量对象 (variable object) 是与执行上下文相关的 数据作用域 (scope of data) 。 它是与上下文关联的特殊对象,用于存储被定义在上下文中的 变量 (variables) 和 函数声明 (function declarations) 。

进入执行上下文时,VO 的初始化过程具体如下:

  • 函数的形参 成为变量对象的属性,其属性名就是形参的名字,其值就是实参的值,对于没有传递的参数,其值为 undefined。

  • 函数声明 (FD: Function Declaration) 成为变量对象的属性,其属性名即为函数名,其值就是函数对象。如果变量对象中已经包含了相同名字的属性,则替换它的值。

  • 变量声明 (VD: Variable Declaration) 成为变量对象的属性,其属性名即为变量名,其值为 undefined,如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性。

注意:函数声明如已有相同属性名则会替换值,而变量声明如已有相同属性名则不影响已存在的属性,证明 Javascript 的世界里函数是第一公民。

function foo (a, b) {
  console.log(a) // ƒ a() {}
  console.log(b) // 2

  var b = 22

  function a () {}

  console.log(b) // 22
}

foo(1, 2)

// 伪代码模拟 VO
VO[GlobalContext] = {
  foo: <reference to FunctionDeclaration "foo">
}
VO[foo FunctionContext] = {
  a: <reference to FunctionDeclaration "a">,
  b: 2
}

上例形参 a 的实参本是 1,可 foo 函数内部有相同属性名的函数声明 a,因此 a 的值被替换为内部声明函数。虽然 foo 函数内有变量声明 b 并且赋值为 22,但是在进入 foo 函数执行上下文时 b 的值仍然是实参 2,直到函数执行到 b 的赋值语句,b 的值才被替换为 22。

在具体实现层面(以及规范中)变量对象只是一个抽象概念,从本质上说,在具体执行上下文中,VO 名称是不一样的,并且初始结构也不一样。

全局上下文中的变量对象

全局对象 (Global object) 是在进入任何执行上下文之前就已经创建了的对象; 这个对象只存在一份,它的属性在程序中任何地方都可以访问,全局对象的生命周期终止于程序退出那一刻。

全局上下文中的变量对象 (VO) 指向的就是全局对象本身,所以可以从全局对象拿到全局变量。

函数上下文中的变量对象

在函数执行上下文中,VO 是不能直接访问的,此时由 活动对象 (AO: activation object) 扮演 VO 的角色。

函数上下文中的变量对象被表示为 活动对象

活动对象 (AO: Activation Object)

活动对象在函数上下文中作为变量对象使用

函数被激活(调用),活动对象被创建,包含普通参数 (formal parameters) 与特殊参数 (arguments) 对象(具有索引属性的参数映射表)。

即:函数的变量对象保持不变,但除了存储变量与函数声明之外,还包含特殊对象 arguments

function foo (a, b) {
  var c = 3
  function d () {}
  var e = function _e () {}
  (function x () {})
}

foo(1)

// AO 伪代码 进入 foo 函数时
{
  a: 1,
  b: undefined,
  arguments: {
    0: 1,
    1: undefined
    ...
  },
  c: undefined,
  d: <reference to FunctionDeclaration "d">,
  e: undefined
}

因为 _ex 都是函数表达式,不是函数声明,所以没有包含在 AO 内。

执行函数的时候,AO 的一些 undefined 值会被确定,上例就是 3 赋值给变量 c,函数表达式 _e 赋值给变量 e

this

this 是执行上下文中的一个属性,与上下文中可执行代码的类型有直接关系,this 的值在进入上下文时确定,并且在上下文运行期间永久不变。

在一个函数上下文中,this 由调用者提供,由调用函数的方式来决定。

引用类型 (Reference type)

非常重要但不好理解的概念

使用伪代码我们可以将引用类型的值可以表示为拥有两个属性的对象:base(即拥有属性的那个对象)、base 中的 propertyName。

var valueOfReferenceType = {
  base: <base object>,
  propertyName: <property name>
}

引用类型的值只有两种情况:标识符属性访问器

标识符是变量名,函数名,函数参数名和全局对象中未识别的属性名 属性访问器包括 点 (.) 语法 和 括号 ([]) 语法,比如 foo.barfoo['bar']

function bar () {
  console.log(this)
}

var foo = {
  baz: bar
}

foo.baz()

// 伪代码模拟引用类型值
var fooReference = {
  base: global,
  propertyName: 'foo'
}
var barReference = {
  base: global,
  propertyName: 'bar'
}
var fooBazReference = {
  base: foo,
  propertyName: 'baz'
}

如果调用括号 () 左边是引用类型的值,this 将设为引用类型值的 base 对象 (base object)。如果不是,则为 undefined(第 5 版 ECMAScript)

上例打印出的是 foo 对象,因为 foo.baz(属性访问器)是引用类型值,所以 this === fooBazReference.base === foo;如果直接调用 bar 函数,即 bar(),因为 bar(标识符)也是引用类型值,所以 this === barReference.base === global

一般情况下我们不会写出非引用类型值函数调用,但还是需要了解。

首先,从引用类型中得到一个对象真正的值,伪代码 GetValue 描述如下:

function GetValue (value) {

  if (Type(value) != Reference) {
    return value;
  }

  var base = GetBase(value);

  if (base === null) {
    throw new ReferenceError;
  }

  return base.[[Get]](GetPropertyName(value));
}

内部的 [[Get]] 方法返回对象属性真正的值,包括对原型链中继承的属性分析。

复杂示例:

var foo = {
  bar: function () {
    console.log(this)
  }
}

foo.bar() // foo
(foo.bar)() // foo

(foo.bar = foo.bar)() // global
(false || foo.bar)() // global
(foo.bar, foo.bar)() // global

后面的三个调用,在应用一定的运算操作之后(调用了 GetValue 求值),在调用括号的左边的值不再是引用类型,而是匿名函数表达式,最终执行实为立即执行函数表达式 (function () { console.log(this) })()

引用类型和 this 为 null

this 如果为 null,会被隐式转化成 global。当引用类型值的 base 对象是活动对象时,这种情况就会出现。

function foo() {
  function bar() {
    console.log(this) // global
  }
  bar() // the same as AO.bar()
}

活动对象总是作为 this 返回,值为 null,即伪代码的 AO.bar() 相当于 null.bar()

构造函数中的 this

指向实例对象

bind、call、apply

函数原型中的这三个方法允许手动设置函数调用的 this 值。

函数

函数声明 (FD: Function Declaration)

  1. 有一个特定的名称;
  2. 在源码中的位置:要么处于程序级 (Program level),要么处于其它函数的主体 (FunctionBody) 中,不能处于代码块中,比如 if () {} else {} 内;
  3. 影响变量对象;
  4. 进入上下文阶段创建

函数声明只能在全局上下文中或函数体内

function foo () { // 全局
  function bar () {} // 函数体内
}

函数表达式 (FE: Functional Expression)

  1. 在源码中须出现在表达式的位置;
  2. 有可选的名称;
  3. 不会影响变量对象;
  4. 代码执行阶段创建

为什么需要函数表达式? 函数表达式不会污染全局变量

命名函数表达式的特性

(function foo(bar) {

  if (bar) return

  foo(true) // foo 可被执行

})()

foo() // foo is not defined

Q:foo 储存在什么地方?

A:当解释器在代码执行阶段遇到命名函数表达式 foo 时,会先创建一个辅助的特殊对象并置于当前上下文作用域链最前端,然后再创建函数表达式并获取作用域 [[Scope]] 属性,进入 foo 函数上下文阶段,创建 foo 函数上下文作用域链并将 foo 的名称添加到特殊对象上作为唯一属性,这个属性的值就是命名函数表达式 foo 本身。最后再从父作用域链移除辅助的特殊对象。以下是伪代码分析:

specialObject = {} // 创建特殊对象

Scope = specialObject + Scope // 把特殊对象置于作用域链最前端

foo = new FunctionExpression // 创建函数表达式 foo
foo.[[Scope]] = Scope // 确定函数的作用域 [[Scope]]
specialObject.foo = foo // {DontDelete}, {ReadOnly} 这样函数内部就能访问函数本身

delete Scope[0] // 从作用域链中删除定义的特殊对象 specialObject

通过函数构造器创建的函数

这种函数的 [[Scope]] 属性仅包含全局对象

var a = 1

!function foo () {
  var b = 2

  var bar = new Function('console.log(a);console.log(b);')

  bar() // 1, b is not defined
}()

函数创建

声明式函数在解析时创建,表达式函数在执行时创建。

创建时,不仅包含代码块,还已确定作用域([[scope]]:所有父级作用域的作用域链)。

函数被调用会创建执行上下文,上下文状态包含:变量对象 (VO: Variable Object) 、this 指针 (this value) 和 作用域链(SC: Scope Chain)。

变量对象包括形参、变量声明和函数声明,属于规范实现(Javascript 引擎实现),不可在 Javascript 环境中获取。全局上下文中的变量对象指向全局对象,所以可以在全局对象中拿到全局变量。函数上下文中的变量对象以活动对象(AO: Activation Object)的方式表现,活动对象另外还有 arguments 属性(具有索引属性的参数映射表)。

在函数执行阶段,全局对象和活动对象在进入上下文阶段中的一些值为 udnefined 的属性的值会被确定。

函数内 this 的指向由函数调用者提供,由调用方式决定。如果调用者是引用类型值,则为引用类型值的 base 对象,如果不是则为 undefined

引用类型值只有两种情况:标识符和属性访问器

标识符:在全局上下文中 base 对象为全局对象,在函数上下文中 base 对象为活动对象,但活动对象作为 this 返回时值为 null。

属性访问器:base 对象为属性所在对象

作用域链是当前执行函数的活动对象 + [[scope]]

变量值的查找 之 二维作用域链查找

先从当前活动对象中查找,再从 [[scope]] 一级一级往父作用域查找,直到全局对象,如果全局对象依然没有找到,则继续从全局对象的原型链查找(PS:活动对象没有原型),最终到 Object.prototype

cfour-hi avatar Dec 01 '17 10:12 cfour-hi