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为函数体作用域内的变量,在函数体外无法访问,执行将会报错。