TS 筆記六 函數(shù) 關鍵字this

參考https://github.com/zhongsp/TypeScript

函數(shù)是JavaScript應用程序的基礎。 它幫助你實現(xiàn)抽象層,模擬類,信息隱藏和模塊。 在TypeScript里,雖然已經支持類,命名空間和模塊,但函數(shù)仍然是主要的定義行為的地方。 TypeScript為JavaScript函數(shù)添加了額外的功能,讓我們可以更容易地使用。

參考js紅寶書筆記十一 第十章 函數(shù) 閉包

一、JS中的函數(shù)

通過下面的例子可以迅速回想起這兩種JavaScript中的函數(shù):

// Named function
function add(x, y) {
    return x + y;
}

// Anonymous function
let myAdd = function(x, y) { return x + y; };

在JavaScript里,函數(shù)可以使用函數(shù)體外部的變量。 當函數(shù)這么做時,我們說它‘捕獲’了這些變量。 至于為什么可以這樣做以及其中的利弊超出了本文的范圍,但是深刻理解這個機制對學習JavaScript和TypeScript會很有幫助。

let z = 100;

function addToZ(x, y) {
    return x + y + z;
}
二、為函數(shù)定義類型
function add(x: number, y: number): number {
    return x + y;
}

let myAdd = function(x: number, y: number): number { return x + y; };

我們可以給每個參數(shù)添加類型之后再為函數(shù)本身添加返回值類型。 TypeScript能夠根據(jù)返回語句自動推斷出返回值類型,因此我們通常省略它。

三、參數(shù)
1.可選參數(shù)

JavaScript里,每個參數(shù)都是可選的,可傳可不傳。 沒傳參的時候,它的值就是undefined。

TypeScript里的每個函數(shù)參數(shù)都是必須的。 這不是指不能傳遞null或undefined作為參數(shù),而是說編譯器檢查用戶是否為每個參數(shù)都傳入了值。 編譯器還會假設只有這些參數(shù)會被傳遞進函數(shù)。 簡短地說,傳遞給一個函數(shù)的參數(shù)個數(shù)必須與函數(shù)期望的參數(shù)個數(shù)一致。

function buildName(firstName: string, lastName: string) {
    return firstName + " " + lastName;
}
// error, too few parameters
let result1 = buildName("Bob"); 

// error, too many parameters
let result2 = buildName("Bob", "Adams", "Sr."); 

// ah, just right
let result3 = buildName("Bob", "Adams");         

在TypeScript里我們可以在參數(shù)名旁使用?實現(xiàn)可選參數(shù)的功能。 可選參數(shù)必須跟在必須參數(shù)后面。

function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}
2.參數(shù)默認值

在TypeScript里,我們也可以為參數(shù)提供一個默認值當用戶沒有傳遞這個參數(shù)或傳遞的值是undefined時。 它們叫做有默認初始化值的參數(shù)。

function buildName(firstName: string, lastName = "Smith") {
    return firstName + " " + lastName;
}
3.剩余參數(shù)

必要參數(shù),默認參數(shù)和可選參數(shù)有個共同點:它們表示某一個參數(shù)。 有時,你想同時操作多個參數(shù),或者你并不知道會有多少參數(shù)傳遞進來。 在JavaScript里,你可以使用arguments來訪問所有傳入的參數(shù)。

在TypeScript里,你可以把所有參數(shù)收集到一個變量里:

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

剩余參數(shù)會被當做個數(shù)不限的可選參數(shù)。 可以一個都沒有,同樣也可以有任意個。 編譯器創(chuàng)建參數(shù)數(shù)組,名字是你在省略號(...)后面給定的名字,你可以在函數(shù)體內使用這個數(shù)組。

四、this
1.JS中的this

如果你以前沒用過JS,有一件事可能會令你驚訝:JS中的每個函數(shù)都有this變量,而不局限于類中的方法。以不同的方式調用函數(shù),this的值也不同,這極易導致代碼脆弱、難以理解。鑒于此,很多團隊禁止在類方法以外使用this。如果你也想這么做,開啟eslint的no-invalid-this規(guī)則。

以下參考
js紅寶書筆記十一 第十章 函數(shù) 閉包
js es6 => arrow function箭頭函數(shù)

window.color = 'red'; 
let o = { 
 color: 'blue' 
}; 
function sayColor() { 
 console.log(this.color); 
} 
sayColor(); // 'red' 
o.sayColor = sayColor; 
o.sayColor(); // 'blue' 

定義在全局上下文中的函數(shù) sayColor()引用了 this 對象。這個 this 到底引用哪個對象必須到函數(shù)被調用時才能確定。因此這個值在代碼執(zhí)行的過程中可能會變。如果在全局上下文中調用sayColor(),這結果會輸出"red",因為 this 指向 window,而 this.color 相當于 window.color。而在把 sayColor()賦值給 o 之后再調用 o.sayColor(),this 會指向 o,即 this.color 相當于o.color,所以會顯示"blue"。

此時將回調函數(shù)寫成箭頭函數(shù)就可以解決問題。這是因為箭頭函數(shù)中的 this 會保留定義該函數(shù)時的上下文。

window.color = 'red'; 
let o = { 
 color: 'blue' 
}; 
let sayColor = () => console.log(this.color); 
sayColor(); // 'red' 
o.sayColor = sayColor; 
o.sayColor(); // 'red' 

在es5中有很多方法解決這個問題,但是一般比較常用的方法就是,在調用上面聲明個變量,一般叫做self或者vm,然后把這個用在函數(shù)中

let obj = {
    name: "asim",
    sayLater: function () {
        let self = this; // Assign to self
        console.log(self);
        setTimeout(function () {
            console.log(`${self.name}`); // Use self not this
        }, 1000);
    }
};

但是在es6中,我們有更好的方式解決這個問題,如果我們使用箭頭函數(shù),那箭頭函數(shù)里面的this的值和外面的值是一樣的

let obj = {
    name: "asim",
    sayLater: function () {
        console.log(this); // `this` points to obj
        setTimeout(() => {
            console.log(this); // `this` points to obj
            console.log(`${this.name}`); // `this` points to obj
        }, 1000);
    }
};
obj.sayLater();
2.Typescript的This

參考
詳解Typescript里的This

this可以說是Javascript里最難理解的特性之一了,Typescript里的 this 似乎更加復雜了,Typescript里的 this 有三中場景,不同的場景都有不同意思。

  • this 參數(shù): 限制調用函數(shù)時的 this 類型
  • this 類型: 用于支持鏈式調用,尤其支持 class 繼承的鏈式調用
  • ThisType:用于構造復雜的 factory 函數(shù)
3.TS this 類型: 用于支持鏈式調用

參考《TypeScript編程》第5.3節(jié)
實現(xiàn)ES6中 Set數(shù)據(jù)結構的簡化版,有以下兩個操作:

let set = new Set;
set.add(1).add(2).add(3);
set.has(2);//true
set.has(4);//false

這里要支持鏈式調用,大概這樣的:

class Set{
   has(value:number):boolean{}
   add(value:number):Set{}
}

這樣做是可以的,但是如果想定義Set的子類呢?

class MutableSet extends Set{
   delete(value:number):boolean{}
   add(value:number):MutableSet{}
}

這就需要又寫一遍add去覆蓋,其實可以在父類Set中返回this就不用這么麻煩了:

class Set{
   has(value:number):boolean{}
   add(value:number):this{}
}
4.TS中this做為對象方法調用,這一點和JS中的this表現(xiàn)一致

這也是絕大部分 this 的使用場景,當函數(shù)作為對象的 方法調用時,this 指向該對象

const obj = {
  name: "yj",
  getName() {
    return this.name // 可以自動推導為{ name:string, getName():string}類型
  },
}
obj.getName() // string類型

這里有個坑就是如果對象定義時對象方法是使用箭頭函數(shù)進行定義,則 this 指向的并不是對象而是全局的 window,Typescript 也自動的幫我推導為 window

const obj2 = {
  name: "yj",
  getName: () => {
    return this.name // check 報錯,這里的this指向的是window
  },
}
obj2.getName() // 運行時報錯
5.TS中this在普通函數(shù)中使用

即使是通過非箭頭函數(shù)定義的函數(shù),當將其賦值給變量,并直接通過變量調用時,其運行時 this 執(zhí)行的并非對象本身

const obj = {
  name: "yj",
  getName() {
    return this.name
  },
}
const fn1 = obj.getName
fn1() // this指向的是window,運行時報錯

很不幸,上述代碼在編譯期間并未檢查出來,我們可以通過為getName添加this的類型標注解決該問題

interface Obj {
  name: string
  // 限定getName調用時的this類型
  getName(this: Obj): string
}
const obj: Obj = {
  name: "yj",
  getName() {
    return this.name
  },
}
obj.getName() // check ok
const fn1 = obj.getName
fn1() // check error

這樣我們就能報保證調用時的 this 的類型安全.

關于這個作用,《TypeScript編程》第4.1.4節(jié)這樣描述,此時this不是常規(guī)的參數(shù),而是保留字,是函數(shù)簽名的一部分。

6.TS中this在構造函數(shù)中使用
class People {
  name: string
  constructor(name: string) {
    this.name = name // check ok
  }
  getName() {
    return this.name
  }
}

const people = new People("yj") // check ok

這里面還是有坑的,具體可以參考原文:詳解Typescript里的This

7.call 和 apply 調用

call 和 apply 調用沒有什么本質區(qū)別,主要區(qū)別就是 arguments 的傳遞方式,不分別討論。和普通的函數(shù)調用相比,call 調用可以動態(tài)的改變傳入的 this, 幸運的是 Typescript 借助 this 參數(shù)也支持對 call 調用的類型檢查

interface People {
  name: string
}
const obj1 = {
  name: "yj",
  getName(this: People) {
    return this.name
  },
}
const obj2 = {
  name: "zrj",
}
const obj3 = {
  name2: "zrj",
}
obj1.getName.call(obj2)
obj1.getName.call(obj3) // check error
五、函數(shù)重載

JavaScript本身是個動態(tài)語言。 JavaScript里函數(shù)根據(jù)傳入不同的參數(shù)而返回不同類型的數(shù)據(jù)是很常見的。

let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x): any {
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    // Otherwise just let them pick the card
    else if (typeof x == "number") {
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}

let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, 
{ suit: "hearts", card: 4 }];

let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

pickCard方法根據(jù)傳入?yún)?shù)的不同會返回兩種不同的類型。 如果傳入的是代表紙牌的對象,函數(shù)作用是從中抓一張牌。 如果用戶想抓牌,我們告訴他抓到了什么牌。 但是這怎么在類型系統(tǒng)里表示呢。

方法是為同一個函數(shù)提供多個函數(shù)類型定義來進行函數(shù)重載。 編譯器會根據(jù)這個列表去處理函數(shù)的調用。 下面我們來重載pickCard函數(shù)。

let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    // Otherwise just let them pick the card
    else if (typeof x == "number") {
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}

let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, 
{ suit: "hearts", card: 4 }];

let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

這樣改變后,重載的pickCard函數(shù)在調用的時候會進行正確的類型檢查。

為了讓編譯器能夠選擇正確的檢查類型,它與JavaScript里的處理流程相似。 它查找重載列表,嘗試使用第一個重載定義。 如果匹配的話就使用這個。 因此,在定義重載的時候,一定要把最精確的定義放在最前面。

注意,function pickCard(x): any并不是重載列表的一部分,因此這里只有兩個重載:一個是接收對象另一個接收數(shù)字。 以其它參數(shù)調用pickCard會產生錯誤。

1.重載簽名

參考
TypeScript中函數(shù)重載寫法,你在第幾層!

TypeScript中的函數(shù)重載讓我們定義以多種方式調用的函數(shù)。使用函數(shù)重載需要定義重載簽名:一組帶有參數(shù)和返回類型的函數(shù),但沒有主體。這些簽名表明應該如何調用該函數(shù)。

// 重載簽名
function greet(person: string): string;
function greet(persons: string[]): string[];

// 實現(xiàn)簽名
function greet(person: unknown): unknown {
 if (typeof person === 'string') {
 return `Hello, ${person}!`;
 } else if (Array.isArray(person)) {
 return person.map(name => `Hello, ${name}!`);
 }
 throw new Error('Unable to greet');
}
2.參考如何看待Typescript中的重載(Overload)? - 賀師俊的回答 - 知乎

第一,常見的靜態(tài)類型語言中的overload是發(fā)生在編譯時的,編譯器可以清楚的將每一處同名函數(shù)調用對應到你寫的不同的函數(shù)實現(xiàn)。也就是,同名只是一個(讓程序員看到的)假象。如你寫了fun(x),有兩個實現(xiàn)fun(x: string)、fun(x: int),真正編譯后的程序里實際會有兩個函數(shù),假設記做fun_string和fun_int,而每個fun(x)調用會被自動替換成fun_string(x)或fun_int(x)。有沒有可能編譯器無法確定替換成哪一個?當然有可能,這個時候編譯器就報錯了嘛,意思是你代碼寫錯啦!

第二,JavaScript是動態(tài)類型,所以是沒有上面這種意義上的overload的。但JS程序員可以在運行時判斷類型,也就是 function fun(x) { if (typeof x === 'string') ... else/* assume x is int */ ... }。TypeScript 的『overload』只是允許給這樣的函數(shù)標注多個類型。某輪說這是『繞過編譯器類型檢查』,是有問題的。這不是繞過,把函數(shù)參數(shù)標記為 (x: any) 才叫『繞過』。不過因為函數(shù)的具體實現(xiàn)只有一個,代碼本身會比上面那種overload要麻煩一些,比如說為了檢測類型偶爾你需要自己實現(xiàn)一些 type guard。至于說[下標函數(shù)]也不能自己寫,這個很傻逼』,我估計某輪指的是 operator overload,然而很多語言都不允許(比如 java)。所以單單罵 TS/JS 有點扯。

第三,TS理論上當然是可以實現(xiàn)傳統(tǒng)的 overload 的,比如直接生成兩個函數(shù),fun1、fun2。問題是從TS與JS的互操作性上來說,這事情就比較麻煩,比如一個js項目用了ts的庫,我不能直接寫fun,而得寫fun1、fun2。本來 overload 就是希望給程序員提供便利,但現(xiàn)在就并沒有什么卵用。其實像java之類有『真』重載的語言編譯到js,或直接和js互操作,都有類似的問題。早在二十年前rhino里就有這問題——你在js里要指定到底調用的是哪一個java的重載方法是非常煩人的。特別是構造器,一般函數(shù)你說編譯成fun_string、fun_int也就算了,但構造器呢?相當棘手。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容