编译器的预解析与引擎的查询


代码解析参与者

需要了解变量是如何进行预解析的,首先要知道解析代码的参与者,有三个:引擎编译器作用域

  • 编译器

    对 JavaScript 源代码通过某种编译原理进行解析,并生成可供引擎执行的代码

  • 作用域

    负责保存变量以及提供对变量的查询与访问,并通过一套非常严格的规则,确定当前执行的代码对这些变量的访问权限

  • 引擎

    负责执行编译器生成的代码

现在我有如下的代码 var a = 1; 下面我们将这段代码分解,来看看浏览器是如何对这段代码进行解析的:

  1. 遇到 var a,编译器会询问作用域命名为 a 的这个变量是是否已经有一个存在于当前作用域中

    如果含有,则编译器会忽略 var a ,继续进行编译
    
    如果没有,编译器会要求作用域在当前作用域中声明一个新的变量,命名为 a
    
  2. 接下来,编译器会开始生成可供引擎执行的代码,这些代码被用来处理 a = 2 这个赋值操作,引擎会在当前作用域中开始查找是否有 a 这个变量(从当前作用域开始,层层向上查找,这就是作用域链

    如果查找到了,就对 a 进行赋值
    
    如果直到全局作用域都没查找到,则会抛出异常
    

缩略版:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值

可以看到,步骤1是编译器对代码进行的处理,步骤2是引起对代码进行的处理,步骤2才是 JavaScript 代码真正开始执行的时刻,而步骤1则被称为预解析

预解析

定义

在代码开始按照顺序从上到下执行之前( JavaScript 代码运行之前),当前作用域下,会把带有 varfunction 关键字的事先声明,并在当前作用域的内存中安排好(这也就是变量声明提升和函数声明提升的原因

  • 如果是变量,则赋值为 undefined

  • 如果是函数声明,则将整个函数块放在代码的最顶端

为何这里要说是在当前作用域呢?
因为存在函数,每声明一个函数,就会为它自身新生成一个单独的作用域,不通过特殊手段,外部是无法访问到内部的变量的,这里又分为函数声明和函数表达式两种情况:
1. 当编译器第一次遇到函数声明时,只会对函数名进行预解析,并进行一次函数声明提升,并不会对函数的内容进行处理,当引擎执行代码到函数时,编译器会再次开启,对函数内的代码进行一次新的预解析,且函数内也会存在变量声明提升和函数声明提升
2. 当引擎执行代码时,遇到函数表达式然后对变量进行赋值,这里也会再次开启编译器,对函数内的代码进行一次预解析,且函数内也会存在变量声明提升和函数声明提升

注意:其实这里使用“当前作用域”来描述不太合理,后面会讲到这点

实例

介绍完了定义,那我们来看如下代码:

实例1:变量的预解析

console.log(a);     // undefined
var a = 1;

这里是对变量进行的预解析,实际上代码的执行步骤是这样的

/* ----- 编译器进行的处理 ----- */
var a;
/* ----- 处理完毕,引擎开始执行整段代码 ----- */
console.log(a);
a = 1;

实例2:变量的预解析和函数声明的预解析

console.log(fn);    // undefined
var fn = function () {
    return 1;
}
console.log(fn);    // 输出函数的代码片段
function fn () {
    return 1;
}

同为函数,为什么输出的结果不同呢?

因为一个是函数表达式赋值给变量,一个是函数声明,这也就是定义里面说到的两种情况,遇到了 var 关键字和遇到了 function 关键字

第一段代码解析过程如下:

/* ----- 编译器进行的处理 ----- */
var fn;
/* ----- 处理完毕,引擎开始执行整段代码 ----- */
console.log(fn);
fn = function () {
    return 1;
}

可以看到,是对 var fn 进行了一个处理,直接忽视了后面的代码

第二段代码解析过程如下:

/* ----- 编译器进行的处理 ----- */
function fn () {
    return 1;
}
/* ----- 处理完毕,引擎开始执行整段代码 ----- */
console.log(fn);

可以看到,是将整个函数都放在了代码的顶部,然后再去执行打印的操作

注意:说句题外话,这里打印的是函数本身,并不是函数的执行结果,所以这里输出的是函数的代码片段,而不是1

变量声明提升和函数声明提升的顺序

既然这两货都会进行预解析,那肯定得来判断一下它们是否存在先后顺序,如果有,那谁的优先级更高呢?

首先来看看这段代码

function fn () {
    return 1;
}
console.log(fn());  // 2
function fn () {
    return 2;
}
console.log(fn());  // 2
var fn = function () {
    return 1;
}
console.log(fn());  // 1
var fn = function () {
    return 2;
}
console.log(fn());  // 2

第一个 console 的输出结果是不同的,原因就是变量声明提升和函数声明提升它们的解析机制是不同的(回顾一下上面的知识,与这里的知识无关)

第二个 console 的输出结果都为2,由于都为函数声明或者函数表达式,那么它们不存在预解析时的先后顺序,而是从上往下,代码依次执行时,进行的重新赋值

那么我们再来看看看看这两段代码

var fn = function () {
    return 1;
}
function fn () {
    return 2;
}
console.log(fn());
function fn () {
    return 2;
}
var fn = function () {
    return 1;
}
console.log(fn());

根据上面的结果,如果说函数声明提升和变量声明提升不存在预解析的排序,而是按照代码执行时的执行顺序来进行重新赋值的,那么这两段代码 console 出来的内容应该不同

如果尝试在浏览器中 console 出结果,会发现结果是相同的,都为1,这说明变量声明提升和函数声明提升是存在先后顺序的,而且函数声明提升是在变量声明提升之前,优先级更高

我们再来看一段误导性很强代码

console.log(fn);    // 函数代码片段
var fn = 1;
function fn () {
    return 2;
}

我们来对代码进行解析

/* ----- 编译器进行的处理 ----- */
function fn () {
    return 2;
}
var fn;
/* ----- 处理完毕,引擎开始执行整段代码 ----- */
console.log(fn);
fn = 1;

很多人以为,变量声明提升既然是在函数声明之前,那么这段代码的输出结果应该为 undefined,那么我在这里告诉你,var fn 并不会覆盖掉原来的函数声明,其实你可以使用另外一种方式验证一下

var a = 1;
var a;
console.log(a); // 1

这里在对变量 a 赋值后,又重新声明了一次,可是输出结果还是为1

所以那段误导性很强的代码并不能说明变量声明提升比函数声明提升的优先级高,这是由 ECMAScript 制定的代码规范,那就是函数声明提升比变量声明提升优先级高

遗留问题

简单介绍完了代码的解析模式,那么来说一下上面遗留的问题,之前在定义里面提到说使用“当前作用域”这个说法不太合理,那么请看下面的代码

<script>
var a = 1;
</script>

<script>
console.log(a);     // 1
</script>

通过这行代码我们可以知道,实际上这两个 <script> 标签共享的是同一个作用域,那么看看下面这个代码

<script>
console.log(a);     // 报错:Uncaught ReferenceError: a is not defined
</script>

<script>
var a = 1;
</script>

虽然它们都是共享的同一个作用域,但是进行代码预解析的步骤是不同的,首先会对第一个 <script> 标签内的代码进行预解析,然后去执行它;执行完后才会对第二个 <script> 标签内的代码进行预解析,再执行

<script>
    var a = 1;
</script>
<script>
    console.log(a); // 1
</script>

这里可以获得 a 的结果,说明不同 <script> 标签,它们共享的是同一个作用域

那么如果修改一下代码顺序,又会是什么样子呢?

<script>
    console.log(a); // Uncaught ReferenceError: a is not defined
</script>
<script>
    var a = 1;
</script>

虽然存在预解析,但是这里却报出了异常,这是因为不同 <script> 标签,它们要分别去进行预解析,在第一个script标签执行的时候,并没有声明 a 这个变量,所以会导致报错
在不同的script标签内,代码的执行顺序是不同的,如果处于不同的script标签内,虽
由于这两个代码存在不同的代码块,虽然他们的作用域是公用的,但是在解析上面这块代码时,并没有开始进行下面一块代码的预解析,所以会导致报错

再来看一段代码

console.log(a); // Uncaught ReferenceError: a is not defined
function fn () {
    a = 1;
}
fn();

刚刚也说过,如果在直接使用一个未声明的变量,在非严格模式下,会导致这个变量成为一个全局变量从而污染全局作用域,那么在这里,为什么 console.log(a) 会报错呢?
这里也存在一个预解析的问题,在script标签进行预解析的时候,只会对fn进行一个预解析,并不会去对函数内部的变量进行解析,只有当执行函数的时候,才会对函数内部的作用域进行一次新的预解析,所以这里 a 变量没有声明且根本无法获取到,所以报错

小结

  • 只有声明本身会被提升,而赋值或其他运行逻辑会留在原地

  • 每个作用域都会进行提升操作

  • 函数声明先被提升,然后才是变量声明

  • 不同 script 标签是分开解析的

引擎的查询方式

预解析洋洋洒洒的写了这么多,也只是讲解了 var a = 1 这段代码里面的 var a 的机制,并没有讲解到赋值,那么接下来就来说说引擎又是如何在作用域中对变量进行查询以及赋值的

分类

引擎查询方式分为两种:

  • RHS 查询:查找某个变量的值

  • LHS 查询:找到变量的容器本身,从而可以对其赋值

如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询

光说定义不太好理解,那么来看看这段代码,从实例中理解一下引擎的查询机制

实例

实例一

a = 2;

这里是对 a 进行了一次 LHS 查询,因为我需要获取到这个变量本身,然后才能对其进行赋值操作

实例二

console.log(a);

这里进行了两次 RHS 查询,第一次对 console 这个对象进行 RHS 查询,获取它身上是否有 log 这个方法,然后对 a 的值进行一次 RHS 查询,获取到 a 的值,从而使值在控制台显示

实例三

function fn (a) {
    return a;
}
fn(1);

这里就是既有 LHS 查询也有 RHS 查询,引擎执行到 fn(1) 这里,得知要去执行一段函数,便会去找这个函数,由于需要获取到函数的值才能执行,所以会对函数进行一次 RHS 查询;接着得知函数内有参数,要对函数的参数赋值,所以需要进行一次 LHS 查询,拿到函数作用域中的变量 a 然后对其赋值;最后碰到了 return a,得知需要将 a 的值返回,那么这里还要进行一次 RHS 查询去获取 a 的值

这些例子也简单的说明了上述的定义,那就是,需要获取变量本身时,执行 LHS 查询;需要获取到变量的值时,执行 RHS 查询

那么花了这么大篇幅去讲解引擎的查询机制到底对我们学习 JavaScript 有没有帮助呢?答案是有的。理解查询方式不仅可以让我们去理解代码的执行机制,也可以轻松的理解浏览器抛出的异常信息,下来我们就来看看

为什么区分 LHS 和 RHS 是一件重要的事情?

因为变量还没有声明(在任何作用域中都无法找到该变量)的情况下,这两种查询的行为是不一样的。不一样的行为会带来不同的结果,出现错误时浏览器的报错信息也会不同,理解这个对于理解浏览器的报错会有很大的帮助

ReferenceError异常

RHS 查询实例

考虑如下代码

function fn (a) {
    console.log( a + b );   // Uncaught ReferenceError: b is not defined
    b = a;
}
fn( 2 );

执行函数时,需要输出 a + b 的值,所以需要对 b 进行一次 RHS 查询,可是一层层的往上级查找,直到全局作用域都找不到这个变量,此时引擎就会抛出 ReferenceError 异常

LHS 查询实例

上面是有关于 RHS 查询的,那么再看看关于 LHS 查询的这段代码

function fn () {
    a = 1;
}
fn();
console.log(a);     // 1
function fn () {
    "use strict";
    a = 1;
}
fn();
console.log(a);     // Uncaught ReferenceError: a is not defined

可以看到,函数内的变量 a 没有使用 var 来声明,是直接进行使用的,当在执行 console.log 的时候,会对 a 进行一次 LHS 查询。在非严格模式下,LHS 查询会逐级向上查找,找到全局作用域时就会停止查找,如果没有找到该变量,则会自动在全局作用域声明一个这个变量(这也是为什么不使用 var,直接声明变量会导致该变量污染全局作用域的原理);在严格模式下,LHS 查询会逐级向上查找,找到全局作用域时就会停止查找,如果没有找到该变量,则会抛出 ReferenceError 异常

总结:不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛出 ReferenceError 异常(严格模式下)

TypeError异常

如果 RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的属性,那么引擎会抛出另外一种类型的异常,叫作 TypeError

小结

在了解了这些机制以后,就可以知道异常的根本原因了:

  • ReferenceError 异常同作用域判别失败相关

  • TypeError 异常则代表作用域判别成功了,但是对结果的操作是非法或者不合理的

这样了解了引擎对变量的查询机制,以后在看到浏览器报错信息时,就可以从根本出发,找到问题的根源了

本文大部分内容来自《you don't know JavaScript》,经过自己的理解和整理记录成的笔记

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容