官網鏈接:https://www.tslang.cn/docs/handbook/variable-declarations.html 本文知識體系:
- 變數宣告
- 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;
這里定義了一個名為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;上面的例子中,g可以獲取到f函式里定義的a變數,每當g被呼叫時,它都可以訪問到f里的a變數,即當g在f已經執行完后才被呼叫,它仍然可以訪問以及修改a,
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允許你將物件的成員設定成只讀的, 介面一章有詳細說明,
letvs.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!其次,TypeScript編譯器不允許展開泛型函式上的型別引數, 這個特性會在TypeScript的未來版本中考慮實作,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/238998.html
標籤:JavaScript
上一篇:物件的隱式型別轉換
