ECMA-262-5 词法环境:ECMA实现(二)--- 环境记录项

环境记录类型

  ES5中有两种类型的环境记录项,分别是声明式环境记录项和对象式环境记录项。

声明式环境记录项

    声明式环境记录用于处理函数作用域和catch语句中出现的变量,函数声明,形式参数等(在这种情况下,这是我们从ES3系列中了解到激活对象很像)(译注:在这里,我也给出最新ES2018官方的定义:声明式环境记录项是用来定义那些直接将标识符与语言值直接绑定的ES语法元素,例如变量,常量,let,class,module,import以及函数声明等,感兴趣可以去看最新ecma-262,但本文只介绍ES5)。
  例如:

// 所有标识符
// "a", "b" 和"c" 
//都绑定在同一个声明式环境记录项上
function foo(a) {
  var b = 10;
  function c() {}
}
在cahtch从句中,异常参数会被绑定到记录项上:
try {
  ...
} catch (e) { // "e" 被绑定到一个声明式环境记录项上
  ...
}

   通常情况下,我们会认为声明式环境记录项的绑定内容会被直接存储在底层实现上(例如,虚拟机的寄存器上,以及快速的访问)。这是与老版ES3中的激活对象主要区别。
  也就是说,规范不要求(甚至间接不推荐)将声明式环境记录项实现为效率低下的简单对象。基于这个事实,声明式环境记录项并不会直接暴露给用户层,这意味着我们不能像访问属性那样获取记录项的绑定内容。实际上,在之前的版本中,ES3的用户同样无法访问那时的激活对象(除了Rhino 引擎实现中,会通过__parent__ 属性暴露出来)。
  潜在的,声明式环境记录项的特性允许使用完整的词法地址(译注:注1)技术,使用这种技术可以直接访问所需的变量,而无需通过作用域链向上遍历查找—无论作用域嵌套的深度如何(这种技术的必要条件就是所有变量的地址在编译阶段就能确定,并且是固定不变)。不过,在ES5规范中并没有直接提及这块。
  所以我们应该理解为什么用用声明式环境记录项替代老版激活对象概念,这首先是实现的效率。
  因此,正如Brendan Eich(译注:JavaScript发明者)也提到过,ES3中的激活对象实现是个“bug”而非“feature”,但他也承认但是实现JS太过匆忙:

“I will note that there are some real improvements in ES5, in
particular to Chapter 10 which now uses declarative binding
environments. ES1-3’s abuse of objects for scopes (again I’m to blame
for doing so in JS in 1995, economizing on objects needed to implement
the language in a big hurry) was a bug, not a feature”。

  抽象的,环境和环境记录项,可以用下面这种方式来表示(另外,type属性并非来自规范,这里是为了更好解释):

environment = {
  // storage
  environmentRecord: {
    type: "declarative",
    // storage
  },
  // reference to the parent environment
  outer: <...>
};

eval和inner函数可能会阻断优化

  注意,eval方法会阻断优化,并且会使代码回到未优化的版本,因为引擎无法确定eval需要哪些绑定。
  例如,在V8实现中(其他引擎也许类似),会对一些函数进行优化,既不会创建函数的参数对象(如果不需要)也不会保存父函数中的变量(函数不使用的情况下)。这样函数被叫做轻量级函数,只会保存自身用到的词法变量。此外,因为这函数不使用任何父函数变量(译注:即自由变量),这种函数甚至不是闭包。
  看下面来自Chrome Dev中显示的一个作用域变量,未使用eval的情况下。
withoutEval

下面再看下同样一个函数,使用了一个空eval
withEval
  因为事先无法知道在eval中会使用哪些绑定,所以不得不创建所有的“重量级描述”—可以看到,这里有函数的激活对象和闭包属性,即父函数环境等。
  此外,再看下后一个例子中的outerFn 。它还创建了参数对象,因为它有内部函数,而它无法确定内部函数是否使用了父函数的参数。
  当然,这也仅是其中一种实现方案,通过这个例子来展示优化是如何进行的,以及优化是如何被阻止的。
下面让我们看了解下第二种环境记录项类型—对象是环境记录项。

对象式环境记录项

  相反,对象式环境记录项是用来定义出现在全局上下文和with语句中的,变量和函数的关联。(译注:这里也给出262-2018中对此的定义:对象式环境记录项用来定义那些将标识符与某些对象属性相绑定的ES语法元素,例如with语句 )。对象式环境记录项是通过一个简单对象的形式来存储这些变量或函数标识符,在上面也提到过,这是一种低效率方式。

这种在一个上下文中用来储存绑定关系的对象叫做绑定对象(binding object)

  在全局上下文中,变量是与全局对象本身关联在一起,正因为如此,我们能够通过全局对象的属相来访问这些变量。

var a = 10;
console.log(a); // 10
 
// "this" 在全局上下文中
// 指代全局对象本身
console.log(this.a); // 10
 
// 在浏览器环境中"window" 
// 就是全局对象
console.log(window.a); // 10

  在with语句中,变量与with对象的属性关联在一起:

with ({a: 10}) {
  console.log(a); // 10
}

  每次执行with语句,就会创建一个带有对象式环境记录项的新词法环境。而此时,当前运行上下文环境被设置为这个新创建环境的外部环境。接着,当前运行上下文环境被替换为新创建的这个(译注:当之前运行的词法环境会被储存起来)。当with执行结束,就会恢复到之前的状态(译注:这个过程和栈帧类似)。

var a = 10;
var b = 20;
 
with ({a: 30}) {
  console.log(a + b); // 50
}
 
console.log(a + b); // 30, 恢复了

  用伪代码形式表示:

// 初始化
context.lexicalEnvironment = {
  environmentRecord: {a: 10, b: 20},
  outer: null
};
 
// "with" 执行
previousEnvironment = context.lexicalEnvironment;
 
withEnvironment = {
  environmentRecord: {a: 30},
  outer: context.lexicalEnvironment
};
 
// 替换当前环境
context.lexicalEnvironment = withEnvironment;
 
// "with" 执行完成,将当前环境恢复到previousEnvironment
context.lexicalEnvironment = previousEnvironment;

  同样效果的还有catch从句,它也会将运行上下文的词法环境替换为通过catch新创建的这个。但与with语句相反,catch从句创建的是声明式环境记录项而不是对象式环境记录项:

var e = 10;
 
try {
  throw 20;
} catch (e) { // 替换环境
  console.log(e); // 20
}
 
// and now it's restored back
console.log(e); // 10

   在下面我们将会看到,这些通过with或catch从句创建的临时环境,在其内部有函数表达式的时候将会起到的作用。
  因为对象式环境记录项的效率较低,在ES5的严格模式下with语句已经被移除了。
  此外,因为with语句经常会引起一些混淆(与变量与函数的声明提升有关),而有些问题的确会带来令人困扰的情况。这也是为什么with从ES5的严格移除的原因之一。
  ES的下个版本还在计划将全局对象从作用域链的底端移除。这就是说,全局环境记录项将会由对象式变为声明式(译注:上面提到过,全局上下文声明的变量可以通过全局对象的属性访问)。一些之前我们熟悉的全局绑定,诸如parseInt,Math等会被导入全局上下文中,无法再通过属性的形式访问全局变量,因为不会再有全局对象,。
  最后,通过代码的形式看看一个拥有对象式环境记录项的抽象环境结构:

environment = {
  // storage
  environmentRecord: {
    type: "object",
    bindingObject: {
      // storage
    }
  },
  // reference to the parent environment
  outer: <...>
};

  规范中的绑定对象其实是真实对象(例如全局对象)的一种反射,但并非所有原始对象的属性都会作为绑定对象的属性。例如,不是标识符的属性名称不包括在绑定对象中,这是非常合乎逻辑的,因为我们本来就不能像普通变量那样在代码中去访问。

// global properties
this['a'] = 10; // 包含在绑定对象中
this['hello world'] = 20; //不包含
 
console.log(a); // 10, can refer
console.log(hello world); // cannot, syntax error

然而,规范中省略了绑定对象和原始对象如何同步的实现细节。
下面这部分将介绍执行上下文相关,包括词法环境和变量环境。见ECMA-262-5 词法环境:ECMA实现(三)— 执行上下文

原文链接

ECMA-262-5 in detail. Chapter 3.2. Lexical environments: ECMAScript implementation.

词法环境理论:文章列表

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值