在说有关知识之前先让大家看几段代码:
var i = "Hellow world!";
alert(i);
很简单对吧?输出结果是“Hellow world!”,没有任何问题,好,我们继续:
var i = "Hellow World!";
function myFunction(){
alert(i);
}
myFunction();
结果不出意料,还是“Hellow World!”,我们接着看:
var i = "Hello World!";
function myFunction(){
alert(i);
var i = "I love you";
}
myFunction();
大家来猜猜看,结果是什么?
有多少人猜到了运行结果是 undefined
?
那这一段代码呢?
var i = 1;
function myFunction(){
if(!i){
var i = 2;
alert(i);
}
}
myFunction();
为什么结果是 2?
这其中有关 JavaScript 中的一个术语** “提升”(Hoisting)**。
作用域(scope)
在说有关提升的知识之前必须要说一下作用域(Scope)。对于初学者来说,弄清楚作用域是十分重要的。
什么是作用域?
摘自 维基百科
在电脑程序设计中,作用域(scope,或译作有效范围)是名字(name)与实体(entity)的绑定(binding)保持有效的那部分计算机程序。不同的编程语言可能有不同的作用域和名字解析)。而同一语言内也可能存在多种作用域,随实体的类型变化而不同。作用域类别影响变量的绑定方式,根据语言使用静态作用域还是动态作用域变量的取值可能会有不同的结果。
- 包含标识符的宣告或定义;
- 包含语句和/或表达式,定义或部分关于可运行的算法;
- 嵌套嵌套或被嵌套嵌套。
名字空间是一种作用域,使用作用域的封装性质去逻辑上组群起关相的众识别子于单一识别子之下。因此,作用域可以影响这些内容的名字解析。
程序员常会缩进他们的源代码中的作用域,改善可读性。
简单说就是,我们定义一个变量,该变量的有效范围,就是它的作用域。就有了两个名词全局变量、局部变量。其中就涉及到变量的可见性,以及变量的生命周期等细节,这里不做太多赘述,有不清楚的同学可以自行查阅相关资料。
懂得 C 语言的同学来看一下以下这一段代码,如果不懂的同学可以跳过:
#include<stdio.h>
void main(){
int i = 1;
printf("%d,",i);
if(i){
int i = 2;
printf("%d,",i);
}
printf("%d",i);
}
输出结果为1,2,1 。
熟悉 C语言的人都知道为什么会是这么输出。在 C 语言中是块级作用域,在 if()
语句中,声明了同名的局部变量,覆盖了全局变量的值。而当出了块后,局部变量销毁,重新输出全局变量的值。
再稍微科普一下什么是块级作用域:
任何一对花括号({ 和 })中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。
我们再来看一段 JavaScript 代码:
var i = 1;
alert(i);
if (i){
var i = 2;
alert(i);
}
alert(i);
输出的结果为 1,2,2 。
这是因为,在 JavaScript 中是函数级作用域。类似 if
等块语句,并不会创建新的作用域。只有在函数中,才会创建新的作用域,定义在函数中的变量在函数外是不可见的。
那么,该如何解决呢?我们只需要在块级作用域中临时创建新的作用域就可以了:
var i = 1;
if (i) {
function temporary() {
var i = 2;
//其他代码
}
temporary();
}
// i 的值依然为 1
不过在 ES 5 中规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。但是浏览器并没有遵守这个规定,上面的代码实际情况下还是可以正常运行。但是在严格模式中是会报错的。而且在不同的浏览器中对该语句的支持情况不一样,考虑到环境导致差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句(函数提升影响,下文会说)。
看到这里是不是已经有人认为 JavaScript 没有块级作用域,只有函数级作用域了?这句话在前年说,那就是对的。但是在 2015 年 6 月,随着 ECMAScript 6(即 ECMAScript 2015,以后简称ES 6)的发布 ,JavaScript 也拥有块级作用域了。
var
在 JavaScript 中声明的变量要么是全局变量,要么为函数级变量,在 ES 6之前,是没有块级作用域的。在 ES 6 中新增了两个声明变量的方式 let
和 const
。
-
let
声明了一个块级作用域的局部变量,它的作用范围仅仅为最接近的块作用域(如果在所有块以外就是全局作用域),这将会比var
的函数作用域更小。 -
const
声明的是一个只读的常量,而该常量一旦声明,常量的值就无法更改。const
声明的变量不得改变值,也就是说,const
一旦声明变量,就必须立即初始化,不能留到以后赋值。
注意:
-
const
与let
只在声明所在的块级作用域内有效。 -
const
和let
不存在提升。
console.log(i); // 输出 undefined
console.log(j); // 报错
var i = 1;
let j = 1;
- 只要块级作用域内存在
let
或const
命令,它所声明的变量就 绑定(binding)这个区域,不再受外部的影响。ES 6 明确规定,如果区块中存在let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错,在声明变量之前,该变量都是不可用的,只能在声明的位置后面使用。这在语法上,称为暂时性死区(temporal dead zone,简称TDZ)。暂时性死区中不存在变量的提升。
var i = 1;
if (true) {
i = "1"; // 报错
let i;
}
-
count
与let
不可重复声明参数。 - 必须在严格模式下才可以使用
count
和let
。
为什么我先那么大篇幅的介绍了作用域?如果我们能充分理解了作用域,再去理解提升就会比较容易。
提升(hoisting)
变量提升
JavaScript 的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部。但是,变量提升所提升的仅仅为变量的声明,并不会将变量的赋值也提升上来。
"use strict";
function myFunction() {
var x = "Hellow," + y;
alert(x);
var y = "World";
}
myFunction();
输出结果为:Hellow,undefined。
虽然是 strict
模式,但是也不会报错,因为在运行之前,浏览器会先进行一次预编译,会将变量和函数先在函数的最顶部进行预编译。上面的代码经过编译后输出的结果为:
"use strict";
function myFunction() {
var x;
var y;
x = "Hellow," + y;
alert(x);
y = "World";
}
myFunction();
所以结果并不会报错,而会输出 undefined。
所以,在我们写 JavaScript 代码的时候,需要养成习惯,要把变量放在函数级作用域的最顶端,防止出现意外。
函数提升
变量提升是将变量提升到函数级作用域的最顶端,而函数的提升则是将整个函数都提到整个作用域的最顶端。不过函数的声明跟变量的声明有一点不一样。函数的声明会连函数体也会被一同提升。函数有两种声明方式,一种是变量指向的函数表达式,另外一种是函数的声明。需要注意的是,只有函数的声明形式才会被提升。二话不多说,先上代码:
function myFunctionOne(){
myFunctionTwo();
function myFunctionTwo(){
alert("我是 myFunctionTwo");
}
}
myFunctionOne();
输出结果为 :我是 myFunctionTwo。
我们再来看第二段:
function myFunctionOne(){
myFunctionTwo();
var i =function myFunctionTwo(){
alert("我是 myFunctionTwo");
}
}
myFunctionOne();
结果报错。
好的,有关的知识到这里就结束了,现在大家应该明白了有关 JavaScript 中的提升和作用域了吧。
有没有同学在看完了整篇文章之后再回头看第一段代码发现还是没看懂?先把最早的代码贴上来:
var i = "Hello World!";
function myFunction(){
alert(i);
var i = "I love you";
}
myFunction();
不知道有没有人有疑惑,在外部有一个全局变量 i
,但是为什么在函数内部为什么无法引用输出外面的全局变量。就算应用了作用域和提升后也无法解释。
我们都知道,在 JavaScript 中是可以重复声明的,而且重复声明并不会修改赋值。但是,在局部变量中如果声明了重名的全局变量,局部变量就会在作用域中覆盖掉全局变量。
我们只需要对 var i = "I love you";
中的赋值注释掉后运行,然后再将整个语句注释后再运行,就可以的得到验证,两次输出的结果先后为 undefined,Hello World!。
结尾
有关作用域和提升相关的知识就总结了这一些,如有遗漏希望读者们评论补充。这些特性我们总会在不经意间遇到,当我们有时候遇到某些“坑”的时候,要记得想想这些 JavaScript 中的特性,说不定就能找到问题的所在。