前端基础系列--2. 变量提升与作用域那些事

上一篇:1. 浏览器缓存

一、前言

尽管通常JS被归类为 ”动态“ 或 ”解释执行“ 语言,但事实上他是一门编译语言。但与传统的编译语言不同,JS不是提前编译的,编译结果也不能在分布式系统中进行移植。

  • 解释执行: 存在一个解释器,对于JS来说,浏览器的JS引擎就是一个解释器,对预编译后的JS代码进行解释执行,解释一行,执行一行,这个过程中不会生成中间文件。
  • 编译执行:对于平台无法识别的带啊吗,进行编译转换成平台可执行的机器码,一般会生成中间文件,即转换后的文件。

二、 预编译

1. 传统编译过程中的3个步骤

  • 分词/词法分析:这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元。在这个过程中,会分析一些代码的基础语法以及检查出一些基础的错误。
  • 解析/语法分析:这个过程也被称为预编译,将词法单元流转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为抽象语法树(AST)
  • 代码生成:这个过程将AST转换为可执行代码。

JS引擎编译过程也可以分为上面三步,只是其中复杂了许多。例如,在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括冗余元素进行优化等。

2. 预编译四部曲

先熟悉一个最简单的例子,最后输出的是underfined,那么这个过程发生了什么呢?

function fun() { 
    console.log(a);  //underfined
    var a = 2;
}

var a = 3;
fun();

预编译四部曲:

  • 1. 创建执行期上下文(函数执行作用域),即GO/AO对象
  • 2. 找形参和变量声明,将变量和形参明作为AO的属性名,值为undefined(let, const例外)
  • 3. 将实参和形参值统一
  • 4. 找函数声明,值赋予函数体

分析上面的例子:

1. 创建GO对象,并生成对应的属性值

GO: {
    a: undefined,
    fun: function() {...}
}

2. 执行,a赋值3,编译函数fun,再进行fun内部预编译,生成AO

AO: {
    a: undefined
}

3. 执行,输出undefined,a赋值2。

再看一个例子:

var a = 'global';
function fun1 (a) {
    console.log(a); // function a () {}
    function a (a) {
        console.log(a); //undefined
    }
    console.log(a); // function a () {}
    a();
    var a = 'fun1-a';
    console.log(a); //fun1-a
}

function fun2 () {
    var a = 'fun2-a'; 
    fun3();
}

function fun3 () {
    console.log(a); //global
}

fun1();
fun2();

1.  生成GO, 

GO: {
    a: undefined,
    fun1: function fun1() {...},
    fun2: function fun2() {...},
    fun3: function fun3() {...},
}

2. 执行,a赋值global,进行fun1的预编译,生成fun1的AO,将AO写在GO中,是为了体现作用域的层级性,在执行过程中,变量会根据当前执行环境一级一级往上找,直到找到定义的变量为止,若在最外层的GO中都未找到,则抛出 Uncaught ReferenceError

GO: {
    a: 'global',
    fun1: function () {},
    fun2: function () {},
    fun3: function () {},
    AO(fun1): {
        a: function a() {}, //形参和变量整合为undefined,然后找到函数a,将a重新赋值为函数
    }
}

3. 执行,a赋值为fun1-a,函数a的预编译就不写了,很简单。

4. 然后执行到fun2,进行fun2函数内的预编译,生成AO

GO: {
    a: 'global',
    fun1: function () {},
    fun2: function () {},
    fun3: function () {},
    AO(fun1): {
        a: 'fun1-a', //已经赋值
    },
    AO(fun2): {
        a: undefined, //初始化为undefined
    }
}

5. 执行,找到GO中的fun3,进行fun3中的预编译

GO: {
    a: 'global',
    fun1: function () {},
    fun2: function () {},
    fun3: function () {},
    AO(fun1): {
        a: 'fun1-a', //已经赋值
    },
    AO(fun2): {
        a: undefined, //初始化为undefined
    },
    AO(fun3): {
    }
}

6. 执行fun3,在fun3中未定义变量a,则往上找,找到GO中的a,输出global,到这里整个编译过程结束。

三、ES6的let和const的特殊

首先,let和const存在变量提升吗? 更多可参考:https://www.jianshu.com/p/0f49c88cf169

在回答这个问题之前,先看看let的特性:

  • let 声明的变量作用域是块级的
  • let 不能重复定义已存在的变量
  • let 有暂时死区,不会被提升

实践得真知,咱们继续来看例子:

console.log(b);  //Uncaught ReferenceError: Cannot access 'b' before initialization
let b = '111';

咋一看,确实没提升呀。难道答案就是不提升?不存在的,再来看个例子:

let a = 123;
function fun() {
    console.log(a); //Uncaught SyntaxError: Identifier 'a' has already been declared
    let a = 3;
} 

如果真的没有提升,为什么输出的不是undefined?

结论:let 出现了创建变量的提升,但并不会初始化值为undefined,也就是形成了一个暂时性的死区。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值