1. 彻底搞懂javascript-词法环境(Lexical Environments)

8,998 阅读8分钟

开始之前先来看两段代码:

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

function bar() {
    var a = 3;
    foo();
}

var a = 2;
bar();
var foo = 1;
function bar() {
    if (!foo) {
        var foo = 10;
    }
    console.log(foo);
}
bar();
  1. 你是否会疑惑在function作用域内不是先查找本作用域的变量,找不到才沿作用域链往上找吗?为什么 函数内有var a = 3;foo()还是引用全局的a=2呢?

  2. 为啥foo会是10,难道var foo = 10 被执行了?

针对这种代码中的现象,之前笔者也是查找各种帖子文章,在查看这些文章的时候,scope、词法环境、静态作用域、执行上下文等名词不断出现。从中大概知道,哦,js中变量是有提升的现象,哦,还有函数的作用域是在它创建的时候的词法环境决定的,哦,还有this是在运行是决定的不是在创建时决定的(what?TMD到底是怎么决定的)。。。等等,再加上原型链、作用域链、闭包、匿名函数、立即执行函数。。。什么鬼!相信小伙伴们都有类似的经历,对这些概念听过,知道,但是模糊。

为了彻底了解自己写的代码到底再干些啥,G哥决定一探究竟,花了一些时间去探索,现如今我觉得已经真相大白了,可以把我所了解的分享给大家。不是为了搞些奇技淫巧,而是为了更加透彻了此言语特性。

注意:需要说明的是本系列文章是居于ECMAScript 5.1(ES5) 为参考的,所以如何有小伙伴会疑惑,“为啥你说的运行环境和我看大不大一样,我看到的有变量对象(ES3),有realm(ES6)”。ES6的我会另出出一个系列来专门聊聊。本系列大部分只涉及ES5。

户口登记制度

在开始之前,我们先来想一个问题?JS引擎在执行代码时,是从哪找到要引用的变量的值、函数调用时是如何找到要执行的函数的呢?答案其实简单,简单得有点意外---就是先登记,后使用。就像小伙伴们小时候上学,报道第一天先注册,把名字、性别啥先登记了。登记完了,上课时,老师喊“小明”,小明才能回应“嗯,哥在这”,是不是。比如说没有登记小红“这个”,登记簿上根本没小红,老师要怎么叫,所以没法叫(ReferenceError “not defined”)。但是如果“小李”开学有登记,表上也有它得名字“小李”,但是这天小李打电动旷课了,老师喊“小李”,没人应(默认值undefined,这里小伙伴要搞清楚了,undefined不是说没定义,而是说这个变量定义了,它的值为undefined,undefined是js语言中的一个值,记得否?)。

开学时,学生找老师来注册登记入学,那我们JS里的变量名、函数名找谁注册登记呢?找Lexical Environments(词法环境)登记!

Lexical Environments(词法环境)

Lexical Environments(词法环境),之所以叫词法环境,是因为它是和源程序的结构对应,就是和你所写的那些源码的文字的结构对应,你写代码的时候这个环境就定了。Lexical Environments(词法环境)和四个类型的代码结构相对应:

  • Global code:通俗点讲就是源文件代码,就是一个词法环境
  • 函数代码 :一个函数块内自己是一个新的词法环境
  • eval:进入eval调用的代码有时会创建一个新的词法环境
  • with结构:一个with结构块内也是自己一个词法环境
  • catch结构:一个catch结构快内也是自己一个词环境

为什么?不知道,ES5就是这么规定设计的。。。

读到这里有些小伙伴急了,“不对,不对,我记得只有在全局代码、函数代码、和eval代码三种情况,才会创建运行上下文,你专门有5种”。

对,你说的没错,只有在全局代码、函数代码、和eval代码三种情况,才会创建运行上下文,但我这里说的是词法环境,Lexical Environments。不是运行上下文。

看起有点像是这样的:

图1

这个有点像一个学校,每个班级由班主任负责登记班级学生,年段段长负责登记本年段老师,校长负责登记学校其他行政人员。

但是这其中有些微妙的差异,with结构,catch结构 的词法环境只用来检索,不用来登记,就是说with结构,catch结构的var声明和函数声明也是到其他(到哪?后续说明)词法环境上登记绑定的,它其实就是一个户口检索代理机构(代理嘛,有可能没有户口登记的给你返回有此人。。),这些细节后续会深入解释。

观察上面第一张图,有几点需要注意:

  1. 图中画了和4种结构对应的词法环境是为了展示,并不是说一下子就建立4个词法环境,记住,词法环境是在进入上述几种代码结构之前之前创建的,就像学校开课之前这些学生登记工作就要做了。
  2. 从图中,可以发现,词法环境是嵌套的,那怎么实现嵌套呢?这就需要了解词法环境的结构了。

老师把学生信息登记到登记簿, Lexical Environments(词法环境)要把变量信息登记在哪呢?哈哈, Lexical Environments(词法环境)也有自己的“登记簿”-Environment Records。先看下Lexical Environments(词法环境)结构是什么样:

词法环境由两部分组成:

  • Environment Records(环境记录):这个就是变量登记的地方了
  • outer:outer 是个指向,包含(包围)本词法环境的外部词法环境

用伪代码表示就是类似这样的:

function LexicalEnvironment() {
    this.EnvironmentRecord = undefined;
    this.outer = undefined; //outer Environment Reference
}

这个outer很重要,它是作用域链能够链起来的关键。那outer是什么呢,我又要比喻了,好比是班主任手里有一个登记簿,还有一个本年段段长的电话号码?这个outer就是段长的电话号码。什么作用呢?想一下,班主任要找一个叫“9527”的人,发现自己班级里没有,这时候,就可以打outer这个电话问问段长:“我找一个9527的人,我这名单上找不到,可能不是学生,是老师,你能在你登记簿上找一下吗?”。段长就开始在自己的登记簿上找,如果找不到,段长也有一个电话号码-校长的电话号码,这时段长就给校长打电话:“校长,我在找一个9527的人,我登记簿上找不到,会不会是学校的行政人员,您能在您的登记簿上找一下这个人吗”。就这样一级一级网上链接,到了校长,他的电话号码就是空了(null),他那找不到,就找不到了。

扯这么多,一句话,outer 就是指向外部Lexical Environments(词法环境)的引用。

在我们JS中,global Lexical Environments(全局词法环境)就是“校长”,它还有一个专门的名称叫GlobalEnvironment 因为GlobalEnvironment 是最外层的词法环境,所以GlobalEnvironment的outer = null。而在与GlobalEnvironment关联的代码中定义的函数的Lexical Environments(词法环境)法环境的outer就是GlobalEnvironment,with和catch同理,它们如何关联起来我们后续还要细讲。

现在来看看“登记簿”---EnvironmentRecord。EnvironmentRecord就是真是登记变量信息的地方了。ES5中EnvironmentRecord分为两类,就像有的老师把信息登记在本子上,有的把信息登记在电脑里:

  • declarative environment records 主要用于函数 、catch词法环境
  • object environment records. 主要用于with 和global的词法环境

declarative environment records可以简单理解为字典类型的结构,key-value形式结论变量等对应的名字和值。

而object environment records会关联一个对象,用这个对象的属性-值来登记变量等对应的名字和值。

用伪代码表示(这些代码只是用来说明相关结构用,不保证严谨性和可行性,伪代码!):

function EnvironmentRecord(obj) {

    if(isObject(obj)) {
        this.bindings = object;
        this.type = 'Object';
    }
    this.bindings = new Map();
    this.type = 'Declarative';
}


EnvironmentRecord.prototype.register = function(name) {
    if (this.type === 'Declarative')
        this.bindings.set(name,undefined)
    this.bindings[name] = undefined;
}

EnvironmentRecord.prototype.initialize = function(name,value) {
      if (this.type === 'Declarative')
        this.bindings.set(name,value);
    this.bindings[name] = value;
}

EnvironmentRecord.prototype.getValue = function(name) {
    if (this.type === 'Declarative')
        return this.bindings.get(name);
    return this.bindings[name];
}

全局环境(Global Environment)

全面提过,GlobalEnvironment,其就是一个特殊的Lexical Environments(词法环境),它的outer为null,它的EnvironmentRecord 是一个object environment records,且与全局对象global object(浏览器:window对象)关联。看起来像这样。

var GlobalEnvironment = new LexicalEnvironment();
GlobalEnvironment.outer = null;
GlobalEnvironment.EnvironmentRecord = new EnvironmentRecord(globalobject); ;//globalobject可以看作是浏览器环境下的window

上面提到词法环境(Lexical Environments),是用来登记变量和相关函数名字的,也知道这个名字是登记在 词法环境的 EnvironmentRecord上的。

那问题又来了,什么时候登记,怎么登记?是直接找老师(Lexical Environments)登记,还是设置一个办公厅,办公厅设置登记窗口提供登记服务?

我们下一篇来细细说明!

总结

//Lexical Environment
function LexicalEnvironment() {
    this.EnvironmentRecord = undefined;
    this.outer = undefined; //outer Environment Reference
}

//EnvironmentRecord
function EnvironmentRecord(obj) {

    if(isObject(obj)) {
        this.bindings = object;
        this.type = 'Object';
    }
    this.bindings = new Map();
    this.type = 'Declarative';
}

EnvironmentRecord.prototype.register = function(name) {
    if (this.type === 'Declarative')
        this.bindings.set(name,undefined);
    this.bindings[name] = undefined;
}

EnvironmentRecord.prototype.initialize = function(name,value) {
      if (this.type === 'Declarative')
        this.bindings.set(name,value);
    this.bindings[name] = value;
}

EnvironmentRecord.prototype.getValue = function(name) {
    if (this.type === 'Declarative')
        return this.bindings.get(name);
    return this.bindings[name];
}

//全局环境GlobalEnvironment

function creatGlobalEnvironment(globalobject) {
	var globalEnvironment = new LexicalEnvironment();
	globalEnvironment.outer = null;
	globalEnvironment.EnvironmentRecord = new EnvironmentRecord(globalobject);
	return globalEnvironment;
}

GlobalEnvironment = creatGlobalEnvironment(globalobject)//可以看作是浏览器环境下的window