TypeScript学习笔记 -- (五、函数)

114 阅读9分钟

前言

  • 函数是JavaScript应用程序的基础。 它帮助你实现抽象层,模拟类,信息隐藏和模块,在TypeScript里,虽然已经支持类,命名空间和模块,但 函数仍然是主要的定义行为的地方。

😸简单入门

以下用一个非常非常简单的函数来说明函数包含哪些东西

// 函数声明式
function add(x,y) {
    return x + y;
}


// 函数表达式
var myAdd = function (x,y) {
    return x + y;
};

以上有两个函数(上面两个函数的相同点为有相同数量的参数,并且返回值为两者相加)

函数声明式:函数声明后不会立即执行,只是在初始化的时候会将 函数声明提升,会在我们需要的时候调用到。

函数表达式:实际上是一个 匿名函数 (函数没有名称),函数存储在变量中,不需要函数名称,通常通过 变量名 来调用。

下面进入重点,我们对上面的函数进行改造,为其添加类型

function add(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; 
};

函数类型包含两部分:参数类型和返回值类型。 当写出完整函数类型的时候,这两部分都是需要的。

只要 参数类型匹配 的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。 第二部分是返回值类型。 对于返回值,我们在函数和返回值类型之前使用(=>) 符号,使之清晰明了。

推断类型 下面这个这个例子为什么展示的是,你在赋值语句的一边指定了类型但是另一边没有类型的话,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) {
    return firstName + " " + lastName;
}
let result1 = buildName("Bob");                  // error
let result2 = buildName("Bob", "Adams", "Sr.");  // error
let result3 = buildName("Bob", "Adams");         // success

在我们平时写JS的时候,我们参数都是可选的,可传可不传。 没传参的时候,它的值就是undefined。 但是呢,TypeScript不一样,我们来看一下他的语法:参数名旁使用 ? 实现可选参数的功能。

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

let result1 = buildName("Bob");
let result2 = buildName("Bob", "Adams", "Sr.");  // error
let result3 = buildName("Bob", "Adams");

可选参数必须 跟在 必须参数后面。 如果上例我们想让first name是可选的,那么就必须调整它们的位置,把first name放在后面。

设置默认参数 我们也可以为参数提供一个默认值,当用户没有传递这个参数或传递的值是 undefined 时。它们叫做有默认初始化值的参数。 让我们修改上例,把last name的默认值设置为"张三"

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

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

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

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

image.png

image.png

以上两张图片,我们可以看见两个函数共享同样的类型 (firstName: string, lastName?: string) => string

❤️‍🔥 Tips: 与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。位置不做要求

如果 带默认值的参数 出现在必须参数 前面,用户必须 明确传入 undefined 值来获得默认值。

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

let result1 = buildName("Bob");                  // error
let result2 = buildName("Bob", "Adams", "Sr.");  // error
let result3 = buildName("Bob", "Adams");         // ok
let result4 = buildName(undefined, "Adams");     // ok

result1缺少第二个必须参数,result2多出一个参数

😸剩余参数

必要参数默认参数可选参数 有个共同点:它们表示某一个参数

有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在JavaScript里,你可以使用 arguments来访问所有传入的参数。

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

function buildName(firstName:string,...restOfName:string[]) {

}

剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个

编译器创建参数数组,名字是你在省略号( ...)后面给定的名字,你可以在函数体内使用这个数组。

这个省略号也会在带有剩余参数的函数类型定义上使用到:

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

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

😹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();
console.log("card: " + pickedCard.card + " of " + pickedCard.suit);

好!大家可以先不要急着往下,先想一下上面会输出什么?

-----------------------------------------------------分割线---------------------------------------------------- 我们尝试去执行这个程序的时候,发现报错了。

image.png 想必大家对上面的这个报错非常熟悉,无法读取未定义的属性

为什么会这样子呢?

因为 createCardPicker 返回的函数里的 this 被设置成了 window 而不是 deck 对象。 因为我们只是独立的调用了 cardPicker() 。 顶级的非方法式调用会将 this视为window。 (注意:在严格模式下, thisundefined而不是window)。

image.png

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

var deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function() {
        // NOTE: the line below is now an arrow function, allowing us to capture 'this' right here
        return () => {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);
            return {suit: this.suits[pickedSuit], card: pickedCard % 13};
        }
    }
}
var cardPicker = deck.createCardPicker();
var pickedCard = cardPicker();
console.log("card: " + pickedCard.card + " of " + pickedCard.suit);

更好事情是,TypeScript会警告你犯了一个错误,如果你给编译器设置了--noImplicitThis标记。 它会指出 this.suits[pickedSuit]里的 this 的类型为 anyimage.png

😹this 参数

不幸的是,this.suits[pickedSuit]的类型依旧为any。 这是因为 this来自对象字面量里的函数表达式。 修改的方法是,提供一个显式的 this参数。 this参数是个假的参数,它出现在参数列表的最前面:

function f(this: void) { //确保“this”在此独立函数中不可用 }

让我们往例子里添加一些接口,Card 和 Deck,让类型重用能够变得清晰简单些:

interface Card {
    suit: string;
    card: number;
}

interface Deck {
    suits: string[];
    cards: number[];
    createCardPicker(this:Deck): () => Card;
}

var deck: Deck = {
    suits:[],
    cards:[],
    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};
        }
    }
};

现在TypeScript知道createCardPicker期望在某个Deck对象上调用。 也就是说 thisDeck类型的,而非any

image.png

😹this参数在回调函数里

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

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


class Handler {
    info: string;
    onClickBad(this: Handler, e: Event){
        this.info = e.type;
    }
    onClickGood(this: void, e: Event) {
        console.log("say hello");
    }
}
let h = new Handler();
let uiElement: UIElement;
uiElement.addClickListener(h.onClickBad);    // error
uiElement.addClickListener(h.onClickGood);   // correct

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

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


class Handlers {
    info: string;
    onClickGood = (e: Event) => { this.info = "xxx" }
}

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

😺重载

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

方法是为同一个函数提供多个函数类型定义来进行函数重载。 编译器会根据这个列表去处理函数的调用。 下面我们来重载 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 {
  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);

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

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

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

😸总结

  • 首先非常感谢掘金的各位小伙伴的阅读,本人在掘金开启自己的学习记录分享😄(这也是我在掘金的第三篇博客)。

  • 上文如果描述有错误的,欢迎指正哦。也欢迎各位小伙伴一起交流学习,分享工作经验😃。

  • 本文是依照TypeScript官网 + 自己的语言描述结合写出来的。希望对各位刚开始学TypeScript有帮助哦 🥰!

  • 本文仅用作学习用途