let
和const
是JavaScript里相對較新的變量聲明方式。 像我們之前提到過的, let
在很多方面與var
是相似的,但是可以幫助大家避免在JavaScript里常見一些問題。 const
是對let
的一個增強(qiáng),它能阻止對一個變量再次賦值。
因?yàn)門ypeScript是JavaScript的超集,所以它本身就支持let
和const
。 下面我們會詳細(xì)說明這些新的聲明方式以及為什么推薦使用它們來代替 var
。
如果你之前使用JavaScript時沒有特別在意,那么這節(jié)內(nèi)容會喚起你的回憶。 如果你已經(jīng)對 var
聲明的怪異之處了如指掌,那么你可以輕松地略過這節(jié)。
var
聲明一直以來我們都是通過var
關(guān)鍵字定義JavaScript變量。
var a = 10;
大家都能理解,這里定義了一個名為a
值為10
的變量。
我們也可以在函數(shù)內(nèi)部定義變量:
function f() {
var message = "Hello, world!";
return message;
}
并且我們也可以在其它函數(shù)內(nèi)部訪問相同的變量。
function f() {
var a = 10;
return function g() {
var b = a + 1;
return b;
}
}
var g = f();
g(); // returns 11;
上面的例子里,g
可以獲取到f
函數(shù)里定義的a
變量。 每當(dāng) g
被調(diào)用時,它都可以訪問到f
里的a
變量。 即使當(dāng)g
在f
已經(jīng)執(zhí)行完后才被調(diào)用,它仍然可以訪問及修改a
。
function f() {
var a = 1;
a = 2;
var b = g();
a = 3;
return b;
function g() {
return a;
}
}
f(); // returns 2
對于熟悉其它語言的人來說,var
聲明有些奇怪的作用域規(guī)則。 看下面的例子:
function f(shouldInitialize: boolean) {
if (shouldInitialize) {
var x = 10;
}
return x;
}
f(true); // returns '10'
f(false); // returns 'undefined'
有些讀者可能要多看幾遍這個例子。 變量 x
是定義在*if
語句里面*,但是我們卻可以在語句的外面訪問它。 這是因?yàn)?nbsp;var
聲明可以在包含它的函數(shù),模塊,命名空間或全局作用域內(nèi)部任何位置被訪問(我們后面會詳細(xì)介紹),包含它的代碼塊對此沒有什么影響。 有些人稱此為* var
作用域或函數(shù)作用域*。 函數(shù)參數(shù)也使用函數(shù)作用域。
這些作用域規(guī)則可能會引發(fā)一些錯誤。 其中之一就是,多次聲明同一個變量并不會報錯:
function sumMatrix(matrix: number[][]) {
var sum = 0;
for (var i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (var i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}
這里很容易看出一些問題,里層的for
循環(huán)會覆蓋變量i
,因?yàn)樗?code>i都引用相同的函數(shù)作用域內(nèi)的變量。 有經(jīng)驗(yàn)的開發(fā)者們很清楚,這些問題可能在代碼審查時漏掉,引發(fā)無窮的麻煩。
快速的猜一下下面的代碼會返回什么:
for (var i = 0; i < 10; i++) {
setTimeout(function() { console.log(i); }, 100 * i);
}
介紹一下,setTimeout
會在若干毫秒的延時后執(zhí)行一個函數(shù)(等待其它代碼執(zhí)行完畢)。
好吧,看一下結(jié)果:
10
10
10
10
10
10
10
10
10
10
很多JavaScript程序員對這種行為已經(jīng)很熟悉了,但如果你很不解,你并不是一個人。 大多數(shù)人期望輸出結(jié)果是這樣:
0
1
2
3
4
5
6
7
8
9
還記得我們上面講的變量獲取嗎?
每當(dāng)
g
被調(diào)用時,它都可以訪問到f
里的a
變量。
讓我們花點(diǎn)時間考慮在這個上下文里的情況。 setTimeout
在若干毫秒后執(zhí)行一個函數(shù),并且是在for
循環(huán)結(jié)束后。 for
循環(huán)結(jié)束后,i
的值為10
。 所以當(dāng)函數(shù)被調(diào)用的時候,它會打印出 10
!
一個通常的解決方法是使用立即執(zhí)行的函數(shù)表達(dá)式(IIFE)來捕獲每次迭代時i
的值:
for (var i = 0; i < 10; i++) {
// capture the current state of 'i'
// by invoking a function with its current value
(function(i) {
setTimeout(function() { console.log(i); }, 100 * i);
})(i);
}
這種奇怪的形式我們已經(jīng)司空見慣了。 參數(shù) i
會覆蓋for
循環(huán)里的i
,但是因?yàn)槲覀兤鹆送瑯拥拿郑晕覀儾挥迷趺锤?code>for循環(huán)體里的代碼。
let
聲明現(xiàn)在你已經(jīng)知道了var
存在一些問題,這恰好說明了為什么用let
語句來聲明變量。 除了名字不同外, let
與var
的寫法一致。
let hello = "Hello!";
主要的區(qū)別不在語法上,而是語義,我們接下來會深入研究。
當(dāng)用let
聲明一個變量,它使用的是詞法作用域或塊作用域。 不同于使用 var
聲明的變量那樣可以在包含它們的函數(shù)外訪問,塊作用域變量在包含它們的塊或for
循環(huán)之外是不能訪問的。
function f(input: boolean) {
let a = 100;
if (input) {
// Still okay to reference 'a'
let b = a + 1;
return b;
}
// Error: 'b' doesn't exist here
return b;
}
這里我們定義了2個變量a
和b
。 a
的作用域是f
函數(shù)體內(nèi),而b
的作用域是if
語句塊里。
在catch
語句里聲明的變量也具有同樣的作用域規(guī)則。
try {
throw "oh no!";
}
catch (e) {
console.log("Oh well.");
}
// Error: 'e' doesn't exist here
console.log(e);
擁有塊級作用域的變量的另一個特點(diǎn)是,它們不能在被聲明之前讀或?qū)憽?雖然這些變量始終“存在”于它們的作用域里,但在直到聲明它的代碼之前的區(qū)域都屬于 時間死區(qū)。 它只是用來說明我們不能在 let
語句之前訪問它們,幸運(yùn)的是TypeScript可以告訴我們這些信息。
a++; // illegal to use 'a' before it's declared;
let a;
注意一點(diǎn),我們?nèi)匀豢梢栽谝粋€擁有塊作用域變量被聲明前獲取它。 只是我們不能在變量聲明前去調(diào)用那個函數(shù)。 如果生成代碼目標(biāo)為ES2015,現(xiàn)代的運(yùn)行時會拋出一個錯誤;然而,現(xiàn)今TypeScript是不會報錯的。
function foo() {
// okay to capture 'a'
return a;
}
// 不能在'a'被聲明前調(diào)用'foo'
// 運(yùn)行時應(yīng)該拋出錯誤
foo();
let a;
關(guān)于時間死區(qū)的更多信息,查看這里Mozilla Developer Network.
我們提過使用var
聲明時,它不在乎你聲明多少次;你只會得到1個。
function f(x) {
var x;
var x;
if (true) {
var x;
}
}
在上面的例子里,所有x
的聲明實(shí)際上都引用一個相同的x
,并且這是完全有效的代碼。 這經(jīng)常會成為bug的來源。 好的是, let
聲明就不會這么寬松了。
let x = 10;
let x = 20; // 錯誤,不能在1個作用域里多次聲明`x`
并不是要求兩個均是塊級作用域的聲明TypeScript才會給出一個錯誤的警告。
function f(x) {
let x = 100; // error: interferes with parameter declaration
}
function g() {
let x = 100;
var x = 100; // error: can't have both declarations of 'x'
}
并不是說塊級作用域變量不能在函數(shù)作用域內(nèi)聲明。 而是塊級作用域變量需要在不用的塊里聲明。
function f(condition, x) {
if (condition) {
let x = 100;
return x;
}
return x;
}
f(false, 0); // returns 0
f(true, 0); // returns 100
在一個嵌套作用域里引入一個新名字的行為稱做屏蔽。 它是一把雙刃劍,它可能會不小心地引入新問題,同時也可能會解決一些錯誤。 例如,假設(shè)我們現(xiàn)在用 let
重寫之前的sumMatrix
函數(shù)。
function sumMatrix(matrix: number[][]) {
let sum = 0;
for (let i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (let i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}
這個版本的循環(huán)能得到正確的結(jié)果,因?yàn)閮?nèi)層循環(huán)的i
可以屏蔽掉外層循環(huán)的i
。
通常來講應(yīng)該避免使用屏蔽,因?yàn)槲覀冃枰獙懗銮逦拇a。 同時也有些場景適合利用它,你需要好好打算一下。
在我們最初談及獲取用var
聲明的變量時,我們簡略地探究了一下在獲取到了變量之后它的行為是怎樣的。 直觀地講,每次進(jìn)入一個作用域時,它創(chuàng)建了一個變量的 環(huán)境。 就算作用域內(nèi)代碼已經(jīng)執(zhí)行完畢,這個環(huán)境與其捕獲的變量依然存在。
function theCityThatAlwaysSleeps() {
let getCity;
if (true) {
let city = "Seattle";
getCity = function() {
return city;
}
}
return getCity();
}
因?yàn)槲覀円呀?jīng)在city
的環(huán)境里獲取到了city
,所以就算if
語句執(zhí)行結(jié)束后我們?nèi)匀豢梢栽L問它。
回想一下前面setTimeout
的例子,我們最后需要使用立即執(zhí)行的函數(shù)表達(dá)式來獲取每次for
循環(huán)迭代里的狀態(tài)。 實(shí)際上,我們做的是為獲取到的變量創(chuàng)建了一個新的變量環(huán)境。 這樣做挺痛苦的,但是幸運(yùn)的是,你不必在TypeScript里這樣做了。
當(dāng)let
聲明出現(xiàn)在循環(huán)體里時擁有完全不同的行為。 不僅是在循環(huán)里引入了一個新的變量環(huán)境,而是針對 每次迭代都會創(chuàng)建這樣一個新作用域。 這就是我們在使用立即執(zhí)行的函數(shù)表達(dá)式時做的事,所以在 setTimeout
例子里我們僅使用let
聲明就可以了。
for (let i = 0; i < 10 ; i++) {
setTimeout(function() {console.log(i); }, 100 * i);
}
會輸出與預(yù)料一致的結(jié)果:
0
1
2
3
4
5
6
7
8
9
const
聲明const
聲明是聲明變量的另一種方式。
const numLivesForCat = 9;
它們與let
聲明相似,但是就像它的名字所表達(dá)的,它們被賦值后不能再改變。 換句話說,它們擁有與 let
相同的作用域規(guī)則,但是不能對它們重新賦值。
這很好理解,它們引用的值是不可變的。
const numLivesForCat = 9;
const kitty = {
name: "Aurora",
numLives: numLivesForCat,
}
// Error
kitty = {
name: "Danielle",
numLives: numLivesForCat
};
// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;
除非你使用特殊的方法去避免,實(shí)際上const
變量的內(nèi)部狀態(tài)是可修改的。 幸運(yùn)的是,TypeScript允許你將對象的成員設(shè)置成只讀的。 接口一章有詳細(xì)說明。
let
vs. const
現(xiàn)在我們有兩種作用域相似的聲明方式,我們自然會問到底應(yīng)該使用哪個。 與大多數(shù)泛泛的問題一樣,答案是:依情況而定。
使用最小特權(quán)原則,所有變量除了你計劃去修改的都應(yīng)該使用const
。 基本原則就是如果一個變量不需要對它寫入,那么其它使用這些代碼的人也不能夠?qū)懭胨鼈?,并且要思考為什么會需要對這些變量重新賦值。 使用 const
也可以讓我們更容易的推測數(shù)據(jù)的流動。
另一方面,用戶很喜歡let
的簡潔性。 這個手冊大部分地方都使用了 let
。
跟據(jù)你的自己判斷,如果合適的話,與團(tuán)隊(duì)成員商議一下。有趣的是,TypeScript允許您指定對象的成員為只讀。關(guān)于接口的章節(jié)有詳細(xì)說明。
解構(gòu)
Another TypeScript已經(jīng)可以解析其它 ECMAScript 2015 特性了。 完整列表請參見 the article on the Mozilla Developer Network。 本章,我們將給出一個簡短的概述。
最簡單的解構(gòu)莫過于數(shù)組的解構(gòu)賦值了:
let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2
這創(chuàng)建了2個命名變量 first
和 second
。 相當(dāng)于使用了索引,但更為方便:
first = input[0];
second = input[1];
解構(gòu)作用于已聲明的變量會更好:
// swap variables
[first, second] = [second, first];
作用于函數(shù)參數(shù):
function f([first, second]: [number, number]) {
console.log(first);
console.log(second);
}
f(input);
你可以使用...name
語法創(chuàng)建一個剩余變量列表:
let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]
當(dāng)然,由于是JavaScript, 你可以忽略你不關(guān)心的尾隨元素:
let [first] = [1, 2, 3, 4];
console.log(first); // outputs 1
或其它元素:
let [, second, , fourth] = [1, 2, 3, 4];
你也可以解構(gòu)對象:
let o = {
a: "foo",
b: 12,
c: "bar"
}
let {a, b} = o;
這通過 o.a
and o.b
創(chuàng)建了 a
和 b
。 注意,如果你不需要 c
你可以忽略它。
就像數(shù)組解構(gòu),你可以用沒有聲明的賦值:
({a, b} = {a: "baz", b: 101});
注意,我們需要用括號將它括起來,因?yàn)镴avascript通常會將以 {
起始的語句解析為一個塊。
你也可以給屬性以不同的名字:
let {a: newName1, b: newName2} = o;
這里的語法開始變得混亂。 你可以將 a: newName1
讀做 "a
作為 newName1
"。 方向是從左到右,好像你寫成了以下樣子:
let newName1 = o.a;
let newName2 = o.b;
令人困惑的是,這里的冒號不是指示類型的。 如果你想指定它的類型, 仍然需要在其后寫上完整的模式。
let {a, b}: {a: string, b: number} = o;
默認(rèn)值可以讓你在屬性為 undefined 時使用缺省值:
function keepWholeObject(wholeObject: {a: string, b?: number}) {
let {a, b = 1001} = wholeObject;
}
現(xiàn)在,即使 b
為 undefined , keepWholeObject
函數(shù)的變量 wholeObject
的屬性 a
和 b
都會有值。
解構(gòu)也能用于函數(shù)聲明。 看以下簡單的情況:
type C = {a: string, b?: number}
function f({a, b}: C): void {
// ...
}
但是,通常情況下更多的是指定默認(rèn)值,解構(gòu)默認(rèn)值有些棘手。 首先,你需要知道在設(shè)置默認(rèn)值之前設(shè)置其類型。
function f({a, b} = {a: "", b: 0}): void {
// ...
}
f(); // ok, default to {a: "", b: 0}
其次,你需要知道在解構(gòu)屬性上給予一個默認(rèn)或可選的屬性用來替換主初始化列表。 要知道 C
的定義有一個 b
可選屬性:
function f({a, b = 0} = {a: ""}): void {
// ...
}
f({a: "yes"}) // ok, default b = 0
f() // ok, default to {a: ""}, which then defaults b = 0
f({}) // error, 'a' is required if you supply an argument
要小心使用解構(gòu)。 從前面的例子可以看出,就算是最簡單的解構(gòu)也會有很多問題。 尤其當(dāng)存在深層嵌套解構(gòu)的時候,就算這時沒有堆疊在一起的重命名,默認(rèn)值和類型注解,也是令人難以理解的。 解構(gòu)表達(dá)式要盡量保持小而簡單。 你自己也可以直接使用解構(gòu)將會生成的賦值表達(dá)式。
更多建議: