es6必會之let && const

關鍵詞:
let
, const
, blocking scope
, temporal dead zone
, redeclaration
, reassignment
, immutable
, initializer
一直以來都有用let和const,看似深入學習過,但其實沒有真正完全理解,在模稜兩可的用,之前在城西的一次面試就被問的啞口無言,所以我將通過mdn的資料針對性地學習let 和const,並且會寫一些易懂的例子,也會涉及到一些規範裡的內容。
- block,statement和expression的區別是什麼?
- 為什麼選擇了'let'作為block-scoped variable declaration?
- let和const不會像var一樣繫結值到global 物件!
- let和const不能像var一樣同一個scope下宣告多次!
- let和const不會像var一樣變數宣告提升!
let
let
聲明瞭一個block scope local variable,選擇性為其賦值。
let x = 1; if(x === 1) { let x = 2; console.log(x); // 2 } console.log(x); // 1 複製程式碼
let
使變數的作用域限制於block,statement,或者expression。
block,statement和expression的區別是什麼?
這對於作用域的判別很有用。
- block curly bracket,circle bracket等等。
- statement js由各種語句組成,Control Flow,Function,Iterations等等。
- expression 副作用,無副作用;可執行和關鍵詞。
很多東西自以為會了,然而實際上可能只是走馬觀花,所以回過頭來將基礎撿起來,可能比學習高階技術更重要。
block
- group zero or more statements
- block 由 curly bracket包裹(彎曲的支架比花括號更形象)
- blcok statement 在其他語言裡通常叫做computed statement,之所以叫複合語句,因為JavaScript會一句一句執行,在block 裡可以有多個statement,
var x = 1; let y = 1; if (true) { var x = 2; let y = 2; } console.log(x); console.log(y); 複製程式碼
這裡的block指的是 {var x = 2; let y = 2;}
,注意:不包括 if(true)
,因為它位於curly bracket之外。 Block Statement Syntax:
{ StatementList } 複製程式碼
比起block statement syntax,更重要的是 Block Scoping Rules : 1.var rule: 一種是全域性定義,一種是函式區域性定義。區域性函式變數的訪問會從作用域鏈由下往上找。但是這種老式的變數命名符不提倡再使用。
var x = 1; { var x = 2; } console.log(x); // 2 複製程式碼
2.let && const rule:
let x = 1; { x = 2; } console.log(x); // 2 複製程式碼
const x = 1; { x = 2; } console.log(x); // TypeError: Assignment to constant variable. 複製程式碼
在{ }中應該用var / let /const宣告x,而不是讓其變成全域性變數導致與外部的x變數衝突。 3.function rule:
foo('outside'); // TypeError: foo is not a function { function foo(location) { console.log('foo is called' + location); } foo('inside'); // foo is called inside } 複製程式碼
更準確一些的說法是,block statement阻止function declaration 被 hoisted(變數提升)
到作用域的頂部。這種定義方式與函式表示式類似。 函式表示式的變數提升規則是下面這樣:
foo('before'); // Uncaught TypeError: foo is not a function var foo = function(location) { console.log('foo is called' + location); } foo('after'); // foo is called after 複製程式碼
function塊作用域規則同上:
foo('before'); // TypeError: foo is not a function { function foo(location) { console.log('foo is called' + location); } } foo('after'); // foo is called after 複製程式碼
與函式表示式不會提升到var foo = function(){}一樣;{}內部定義的function,不會提升到{}之前。而這正是function的blocking statement rule。
statement
什麼是empty statement?
var array = [1, 2, 3]; for (i=0; i<array.length; array[i++] = 0) /* empty statement */; console.log(array); 複製程式碼
- Javascript 應用就是由符合語法的多個statement組成的。
- 一條statement可能跨越多行。
- 多條statement也可能在一行中出現,每一句由一個分號標記。
- statement可以分為
Control flow
,Declarations
,Functions and classed
,Iterations
,Others
。
Control flow包括 Block
, break
, continue
, Empty
, if...else
, switch
, throw
, try...catch
。 Declarations包括 var
, let
, const
。 Functions and classed包括 function
, function *
, async function
, return
, class
。 Iterations包括: do...while
, for
, for each...in
, for...in
, for...of
, while
。 Others包括: debugger
, export
, import
, import.meta
, label
, with
。
什麼是Expressions?
- expression指的是任何可以解析為value的程式碼單元。
- 2種有效的expression:有side effect的,例如x = 7;某些情況下執行並且解析為值,例如3 + 4。
- 還有2種分類,一類是執行後為number,string,boolean型的;一類是關鍵詞型別,這種型別又分為Primary expression和Left-hand-side expressions。
Primary expressions Basic keywords and general expressions in JavaScript,例如this,grouping operator.
- this 當前物件的引用,2種呼叫物件方法的方式
this['propertyName']
,this.propertyName
。
function validate(obj, lowval, hival) { if ((obj.value < lowval) || (obj.value > hival)) console.log('Invalid Value!'); } <p>Enter a number between 18 and 99:</p> <input type="text" name="age" size=3 onChange="validate(this, 18, 99);"> 複製程式碼
上面的例子中,this指代input這個DOM物件,它由於具有屬性value,因此可以呼叫validate函式,並且每次輸入值發生變化時都觸發onChange回撥。
- Grouping operator
()
var a = 1; var b = 2; var c = 3; // default precedence a + b * c// 7 // evaluated by default like this (a + b) * c// 9 複製程式碼
Left-hand-side expressions 左值是賦值的目標,例如 new
, super
, Spread operator
。 new 建立一個使用者自定義object型別
var objectName = new objectType([param1, param2, ..., paramN]); 複製程式碼
super 呼叫當前object的父object上的函式,在class中常用。
super([arguments]); // 呼叫parent constructor super.functionOnParent([arguments]); // 呼叫parent上的方法 複製程式碼
Spread operator 允許表示式被展開,可以是函式引數處展開,也可以是陣列迭代處展開。 陣列某處插入陣列元素。
var parts = ['shoulders', 'knees']; var lyrics = ['head', ...parts, 'and', 'toes']; 複製程式碼
一個完整陣列作為引數傳入函式
function f(x,y,z){} var args = [0,1,2]; f(...args); 複製程式碼
通過對block,statement,expression的回顧。我們發現,其實塊作用域不僅僅是curly bracket,{}。在 for(){}
, for(key in object)
, for(item of array)
等等的 ()
內,其實也屬於塊作用域,不僅僅是if else的{},for{}中的{}才算是塊作用域,let都有效。
let a = 1; for(let a = 2; a<3; a++){ console.log(a); }; console.log(a); // 2 1 複製程式碼
let key = 'hello world'; for(let key in {foo:1,bar:2}){ console.log(key); } console.log(key); // foo bar hello world 複製程式碼
若是不用let,會將全域性的key override,所以在for系列的迴圈控制語句中使用let很有必要。
let key = 'hello world'; for(key in {foo:1,bar:2}){ console.log(key); } console.log(key); // foo bar bar 複製程式碼
在 for(item of array)
中也一樣。
let item = 4; for(let item of [1,2,3]){ console.log(item); } console.log(item); // 1 2 3 4 複製程式碼
let item = 4; for(item of [1,2,3]){ console.log(item); } console.log(item); // 1 2 3 3 複製程式碼
使用let以後,井水不犯河水,不用擔心改寫全域性中的同名變數,但是一定要明確,let不僅僅作用於{},()也同樣作用。
為什麼選擇了'let'作為block-scoped variable declaration?
可以看這個stack overflow上的question: ofollow,noindex">Why was the name 'let' chosen for block-scoped variable declarations in JavaScript? 。 有兩點比較重要:
- 參考了scala,F#等語言裡比variable用作更高階抽象的let;
- 一個很有趣的解釋:let myPet = 'dog', let my pet be a dog。
let和const不會像var一樣繫結值到global 物件!
眾所周知,var會繫結變數到global物件(不一定是window,global,還可能是Vue instance),但是let和const不會。
var foo = 1; let bar = 2; const baz = 3; console.log(this.foo, this.bar, this.baz); //1 undefined undefined 複製程式碼
let和const不能像var一樣同一個scope下宣告多次!
let foo = 1; let foo = 2; // Uncaught SyntaxError: Identifier 'foo' has already been declared 複製程式碼
const foo = 1; const foo = 2; // Uncaught SyntaxError: Identifier 'foo' has already been declared 複製程式碼
var foo = 1; var foo = 2; // everything is ok 複製程式碼
let和const不會像var一樣變數宣告提升!
原因是:const,let存在temporal dead zone!
因此不能let ,const賦值前使用變數。
在說變數提升之前,先了解一個概念,Temporal Dead Zone,指的是從block建立到初始化完成之間的時間。用var不會存在Temporal Dead Zone,因為用var宣告的變數,初始值立即預設賦予undefined,不會像let這樣,存在Temporal Dead Zone,不會立即為其賦undefined,所以會報ReferenceError錯誤。
function do_something() { console.log(bar); // undefined console.log(foo); // ReferenceRrror var bar = 1; let foo = 2; } 複製程式碼
正是由於let存在temporal dead zone,沒有 立即 為變數賦初始值為undefined,所以typeof的結果為ReferenceRrror。
console.log(typeof undeclaredVariable); // undefined console.log(typeof i);// ReferenceError,存在temporal dead zone let i = 10; 複製程式碼
var,let,const都會變數提升,但是僅僅是對var foo;let foo;const foo的提升,而不是var foo = 1;let foo =1;const foo = 1;整體提升!
let不會立即為變數賦undefined初值是好是壞呢?當然是好事!這樣將變數的管理更加精細,避免引用重名變數覆蓋後出現bug還發現不了的情況。
還有兩個temporal dead zone的情況:
function test(){ var foo = 33; if (true) { let foo = (foo + 55); // ReferenceError,let foo 存在temporal dead zone } } test(); 複製程式碼
function go(n) { console.log(n); for (let n of n.a) { // ReferenceError,let n 存在temporal dead zone console.log(n); } } go({a: [1, 2, 3]}); 複製程式碼
const
其實在let模組已經寫了很多關於const的內容,所以在這裡就寫一些const特有的特性。
- const也是block-scoped的,和用let定義的變數類似。
- 不可以修改變數值,也就是不可以reassignment,並不是immutable
- 不可以重新定義
- const foo = [value],value可以是function,而let也可以!
- 必須為const賦一個初值且存在temporal dead zone,比let更加嚴格!
const foo = 1; { const foo =2; } 複製程式碼
const foo = 1; foo = 2; // Uncaught TypeError: Assignment to constant variable. 複製程式碼
const foo = 1; const foo = 2; // Uncaught SyntaxError: Identifier 'foo' has already been declared 複製程式碼
let定義的變數賦值function會有什麼錯誤提示呢?
let foo = function(){ console.log('foo'); } foo();// foo 複製程式碼
不會報錯,但是因為let可以reassignment,所以不如const更加安全,因為一般來說,我們建立一個函式以後,不太會再去覆蓋這個函式。
const不可以reassignment,並不是immutable什麼意思?
immutable指的是變數的值完全不可改變,例如'hi',{foo:1,bar:2},若這個字串和物件是immutable的,那麼'hi'完全不能被修改,而且物件{foo:1,bar:2}也完全不能修改,也就是說它的屬性foo和bar值都不能修改,但是const只是約束了reassignment,沒有約束mutable。
下面這種寫法是完全OK的:
const obj = { foo: 1, bar: 2, } obj.foo = 3; console.log(obj); // {foo: 3,bar:2} 複製程式碼
cosnt不賦初值有什麼報錯?
cosnt foo;// Uncaught SyntaxError: Missing initializer in const declaration 複製程式碼
假設修改了原型鏈上的屬性會怎樣?
const foo = 'foo'; foo.length = 5; // return 5 console.log(foo.length); // 3 複製程式碼
我們可以看出,const還是很包容的,即使你試圖修改原型鏈上的屬性,也不會報錯,他只是一笑而過,並且這種修改不會生效。
const真的很嚴格!
型別 | 是否必須賦值 | 是否存在temporal dead zone | 是否支援redeclaration | 是否支援reassignment |
---|---|---|---|---|
var | 否 | 否 | 是 | 是 |
let | 否 | 是 | 否 | 是 |
const | 是 | 是 | 否 | 否 |