let
和const
是JavaScript裏相對較新的變量聲明方式。 像咱們以前提到過的, let
在不少方面與var
是類似的,可是能夠幫助你們避免在JavaScript裏的常見一些問題。 const
是對let
的一個加強,它能阻止對一個變量再次賦值。html
由於TypeScript是JavaScript的超集,因此它自己就支持let
和const
。 下面咱們會詳細說明這些新的聲明方式以及爲何推薦使用它們來代替 var
。數組
var
聲明var
關鍵字定義JavaScript變量。
var a=10;
function f(){ var mes = "Hello world!"; return mes; }
function f(){ var a = 10; return function g(){ var b = a + 1; return b; } } var g = f(); g(); //return 11;
function f(){ var a = 1; a = 2; var b = g(); a = 3; return b; function g(){ return a; } } f(); //return 2;
var
聲明有些奇怪的做用域規則。 看下面的例子:
function f(shouldInittialize:boolean){ if (shouldInitialize){ var x=10; } return x; } f(true); //returns '10' f(false); //returns 'undefined'
變量 x
是定義在*if
語句裏面*,可是咱們卻能夠在語句的外面訪問它。 這是由於 var
聲明能夠在包含它的函數,模塊,命名空間或全局做用域內部任何位置被訪問(咱們後面會詳細介紹),包含它的代碼塊對此沒有什麼影響。 有些人稱此爲* var
做用域或 函數做用域*。 函數參數也使用函數做用域。app
這些做用域規則可能會引起一些錯誤。 其中之一就是,屢次聲明同一個變量並不會報錯:函數
function sumMatrix(matrix: number[][]){ var sum = 0; for(var i = 0;i < matrix.length;i++){ var cur = matrix[i]; for (var i = 0;i < cur.length;i++){ sum += cur[i]; } } return sum; }
for
循環會覆蓋變量i
,由於全部i
都引用相同的函數做用域內的變量。 有經驗的開發者們很清楚,這些問題可能在代碼審查時漏掉,引起無窮的麻煩。
快速的猜一下下面的代碼會返回什麼:ui
for (var i = 0; i < 10; i++) { setTimeout(function() { console.log(i); }, 100 * i); }
介紹一下,setTimeout
會在若干毫秒的延時後執行一個函數(等待其它代碼執行完畢)。this
好吧,看一下結果:spa
10 10 10 10 10 10 10 10 10 10
還記得咱們上面提到的捕獲變量嗎?咱們傳給setTimeout
的每個函數表達式實際上都引用了相同做用域裏的同一個i
。prototype
讓咱們花點時間思考一下這是爲何。 setTimeout
在若干毫秒後執行一個函數,而且是在for
循環結束後。 for
循環結束後,i
的值爲10
。 因此當函數被調用的時候,它會打印出 10
!rest
一個一般的解決方法是使用當即執行的函數表達式(IIFE)來捕獲每次迭代時i
的值:code
for (var i = 0; i < 10; i++) { // 捕捉'i'的當前狀態 // 經過調用函數的當前值 (function(i) { setTimeout(function() { console.log(i); }, 100 * i); })(i); }
這種奇怪的形式咱們已經司空見慣了。 參數 i
會覆蓋for
循環裏的i
,可是由於咱們起了一樣的名字,因此咱們不用怎麼改for
循環體裏的代碼。
let
聲明如今你已經知道了var
存在一些問題,這剛好說明了爲何用let
語句來聲明變量。 除了名字不一樣外, let
與var
的寫法一致。
let hello = "Hello world!"
主要的區別不在語法上,而是語義,接下來深刻研究。
塊做用域
當用let
聲明一個變量,它使用的是詞法做用域或塊做用域。 不一樣於使用 var
聲明的變量那樣能夠在包含它們的函數外訪問,塊做用域變量在包含它們的塊或for
循環以外是不能訪問的。
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
函數體內,而b
的做用域是if
語句塊裏。
在catch
語句裏聲明的變量也具備一樣的做用域規則。
try { throw "oh no!"; } catch (e) { console.log("Oh well."); } // Error: 'e' doesn't exist here console.log(e);
擁有塊級做用域的變量的另外一個特色是,它們不能在被聲明以前讀或寫。 雖然這些變量始終「存在」於它們的做用域裏,但直到聲明它的代碼以前的區域都屬於暫時性死區。 它只是用來講明咱們不能在 let
語句以前訪問它們,幸運的是TypeScript能夠告訴咱們這些信息。
a ++; //在聲明以前使用'a'是違法的; let a;
注意一點,咱們仍然能夠在一個擁有塊做用域變量被聲明前獲取它。 只是咱們不能在變量聲明前去調用那個函數。 若是生成代碼目標爲ES2015,現代的運行時會拋出一個錯誤;然而,現今TypeScript是不會報錯的。
function foo() { // okay to capture 'a' return a; } // 不能在'a'被聲明前調用'foo' // 運行時應該拋出錯誤 foo(); let a;
關於暫時性死區的更多信息,查看這裏Mozilla Developer Network.
咱們提過使用var
聲明時,它不在意你聲明多少次;你只會獲得1個。
function f(x) { var x; var x; if (true) { var x; } }
x
的聲明實際上都引用一個相同的x
,而且這是徹底有效的代碼。 這常常會成爲bug的來源。 好的是, let
聲明就不會這麼寬鬆了。
let x = 10; let x = 20;// 錯誤,不能在1個做用域裏屢次聲明`x`
並非要求兩個均是塊級做用域的聲明TypeScript纔會給出一個錯誤的警告。
function f(x) { let x = 100; // error: 干擾參數說明 } function g() { let x = 100; var x = 100; // error: 不能同時聲明一個「x」 }
function f(condition, x) { if (condition) { let x = 100; return x; } return x; } f(false, 0); // returns 0 f(true, 0); // returns 100
let
重寫以前的sumMatrix
函數。
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; }
這個版本的循環能獲得正確的結果,由於內層循環的i
能夠屏蔽掉外層循環的i
。
一般來說應該避免使用屏蔽,由於咱們須要寫出清晰的代碼。 同時也有些場景適合利用它,你須要好好打算一下。
塊級做用域變量的獲取
在咱們最初談及獲取用var
聲明的變量時,咱們簡略地探究了一下在獲取到了變量以後它的行爲是怎樣的。 直觀地講,每次進入一個做用域時,它建立了一個變量的 環境。 就算做用域內代碼已經執行完畢,這個環境與其捕獲的變量依然存在。
function theCityThatAlwaysSleeps() { let getCity; if (true) { let city = "Seattle"; getCity = function() { return city; } } return getCity(); }
由於咱們已經在city
的環境裏獲取到了city
,因此就算if
語句執行結束後咱們仍然能夠訪問它。
回想一下前面setTimeout
的例子,咱們最後須要使用當即執行的函數表達式來獲取每次for
循環迭代裏的狀態。 實際上,咱們作的是爲獲取到的變量建立了一個新的變量環境。 這樣作挺痛苦的,可是幸運的是,你沒必要在TypeScript裏這樣作了。
當let
聲明出如今循環體裏時擁有徹底不一樣的行爲。 不只是在循環裏引入了一個新的變量環境,而是針對 每次迭代都會建立這樣一個新做用域。 這就是咱們在使用當即執行的函數表達式時作的事,因此在 setTimeout
例子裏咱們僅使用let
聲明就能夠了。
for (let i = 0; i < 10 ; i++) { setTimeout(function() {console.log(i); }, 100 * i); }
0 1 2 3 4 5 6 7 8 9
const
聲明const
聲明是聲明變量的另外一種方式。
const num = 9;
它們與let
聲明類似,可是就像它的名字所表達的,它們被賦值後不能再改變。 換句話說,它們擁有與 let
相同的做用域規則,可是不能對它們從新賦值。
這很好理解,const引用的值是不可變的。
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--;
除非你使用特殊的方法去避免,實際上const
變量的內部狀態是可修改的。 幸運的是,TypeScript容許你將對象的成員設置成只讀的。 接口一章有詳細說明。
let
vs. const
如今咱們有兩種做用域類似的聲明方式,咱們天然會問到底應該使用哪一個。 與大多數泛泛的問題同樣,答案是:依狀況而定。
使用最小特權原則,全部變量除了你計劃去修改的都應該使用const
。 基本原則就是若是一個變量不須要對它寫入,那麼其它使用這些代碼的人也不可以寫入它們,而且要思考爲何會須要對這些變量從新賦值。 使用 const
也可讓咱們更容易的推測數據的流動。
解構數組 []
let input = [1, 2]; let [first, second] = input; console.log(first); // outputs 1 console.log(second); // outputs 2
這建立了2個命名變量 first
和 second
。 至關於使用了索引,但更爲方便:
first = input[0];
second = input[1];
解構做用於已聲明的變量會更好:
// swap variables [first, second] = [second, first];
做用於函數參數:
ts: function f([first, second]: [number, number]) { console.log(first); console.log(second); } f([23,2]); js: function f(_a) { var first = _a[0], second = _a[1]; console.log(first); console.log(second); } f([23, 2]);
你能夠在數組裏使用...
語法建立剩餘變量:
ts: let [first, ...rest] = [1, 2, 3, 4]; console.log(first); // outputs 1 console.log(rest); // outputs [ 2, 3, 4 ] js: var _a = [1, 2, 3, 4], first = _a[0], rest = _a.slice(1); console.log(first); console.log(rest);
固然,因爲是JavaScript, 你能夠忽略你不關心的尾隨元素:
ts: let [first] = [1, 2, 3, 4]; console.log(first); // outputs 1 js: var first = [1, 2, 3, 4][0]; console.log(first); // outputs 1
或其它元素:
let [, second, , fourth] = [1, 2, 3, 4];
你也能夠解構對象:
ts: let o = { a: "foo", b: 12, c: "bar", } let { a, b } = o; js: var o = { a: "foo", b: 12, c: "bar", }; var a = o.a, b = o.b;
經過 o.a
and o.b
建立了 a
和 b
。 注意,若是你不須要 c
你能夠忽略它。
就像數組解構,你能夠用沒有聲明的賦值:
ts: ({ a, b } = { a: 'foo', b: 102 }); js: var _a; (_a = { a: 'foo', b: 102 }, a = _a.a, b = _a.b);
注意:咱們須要用括號將它括起來,由於Javascript一般會將以 {
起始的語句解析爲一個塊。
你能夠在對象裏使用...
語法建立剩餘變量:
ts: let o = { a: "foo", b: 12, c: "bar" }; let { a, ...pass } = o; // output foo,[12,bar] let total = pass.b + pass.c.length; //output 15 js: var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) if (e.indexOf(p[i]) < 0) t[p[i]] = s[p[i]]; return t; }; var o = { a: "foo", b: 12, c: "bar" }; var a = o.a, pass = __rest(o, ["a"]); var total = pass.b + pass.c.length;
你也能夠給屬性以不一樣的名字:
ts: let { a: newName1, b: newName2 } = o; js: var newName1 = o.a, newName2 = o.b;
a: newName1
讀作 "a
做爲 newName1
"。 方向是從左到右,好像你寫成了如下樣子:
let newName1 = o.a;
let newName2 = o.b;
使人困惑的是,這裏的冒號不是指示類型的。 若是你想指定它的類型, 仍然須要在其後寫上完整的模式。
let {a, b}: {a: string, b: number} = o;
默認值可讓你在屬性爲 undefined 時使用缺省值:
ts: function keepWholeObject(wholeObject: { a: string, b?: number }) { let { a, b = 1001 } = wholeObject; } js: function keepWholeObject(wholeObject) { var a = wholeObject.a, _a = wholeObject.b, b = _a === void 0 ? 1001 : _a; }
如今,即便 b
爲 undefined , keepWholeObject
函數的變量 wholeObject
的屬性 a
和 b
都會有值。
解構也能用於函數聲明。 看如下簡單的狀況:
ts: type C = { a: string, b?: number } function f({ a, b }: C): void { // ... } js: function f(_a) { var a = _a.a, b = _a.b; // ... }
可是,一般狀況下更多的是指定默認值,解構默認值有些棘手。 首先,你須要在默認值以前設置其格式。
ts: function f({ a="", b=0 } = {}): void { // ... } f(); js: function f(_a) { var _b = _a === void 0 ? {} : _a, _c = _b.a, a = _c === void 0 ? "" : _c, _d = _b.b, b = _d === void 0 ? 0 : _d; // ... } f();
上面的代碼是一個類型推斷的例子,將在本手冊後文介紹。
其次,你須要知道在解構屬性上給予一個默認或可選的屬性用來替換主初始化列表。 要知道 C
的定義有一個 b
可選屬性:
ts: 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 js: function f(_a) { var _b = _a === void 0 ? { a: "" } : _a, a = _b.a, _c = _b.b, b = _c === void 0 ? 0 : _c; // ... } 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
要當心使用解構。 從前面的例子能夠看出,就算是最簡單的解構表達式也是難以理解的。 尤爲當存在深層嵌套解構的時候,就算這時沒有堆疊在一塊兒的重命名,默認值和類型註解,也是使人難以理解的。 解構表達式要儘可能保持小而簡單。 你本身也能夠直接使用解構將會生成的賦值表達式。
展開操做符正與解構相反。 它容許你將一個數組展開爲另外一個數組,或將一個對象展開爲另外一個對象。 例如:
ts: let first = [1, 2]; let second = [3, 4]; let bothPlus = [0, ...first, ...second, 5]; js: var first = [1, 2]; var second = [3, 4]; var bothPlus = [0].concat(first, second, [5]);
這會令bothPlus
的值爲[0, 1, 2, 3, 4, 5]
。 展開操做建立了 first
和second
的一份淺拷貝。 它們不會被展開操做所改變。
你還能夠展開對象:
ts: let defaults = { food: "spicy", price: "$$", ambiance: "noisy" }; let search = { ...defaults, food: "rich" }; js: var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var defaults = { food: "spicy", price: "$$", ambiance: "noisy" }; var search = __assign({}, defaults, { food: "rich" });
search
的值爲{ food: "rich", price: "$$", ambiance: "noisy" }
。 對象的展開比數組的展開要複雜的多。 像數組展開同樣,它是從左至右進行處理,但結果仍爲對象。 這就意味着出如今展開對象後面的屬性會覆蓋前面的屬性。 所以,若是咱們修改上面的例子,在結尾處進行展開的話:
ts: let defaults = { food: "spicy", price: "$$", ambiance: "noisy" }; let search = { food: "rich", ...defaults }; js: var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var defaults = { food: "spicy", price: "$$", ambiance: "noisy" }; var search = __assign({ food: "rich" }, defaults); //{food: "spicy", price: "$$", ambiance: "noisy"}
那麼,defaults
裏的food
屬性會重寫food: "rich"
,在這裏這並非咱們想要的結果。
對象展開還有其它一些意想不到的限制。 首先,它僅包含對象 自身的可枚舉屬性。 大致上是說當你展開一個對象實例時,你會丟失其方法:
ts: class C { p = 12; m() { } } let c = new C(); let clone = { ...c }; clone.p; // ok // clone.m(); // error! js: var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var C = /** @class */ (function () { function C() { this.p = 12; } C.prototype.m = function () { }; return C; }()); var c = new C(); var clone = __assign({}, c); clone.p; // ok // clone.m(); // error!