TypeScript学习-函数

130 阅读11分钟

函数

介绍

函数是JavaScript应用程序的基础,它帮助你实现抽象层,模拟层,信息隐藏和模块。在TypeScript中,虽然已经支持了类,命名空间个模块,但是函数仍然是主要的定义行为的地方。TypeScriptJavaScript添加了额外的功能,让开发者可以更容易的利用。

函数

TypeScritp可以创建带有名称的函数和匿名函数,开发者可以随意选择适合应用程序的方式,不管是定义一系列API函数还是只使用一次函数。

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

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

JavaScript中,函数可以使用函数体外部的变量,这叫做捕获变量。其实这就涉及到函数作用域相关的知识。

let z = 100

function add(x: number, y: number) {
    return x + y + z
}

console.log(add(1, 2)) // 103

函数类型

函数定义类型

函数的返回值也可以定义类型。

function add(x: number, y: number): number {
    return x + y
}

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

可以为每个函数的参数进行类型规定,然后再为函数本身添加返回值类型,TypeScript能够根据返回语句自动推断出返回值的类型,所以在平时开发的时候会省略它。

书写完整的函数类型

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

函数的类型包括参数类型以及返回值类型,当开发者写出完整的类型的时候,这两部分是需要的,开发者参数列表的形式写出参数类型,为每个参数指定一个名字和类型,添加代码的可读性,当然你也可以这样写。

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

只要是参数类型是匹配的,参数名称是不需要一致的(在合法范围内)。

对于函数返回值,在函数返回值类型之前使用(=>)符号。函数返回值类型是函数的必要部分,如果函数没有任何返回值,你也必须指定返回值类型为void而不是空。

函数的类型只是由参数类型和返回值类型组成的。函数中使用的捕获变量不会体现在类型上,实际上,这些变量的函数的隐藏状态并不是组成API的一部分。

推断类型

当你在赋值语句的一边指定了类型,但是另外一边却没有指定类型,TypeScript编译器会自动识别出类型。

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

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

这叫做按上下文归类,是类型推断的一种,能够帮助开发者更好的为程序指定类型。

可选参数和默认参数

TypeScript中的每一个函数参数都是必须的,这不是指不能传递nullundefined作为参数,而是说编译器检查用户是否为每个参数都传了值。编译器还会假设只有这些参数会被传递进函数。也就是说传递给一个函数的参数个数必须与函数期望的参数个数一致。

function buildName(firstName: string, lastName: string): string {
    return firstName + "-" + lastName
}

let result1 = buildName("Bob") // error 应有 2 个参数,但获得 1 个

let result2 = buildName("Bob", "Adams", "Sr") // error 应有 2 个参数,但获得 3 个。

let result3 = buildName("", "Bob")
console.log(result3) // -Bob

let result4 = buildName("Bob", "Adams")
console.log(result4) // Bob-Adams

JavaScript中,每个参数是可传可不传,没有传参的时候默认undefined,在TypeScript中,开发者可以在参数旁边使用?实现参数的可传和可不传,比如我们想让lastName是可选的。

function buildName(firstName: string, lastName?: string): string {
    return firstName + "-" + lastName
}

let result1 = buildName("Bob") // error 应有 2 个参数,但获得 1 个
console.log(result1) // Bob-undefined
let result2 = buildName("Bob", "Adams", "Sr") // error 应有 2 个参数,但获得 3 个。

let result3 = buildName("", "Bob")
console.log(result3) // -Bob

let result4 = buildName("Bob", "Adams")
console.log(result4) // Bob-Adams

上面代码只有result2是错误的,因为它的传递给函数参数个数超过了函数的参数个数。

值得注意的是,可选参数必须跟在必须参数后面。

TypeScript中,我们也可以为参数提供一个默认值,当用户没有传递这个参数或者传递的值为undefined时,这叫做默认初始化的参数。

将上面代码改写成带有默认参数的函数,把lastName默认值设置为Smith

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

let result1 = buildName("Bob") // error 应有 2 个参数,但获得 1 个
console.log(result1) // Bob-Smith
let result2 = buildName("Bob", "Adams", "Sr") // error 应有 2 个参数,但获得 3 个。

let result3 = buildName("", "Bob")
console.log(result3) // -Bob

let result4 = buildName("Bob", "Adams")
console.log(result4) // Bob-Adams

在所有必须参数后面的带默认初始化的参数都是可选的,与其他可选参数一样,在调用函数的时候可以省略,也就是说可选参数与末尾的默认参数共享数据类型。

function buildName(firstName: string, lastName?: string): string {
    // ...
}

function buildName(firstName: string, lastName = "Smith"): string {
    // ...
}

共享数据类型(firstName: string, lastName?: string) => string。默认参数的默认值消失了,只保留它是一个可选参数的信息。

与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面,如果带默认值的参数出现在必须参数前面,用户必须明确传入undefined来换取默认值,例如,重写上面的一个代码,让firstName是带默认值的参数。

function buildName(firstName = "Will", lastName: string) {
    return firstName + "-" + lastName
}

let result1 = buildName("Bob") // error 应有 2 个参数,但获得 1 个。

let result2 = buildName("Bob", "Adams", "Sr") // error 应有 2 个参数,但获得 3 个。

let result3 = buildName("Bob", "Adams")
console.log(result3) // Bob-Adams

let result4 = buildName("", "Adams")
console.log(result4) // -Adams

let result5 = buildName(undefined, "Adams")
console.log(result5) // Will-Adams

剩余参数

必要参数,默认参数和可选参数都有共同点,它们表示某一个参数,有事,你想同时操作多个参数,或者你并不知道会有多少个参数传递进来。在JavaScript里,你可以使用arguments来访问所传入的参数。

TypeScript中,你可以把所有参数收集到一个变量里。

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

let result = buildName("Joseph", "Samuel", "Lucas", "MacKinzie")
console.log(result) // Joseph Samuel Lucas MacKinzie

剩余参数会被当做数量不限的可选参数,可以都没有,也可以是任意个,编译器创建参数数组,名字是你的restOfName,通过...拓展运算符将剩余参数依次加入了数组restOfName中,然后就可以在函数内部使用这个数组了。

同样...拓展运算符也可以在带有剩余参数的函数类型定义上使用到。

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

let buildNameFun: (fname: string, ...rest: string[]) => string = buildName

this

如何学会在JavaScript中使用this就好比是一场成人礼,由于TypeScriptJavaScript的超集,TypeScript程序员也需要弄清楚this的工作机制以及当前bug存在的位置并且找出错误,很幸运,TypeScript能通知你错误的使用this的地方,这里就简单的介绍一下this的基本应用。

this和箭头函数

JavaScript中,this的值在函数被调用时才会被指定,这是个既强大又灵活的特点,但是你需要花时间去弄清楚函数调用的上下文是什么,但是这不是一件很容易的事,尤其是在返回一个函数或者将函数当做参数传递的时候。

let deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function () {
        return function () {
            let pickerCard = Math.floor(Math.random() * 52)
            let pickerSuits = Math.floor(pickerCard / 13)
            return { suit: this.suits[pickerSuits], card: pickerCard % 13 } // this" 隐式具有类型 "any",因为它没有类型注释。
        }
    }
}

let cardPicker = deck.createCardPicker()
let pickerCard = cardPicker()

console.log("card " + pickerCard.card + " of " + pickerCard.suit)

上面代码中。可以看到createCardPicker是一个函数,并且它又返回一个函数,如果我们尝试运行这个程序,会发现直接是报错的,因为createCardPicker返回的函数里面this被设置成window而不是deck对象,因为我们只是独立调用了cardPicker(),而顶级的非方法会调用将this视为window。(注意在严格模式下,thisundefined而不是window)。

为了解决上面这个问题,我们可以在函数被返回的时候就绑定好this,这样无论你怎么使用它,都会引用绑定的deck对象,只需要将返回函数改为箭头函数=>,箭头函数能保存函数创建时的this,而不是调用之后的this

let deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function () {
        return () => {
            let pickerCard = Math.floor(Math.random() * 52)
            let pickerSuits = Math.floor(pickerCard / 13)
            return { suit: this.suits[pickerSuits], card: pickerCard % 13 }
        }
    }
}

let cardPicker = deck.createCardPicker()
let pickerCard = cardPicker()

console.log("card " + pickerCard.card + " of " + pickerCard.suit) // card 8 of diamonds

更好的事情是,如果你给TypeScript编译器设置了--noImplicitThis标记,它会指出this.suits[pickerSuits]里的this类型为any

this参数

上面代码中还存在问题是,this.suits[pickerSuits]的类型依旧为any,这是因为this来自对象字面量的函数表达式。修改的方法是,提供一个显式的this参数。this参数是个假的参数,它会出现在参数列表的最前面。

function f(this: void) {
    // ...
}

为上面的代码添加一些接口,CardDeck,让类型重用能够变得清晰简单些。

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),
    createCardPicker: function (this: Deck) {
        return () => {
            let pickerCard = Math.floor(Math.random() * 52)
            let pickerSuits = Math.floor(pickerCard / 13)
            return { suit: this.suits[pickerSuits], card: pickerCard % 13 }
        }
    }
}

let cardPicker = deck.createCardPicker()
let pickerCard = cardPicker()

console.log("card " + pickerCard.card + " of " + pickerCard.suit) // card 9 of clubs

现在TypeScript知道createCardPicker期望在某个Deck对象上调用,也就是thisDeck类型的,而非any,因此--noImplicitThis不会再报错了。

this参数在回调函数里面

你也许看到过在回调函数里面this报错,当你将一个函数传递到某个库函数里面稍后会被调用时,当回调函数被调用时,它就会被当做一个普通函数调用,this将为undefined,稍微做点改动,就可以通过this参数来避免错误。首先库函数的作者要指向this的类型。

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

this: void意味着addClickListener期望onclick是一个不需要this类型的函数。其次,用这个注释你的调用代码。

class Handler {
    info: string
    onClickBad(this: Handler, e: Event) {
        this.info = e.message
    }
}

let h = new Handler()
uiElement.addClickListener(h.onClickBad)

指定了this类型后,你显式声明的onClickBad必须在Handler的实例上调用,然后TypeScript会检测到addClickListener要求函数带有this: void。改变this类型来修复这个错误。

class Handler {
    info: string
    onClickGood(this: Handler, e: Event) {
        console.log("clicked")
    }
}

let h = new Handler()
uiElement.addClickListener(h.onClickGood)

因为onClickGood指定了this类型为void,因此传递addClickListener是合法的。当然了,这也意味着不能使用 this.info。 如果你两者都想要,你不得不使用箭头函数了。

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

这里可行的原因是箭头函数不会捕获this,所以你总是可以把它传递给期望this: void的函数,缺点是每个Handler对象都会创建一个箭头函数,另一方面,方法只会被创建一次,添加到Handler的原型链上,它们在不同Handler对象之间共享。

重载

JavaScript本身就是一个动态语言,JavaScript里函数根据传入不同的参数而返回不同类型的数据是很常见的。

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

function pickCard(x: any): any {
    if (typeof x === "object") {
        let pickCard = Math.floor(Math.random() * x.length)
        return pickCard
    } 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)]
console.log("card " + pickedCard1.card + " of " + pickedCard1.suit) // card 10 of spades

let pickedCard2 = pickCard(15)
console.log("card " + pickedCard2.card + " of " + pickedCard2.suit) // card 2 of spades

pickCard方法根据传入参数的不同会返回两种不同的类型。如果传入的是代表纸牌的对象,函数作用是从中抓一张牌。如果用户想抓牌,我们告诉他抓到了什么牌。但是这怎么在类型系统里表示呢。

方法是为同一个函数提供多个函数类型定义来进行函数重载。编译器会根据这个列表去处理函数的调用。 面我们来重载 pickCard函数。

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): 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)];
console.log("card: " + pickedCard1.card + " of " + pickedCard1.suit); // card: 4 of hearts

let pickedCard2 = pickCard(15);
console.log("card: " + pickedCard2.card + " of " + pickedCard2.suit) // card: 2 of spades

这样改变后,重载的pickCard函数在调用的时候会进行正确的类型检查。

为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。如果匹配的话就使用这个。因此,在定义重载的时候,一定要把最精确的定义放在最前面。

注意,function pickCard(x): any并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字。 以其它参数调用pickCard会产生错误。