塊級作用域
es5語法規則中,只有全局作用域和函數作用域,不存在塊級作用域(一個塊級作用域即爲一個{}內)。
這會導致很多場景不合理。
第一種場景,內層變量可能會覆蓋外層變量。
function test4(){
var tmp =new Date();
function f () {
console.log(tmp); // undefined
if (false) {
var tmp = `hello world`;
}
}
f () ;
};
test4();
第二種場景,用來計數的循環變量泄露爲全局變量。
var s = `hello`;
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
};
console.log(i); // 5
塊級作用域的應用:實際上使得原來廣泛應用的立即執行匿名函數(IIFE)不再必要。
// IIFE 寫法
( function(){
var tmp = `hello dengjing!`;
// 該變量只在該匿名函數作用域內有效
}() );
// 塊級作用域 寫法
{
let tmp = `hello dengjing!`;
}
塊級作用域與函數聲明
function test9(){
function f(){ console.log(`I am outside!`) }
( function(){
if(false){
// 重複聲明一次函數f
function f(){ console.log(`I am inside`); }
}
f();
}() )
}
test9();
上面代碼,在ES5中運行結果:I am inside。因爲被聲明的函數會被提升到函數頭部,實際運行代碼如下:
function test9(){
function f(){ console.log(`I am outside!`) }
( function(){
function f(){ console.log(`I am inside`); }
if(false){
// 重複聲明一次函數f
}
f();
}() )
}
test9();
ES6瀏覽器實現由如下規則:
1、允許在塊級作用域內聲明函數
2、函數聲明類似於var,即會提升到 全局作用域 或 函數作用域 的頭部
3、同時,函數聲明還會提升到所在的塊級作用域的頭部
上段代碼,在符合ES6的瀏覽器中都會報錯,其實際運行的代碼如下所示:
function test9(){
function f(){ console.log(`I am outside!`) }
( function(){
var f = undefined;
if(false){
// 重複聲明一次函數f
function f(){ console.log(`I am inside`); }
}
// console.log(f); // undefined
f();
}() )
}
test9(); //Uncaught TypeError: f is not a function.
總結:考慮到環境導致的行爲差異太大,應避免在塊級作用域內聲明函數。如確有需求,也應寫成函數表達式的形式,而非函數聲明語句。
// 函數聲明語句
{
let a = `hello dengjing!`;
function f(){
return a;
}
}
// 函數表達式
{
let a = `hello dengjing!`;
let f = function(){
return a;
}
}
let
function test5(){
// snippet 1
var a = [];
for(var i = 0;i<10;i++){
a[i] = function(){
console.log(i);
}
}
a[6](); // => 10
}
test5();
上面代碼:變量i是var聲明的,全局範圍有效。全局只有一個變量i,只是每次循環變量i都會重新賦值。而循環內,被賦給數組a函數內部的console.log(i)中的i指向全局變量i。也就是說,所有數組a的成員中的i指向的都是同一個i,導致運行時輸出的是最後一輪的i值,也就是10。
function test6(){
// snippet 2
{
let n = 11;
{ let n = 4;let m = 100; console.log(n);}
// 4
{console.log(n);}
// 11
}
let m = 0;
var a = [];
let i = 9; // location 1
for(let i = 0,m = 10;i<10;i++){ // location 2
// 使用let聲明循環變量i,相當於此處又重新聲明瞭一個變量i,let i ;
// 使用let聲明循環變量i 類似於 {let i = 0;{let i }}
a[i] = function(){
console.log(i);
}
// console.log(ff); // 報錯:ff is not defined
// console.log('m:',m); // Uncaught ReferenceError: Cannot access 'm' before initialization
let m = 20;
console.log('m:',m); // 20
}
a[6](); // => 6
};
test6();
// location 1、location 2、location 3 注意此3處let i的聲明。
let聲明的變量僅在塊級作用域內有效{}。
上面代碼:變量i是let聲明,當前的i只在本輪循環{}有效。so每一次循環的i其實都是一個新的變量。js引擎內部會記住上一輪循環的值,初始化本輪的變量i時,就在上一輪循環的基礎上進行計算。
attention:for循環有一個特別之處,那就是設置循環變量的那部分 是一個父作用域,而循環體內部是一個單獨的子作用域;可以理解爲 {let i = 0;{let i }}。for循環聲明的變量i 與 循環變量i 不在同一個作用域,而是有各自單獨的作用域。
function test7(){
// snippet 3
var a = [];
let i = 0,m = 10;
for(;i<10;i++){
a[i] = function(){
console.log(i);
}
}
console.log('a = '+a);
console.log('i = '+i); // => 10
a[6](); // => 10
};
test7();
1、不存在變量提升
console.log(bar); // Uncaught ReferenceError
let bar = 2;
2、暫時性死區
只要塊級作用域內存在let命令,它所聲明的變量就‘綁定’(binding)這個區域,不再受外部的影響。
ES6明確規定:如果區塊中存在let和const命令,則這個區塊對這些命令聲明的變量從一開始就形成封閉作用域。只要在聲明之前就使用這些變量,就會報錯。
總之,在代碼塊內,使用let命令聲明變量之前,該變量都是不可用的。在語法上稱爲“暫時性死區”(temporal dead zone,簡稱TDZ)。
function test8(){
if(true){
// TDZ開始
tmp = `abc`; // ReferenceError
console.log(tmp); // ReferenceError
let tmp ; // TDZ結束
console.log(tmp); // undefined
tmp = '123';
console.log(tmp); // 123
}
typeof x; // ReferenceError
let x;
// 如一個變量根本沒被聲明,使用typeof則不會報錯
typeof undeclared_variable; // undefined
function bar(x = y,y = 2){
return [x,y];
}
bar(); // 報錯
function bar1(x = 2,y = x){
return [x,y];
}
bar1(); // [2,2]
var y = 1;
var y = y; // 不報錯
let y = y; // ReferenceError: x is not defined
// 以上報錯也是因爲暫時性死區
};
test8();
暫時性死區的本質就是:只要進入當前作用域,所要使用的變量就已經存在,但是不可獲取,只有等到聲明變量的那一行代碼出現後,纔可以獲取和使用該變量。
3、不允許重複聲明
let不允許在同一作用域內部重複聲明同一個變量。
{
{ let insane = `hello world` }
console.log(insane); //報錯
}
{
let insane = `hello world0`;
{ let insane = `hello world1` }
console.log(insane); //報錯
}
const
const聲明一個只讀常量。一旦聲明就必須立即賦值,不能留後賦值,過後變量就不能再次賦值。
因此,對於const而言,只聲明不賦值就會報錯。
const作用域:只在聲明所在的塊級作用域內有效,這點與let相同。
const也存在以下特性:
1、不存在變量提升
2、暫時性死區
3、不允許重複聲明
if(1){
const MAX = 10;
}
// console.log(MAX); // Uncaught ReferenceError: MAX is not defined
if(1){
// console.log(MAX); // Uncaught ReferenceError: Cannot access 'MAX' before initialization
const MAX = 10;
}
var message = `hello dengjing`;
let age = 18;
// Uncaught SyntaxError: Identifier 'message' has already been declared
// const message = `hello dengjing`;
// Uncaught SyntaxError: Identifier 'age' has already been declared
// const age = 18;
const本質
const實際上保證的並不是變量的值不得改動,而是變量指向的那個內存地址不得改動。
我們知道,對於簡單數據類型(數值,字符串,布爾值等)而言,值就保存在變量指向的內存地址中。但是,對於複合數據類型(對象、數組等)而言,變量指向的內存地址保存的只是一個指針,而const只能保證這個指針是固定的,至於指針它指向的數據結構是不是可變的,這是完全無法控制的。
故而,當將一個對象聲明爲常量時必須當心。
const foo = {};
// 爲foo字面量對象添加一個屬性,成功
foo.prop = `I am dengjing.`;
console.log(foo.prop);
// 將foo地址指針改變指向,報錯
// foo = {}; // Uncaught TypeError: Assignment to constant variable.
const a = [];
a.push(`hello`);
a.length = 0;
// a = ['kelly']; // Uncaught TypeError: Assignment to constant variable.
如真想將對象凍結,應使用Object.freeze方法。
對象凍結後,添加新屬性時不起作用,嚴格模式時甚至還會報錯。
const foo = Object.freeze({});
// 常規模式時,下行代碼不起作用
// 嚴格模式時,下行報錯
foo.prop = `I am dengjing.`;
console.log(foo.prop); // undefined
除了將對象本身凍結,對象的屬性也應該凍結。徹底凍結對象函數如下:
var constantize = (obj)=>{
Object.freeze(obj);
Object.keys(obj) && Object.keys(obj).forEach( (key,i)=>{
if(typeof obj[key] === 'object'){
constantize(obj[key]);
}
})
};