TS 變量聲明

本文知識體系:
  • 變量聲明
  • var聲明
    • 做用域規則
    • 捕獲變量怪異之處
  • let聲明
    • 塊做用域
    • 重定義及屏蔽
    • 塊級做用域變量的獲取
  • const聲明
  • let vs const
  • 解構
    • 解構函數
    • 對象函數
    • 屬性重命名
    • 默認值
    • 函數聲明
  • 展開

 

  • 變量聲明

letconst是JavaScript裏相對較新的變量聲明方式。 像咱們以前提到過的, let在不少方面與var是類似的,可是能夠幫助你們避免在JavaScript裏的常見一些問題。 const是對let的一個加強,它能阻止對一個變量再次賦值。html

由於TypeScript是JavaScript的超集,因此它自己就支持letconst。 下面咱們會詳細說明這些新的聲明方式以及爲何推薦使用它們來代替 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做用域或 函數做用域*。 函數參數也使用函數做用域。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的每個函數表達式實際上都引用了相同做用域裏的同一個iprototype

讓咱們花點時間思考一下這是爲何。 setTimeout在若干毫秒後執行一個函數,而且是在for循環結束後。 for循環結束後,i的值爲10。 因此當函數被調用的時候,它會打印出 10rest

一個一般的解決方法是使用當即執行的函數表達式(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語句來聲明變量。 除了名字不一樣外, letvar的寫法一致。

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個變量ab。 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也可讓咱們更容易的推測數據的流動。

  • 解構
Another TypeScript已經能夠解析其它 ECMAScript 2015 特性了。 完整列表請參見 the article on the Mozilla Developer Network。 本章,咱們將給出一個簡短的概述。

解構數組 []

最簡單的解構莫過於數組的解構賦值了:
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]。 展開操做建立了 firstsecond的一份淺拷貝。 它們不會被展開操做所改變。

你還能夠展開對象:

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的將來版本中考慮實現。
相關文章
相關標籤/搜索