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)呛梆,塊級作用域只能在塊中被訪問,以下兩種情況可以創(chuàng)建塊級作用域的變量磕诊。
- 在函數中
- 在被
{
和}
包裹的塊中
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
聲明)炮障,否則將會觸發(fā)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
變量沒有進行初始化,因此將觸發(fā)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 臨時死區(qū)
使用let
和const
聲明的變量思币,只有在聲明之后才能夠使用,否則會觸發(fā)ReferenceError
錯誤羡微,即使在ES5中可以安全使用的typeof
關鍵字在ES6中也不能保證可以安全使用谷饿。這種特殊行為就叫做臨時死區(qū)(Temporal Death Zone)。
'use strict';
if(true){
console.log(typeof value); // ReferenceError: value is not defined
const value = 'blue';
}
如上面代碼妈倔,由于臨時死區(qū)的存在博投,value
變量在聲明之前是不能被訪問的。
當Javascript
引擎遇到一個代碼塊并且代碼塊中存在變量聲明启涯,那么要么進行變量聲明提升(hoisting)贬堵,將變量提升到函數或者全局作用域頂部進行聲明(使用var
關鍵字);要么將變量放入臨時性死區(qū)(使用const
或者let
關鍵字)结洼,嘗試訪問臨時性死區(qū)中的變量將會導致ReferenceError
錯誤。在遇到變量聲明(const
或者let
關鍵字)后叉跛,該變量將被移出臨時性死區(qū)松忍,變量就可以被訪問了。
'use strict';
console.log(typeof value); // undefined
if(true){
let value = 'blue';
}
如上面代碼筷厘,value
變量并未放入臨時性死區(qū)鸣峭,使用typeof
關鍵字仍然是安全的。
4. 循環(huán)
4.1 循環(huán)的塊級作用域
實際開發(fā)中比較長使用的是在循環(huán)中使用塊級作用域酥艳,在Javascript
中摊溶,var
關鍵字在循環(huán)中使用有著很多不被其他語言開發(fā)者了解的缺陷。
for(var i=0;i<10;i++){
// do something
}
console.log(i); // 10
例如上面代碼中充石,開發(fā)者希望達到的效果是i
在循環(huán)之后不能訪問莫换,但是由于Javascript
會進行變量聲明“提升”,i
在循環(huán)結束之后骤铃,仍然能夠訪問拉岁。
使用let
關鍵字,可以有效避免這個問題惰爬。
for(let i=0;i<10;i++){
// do something
}
console.log(i); // ReferenceError
4.2 函數的循環(huán)
使用var
關鍵字創(chuàng)建的變量在循環(huán)內部的函數中使用有一些問題喊暖。例如下面的代碼,開發(fā)者期待輸出從0
到9
撕瞧,但是由于i
在循環(huán)之后仍然能夠訪問陵叽,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
});
上面的問題可以通過立即執(zhí)行函數(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
});
如上面的代碼沪么,使用立即執(zhí)行函數強制將i
作為參數傳入到每個函數中,這樣每個函數保存的是循環(huán)過程中i
的副本锌半,因此可以正確輸出禽车。
4.3 在循環(huán)中使用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
會在每次循環(huán)過程中殉摔,創(chuàng)建一個新的變量i
,因此每個函數讀取到的是每次循環(huán)的i
的副本记焊,每個i
的副本的值由循環(huán)初始化時i
的值決定逸月。
在for in
和for of
循環(huán)中,let
關鍵字有同樣的特性遍膜。如下面代碼碗硬,在每次循環(huán)時創(chuàng)建一個新key
綁定,這樣每次循環(huán)都一個新的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
關鍵字在循環(huán)中的特性在規(guī)范中定義翰意,但是跟let
關鍵字的“非提升”特性并不相關。實際上信柿,早起的一些let
實現(xiàn)并沒有實現(xiàn)上述在循環(huán)中的特性冀偶。這些特性是逐漸添加的。
4.4 在循環(huán)中使用const
EcmaScript 6規(guī)范并沒有在循環(huán)中使用const
給予禁止渔嚷,但是在不同的循環(huán)類型(for, for in, for of
)进鸠,其行為是不同的。在for
循環(huán)中形病,const
可以在循環(huán)初始化時使用客年,但是如果嘗試修改其值,那么將會拋出錯誤窒朋。如下面代碼搀罢,i
在初始化時時沒問題的,但是在調用i++
時侥猩,將會發(fā)生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
循環(huán)中使用const
關鍵字則不會發(fā)生錯誤
'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
循環(huán)中,每次遍歷都會綁定一個新的key
而不是嘗試修改原來的key
枫弟,因此const
關鍵字在for in, for of
中使用是沒問題的邢享。
4.5 全局作用域
在全局作用域使用let
或者const
關鍵字與var
是不同的。當在全局作用域使用var
關鍵字時淡诗,創(chuàng)建一個新的全局變量骇塘,并將其綁定到全局對象的屬性上。因此韩容,可以使用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
關鍵字會創(chuàng)建在全局作用域創(chuàng)建一個變量请梢,但是并不會寫入到全局對象的屬性赠尾。因此,let
與const
關鍵字創(chuàng)建的變量不會覆蓋全局變量毅弧,而只能創(chuàng)建一個優(yōu)先讀取的“影子”變量气嫁。
// 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對象上杉编。因此,當開發(fā)者不需要在全局對象上創(chuàng)建變量咆霜,使用let
和const
關鍵字在全局作用域創(chuàng)建對象相比var
是安全的。
5. 現(xiàn)有的最佳實踐
由于let
關鍵字的特性更符合其他語言的習慣嘶朱,不會產生var
關鍵字導致的各種問題蛾坯,因此盡量使用let
關鍵字廣被開發(fā)者倡導。
由于大部分變量實際上不會改變疏遏,而修改變量會導致不可測的bug脉课,因此對于那些不會發(fā)生改變的變量,要使用const
關鍵字