JavaScript中的作用域和别的语言有所不同,在es6之前只有全局作用域、函数作用域,而es6之后还多了一个块级作用域。而let、var、const三个关键词在不同作用域下定义变量时各有什么特点,会引起什么样的“波澜”?让我们一起揭秘~
js的执行机制
1. JavaScript引擎种类:
JavaScript引擎是一种解释和执行JavaScript代码的软件或硬件组件,也就是JavaScript中能读的懂代码的一个东西,它有两种:
- 浏览器 例如Google Chrome浏览器中是V8引擎。
- node,和浏览器有一些不一样。
2. js代码执行过程
js的代码执行过程分为三步:
- 解析:词法分析(找出标记,如关键字、变量名、操作符)+ 语法分析(形成抽象语法树)。
- 编译:生成可执行的代码。
- 执行:执行代码,并产生相应的输出。
我们以var a = 1为例,在js中代码首先会读取到var、a、=、1,其中空格没有意义,所以不会读取。然后在生成js可以读懂的代码,最后执行声明一个变量a,并把2赋值给a的操作。
作用域
人生tips,不会的就找百度!哈哈哈~作用域的概念还得是百度一下。
1. 程序设计概念
作用域(scope),通常来说,一段程序代码中所用到的变量并不总是有效/可用的,而限定这个变量的可用性的代码范围就是这个变量的作用域。
通俗一点就是,一个变量可以被使用的范围就是作用域。
2. 全局作用域与函数作用域
看一段代码:
// 创建一个变量
var a = 1;
// 声明一个函数
function foo(){
var b = 2;
console.log(a);
}
foo(); // 输出1
console.log(b); // b is not defined
首先,在V8的眼里,整份js文件的代码是写在全局里面的,是一个全局作用域;或<script>标签**里的所有js代码是一个全局作用域。
而一个函数内部就是函数作用域,函数被调用时创建,结束调用后销毁。特别注意的是,形参也是函数体内的有效标识符,就是被声明在函数体内的变量。
在代码中,a在全局中被创建,b在函数foo中被创建,在函数foo被调用时,第七行我们成功输出了a的值为1;如图:
而第十行代码我们在全局中访问函数中声明的b时,显示b没有声明,如图:
由此我们可以得出作用域法则是:内层作用域可以访问外层作用域,外层作用域不能访问内层。
欺骗词法
1. eval()
eval()函数会将传入的字符串当做 JavaScript 代码进行执行。举个例子:
function foo(str,a){
eval(str); // 欺骗词法:转变为var b = 3
// 形参也是有效标识符
console.log(a,b);
}
var b = 2;
foo('var b = 3',1);
在foo调用时,我们传入的是类似于代码的字符串'var b = 3',按道理是没办法被识别成有效标识符的,但是使用了eval函数,让字符串转变为代码var b = 3,即在函数作用域声明了b,并执行在之后的代码。
2. with{}
JavaScript中有一个语句with{},可以修改对象里面的属性。举个例子:
var obj = {
a:1,
b:2,
c:3
}
// obj.a = 2;
// obj.b = 3;
// obj.c = 4;
// with 就是改变对象的属性
with(obj){
a = 2,
b = 3,
c = 4
}
console.log(obj); // obj {a:2,b:3,c:4}
常见的修改对象的属性,我们会使用第七行至第九行的写法;其实也可以使用with,写成12-16行代码的格式修改,打印obj结果就是修改后的对象。
但是,现在官方已经不推荐使用with了,因为它会产生一个bug,来看代码:
var o2 ={
b:2
}
function f(obj){
// with将obj的属性修改的时候,不存在的属性会泄露在全局
with(obj){
a = 2
}
}
f(o2); // 将o2的a改为2
console.log(o2); // { b: 2 }
console.log(a); // 2
我们看到o2中并没有属性a,所以调用f函数后打印o2中也没有a属性添加,这是理所当然的。但是当我们在全局访问变量a时,竟然不是‘a is not defined’,而是存在且值对应了with修改的值。这就是with的bug:当对象中没有属性X时,with修改X属性会导致X泄露到全局。
var、let、const
1. var 声明提升
在js中,var声明的变量会存在声明提升(将变量的声明提升到作用域的顶部);let不会。
我们来看一段代码:
// 先做词法分析,变量的声明提升,提升到作用域的顶部
console.log(a); //undefined -> 能找到a 但是没有定义
var a = 1
// var a
// console.log(a);
// a = 1
// 'Cannot access 'b' before initialization' 定义之前不能访问它
console.log(b)
let b = 1
在代码中我们可以看到,在第三行代码声明变量之前,我们竟然可以访问到a,但是还没有值。其实是js语言在词法分析时(类似是代码5-7行注释),会先将a的声明提升到全局作用域的顶部,才可以访问到a,最后给a赋值。
这其实与大多数语言不相符,所以官方在es6之后打造了一个let关键词,它不会出现声明提升。即在变量没有声明时不允许被访问,所以代码第十行会出现‘定义之前不能访问它’的报错。
2. var 重复声明
var 可以重复声明变量, let不行 在我们的认知里,同一作用域中变量被声明后,是不允许再次被声明的,但是var又有点不一样。
var c = 1
var c = 2 // 可以重复声明变量
console.log(c); // 2
let d = 1
let d = 2 // 报错
在代码中,我们重复使用var声明c时,代码不会报错,并且输出了c值为2,c是被重新赋值了;而我们重复使用let声明变量d时,js就直接给我们提示了代码错误,不能重复声明。所以var是可以重复声明变量的,第二次的声明把第一次的声明覆盖了,效果就是对变量重新赋值,而let是不允许的。
3. 浏览器中的var
var 在全局声明的变量会默认添加在window对象上,let不会。
浏览器环境下,全局是window对象。如图,我们使用var和let分别声明变量a和b,打印window.a和window.b。
我们会发现,用var声明的变量会默认往window对象上添加一个属性为a,而let声明的变量b则不会默认添加在window上。
4. const 声明
const的区别只有:const声明的变量是无法修改的。其他和let一样。
一个细节:
const s = 1
// s = 2
console.log(s);
const person = {
name:'美美',
age:18
}
person.age = 19 // 不会报错
console.log(person);
const在声明对象person后,使用person.age修改person的属性不是修改person变量,所以不会报错的。
块级作用域
什么是块级作用域?我们先看一段代码:
for(var i = 0; i < 10; i++){
console.log(i);
}
console.log(i); // i 声明在全局,可以访问到 i = 10
for(let j = 0; j < 10; j++){
console.log(j);
}
console.log(j); // j is not defined
首先,在for语句中,var声明变量a在全局,所以第四行访问的到i。for语句无法像函数一样形成自己的作用域,致使内部声明的变量污染到全局中,如果外面有需要声明的i就会冲突。所以我们就在思考如果for中形成自己的作用域,那就好了。
我们在for语句中使用let声明变量j,发现在全局找不到它,但是在for循环中可以输出。那这里的for语句就相当于是有了自己的作用域,这个作用域就叫块级作用域。
let + { } 形成块级作用域,不会污染全局。
这有一个细节,我们再看一段代码:
if(true){
let a = 1
var b = 2
}
console.log(a); // a找不到
console.log(b); // 输出2
在上述代码中,我们使用了let + { }创建了一个块级作用域,但是我们会发现{ }中用var声明的变量b是在全局中的。所以let声明的变量和花括号才是块级作用域的变量,不是整个花括号都是块级作用域;所以b还是全局里的。
还有一种情况,看代码:
var foo = true,baz = 10
if(foo){
let bar = 3
if(baz > bar){
console.log(baz); // 输出10
}
}
按道理,在第二层if也是写在全局中,不应该访问到bar,但实际访问到了。所以这里我们还是把第二层if也看做是块级作用域里的更好理解。
最后一个知识点——暂时性死区。
let a = 1
if(true){
// 不能在声明前使用它 (暂时性死区)
console.log(a);
let a = 2
}
按照之前的内层作用域可以访问外层的,内部在还没声明变量a时应该可以在全局访问到a,但是不行,这就是暂时性死区。在V8引擎执行花括号内部的语句时,它先做词法分析知道有let声明了一个变量a,它不会在外面找a。在块级作用域里面,let之前的叫let的死区。
好了,讲到这里,相信你也搞定了全局作用域、函数作用域和块级作用域三个概念~
那我们就下期再见