过去,JavaScript的变量声明机制不像C语言一样,在声明变量的同时也会创建变量(绑定)。在以前的JavaScript中何时创建变量要看怎么声明变量。在以前的变量作用域有全局作用域和局部作用域,但不像其他的程序语言有块作用域一说。在ES6中新引入的块级作用域绑定机制。
var
声明及变量提升
在函数作用域或全局作用域中通过var
声明的变量,不管是在哪声明的,都会被当成在当前作用域顶部声明的变量,这也被称之为变量提升。拿个示例来说:
function getValue (condition) {
console.log(value); // => undefined
if (condition) {
var value = 'w3cplus';
console.log(value); // => w3cplus
return value; // => 如果condition为true,返回w3cplus
} else {
console.log(value); // => undefined
return null; // => 如果condition为false,返回null
}
console.log(value); // => undefined
}
getValue(true); // => w3cplus
getValue(false); // => null
刚接触JavaScript的时候,一直以为condition
为true
时才会创建value
变量。而事实上,不管condition
不管是为true
还是false
都已经创建了value
变量。在预编译阶段,JavaScript引擎会将上面的getValue()
函数修改成:
function getValue(condition) {
var value;
console.log(value);
if (condition) {
var value = 'w3cplus';
console.log(value);
return value;
} else {
console.log(value);
return null;
}
console.log(value);
}
变量value
被提升到函数顶部,而初始化操作依旧留在原处执行,这也就是说else {}
中也可以访问到value
变量,而且此时的value
并未初始化,所以其值为undefined
。
变量提升,简单的理解,就是把变量提升至函数的最顶部地方。需要说明的是:变量提升只是提升变量的声明,并不会把赋值也提升上来,没有赋值的变量初始值是undefined
。所以上面就出现了声明为undefined
的var
,因为赋值在后面声明提升在了前面。
还有一点需要注意的是因为JavaScript是函数级作用域,只有函数才会创建新的作用域,而不像其他语言有块级作用域,比如if
语句块。就上面的示例而言,不管会不会进入if
语句块,函数声明都会提升到当前作用域的顶部,得到执行。在JavaScript并不会创建一个新的作用域。
扩展阅读:
- JavaScript的变量:变量提升
- JavaScript中的作用域
- 变量提升
- 变量作用域
- JavaScript的作用域和提升机制
- 一篇文章弄懂JavaScript中作用域和上下文
- 深入理解javascript中的作用域
- 解释 JavaScript 的作用域和闭包
- 图解Javascript上下文与作用域
块级声明
把上面的示例做一下调整,如下:
console.log(value); // => ReferenceError: value is not defined
function getValue(condition) {
console.log(value); // => undefined
if (condition) {
var value = 'w3cplus';
console.log(value); // => 如查condition为true, 输出w3cplus
return value;
} else {
console.log(value); // => 如果condition为false, 输出undefined
return null;
}
console.log(value); // => undefined
}
getValue(true); // => w3cplus
getValue(false); // => null
在函数外调用value
会报错ReferenceError: value is not defined
错误信息。也就是说在函数体内声明的变量,在函数体外是无法调用的。这里就涉及到了全局作用域和局部作用域相关的概念。这里暂且不说。但在函数内部的我们称之为块级作用域。上面的示例也说明,块级里面声明的变量只能经块级作用域中使用,在指定块的作用域之外无法访问块级声明。简而言之,块内声明的变量,在块外无法使用。
在JavaScript中块级作用域不仅存在于函数内部,也存在于块中,比如{}
(if
,for
这样的语块)。如果在if
或for
这样的语句块中,使用var
声明的变量,在外部(除函数体外)是可以被访问到的,只不过有可能其值是undefined
。为了让JavaScript中能像其他的程序语言一样,所以引入了块级作用域,让JavaScript变得更灵活也更普通。
let
和const
在ES6中引入了let
和const
关键词用来声明变量。let
和var
类似都是用来声明变量的,不同的是,let
声明的变量的作用域名限制在当前代码块中。比如文章开头的示例,把if
语句块中的var
替换成let
,结果就又将不一样:
function getValue(condition) {
console.log(value); // => ReferenceError: value is not defined
if (condition) {
let value = 'w3cplus';
console.log(value); // => w3cplus
return value; // => w3cplus
} else {
console.log(value); // => 如果condition为false,程序执行到此报错:ReferenceError: value is not defined
return null;
}
console.log(value); // => ReferenceError: value is not defined
}
getValue(true);
由let
声明的变量,不会像var
声明的变量一样被提升至函数顶部。执行流离开if
语句块,value
会立即被销毁。如果condition
的值为false
,就永远不会声明并初始化value
。
let
声明的变量没有变量提升。
ES6中还提供了const
关键词来声明变量,但这个变量的值是不变的,也被称之为常量。其值一旦被设定后不可更改。因此,每个通过const
声明的常量必须进行初始化。
// 有效的常量
const MAX_ITEMS = 30;
// 语法错误:常量未初始化
const MAX_ITEMS;
const
和let
类似,声明的变量都只能在块作用域下有效,所以常量也只在当前代码块内有效,一旦执行到块外会立即被销毁。也就是说,每个通过const
声明的常量也不会有变量提升。
与var
不同,let
和const
声明的变量不会被提升到作用域顶部,如果在声明之前访问这些变量,即使是相对安全的typeof
操作符也会触发引用错误。
在JavaScript中,使用let
和const
声明变量有一个重要的特征,大家常称之为临时死区(TDZ),也常用TDZ来描述let
和const
的不提升效果。
JavaScript引擎在扫描代码发现变量时,要么将它们提升至作用域顶部(遇到var
声明的变量),要么将声明放到TDZ中(遇到let
和const
声明的变量)。访问TDZ中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从TDZ中移出,然后方可正常访问。
有关于这方面的更多信息,可以阅读下面这些文章:
- 深入浅出ES6:
let
和const
- 变量声明
- ES6中的变量和作用域
- ES6中的变量和作用域
- Let’s use const! Here’s why.
- How
let
andconst
are scoped in JavaScript - ES6
let
VSconst
variables - JavaScript的词法作用域
循环中的块作用域绑定
大家在ES6之前写for
循环应该有碰到下面这样的场景:
// 场景一
var arr = [];
for (var i = 0; i < 3; i++) {
arr.push(function(){
return i;
})
}
console.log(arr.map(function(x){return x();})); // => [3, 3, 3]
// 场景二
for (var i = 0; i < 5; ++i) {
setTimeout(function (){
console.log(i); // => 输出'5'五次
}, 100)
}
这不是我们想要的结果。长久以来,var
声明让开发者在循环中创建函数变得异常困难,因为变量到了循环体外还是能被访问。正如上面的代码所示,场景一得到的是[3,3,3]
,场景二得到连续输出五次的5
。这一切都是因为循环里的每次迭代同时共享着变量i
,循环内部创建的函数全都保留了对相同变量的引用。循环结束时变量i
的值为5
,所以每次调用console.log(i)
时就会输出数字5
(上例中的场景二)。
为了解决这个问题,开发者们在循环中使用立即调用函数表达式( IIFE),以强制生成计数器变量的副本,如下所示:
var arr = [];
for (var i = 0; i < 3; i++) {
arr.push(function(value){
return function() {
console.log(value);
}
}(i));
}
arr.map(function(x){
return x(); // => [0, 1, 2]
})
在循环内部,IIFE表达式为接受的每一个变量i
都创建了一个副本并存储为变量value
。这个变量的值就是相应迭代创建的函数所使用的值,因此调用每个函数都会从0
到2
循环一样得到期望的值。
有关于JavaScript中IIFE相关资料:
- JavaScript中的立即执行函数
- 立即调用的函数表达式
- JavaScript中的立即执行函数表达式
- 立即执行函数表达式(IIFE)
- Understanding Immediately-Invoked Function Expressions 1
- Understanding Immediately-Invoked Function Expressions 2
- Immediately-Invoked Function Expression (IIFE)
- IMMEDIATELY INVOKED FUNCTION EXPRESSION
- What (function (window, document, undefined) {})(window, document); really means
在ES6中就不要这么蛋疼了,使用ES6中的let
和const
提供的块级绑定会让事情简单的多。
循环中的let
let
声明模仿了上面所描述的IIFE所做的一切来简化循环过程,每次迭代循环都会创建一个新变量,并以之前迭代中同名变量的值将其初始化。也就是说,上面的代码换成这样即可得到我们想要的值:
var arr = [];
for (let i = 0; i < 3; i++) {
arr.push(function(){
return i;
})
}
console.log(arr.map(function(x){return x();})); // => [0, 1, 2]
循环中运到let
声明的变量都会创建一个新的变量i
,并将其初始化为i
的当前值,所以循环内部创建的每个函数都能得到属于它们自己的i
的副本。
除了在for
中之外,在for-in
和for-of
循环中作用是类似的。
循环中的const
循环中的let
声明的变量能得到IIFE的功效,那么是不是说const
在for
这样的循环体头部也能达到类似IIFE的功效呢?因为前面也说过,const
有点类似于let
。至于是不是如此,先把上同的示例换成下面的代码:
var arr = [];
for (const i = 0; i < 3; i++) {
arr.push(function(){
return i;
})
}
console.log(arr.map(function(x){return x();}));
并不如我们所想,如果把for
循环中的let
直接换成const
之后,执行上面的代码会报错:
// => TypeError: Assignment to constant variable.
为什么会如此呢?仔细回忆一下。前面提到过,使用const
声明的变量是一个常量,那么在上面的示例中,变量i
就声明为常量。在循环的第一次迭代中,i
是0
,迭代执行成功。然后执行i++
,代码试图修改常量。如此一来就违背了const
的原则。使用const
声明的常量,是不能修改的。这样一来就报TypeError
错误。所以说,如果后续循环体内不会修改该变量,那么就可以使用const
来声明,否则不能使用const
声明变量。
在for-in
或for-of
循环中使用const
时的行为与使用let
一致。比如下面的代码就不会报错:
var funcs = [];
var obj = {
name: 'w3cplus'.
age: 7,
job: 'FE'
};
for (const key in obj) {
funcs.push(function (){
console.log(key);
})
}
funcs.map(function(x){
return x(); // => name, age, job
})
const
在for-in
和for-of
循环中能正常运行,那是因为每次迭代不会像for
循环一样修改已有绑定,而是会创建一个新绑定。
全局块作用域绑定
let
和const
与var
的另一个区别是它们在全局作用域中的行为。当var
被用于全局作用域时,它会创建一个新的全局变量作为全局对象的属性。这意味着用var
很可能会无意中覆盖一个已经存在的全局变量,如下:
// 在浏览器中
var RegExp = 'w3cplus';
console.log(window.RegExp); // => w3cplus
var name = 'damo';
console.log(window.name); // => damo
如果在全局作用域中使用let
或const
,会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性。换句话说, 用let
或const
不能覆盖全局变量,而只能遮蔽它。如此一来,如果不想为全局对象创建属性,则使用let
和const
要安全得多。
var
和 let
简单的概括一下:
- 通过
var
声明的变量,它的作用域是在function
或任何外部已经被声明的function
,是全域的 - 通过
let
声明的变量,它的作用域是在一个块
比如:
function varvslet() {
console.log(i); // i 是 undefined 的,因为变量提升
// console.log(j); // ReferenceError: j 没有被定义
for( var i = 0; i < 3; i++ ) {
console.log(i); // 0, 1, 2
};
console.log(i); // 3
// console.log(j); // ReferenceError: j 没有被定义
for( let j = 0; j < 3; j++ ) {
console.log(j);
};
console.log(i); // 3
// console.log(j); // ReferenceError: j 没有被定义
}
两者区别:
- 变量提升:
let
不会被提升到整个块的作用域。相比之下,var
可以被提升 - 循环中的闭包:
let
在每次循环可以重新被绑定,确保在它之前结束的循环被重新赋值,所以在闭名中它被用来避免一些问题
那我们应该用let
替代var
吗?
不是的,
let
是新的块作用域。语法强调在var
已经是区块作用域时,let
应该替换var
,否则请不要替换var
。let
改善了在 JavaScript 作用域的选项,而不是取代。var
对于变量依旧是有用的,可被用在整个function
之中。
块级绑定最佳实践
很多人都认为,在ES6中应该默认使用let
而不是var
。对于很多JavaScript开发者而言,let
实际上与他们想要的var
一样,直接替换也符合逻辑。这种情况下,对于需要写保护的变量则要使用const
。
如果你开始使用ES6的话,默认使用const
,只有确实需要改变变量的值时使用let
。因为大部分变量的值在初始化之后不应该再改变,而预料外的变量值的改变是很多Bug的源头。
总结
块级作用域绑定的let
和const
为JavaScript引入词法作用域,它们声明的变量不会提升,而且只可以在声明这些变量的代码块中使用。虽然这个功能给我们带来很多方便之处,但也存在一个副作用:不能在声明变量前访问它们,就算是typeof
这样安全的操作符也不行。在声明前访问块绑定会导致错误,因为绑定还在临时死区(TDZ)中。
let
和const
的行为很多时候和var
一致。然而,它们在循环中的行为运不一样。在for-in
和for-of
循环中,let
和const
都会每次迭代时创建新绑定,从而使循环体内创建的函数可以访问到相应的迭代值,而非最后一次迭代后的值(像使用var
一样)。let
在for
循环中同样如此,但在for
循环中使用const
声明则有可能会引发错误。
综合所述,在ES6中声明变量时,默认使用const
,只在确实需要改变变量的值时使用let
。这样就可以在某种程度中实现代码的不可变,从而防止某些错误的产生。
如需转载,烦请注明出处:https://www.w3cplus.com/javascript/es6-block-scoping.html