- 變量聲明
- var聲明
- 作用域規則
- 捕獲變量怪異之處
- let聲明
- 塊作用域
- 重定義及屏蔽
- 塊級作用域變量的獲取
- const聲明
- let vs const
- 解構
- 解構函數
- 對象函數
- 屬性重命名
- 默認值
- 函數聲明
- 展開
-
變量聲明
let
和const
是JavaScript里相對較新的變量聲明方式。 像我們之前提到過的, let
在很多方面與var
是相似的,但是可以幫助大家避免在JavaScript里的常見一些問題。 const
是對let
的一個增強,它能阻止對一個變量再次賦值。
因為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
作用域或 函數作用域*。 函數參數也使用函數作用域。
這些作用域規則可能會引發一些錯誤。 其中之一就是,多次聲明同一個變量並不會報錯:
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
都引用相同的函數作用域內的變量。 有經驗的開發者們很清楚,這些問題可能在代碼審查時漏掉,引發無窮的麻煩。
捕獲變量怪異之處
快速的猜一下下面的代碼會返回什么:
for (var i = 0; i < 10; i++) { setTimeout(function() { console.log(i); }, 100 * i); }
介紹一下,setTimeout
會在若干毫秒的延時后執行一個函數(等待其它代碼執行完畢)。
好吧,看一下結果:
10 10 10 10 10 10 10 10 10 10
還記得我們上面提到的捕獲變量嗎?我們傳給setTimeout
的每一個函數表達式實際上都引用了相同作用域里的同一個i
。
讓我們花點時間思考一下這是為什么。 setTimeout
在若干毫秒后執行一個函數,並且是在for
循環結束后。 for
循環結束后,i
的值為10
。 所以當函數被調用的時候,它會打印出 10
!
一個通常的解決方法是使用立即執行的函數表達式(IIFE)來捕獲每次迭代時i
的值:
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!