TypeScript学习之路——(4)Functions(函数)

TypeScript v3.1

函数类型

为函数定义类型
function add(x:number,y:number):number{
    return x+y
}
let myAdd = function(x: number, y: number): number { return x + y; };

//可以给每个参数添加类型之后再为函数本身添加返回值类型。 TypeScript能够根据返回语句自动推断出返回值类型,因此我们通常省略它。

书写完整函数类型
let myAdd2:(x:number,y:number)=>number = function(x: number, y: number): number { return x + y; };

//函数类型包含两部分:参数类型和返回值类型。
//当写出完整函数类型的时候,这两部分都是需要的。
//我们以参数列表的形式写出参数类型,为每个参数指定一个名字和类型。
//这个名字只是为了增加可读性。 我们也可以这么写:

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

//只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。
//第二部分是返回值类型。
// 对于返回值,我们在函数和返回值类型之前使用( =>)符号,使之清晰明了。
//如之前提到的,返回值类型是函数类型的必要部分,如果函数没有返回任何值,你也必须指定返回值类型为 void而不能留空。
//函数的类型只是由参数类型和返回值组成的。 函数中使用的捕获变量不会体现在类型里。 实际上,这些变量是函数的隐藏状态并不是组成API的一部分。

推断类型

//尝试这个例子的时候,你会发现如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型:

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

//这叫做“按上下文归类”,是类型推论的一种。 它帮助我们更好地为程序指定类型。

可选参数和默认参数

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

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

// let result1 = buildName("Bob");                  // error, too few parameters
// let result2 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
let result3 = buildName("Bob", "Adams");         // ah, just right

JavaScript里,每个参数都是可选的,可传可不传。
没传参的时候,它的值就是undefined。
在TypeScript里我们可以在参数名旁使用 ?实现可选参数的功能。
比如,我们想让last name是可选的

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

let result4 = buildName2("Bob");  // works correctly now
// let result5 = buildName2("Bob", "Adams", "Sr.");  // error, too many parameters
let result6 = buildName2("Bob", "Adams");  // ah, just right

//可选参数必须跟在必须参数后面。 如果上例我们想让first name是可选的,那么就必须调整它们的位置,把first name放在后面。
//在TypeScript里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是undefined时。 它们叫做有默认初始化值的参数。 让我们修改上例,把last name的默认值设置为"Smith"。

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

let result7 = buildName3("Bob");                  // works correctly now, returns "Bob Smith"
let result8 = buildName3("Bob", undefined);       // still works, also returns "Bob Smith"
// let result9 = buildName3("Bob", "Adams", "Sr.");  // error, too many parameters
let result10 = buildName3("Bob", "Adams");         // ah, just right

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

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

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

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

与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined值来获得默认值。 例如,我们重写最后一个例子,让 firstName是带默认值的参数:

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

// let result11 = buildName4("Bob");                  // error, too few parameters
// let result12 = buildName4("Bob", "Adams", "Sr.");  // error, too many parameters
let result13 = buildName4("Bob", "Adams");         // okay and returns "Bob Adams"
let result14 = buildName4(undefined, "Adams");     // okay and returns "Will Adams"

剩余参数

function buildName5(firstName: string, ...restOfName: string[]) {
    return firstName + " " + restOfName.join(" ");
}
  
let employeeName = buildName5("Joseph", "Samuel", "Lucas", "MacKinzie");
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName5;

this

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

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

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

let deck2 = {
    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};
        }
    }
}

let cardPicker2 = deck2.createCardPicker();
let pickedCard2 = cardPicker2();
console.log("card: " + pickedCard2.card + " of " + pickedCard2.suit);

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

this参数

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

function f(this: void) {
    // make sure `this` is unusable in this standalone function
}

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

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

let deck3:Deck = {
    suits:["hearts", "spades", "clubs", "diamonds"],
    cards:Array(32),
    // NOTE: The function now explicitly specifies that its callee must be of type 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 cardPicker3 = deck3.createCardPicker();
let pickedCard3 = cardPicker3();

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

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

this参数在回调函数里

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

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

this: void意味着addClickListener期望onclick是一个不需要this类型的函数。
其次,使用以下方法注释您的调用代码this:

class Handler {
    info: string;
    onClickBad(this: Handler, e: Event) {
        // oops, used this here. using this callback would crash at runtime
        this.info = e.type;
    }
}
let h = new Handler();
document.querySelector('body').addEventListener('click',h.onClickBad); // error!

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

class Handler2 {
    info: string;
    onClickGood(this: void, e: Event) {
        // can't use this here because it's of type void!
        console.log('clicked!');
    }
}
let h2 = new Handler2();
document.querySelector('body').addEventListener('click',h.onClickBad); // error!

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

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

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

重载

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

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 pickedCard4 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard4.card + " of " + pickedCard4.suit);

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

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

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

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

function pickCard6(x: {suit: string; card: number; }[]): number;
function pickCard6(x: number): {suit: string; card: number; };
function pickCard6(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: suits6[pickedSuit], card: x % 13 };
    }
}

let myDeck6 = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard6 = myDeck[pickCard6(myDeck)];
alert("card: " + pickedCard6.card + " of " + pickedCard6.suit);

let pickedCard7 = pickCard6(15);
alert("card: " + pickedCard7.card + " of " + pickedCard7.suit);

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容