Skip to content

【JavaScript】执行上下文 & 函数上下文中的变量对象 #51

@zh-rocco

Description

@zh-rocco

执行上下文

当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。

在一段 js 代码拿过来真正一句一句运行之前,浏览器已经做了一些“准备工作”

“准备工作”包括:

  • 变量、函数表达式 -- 变量声明,默认赋值为 undefined
  • 函数声明 -- 赋值
  • this -- 赋值

这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。

变量赋值发生在 "执行阶段"。

函数每被调用一次,都会产生一个新的执行上下文环境。
函数在定义的时候(不是调用的时候),就已经确定了函数体内部 自由变量 的作用域(JavaScript 中的作用域是静态作用域)

JS 是静态作用域

所以如果代码段是函数体,“准备工作”还需要包括:

  • 参数 -- 赋值
  • arguments -- 赋值
  • 自由变量的取值作用域 -- 赋值

给执行上下文环境下一个通俗的定义:在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用 undefined 占个空

上下文结构:

执行上下文结构

执行上下文栈

执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。

执行上下文栈

执行上下文栈示例

补充

激活其它上下文的某个上下文被称为 调用者(caller),被激活的上下文被称为 被调用者(callee)

当一个 caller 激活了一个 callee,那么这个 caller 就会暂停它自身的执行,然后将控制权交给这个 callee。于是这个 callee 被放入堆栈,称为进行中的上下文[running/active execution context]。当这个 callee 的上下文结束之后,会把控制权再次交给它的 caller,然后 caller 会在刚才暂停的地方继续执行。在这个 caller 结束之后,会继续触发其他的上下文。一个 callee 可以用返回( return )或者抛出异常( exception )来结束自身的上下文。

所有的 ECMAScript 的程序执行都可以看做是一个执行上下文堆栈[execution context (EC) stack]。堆栈的顶部就是处于激活状态的上下文。

函数上下文中的变量对象

VO ( variable object ) 变量对象:是一个与执行上下文相关的特殊对象,它存储着在上下文中声明的以下内容:变量变量、函数声明 ( FunctionDeclaration, 缩写为 FD )、函数的形参。

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

示例:

function test(a, b) {
  var c = 10;
  function d() {}
  var e = function _e() {};
  (function x() {});
}

test(10); // call

当进入带有参数 10 的 test 函数上下文之前,VO 表现为如下

VO(test) = {
  a: 10,
  b: undefined,
  c: undefined,
  d: <reference to FunctionDeclaration "d">
  e: undefined
};

当进入带有参数 10 的 test 函数上下文时,AO 表现为如下

AO(test) = {
  a: 10,
  b: undefined,
  c: undefined,
  d: <reference to FunctionDeclaration "d">
  e: undefined
};

注意,AO 里并不包含函数 x。这是因为 x 是一个函数表达式( FunctionExpression, 缩写为 FE ) 而不是函数声明,函数表达式不会影响 VO。 不管怎样,函数 _e 同样也是函数表达式,但是就像我们下面将看到的那样,因为它分配给了变量 e,所以它可以通过名称 e 来访问。

test 函数执行时,AO 表现如下

这个周期内,AO/VO 已经拥有了属性( 不过,并不是所有的属性都有值,大部分属性的值还是系统默认的初始值 undefined )。

AO['c'] = 10;
AO['e'] = <reference to FunctionExpression "_e">;

再次注意,因为 FunctionExpression _e 保存到了已声明的变量 e 上,所以它仍然存在于内存中。而 FunctionExpression x 却不存在于 AO/VO 中,也就是说如果我们想尝试调用 x 函数,不管在函数定义之前还是之后,都会出现一个错误 “x is not defined”,未保存的函数表达式只有在它自己的定义或递归中才能被调用。

补充示例

arguments

function foo(x, y, z) {
  // 声明的函数参数数量 foo(x, y, z)
  console.log(foo.length); // 3

  // 真正传进来的参数个数 (x, y)
  console.log(arguments.length); // 2

  // 参数的 callee 是函数自身
  console.log(arguments.callee === foo); // true

  // 参数共享

  console.log(x === arguments[0]); // true
  console.log(x); // 10

  arguments[0] = 20;
  console.log(x); // 20

  x = 30;
  console.log(arguments[0]); // 30

  // 不过,没有传进来的参数 z,和参数的第 3 个索引值是不共享的

  z = 40;
  console.log(arguments[2]); // undefined

  arguments[2] = 50;
  console.log(z); // 40
}

foo(10, 20);

关于 VO 的特殊情况

1. 变量声明

全局对象( global / window )的变量对象( VO )是其自身,即:VO(globalContext) === global / window

任何时候,变量只能通过 var / let / const 关键字才能声明。 a = 10 仅仅是给全局对象创建了一个新属性(但它不是变量)。

关于变量,还有一个重要的知识点。变量相对于简单属性来说,变量有一个特性 (attribute):{DontDelete},这个特性的含义就是不能用 delete 操作符直接删除变量属性。

a = 10;
console.log(window.a); // 10

console.log(delete a); // true

console.log(window.a); // undefined

var b = 20;
console.log(window.b); // 20

console.log(delete b); // false

console.log(window.b); // still 20

但是这个规则在有个上下文里不起作用,那就是 eval 上下文,变量没有 {DontDelete} 特性。

eval('var a = 10;');
console.log(window.a); // 10

console.log(delete a); // true

console.log(window.a); // undefined

2. 全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?

在 ES5 中,顶层对象的属性和全局变量是等价的,var 命令和 function 命令声明的全局变量,自然也是顶层对象。

var a = 12;
function f() {}

console.log(window.a); // 12
console.log(window.f); // f(){}

但 ES6 规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性,但 let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属性。

let aa = 1;
const bb = 2;

console.log(window.aa); // undefined
console.log(window.bb); // undefined

全局作用域中使用 let 和 const 声明变量的作用域

通过上图也可以看到,在全局作用域中,用 letconst 声明的全局变量并没有在全局对象中,只是一个块级作用域(Script)中。

参考

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions