前言
JavaScript是一门动态类型、弱类型的语言。相比其他语言,JS在给变量进行声明时,无需确定变量的具体类型,就可以直接声明,例如: a = 1; 也可以在a的前面加上var或者let进行声明。而提到let和var,这里就必须谈谈JS中一个很重要的概念------------作用域。作用域通俗的来说,就是我在某个地方想要使用"他"时,在那个对应的地方是否能找到"他",这个地方我们称为作用域。而了解JavaScript的作用域对未来编写高效、可维护的代码来说是萌新走向大佬不可或缺的一步。
作用域
作用域指的是一个变量的作用范围。通俗来说,就是你定义的变量并不是在代码的每个地方都能拿到的,而限定你是否能拿到你想要的变量就是作用域。正如人们所熟知的动漫中的领域一样,领域之内,他人不能踏足。变量也是这样,他仅仅只能在自己的作用域内有效,超出了自己的作用域,变量就不起作用了。
作用域的类型及其作用
在JavaScript中,作用域分为全局作用域和函数作用域以及块级作用域(ES6开始有了)。其作用是为了隔离变量,不同作用域下同名变量不会发生冲突。
全局作用域
全局作用域,写在script标签中,或者外部js文件的全局范围内的JavaScript代码都是全局作用域。
js
// 全局作用域
a = 1;
var b = 2;
let c = 3;
if(true){
console.log("a =" + a) // 全局变量在判断语句中能调用
}
function foo(){
console.log("b =" + b) // 全局变量在函数中也能调用
}
foo()
console.log("c =" + c)
// 全局作用域
如上述代码所示,在全局作用域内,我们可以使用var,let或者直接声明变量,只要是在全局作用域内,不管是函数内,还是判断语句内,这些被定义的变量都能够被调用。
函数作用域
一个变量声明在函数体内,即在{}内,这种变量我们称之为局部变量,而{}内的代码区域我们通常称之为函数作用域,局部变量只有在函数作用域内才会有效,出了函数就无法调用,但也有例外,后面我们就会聊到的with关键字就会产生出特殊情况。
js
// 全局作用域
var a = 1
var b = 'helo world'
function foo() {
// 函数作用域
var c = true
console.log(a)
// 函数作用域
}
foo()
//console.log(c) //报错! c is not defined!
// 全局作用域
上述代码我们可以看到在函数作用域内定义的变量c出了函数就无法被调用了,V8引擎找不到变量c,所以执行console.log(c)时会报错。下面引入一段函数嵌套的实例代码,让我们充分了解函数作用域。
js
var a = 1
function foo(){
var a = 2
// console.log(a) // 此处输出结果为2
function bar(){
console.log(a)
}
bar()
// bar在foo函数内部调用,不会报错!
}
foo()
// bar函数放外面,而bar函数放在foo函数内部,并为被编译,所以执行的时候找不到函数bar,执行程序报错!
// bar()
分析上述代码,我们在全局变量中声明了变量a的值为1,在foo函数的函数作用域内同样声明了变量a,其值为2,当我们在foo函数的函数作用域内输出a的值时,V8引擎会从内向外开始找a,显然首先找到的就是声明在foo函数内部的值为2的变量a,从而输出结果2。而foo函数内部的bar函数同样如此,若bar函数内部没有重新声明变量a,则仍然从内向外找,最终在foo函数内部找到了值为2的变量a,最终输出结果仍为2。一句话总结:变量的查找是从内部作用域往外部作用域查找的,不能由外到内查找。下面我们介绍上述在讲解函数作用域时所提及到的例外,在介绍它们之前,我们首先得了解什么是欺骗词法作用域。
词法作用域
在介绍欺骗词法作用域时,我们又得先知道什么是词法作用域,它和作用域有什么区别。其实,词法作用域属于是作用域的一种工作模型,而"作用域"相当于一套规则,用来管理引擎如何在对应的作用域内查找我们所需要的值。作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域,另一种叫做动态作用域。如何理解词法作用域呢,通俗来说,词法作用域是由你在写代码时将变量和块作用域 (这个等下会介绍)写在哪里来决定的,即定义在词法阶段(编译器对字符进行词法化)的作用域。
欺骗词法作用域
了解了词法作用域,你是否想过一个问题,在我们写代码期间,词法作用域会由我们写的函数所声明的位置来决定的,那么我们是否可以修改(换句话说是欺骗)词法作用域呢?答案是肯定的。在JS中,有两种办法来实现这个目的,第一种是JS中的eval(..)函数,它可以接受一个字符串为参数,并将字符串转化为对应的代码语句,下面演示一段代码:
js
function foo(str,a){
eval(str); // 可以直接将字符串转换为语句
console.log(b,a);
}
foo('var b = 3',2)
// 结果: 3 2
分析上述代码可知,在foo()函数作用域内,我们并没有直接声明变量b,而在全局作用域中调用foo()函数时我们也只是传入字符串类型的实参,eval()函数将我们传入的字符串'var b = 3'自动转化为了代码语句 var b = 3,相当于在函数内部声明了一个变量b值为3,所以最终我们还是拿到了b的值,最终输出结果 3 2 。下面我将给大家介绍第二种欺骗词法作用域的方法--with关键字
js
var obj = {
a:1,
b:2,
c:3
}
with(obj){
a = 3
b = 4
c = 5
d = 6
}
console.log(obj)
console.log("d ="+d)
// 最终输出结果:{a: 3, b:4, c:5} d =6
上述代码我们声明了一个对象obj,里面存放了a,b,c三个属性。而with关键字可以直接对obj对象里面的属性进行修改,我们同时修改了a,b,c,d的属性。咦,这时你可能会疑惑,在我们声明的obj对象中并没有d这个属性,而with关键字却修改了obj中的d属性,这样是否会报错呢,对并未声明的值进行修改,通常来看显然执行不会通过,但在这里确是可行的。其原因是因为 with(){} 当修改对象中不存在的属性时,会将该属性直接泄露到全局作用域,所以这里可以直接输出d的值。上述两种方式都能通过特定的方式从而达到欺骗词法作用域的效果,相信看完这些,你对欺骗词法作用域也有了初步的认识。下面我将带大家探索最后一种作用域的秘密--块级作用域。
块级作用域
简单来说,块级作用域就是由{}大括号包裹并且没有和函数结合的被称为块级作用域。这是ES6中新增的,其中,let和const在{}中声明的变量为局部变量,而var在块级作用域中声明的变量是全局变量,let相对于var来说不能重复定义。并且块级作用域之间也是不能相互访问的。简单来说分为以下几种:
js
// 1.光一个{}
{
// 块级作用域
}
// 2.循环语句中的{}
for(;;){
// 块级作用域
}
while(..){
// 块级作用域
}
// 3.判断语句中的{}
if(){
// 块级作用域
}
// 4.定义对象时
var obj = {
// 块级作用域
}
最后的最后我要带领大家去摘下"let关键字"的神秘面纱,在今后代码书写中,我们往往会更多的使用let进行声明而不是var,这是为什么?一段代码带大家领略其中的奥秘。
js
// ----------------------------------------------------------------
var a = 1
function foo() {
console.log(a);
var a = 1;
}
foo()
// ----------------------------------------------------------------
// ----------------------------------------------------------------
function foo1() {
console.log(a);
let a = 2;
}
foo1()
// ----------------------------------------------------------------
上述代码中,调用foo函数,我们能输出a的结果吗,答案是否定的,这是为什么呢,我们不是在函数内部声明了变量a吗,为什么却拿不到a的值呢。其实,在编译器识别到console.log(a); var a = 1; 这两行代码时,会自动将它们读成 var a; console.log(a); a = 1;而当V8引擎执行到代码console.log(a)时,它能知道存在变量a,但由于没有执行第三行代码 a = 1; 并无法修改a的值,即使在全局作用域声明了变量a的值为1,但由于var可以对同一个变量进行二次声明,第二次并未声明值,所以最终输出的结果为undefined,找不到a的值,这就是var关键字所造成的变量提升,很明显这破坏了代码的逻辑性。而改用let则不会造成这样的错误,let在你进行二次声明时就会报错,最后运行代码报错,这是因为console.log(a) 运行时发生暂时性死区 局部变量和全局变量有相同名字定义时,不会去读取全局变量,即便先使用后定义。