深入理解javascript作用域系利第一篇——内部原理

前面的话

javascript拥有一套设计良好的规则来存储变量,并且之后可以很方便找到这些变量,这套规则被称之为作用域。作用域貌似简单,实则复杂,由于作用域与this机制非常容易混淆,使得理解作用域的原理更为重要。

内部原理分为:编译、执行、查询、嵌套和异常

一、编译

var a = 2;为例,说明Javascript内部编译的过程,主要分为三步。

【1】:分词(tokenizing):把由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元(token)。
var a = 2;被分解成为下面这些词法单元:var、a、=、2、;。这些词法单元组成了一个词法单元流数组。

[
    {
        "type": "Keyword",
        "value": "var"
    },
    {
        "type": "Identifier",
        "value": "a"
    },
    {
        "type": "Punctuator",
        "value": "="
    },
    {
        "type": "Numeric",
        "value": "2"
    },
    {
        "type": "Punctuator",
        "value": ";"
    }
]

【2】:解析(parsing):把词法单元流数组装换成一个由元素逐级嵌套所组成的代表程序语法结构的树,这个树被称为“抽象语法树”(AST)。

{
    "type": "Program",
    "body": [
        {
            "type": "VariableDeclaration",
            "declarations": [
                {
                    "type": "VariableDeclarator",
                    "id": {
                        "type": "Identifier",
                        "name": "a"
                    },
                    "init": {
                        "type": "Literal",
                        "value": 2,
                        "raw": "2"
                    }
                }
            ],
            "kind": "var"
        }
    ],
    "sourceType": "script"
}

这里写图片描述

AST(抽象语法树):指的是源代码语法对应的树状结构,也就是说,一种编程语言的源代码通过构建语法树的形式将源代码中的语句映射到树中的每一个节点上。程序本身可以被映射成为一颗语法树,而通过操纵语法树,我们可以精确的获获取程序代码中每一个精确的节点,。例如声明语句、赋值语句。这一过程会把js代码转换为JSON格式

【3】:代码生成:将AST装换为可执行代码的过程叫做代码生成。
var a = 2; 的抽象语法树转为一组机器指令,用来创建一个叫做a的变量(包括内存分配等),并将值2存储在a中。(这里存储的是一个简单类型的数据,所以会把这块内存的地址和变量名a绑定,然后地址空间里面存数值2)。
实际上,javascript引擎的编译过程要复杂的多,包括大量优化操作,上面这三步是编译过程的基本概述。
任何代码片段在执行前都要进行编译,大部分情况下编译发生在代码执行前的几微妙,javascript编译器首先会对var a = 2;这段程序进行编译,然后做好执行它的准备,并且通常都会马上执行它。

二、执行

简而言之,编译过程就是编译器把程序分解成词法单元(token),然后把词法单元解析成语法树(AST),在把语法树变成机器指令等待执行的过程。

实际上:代码执行编译,还要执行。下面深入说明。
【1】:编译
编译器查找作用域是否已经有一个名称为a的变量存在于同一作用域集合中,如果是,编译器会忽略该声明,继续进行编译。否则,它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a。

var a = 1;
  //编译时,首先会查找作用域是否已经有一个名称为a的变量存在于同一个作用域的集合中。
   // 如果是,编译器会忽略该声明,继续进行编译,否则,它会要求作用域在当前作用域集合中声明一个新的变量,
   // 并命名为a
  var a;
  console.log(a); //1;

编译器将var a = 2;这个代码片段编译成用于执行的机器指令。
【注意点】:依据编译器的编译原理,javascript中重复的声明是合法的。

//test在作用域中首次出现,所以声明新变量,并将20赋值给test
var test = 20;
//test在作用域中已经存在,直接使用,将20的赋值替换成30
var test = 30;

【2】:执行
1、引擎运行时首先会查询作用域,在当前作用域集合中是否存在一个叫做a的变量。如果是,引擎就会使用这个变量,如果不是,引擎会继续向上查找该变量。
2、如果引擎最终找到了变量a,就会将2赋值给它。否则引擎会抛出一个异常。

三、查询

在引擎执行的第一步操作中,对变量a进行了查询(查询是否存在这样一个变量a),这种查询叫做LHS查询(找这个变量容器)。实际上,引擎查询分为两种:LHS查询和RHS查询。
从字面意思上去理解,当变量出现在赋值操作的左侧是就是进行LHS查询,出现在右侧时进行RHS查询。
更准确的讲,RHS查询与简单地查找某个变量的值没什么区别,而LHS查询则是试图找到变量的容器本身,从而可以对其赋值

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

这段代码中,总共包括4个查询,分别是:
foo(…)对foo进行了RHS引用(函数调用属于RHS查询,因为函数已经声明好了,现在只是调用)
函数传参a = 2 对a进行了LHS查询(函数传参——实参到形参,相当于找到变量容器本身,并为其赋值)
console.log(…)对console对象进行了RHS引用,并检查其是否有一个log的方法
console.log(a)对a进行了RHS引用,并把得到的值传给了console.log(…)

四、嵌套

在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中查找,直到找打该变量,或抵达全局作用域为止。

function foo(a){
    console.log( a + b ) ;
}
var b = 2;
foo(2);// 4

在这个片段中,作用域foo()函数嵌套在全局作用域中,引擎首先在foo()函数的作用域中查找变量b,并尝试对其进行RHS引用。没有找到,接着,引擎会在全局作用域中查找b,成功找到后,对其进行RHS引用,查到它的值为2。

五、异常

为什么区分LHS和RHS是一件重要的事情?因为在变量还没有声明(在任何作用域中都无法找到变量)的情况下,这两种查询的行为不一样。
RHS
【1】如果RHS查询失败,引擎会抛出ReferenceError(引用错误)异常。

//对b进行RHS查询时,无法找到该变量。也就是说,这是一个“未声明”的变量
function foo(a){
    a = b;  
}
foo();//ReferenceError: b is not defined

【2】如果RHS查询找到了一个变量,但尝试对变量的值进行不合理操作,比如对一个非函数类型值进行函数调用,或者引用null或undefined中的属性,引擎会抛出另外一种类型异常:TypeError(类型错误)异常。

function foo(){
    var b = 0;
    b();
}
foo();//TypeError: b is not a function

LHS
【1】当引擎执行LHS查询时,如果无法找到变量,全局作用域会创建一个具有该名称的变量,并将其返还给引擎。

function foo(){
    a = 1;  
}
foo();
console.log(a);//1

【2】如果在严格模式中LHS查询失败时,并不会创建并返回一个全局变量,引擎会抛出同RHS查询失败时类似的ReferenceError异常。

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

六、以一个案例,说明内部原理。

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

主要分一下几步:

  1. 引擎需要为foo(…)函数进行RHS引用,在全局作用域中查找foo。成功找到并执行。
  2. 引擎需要进行foo函数的传参a = 2;对a进行LHS引用,在foo函数作用域中查找a,成功找到后,将它赋值为2。
  3. 引起需要执行console.log(…),为console对象进行RHS引用,在foo函数作用域中查找console对象,由于console是一个内置对象,被成功找到。
  4. 引擎在console对象中查找log(…)方法,成功找到。
  5. 引擎需要执行console,log(a),对a进行RHS查询,在foo()函数的作用域中查找a,成功找到并执行。
  6. 于是,引擎把a的值,也就是2传到console,log(…)中。
  7. 最终,在控制台输出2。

猜你喜欢

转载自blog.csdn.net/weixin_37972723/article/details/80184892