這一篇文章主要是來探討,為什么要用const和let替代var,還有ES6新特性:解構(gòu)
js里面很多時候,var關(guān)鍵字寫與不寫對運(yùn)行結(jié)果沒有多大關(guān)系,可是如果實在嚴(yán)格模式下就不能運(yùn)行,所以ts要規(guī)范js的編碼風(fēng)格,使之要像面向?qū)ο笳Z言一樣嚴(yán)謹(jǐn),塊級區(qū)域概念深入骨髓,接下來我們就來看看,為什么要替換掉var
var 應(yīng)用場景
一般場景
一直以來我們都是通過var關(guān)鍵字定義JavaScript變量。
function f() {
var a = 10;
return function g() {
var b = a + 1;
return b;
}
}
var gFunc = f();
gFunc(); // returns 11;
上面的例子里,g可以獲取到f函數(shù)里定義的a變量。 每當(dāng) g被調(diào)用時,它都可以訪問到f里的a變量。 即使當(dāng) g在f已經(jīng)執(zhí)行完后才被調(diào)用,它仍然可以訪問及修改a。
作用域規(guī)則
對于熟悉其它語言的人來說,var聲明有些奇怪的作用域規(guī)則。 看下面的例子:
function f(shouldInitialize: boolean) {
if (shouldInitialize) {
var x = 10;
}
return x;
}
f(true); // returns '10'
f(false); // returns 'undefined'
變量x是定義在if語句里面,可是可以在外面訪問,我想會有很多讀者會困惑這個問題吧。那是因為var聲明之后,會在包含它的函數(shù)(function),模塊(閉包,module),命名空間(namespace)或全局內(nèi)部任何位置(window)被訪問,而這里的if只是一個小小的代碼塊,對此沒有影響,有些人稱此為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,因為所有i都引用相同的函數(shù)作用域內(nèi)的變量。 有經(jīng)驗的開發(fā)者們很清楚,這些問題可能在代碼審查時漏掉,引發(fā)無窮的麻煩。
變量獲取怪異之處
快速的猜一下下面的代碼會返回什么:
for (var i = 0; i < 10; i++) {
setTimeout(function() {console.log(i); }, 100 * i);
}
好吧,看一下結(jié)果:
10
10
10
10
10
10
10
10
10
10
還記得我們上面講的變量獲取嗎?
每當(dāng)g被調(diào)用時,它都可以訪問到f里的a變量。
setTimeOut是一個異步的方法,所以for代碼塊很快執(zhí)行完,里面的方法就需要等待之后才會執(zhí)行,而且每次執(zhí)行都是訪問同一個內(nèi)存地址(i的地址),所以最終的結(jié)果就都是10
不過在js里面對于這種情況為我們還是有對應(yīng)的解決方案的,那就是使用閉包來解決,動態(tài)為每個setTimeOut分配一個內(nèi)存塊存儲相關(guān)的臨時變量,這樣就可以解決上面借結(jié)果都是10的問題了。
for (var i = 0; i < 10; i++) {
(function(i) {
setTimeout(function() { console.log(i); }, 100 * i);
})(i);
}
這種奇怪的形式我們已經(jīng)司空見慣了。 參數(shù) i會覆蓋for循環(huán)里的i,但是因為我們起了同樣的名字,所以我們不用怎么改for循環(huán)體里的代碼。
let 聲明
現(xiàn)在你已經(jīng)知道了var存在一些問題,這恰好說明了為什么用let語句來聲明變量。 除了名字不同外, let與var的寫法一致。
let hello = "Hello!";
記住一個概念:你要用規(guī)范的風(fēng)格來編寫TypeScript
塊作用域
當(dāng)用let聲明一個變量,它使用的是詞法作用域或塊作用域。 不同于使用 var聲明的變量那樣可以在包含它們的函數(shù)外訪問,塊作用域變量在包含它們的塊或for循環(huán)之外是不能訪問的。
function f(input: boolean) {
let a = 100;
if (input) {
// 這里是可以訪問到a的
let b = a + 1;
return b;
}
// 報錯:變量b沒有定義
return b;
}
重定義及屏蔽
我們提過使用var聲明時,它不在乎你聲明多少次;你只會得到最新聲明的那一個。
function f(x) {
var x;
var x;
if (true) {
var x;
}
}
在上面的例子里,所有x的聲明實際上都引用一個相同的x,并且這是完全有效的代碼。 這經(jīng)常會成為bug的來源。 好的是, let聲明就不會這么寬松了。
let x = 10;
let x = 20; // 錯誤,不能在1個作用域里多次聲明`x`
并不是要求兩個均是塊級作用域的聲明TypeScript才會給出一個錯誤的警告。
function f(x) {
let x = 100; // 錯誤: 變量x已經(jīng)定義
}
function g() {
let x = 100;
var x = 100; // 錯誤: 不能重復(fù)定義變量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
塊級作用域變量的獲取
在我們最初談及獲取用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();
}
因為我們已經(jīng)在city的環(huán)境里獲取到了city,所以就算if語句執(zhí)行結(jié)束后我們?nèi)匀豢梢栽L問它。
回想一下前面setTimeout的例子,我們最后需要使用立即執(zhí)行的函數(shù)表達(dá)式來獲取每次for循環(huán)迭代里的狀態(tài)。 實際上,我們做的是為獲取到的變量創(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);
}
其結(jié)果與我們預(yù)想的一模一樣
0
1
2
3
4
5
6
7
8
9
const 聲明
const 聲明是聲明變量的另一種方式,它們擁有與 let相同的作用域規(guī)則,但是不能對它們重新賦值。
它們引用的值是不可變的
const numLivesForCat = 9;
const kitty = {
name: "Aurora",
numLives: numLivesForCat,
}
// 錯誤,因為你修改了內(nèi)存塊地址,就修改了引用的值
kitty = {
name: "Danielle",
numLives: numLivesForCat
};
// 以下結(jié)果都正確,因為只會修改內(nèi)存塊的值,沒有修改引用的值(內(nèi)存塊地址)
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;
除非你使用特殊的方法去避免,實際上const變量的內(nèi)部狀態(tài)是可修改的。
let vs const
現(xiàn)在我們有兩種作用域相似的聲明方式,我們自然會問到底應(yīng)該使用哪個。 與大多數(shù)泛泛的問題一樣,答案是:依情況而定。
使用最小特權(quán)原則,所有變量除了你計劃去修改的都應(yīng)該使用const。 基本原則就是如果一個變量不需要對它寫入,那么其它使用這些代碼的人也不能夠?qū)懭胨鼈?,并且要思考為什么會需要對這些變量重新賦值。 使用 const也可以讓我們更容易的推測數(shù)據(jù)的流動。
另一方面,用戶很喜歡let的簡潔性。 這個手冊大部分地方都使用了 let。
跟據(jù)你的自己判斷,如果合適的話,與團(tuán)隊成員商議一下。
解構(gòu)
如果是第一看解構(gòu)的代碼,可能會有點懵逼,到底是什么鬼啊,因為這個是ES6的新語法。
解構(gòu)數(shù)組
最簡單的解構(gòu)莫過于數(shù)組的解構(gòu)賦值了:
let input = [1, 2];
let [first, second] = input;
console.log(first); // 輸出 1
console.log(second); // 輸出 2
你可以使用...name語法創(chuàng)建一個剩余變量列表:
let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // 輸出 1
console.log(rest); // 輸出 [ 2, 3, 4 ]
或其它元素:
let [, second, , fourth] = [1, 2, 3, 4];
對象解構(gòu)
你也可以解構(gòu)對象:
let o = {
a: "foo",
b: 12,
c: "bar"
}
let {a, b} = o;
屬性重命名
你也可以給屬性以不同的名字:
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)值
這是ES6里面的新特性,如果編譯器沒有環(huán)境,或瀏覽器不支持,建議使用最新版Chrome瀏覽器調(diào)試
默認(rèn)值可以讓你在屬性為 undefined 時使用缺省值:
function keepWholeObject(wholeObject: {a: string, b?: number}) {
let {a, b = 1001} = wholeObject;
}
函數(shù)聲明
解構(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è)置其類型。
但是,通常情況下更多的是指定默認(rèn)值,解構(gòu)默認(rèn)值有些棘手。 首先,你需要知道在設(shè)置默認(rèn)值之前設(shè)置其類型。
function f({a, b} = {a: "", b: 0}): void {
// ...
}
f();
其次,你需要知道在解構(gòu)屬性上給予一個默認(rèn)或可選的屬性用來替換主初始化列表。 要知道 C 的定義有一個 b 可選屬性:
function f({a, b = 0} = {a: ""}): void {
// ...
}
f({a: "yes"}) // 正確: a默認(rèn)值為"yes",b默認(rèn)值為0
f() // 正確:先設(shè)置默認(rèn)值a為"",然后設(shè)置默認(rèn)值b為0
f({}) // 錯誤:a參數(shù)缺少
我想有很多小伙伴會問,最后f({})怎么會出錯呢?我不是設(shè)置了默認(rèn)值的嗎?
首先我們來看一看,里面默認(rèn)值的設(shè)置有哪幾步?
首先:{a, b = 0},這是默認(rèn)值設(shè)置第一步,先在符號表里面設(shè)置a或b的默認(rèn)值,
接下來:{a: ""},這是默認(rèn)值設(shè)置第二部,緊接著更新符號表的默認(rèn)值
我們可以看一看下面的小程序:
function f({a=3,b="bbbb"}={b:"sdfsdfs"}){
console.log(a+"---------"+b)
}
//輸出為 3---------sdfsdfs
是不是感覺解構(gòu)很強(qiáng)大呢?哈哈,不過要小心使用解構(gòu)。 從前面的例子可以看出,就算是最簡單的解構(gòu)也會有很多問題。 尤其當(dāng)存在深層嵌套解構(gòu)的時候,就算這時沒有堆疊在一起的重命名,默認(rèn)值和類型注解,也是令人難以理解的。 解構(gòu)表達(dá)式要盡量保持小而簡單。 你自己也可以直接使用解構(gòu)將會生成的賦值表達(dá)式。
【翻譯:原文地址】