TypeScript 学习笔记(四)函数

198 阅读7分钟

函数

在TypeScript里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义行为的地方。

函数类型

为函数定义类型

我们可以在为函数的每个参数定义类型后,为函数的返回值指定类型。但TypeScript能够根据返回语句自动推断出返回值类型,因此我们通常省略它。

// 匿名函数
let myAdd = function(x: number, y: number): number { return x + y; };

// 命名函数
function add(x: number, y: number): number {
    return x + y;
}

完整的函数类型

第一种:参数类型和返回值类型。 参数类型列表中我们为每个参数指定一个参数名和一个参数类型,参数名可以随便起,只要类型正确就可以。 返回值在参数类型后定义,并且要在前面加一个 => 箭头增加可读性,就算这个函数没有返回值,也要指定返回值类型为 void,不能留空。

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

在赋值语句中,我们可以发现,如果在 = 号的一侧指定了类型但是没有在另一侧指定,TypeScript 会帮我们自动识别出类型。

// myAdd has the full function type
let myAdd = function(x: number, y: number): number { return x + y; };

// The parameters `x` and `y` have the type number
let myAdd: (baseValue: number, increment: number) => number =
    function(x, y) { return x + y; };

可选参数和默认参数

在TypeScript中,每个函数参数都是必须的,并且不可以传入多余参数。

可选参数

如果要实现可选参数,可以在参数旁边添加? 例如:

function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}

需要注意的是:可选参数一定要跟在比选参数的后面。

默认参数

我们也可以为参数提供一个默认值,当用户没有传递这个参数或传递的值是undefined时。它们叫做有默认初始化值的参数。 带默认值的参数都是可选参数,可选参数的类型与默认值的类型保持一致。

function buildName(firstName: string, lastName = "Smith") {
    return firstName + " " + lastName;
}

需要注意的是:默认参数可以不跟在比选参数的后面,但需要注意的是,如果默认参数在前面,用户必须明确的传入undefined值来获得默认值。 例如:

function buildName(firstName = "Will", lastName: string) {
    return firstName + " " + lastName;
}
let result4 = buildName(undefined, "Adams"); 

剩余参数

有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。在javascript中,可以通过arguments来访问所有参数。 在TypeScript中,你可以把所有参数都放进一个变量。

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

🌟this指向和箭头函数

JavaScript里,this的值在函数被调用的时候才会指定。 例子:

let deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function() {
        return function() {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return {suit: this.suits[pickedSuit], card: pickedCard % 13};
        }
    }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

可以看到createCardPicker是个函数,并且它又返回了一个函数。 当我们运行这个程序的时候会报错,为什么呢?因为在这个代码片段中,执行createCardPicker时,this指向的不是 deck 而是 window,因为我们只是独立的调用了 createCardPicker顶级的非方法式调用会将 this 视为 window(注意:在严格模式下,this为undefined而不是window)。

怎么解决这个问题呢?我们可以在函数返回时绑定好正确的this值,这样的话,无论之后怎么使用它,都会引用绑定的‘deck’对象。 我们需要改变函数表达式来使用ECMAScript 6箭头语法。 箭头函数能保存函数创建时的this值,而不是调用时的值: 修改后:

let deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function() {
        // 注意: 下面的函数使用了箭头函数语法,可以捕获到这里的 'this'
        return () => {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return {suit: this.suits[pickedSuit], card: pickedCard % 13};
        }
    }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

但是,如果编译选项中设置了noImplicitThis,TypeScript就会警告你出现了一个错误,因为noImplicitThis表示: 当this表达式的值为any类型的时候,生成一个错误。 在这个例子中,this的类型就是any。 如何解决这个问题呢?

this 参数

解决的的方法是,提供一个显式的this参数,this 参数是一个假的参数,它出现在参数列表的最前面。 重构上面的例子:

// 添加接口
interface Card {
    suit: string;
    card: number;
}
interface Deck {
    suits: string[];
    cards: number[];
    createCardPicker(this: Deck): () => Card;
}

let deck: Deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    // 提示: 这个函数现在显式的指定它的调用方式 this 必须是Deck类型
    createCardPicker: function(this: Deck) {
        return () => {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return {suit: this.suits[pickedSuit], card: pickedCard % 13};
        }
    }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

现在typeScript知道createCardPicker期望在某个Deck对象上调用,this指向的Deck对象而不是Any,noImplicitThis就不会报错了。

回调函数中的this参数

当你把函数传递到某个库函数的回调函数内执行时,也会出现this为undefined的问题,这是因为当回调函数被调用时,它被当作普通函数,this为undefined。 如何解决这个问题? 首先,库函数的作者需要指定this的类型

interface UIElement {
    addClickListener(onclick: (this: void, e: Event) => void): void;
}

被调用的函数

class Handler {
    info: string;
    onClickBad(this: Handler, e: Event) {
        // 注意,在这里使用this,调用这个回调时会崩溃
        this.info = e.message;
    }
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // 报错!

为什么会报错呢? 在接口指定了this类型后,你显式声明onClickBad必须在Handler的实例上调用。而Typescript又监测出addClickListener需要一个void类型的this,所以会报错。 改变一下this的指定类型再试一下:

class Handler {
    info: string;
    onClickGood(this: void, e: Event) {
        // 把this的类型改变成void后,就不能在这里使用this了,因为它的类型是void啦!
        console.log('clicked!');
    }
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);

如果你又想在回调函数里面使用this,又不想报错,那就只能用箭头函数了

class Handler {
    info: string;
    onClickGood = (e: Event) => { this.info = e.message }
}

这样是可行的,因为箭头函数并不会捕获this

重载

JavaScript是个动态语言,函数根据传入的不同参数返回不同类型的结果是很常见的事情。比如下面这个例子:

// 牌的类型数组:红桃,黑桃,梅花,方块
let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x): any {
    // 如果类型是 object\array, 从用户手中抽牌,传入你现在的牌的数组,返回随机抽到的你的牌
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    // 如果是数字,则根据数字发对应的牌
    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);

上面这个方法在类型系统中怎么表示呢? 答案是为同一个函数提供多种函数类型定义来进行重载!(没想到吧,呵呵呵)下面是代码

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 {
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    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);

这样,这个函数在调用的时候就会进行正确的类型检查。 注意: 在定义重载的时候,一定要把最精确的定义放在最前面。(为啥?你猜) 注意: function pickCard(x): any并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字。 以其它参数调用pickCard会产生错误。