写给小白的JavaScript作用域及声明提升详解

434 阅读6分钟

JS作用域

全局作用域与函数体作用域

全局作用域是最大的作用域,在全局作用域中定义的变量可以在任何地方使用。

函数体作用域是在全局作用域下产生的一个较小的作用域,在这个函数体作用域里面声明的变量,只能在这一块区域使用。

以下面这段代码为例:

var a = 1

function fun1 () {
    var b = 2
    console.log(a)
    console.log(b)
}

fun1()

执行后会输出1和2。

整段代码为全局作用域,全局变量a可以在任意地方使用,包括函数里面,所以能输出1,而在函数体里面定义的b,只能在函数体作用域里面被使用,如果console.log(b)在函数体之外,则无法输出b的值。

JS的编译与执行

在深入了解JS的作用域之前,我们先来思考一个问题:

 var a = 1

function fun1 () {
    console.log(a)
}

fun1()

看到这段代码,你知道运行之后会输出1,但是你有没有想过,它为什么能输出1?底层原理是什么?

首先我们要知道,JS在执行代码之前是需要先编译的

执行这段代码之前,需要先编译它,即找到有效标识符:全局变量a和函数fun1。之后再执行:将a赋值为1,调用函数fun1并执行,而函数没有被执行之前是不会被编译的,此时进行函数的编译。在函数体里面没有找到有效标识符,之后执行console.log(a),但是在fun1的函数体作用域中没有a啊,那怎么打印a呢?变量的查找会从内到外去作用域中查找,在fun1的作用域中没有找到,就往外面的全局作用域中找,得到a = 1,于是输出。

我们再来看:

var a = 1

function fun1 () {
    var a = 2

    function fun2 () {
    console.log(a)
    }
}

fun1()
fun2()

执行之后会输出什么呢?1?还是2?

都不对,答案是不输出值,报错。因为变量的查找只能从内到外,而不能从外到内

执行它之前先编译,找到有效标识符全局变量a和函数fun1,再执行a = 1,调用fun1,执行之前先编译,找到fun1中的a和fun2,之后执行a = 2,创建函数fun2。到此fun1调用完毕,下一步调用fun2,但是fun2是在fun1的作用域中的,在fun1的作用域之外不能够向内去查找fun2,所以在fun1外调用fun2会报错。

将代码改成这样就能成功执行啦:

var a = 1

function fun1 () {
    var a = 2

    function fun2 () {
    console.log(a)
    }
    fun2()
}

fun1()

此时执行console.log(a)从内到外的作用域去找a的值,首先在fun1的作用域中找到a = 2,所以输出2.

声明提升

在讲剩下的作用域之前,我们先解释什么是声明提升。

如果我们执行以下代码:

console.log(a);
var a = 1

代码从上而下执行,先打印a再声明a,结果肯定是报错,但错误提示不是未声明变量a(Cannot access 'a' before initialization),而是能找到变量a,但是a没有值(undefined)。

为什么呢?这和我们前文讲到的JS执行之前需要先编译有关。

在执行这两行代码之前,需要先编译,编译后会得到变量a,即在执行之前已经声明a(var a)了,编译完成后执行console.log(a),此时有a但是a还未赋值。

所以,上面的代码等同于:

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

再看一个例子:

fun()

function fun(){
    console.log(123)
}

编译得到函数fun,再执行调用fun,函数声明得到整体提升,此时可以输出123。这段代码等同于:

function fun(){
    console.log(123)
}

fun()

这就是JS的声明提升。

let 和 const

我们再来普及一下let和const。

let a = 1;
console.log(a);

let和var一样,也是用来声明变量的。

再看这个:

let a = 1;
a = 'hello'

JS是弱类型动态语言,声明一个变量时不需要指明其类型,且声明可以改变为其他数据类型的变量,如:

var a = 1
a = 'hello'

这点let和var也一样。

那么它们的不同在哪里呢?

1.let 不会声明提升

console.log(a);
let a = 1;
//执行上面两行代码不会出现undefined而是显示a未声明

2.var可以重复声明变量,而let不能重复声明变量

var a = 1
var a = 2
//可以重复声明同一变量
let a = 1
let a = 2
// 报错,不能重复声明变量

接下来说说const

const用来声明常量,它和let一样不能声明提升和重复声明,声明时必须进行初始化,一旦声明,常量的值就不能变

const a = 1
a = 'hello'
//不可行,不能被修改值

块级作用域

块级作用域是指变量或函数在一个代码块内有效,在代码块外无效的作用域。可以简单理解为:使用let和const声明的变量,只在当前花括号{}内生效,由此构建出了块级作用域。

我们用代码示例来说明:

用let来声明a:

if (1) {//1判定为true
let a = 1
}

console.log(a);
//执行显示undefined,{}形成了作用域=>块级作用域

暂时性死区

let a = 1
if(true){
    console.log(a);//暂时性死区
    let a = 2
}

//执行报错

如果执行以上代码,会出现报错,错误提示为暂时性死区,未声明a,这是因为使用let和const命令声明变量之前,该变量都是不可用的。

欺骗词法作用域

词法作用域即当时所处的作用域,而欺骗词法作用域呢?

我们看以下代码:

function fun(str){
eval(str)
var a=1
console.log(a,b);
}
fun('var b = 2')

执行后输出1,2,为什么呢?我们传进去的不是字符串吗?

这是因为eval() 让原本不属于这里的代码变成就是写在这里的

还有:

function fun(obj){
with(obj){
    a = 2
}
}

var x = {
    b:4
}

fun(x)
console.log(a)

with 当修改对象中不存在的属性时,该属性会泄露到全局变成全局变量

with要修改的a在对象x中根本不存在,所以a泄露变成全局变量,此时执行console.log(a),不会报错而是输出2.

这就是欺骗了词法作用域

此外,还有一个小知识点:

声明变量前面没有关键字,不管在哪个作用域时,都为全局变量

function fun () {
    a = 1
}

fun()
console.log(a);

这时,执行结果为输出1,若a前面有关键字,则a为函数体作用域内的变量,在函数体外无法访问,执行将会报错。