1. EcmaScript 5作用域
EcmaScript5的作用域有全局作用域(global scope)与函数作用域(function scope)两种。
1.1 全局作用域
在全局作用域中定义的变量,在整个上下文中都是可以访问的。
var msg = 'Hello world';
console.log(msg); // Hello world
function sayHi(){
console.log(msg);// Hello world
}
sayHi();
上面例子中msg
在sayHi()
函数内外都可以访问。
在NodeJs中,在js文件中直接使用var
关键字声明的变量,将会在模块上声明。
在浏览器中,在script
标签中直接使用var
关键字声明的变量,将定义在全局变量window
上,window对象中的属性拥有全局作用域。
在严格模式(strict mode)下,变量在初始化前必须声明,否则会抛出
ReferenceError
'use strict';
a = 2;
console.log(a); // Uncaught ReferenceError: a is not defined
在非严格模式下,使用未声明的变量将隐式声明为全局变量,定义在window上
a = 2;
console.log(a); // 2
console.log(a === window.a) // true
1.2 函数作用域
在函数作用域中定义的变量,只能在函数中被访问
function fn(){
var a = 1;
console.log(a); // 1
}
console.log(a); // Uncaught ReferenceError: a is not defined
2. 声明提升
声明提升(hoisting)指通过var关键字声明的变量,将提升到函数(或者全局作用域)的顶部进行声明,与声明语句的实际位置无关。
function sayHi(condition){
if(condition){
var msg = 'Hello world';
console.log(msg); // Hello world
}else{
console.log(msg); // undefined
}
}
sayHi(true);
sayHi(false);
上面例子中msg
在if
语句中进行了声明和初始化赋值,但实际上msg
将“提升”到函数顶部进行声明,而赋值的位置不变,还在if
语句中。因此在else
语句中同样可以访问msg
变量,其值为undefined
。Javascript
引擎会将上面的代码处理成类似下面的样子。
function sayHi(condition){
var msg;
if(condition){
msg = 'Hello world';
console.log(msg); // Hello world
}else{
console.log(msg); // undefined
}
}
sayHi(true);
sayHi(false);
3. 块级作用域声明
EcmaScript 6引入了块级作用域(block scope),块级作用域只能在块中被访问,以下两种情况可以创建块级作用域的变量。
- 在函数中
- 在被
{
和}
包裹的块中
3.1 let声明
let
关键字的作用类似var
,用来声明变量,不同的是其声明的变量具有块级作用域。
'use strict';
function sayHi(condition){
if(condition){
let msg = 'Hello world';
console.log(msg); // Hello world
}else{
console.log(msg); // ReferenceError: msg is not defined
}
}
sayHi(true);
sayHi(false);
如上面例子,msg
只能在if
语句块中被访问,在else
中访问会产生ReferenceError
错误
3.2 不能重复声明
使用var
关键字声明的变量,在同一个作用域可以重复进行声明,后面的将会覆盖前面的。而使用let
关键字声明的变量,在同一个作用域不能重复声明(不管之前是用var
,let
, 或者const
声明),否则将会触发SyntaxError
错误。
'use strict';
var a = 1;
var a = 2;
//var b = 1;
//let b = 2; // SyntaxError: Identifier 'b' has already been declared
//let c = 1;
//let c = 2; // SyntaxError: Identifier 'c' has already been declared
const d = 1;
let d = 2; // SyntaxError: Identifier 'd' has already been declared
在变量包含的作用域是用let
关键字声明的变量,不会生成错误。如下面代码,在if
语句中,新的变量a
将覆盖外面的变量a
。
'use strict';
let a = 1;
if(true){
let a = 2;
console.log(a); // 2
}
console.log(a); // 1
3.3 const声明
使用const
关键字声明的变量将作为常量使用,一旦被赋值,将不能再被改变,因此const
关键字声明的变量必须同时进行初始化,否则会抛出SyntaxError
错误
'use strict';
const maxItems = 30;
const name; // SyntaxError: Unexpected token
上面例子中的name
变量没有进行初始化,因此将触发SyntaxError
。
const
关键字与let
关键字相同的地方
- 声明的变量具有块级作用域。
- 在相同的作用域,重复声明变量将会抛出
SyntaxError
错误
const
关键字与let
关键字不同的地方
-
const
声明的变量不能重复进行赋值, 否则将抛出TypeError
错误
const
声明的对象不能改变,但是对象中的属性可以进行改变。const
绑定的是对象的引用,对象中实际的值是可以改变的。
'use strict';
const person = {
name:'Mary'
};
person.name = 'Jim';
person = {
name:'Bary'
}; // TypeError: Assignment to constant variable
3.4 临时死区
使用let
和const
声明的变量,只有在声明之后才能够使用,否则会触发ReferenceError
错误,即使在ES5中可以安全使用的typeof
关键字在ES6中也不能保证可以安全使用。这种特殊行为就叫做临时死区(Temporal Death Zone)。
'use strict';
if(true){
console.log(typeof value); // ReferenceError: value is not defined
const value = 'blue';
}
如上面代码,由于临时死区的存在,value
变量在声明之前是不能被访问的。
当Javascript
引擎遇到一个代码块并且代码块中存在变量声明,那么要么进行变量声明提升(hoisting),将变量提升到函数或者全局作用域顶部进行声明(使用var
关键字);要么将变量放入临时性死区(使用const
或者let
关键字),尝试访问临时性死区中的变量将会导致ReferenceError
错误。在遇到变量声明(const
或者let
关键字)后,该变量将被移出临时性死区,变量就可以被访问了。
'use strict';
console.log(typeof value); // undefined
if(true){
let value = 'blue';
}
如上面代码,value
变量并未放入临时性死区,使用typeof
关键字仍然是安全的。
4. 循环
4.1 循环的块级作用域
实际开发中比较长使用的是在循环中使用块级作用域,在Javascript
中,var
关键字在循环中使用有着很多不被其他语言开发者了解的缺陷。
for(var i=0;i<10;i++){
// do something
}
console.log(i); // 10
例如上面代码中,开发者希望达到的效果是i
在循环之后不能访问,但是由于Javascript
会进行变量声明“提升”,i
在循环结束之后,仍然能够访问。
使用let
关键字,可以有效避免这个问题。
for(let i=0;i<10;i++){
// do something
}
console.log(i); // ReferenceError
4.2 函数的循环
使用var
关键字创建的变量在循环内部的函数中使用有一些问题。例如下面的代码,开发者期待输出从0
到9
,但是由于i
在循环之后仍然能够访问,funcs
中的每个函数引用的是同一个i
,因此在调用后输出了十次10
。
var funcs = [];
for(var i = 0;i < 10;i++){
funcs.push(function(){console.log(i);});
}
funcs.forEach(function(func){
func(); // / outputs 10 ten times
});
上面的问题可以通过立即执行函数(Immidiately Invoked Function Expressions,IIFEs)来解决
var funcs = [];
for(var i = 0;i < 10;i++){
funcs.push((function(val){
return function(){console.log(val);};
})(i));
}
funcs.forEach(function(func){
func(); // outputs 0, then 1, then 2, up to 9
});
如上面的代码,使用立即执行函数强制将i
作为参数传入到每个函数中,这样每个函数保存的是循环过程中i
的副本,因此可以正确输出。
4.3 在循环中使用let
使用let
关键字可以用比较简单清晰的代码解决上面的问题。
'use strict';
var funcs = [];
for(let i = 0;i < 10;i++){
funcs.push(function(){console.log(i);});
}
funcs.forEach(function(func){
func(); // outputs 0, then 1, then 2, up to 9
});
如上面代码,使用let
会在每次循环过程中,创建一个新的变量i
,因此每个函数读取到的是每次循环的i
的副本,每个i
的副本的值由循环初始化时i
的值决定。
在for in
和for of
循环中,let
关键字有同样的特性。如下面代码,在每次循环时创建一个新key
绑定,这样每次循环都一个新的key
变量,因此每个函数输出不同的key
。如果使用var
关键字代替let
,那么所有函数将输出相同的值c
。
var funcs = [],
object = {
a: true,
b: true,
c: true
};
for (let key in object) {
funcs.push(function() {
console.log(key);
});
}
funcs.forEach(function(func) {
func(); // outputs "a", then "b", then "c"
});
注意:let
关键字在循环中的特性在规范中定义,但是跟let
关键字的“非提升”特性并不相关。实际上,早起的一些let
实现并没有实现上述在循环中的特性。这些特性是逐渐添加的。
4.4 在循环中使用const
EcmaScript 6规范并没有在循环中使用const
给予禁止,但是在不同的循环类型(for, for in, for of
),其行为是不同的。在for
循环中,const
可以在循环初始化时使用,但是如果尝试修改其值,那么将会抛出错误。如下面代码,i
在初始化时时没问题的,但是在调用i++
时,将会发生TypeError
错误,因为++
操作尝试修改一个常量的值。
'use strict';
var funcs = [];
for(const i=0;i<10;i++){ // TypeError: Assignment to constant variable
funcs.push(function(){
console.log(i);
});
}
在for in, for of
循环中使用const
关键字则不会发生错误
'use strict';
var funcs = [],
object = {
a: true,
b: true,
c: true
};
// doesn't cause an error
for (const key in object) {
funcs.push(function() {
console.log(key);
});
}
funcs.forEach(function(func) {
func(); // outputs "a", then "b", then "c"
});
如上面代码,使用const
关键字与let
关键字的不同之处只在于,key
变量在块中不能被修改。由于在for in, for of
循环中,每次遍历都会绑定一个新的key
而不是尝试修改原来的key
,因此const
关键字在for in, for of
中使用是没问题的。
4.5 全局作用域
在全局作用域使用let
或者const
关键字与var
是不同的。当在全局作用域使用var
关键字时,创建一个新的全局变量,并将其绑定到全局对象的属性上。因此,可以使用var
覆盖全局对象上已经存在的变量。
// in a browser
var RegExp = "Hello!";
console.log(window.RegExp); // "Hello!"
var ncz = "Hi!";
console.log(window.ncz); // "Hi!"
上面的代码中,window
对象原有的RegExp
被修改,ncz
被定义为一个全局变量,并写入全局对象的属性上。
let
与const
关键字会创建在全局作用域创建一个变量,但是并不会写入到全局对象的属性。因此,let
与const
关键字创建的变量不会覆盖全局变量,而只能创建一个优先读取的“影子”变量。
// in a browser
let RegExp = "Hello!";
console.log(RegExp); // "Hello!"
console.log(window.RegExp === RegExp); // false
const ncz = "Hi!";
console.log(ncz); // "Hi!"
console.log("ncz" in window); // false
如上面代码,RegExp
与window.RegExp
是不同的,ncz
也并不在window对象上。因此,当开发者不需要在全局对象上创建变量,使用let
和const
关键字在全局作用域创建对象相比var
是安全的。
5. 现有的最佳实践
由于let
关键字的特性更符合其他语言的习惯,不会产生var
关键字导致的各种问题,因此尽量使用let
关键字广被开发者倡导。
由于大部分变量实际上不会改变,而修改变量会导致不可测的bug,因此对于那些不会发生改变的变量,要使用const
关键字