看到Var a = 2你想到了什么?

倘若有人让你解释一下var a = 2这句代码背后发生了什么,如果你只想到了不就是一句声明吗?那你可能再去补一补JS比较底层的东西了。

其实这行代码还是涉及了比较多的知识点的。比如编译原理,作用域,LHS和RHS查询等一些知识了。下面就让我们一一揭开它们神秘的面纱。

编译原理

我们知道JavaScript是一门编译语言,而且它不是提前编译的。JS的编译发生在代码执行前的非常短的时间内。

程序中的源代码在执行前会经过分词、解析和代码生成三个阶段,我们把这三个步骤统称为编译。

  • 分词/词法分析

    这个阶段既可以称之为分词,也可以叫做词法分析。以上两者的区别是非常微妙的,同时也是比较难懂的,个人感觉不会区分这两者也无伤大雅。

    在这个过程由字符串组成的源代码会被分解成有意义的代码块,这些代码块计算词法单元。var a = 2就会被分解成var、a、=、2这些词法单元。

  • 解析/语法分析

    这个过程的任务就是将分词阶段生成的词法单元流转换成一个由元素逐级嵌套所组成的代表程序语法结构的树。这个数就是大名鼎鼎的抽象语法树(AST,Abstract Syntax Tree)。

这就是var a = 2这行代码所生成的AST。

  • 代码生成

    我们的计算机只能识别二进制,将AST转换为二进制可执行代码的过程就是代码生成。

作用域

在react中我们经常会用到state来表示组件的状态。状态这个词对于编程语言来说,个人感觉是比较重要的,若没有状态这个词,程序虽然也能执行一些简单的任务,但会限制程序的灵活性。

在编程语言中,我们使用变量来表示状态。几乎任何一种编程语言都有存储、访问和修改变量值的功能,正是这种存储和访问变量值得能力将状态带给了程序。

既然,有了变量,那么我们就需要考虑把变量存在哪里以及如何在需要它们的时候能够找到它们?这时候我们就需要设计一套良好的规则来存储和访问变量。作用域其实就是这套存储和访问变量的规则。

作用域有三种:

  • 全局作用域
  • 函数作用域
  • 块级作用域

在以后总结闭包的时候再来详细解释一下这三种作用域。毕竟,闭包和作用域和预编译过程都是密不可分的。

为了更好地理解作用域,我们还要了解一下引擎和编译器。

  • 引擎

    从头到尾负责整个JS程序的编译和执行过程。

  • 编译器

    引擎的好朋友之一,负责语法分析及代码生成等工作。

当我们看到var a = 2是我们就会把这句话当做一句声明,但引擎会认为这里有两个声明,一个由编译器在编译阶段处理,一个由引擎在运行时处理。

下面我们将var a = 2进行分解。编译器首先会将这行代码分解成词法单元,然后将这些词法单元解析成一个树结构。最后生成可执行的代码。

事实上,编译器会做如下工作。

1.遇到var a,编译器会询问当前作用域内是否已经存在a变量,如果存在,则忽略该变量声明,继续往下编译。如果没有改变量,则在当前作用域内声明一个名为a的变量。

2.接下来编译器会为引擎生成运行时所需的代码来进行a = 2的赋值操作。在执行阶段,引擎会在当前作用域下查找a变量,如果找到a变量,引擎便会使用a变量,如果没有找到,引擎会继续查找该变量。

LHS和RHS(Left/Right Hand Search)

这两种方法就是引擎查找变量的方法。

LHS和RHS的含义是“赋值操作的左侧或右侧”并不意味着就是“=赋值操作符的左侧或右侧”。赋值操作还有其他几种形式,因此在概念上最好将其理解为“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头(RHS)”

  • LHS和RHS 都是出现在引擎对变量进行查询的时候
  • LHS 变量赋值或写入内存(将文本文件保存到硬盘中)
  • RHS 变量查找或从内容中读取 想象为从硬盘打开文本文件

特性:

  • 都会在所有作用域中查询
  • 严格模式下,找不到所需的变量,引擎会ReferenceError异常
  • 非严格模式下,LHS会自动创建一个全局变量
  • 查询成功后,如果对变量的进行不合理的操作会产生TypeError,例如下面代码就会报错

var a = 2;

a();

其实内部原理就是变量查询。

下面通过一个例子练习一下。

function foo(a) {

var b = a;

return a + b;

}

var c = foo(2);

LHS有3处:c = ..; a = 2(隐式变量分配),b = ..

RHS有4处:foo(2..、 = a; return 中的a和b

作用域链

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到改变量或抵达最外层的作用域(全局作用域)为止。

作用域链查找其实和预编译是紧密相连的。

预编译四步曲:

  1. 创建AO对象 (Activation Object, 执行期上下文,可以理解为作用域)
  2. 找形参和变量声明,将变量和形参名作为AO属性名,值为undefined
  3. 将实参值和形参统一
  4. 在函数体里面找函数声明,值赋予函数体

function fn(a) {

console.log(a);

var a = 123;

console.log(a);

functiona() {}

console.log(a);

var b = function () {};

console.log(b);

functiond() {}

}

fn(1);

1.创建AO

AO {

}

2. 找形参和变量声明,将变量和形参名作为AO属性名,值为undefined

AO {

a: undefined,

b: undefined,

}

3. 将实参值和形参统一

AO {

a: 1,

b: undefined,

}

4. 在函数体里面找函数声明,值赋予函数体

AO { a: function a(){},

b: undefined,d: function d() {}}

预编译完成后,引擎便要开始执行代码了。

function fn(a) {

console.log(a);//此时的AO中为functiona(){}

var a = 123;// var a 在预编译时已经执行,不用再执行,此刻执行a = 123, AO中的a由函数变成a: 123

console.log(a);// 123

functiona() {};// 预编译已经执行,不用再执行

console.log(a);// AO中a还是123

var b = function () {};// var b已经执行,执行 b = function(){},AO中b的值由undefined变成匿名函数

console.log(b);// function() {}

functiond() {}:// 已经在编译阶段执行了,不在执行

}

fn(1);

所以以上代码的结果为function a(){},123,123,function(){}

再来练习一个

function a(age) {

console.log(age);

var age = 20;

console.log(age);

functionage() {

}

console.log(age);

}

a(18);

  1. AO{}
  2. AO{age:undefined}
  3. AO{age:18}
  4. AO{age:function age(){}}

预编译执行完成后,引擎开始执行代码

function a(age) {

console.log(age);// functionage(){}

var age = 20;// var age在预编译是已经执行完,引擎执行age = 20, AO{age:functionage(){}}变成AO{age:20}

console.log(age);// 20

functionage() {

}// 预编译时已经执行完

console.log(age);// AO中的age还是20

}

a(18);

所以以上结果为function age(){},20,20


如果我们理解了预编译和AO,再来理解闭包就会容易很多了。

以上是 看到Var a = 2你想到了什么? 的全部内容, 来源链接: utcz.com/a/33750.html

回到顶部