typescript 函数
日期: 2019 年 9 月 3 日
typescript 函数
具名函数与匿名函数
和JavaScript一样,TypeScript函数可以创建有名字的函数和匿名函数
/**
* @description Named function
* @param {*} x
* @param {*} y
* @returns
*/
function add(x, y){
return x + y;
}
/**
* Anonymous function
* @param x
* @param y
*/
let myAdd = function(x, y){
return x + y;
}
在JavaScript里,函数可以使用函数体外部的变量。 当函数这么做时,我们说它‘捕获’了这些变量
/**
* 捕获——函数使用函数体外部的变量
*/
let z = 100;
function addToZ(x, y){
return x + y + z;
}
函数类型
为函数定义类型
让我们为上面那个函数添加类型:
/**
* @description 为函数定义类型
* @param {number} x
* @param {number} y
* @returns {number}
*/
function add1(x: number, y: number): number{
return x + y;
}
let myAdd1 = function(x: number, y: number): number{ return x + y };
完整的函数类型
现在我们已经为函数指定了类型,下面让我们写出函数的完整类型
/**
* 书写完整的函数类型
* @param x
* @param y
*/
let myAdd2: (x: number, y: number) => number =
function(x: number, y: number): number{ return x + y; }
函数类型包含两部分:参数类型和返回值类型。 当写出完整函数类型的时候,这两部分都是需要的。 我们以参数列表的形式写出参数类型,为每个参数指定一个名字和类型。 这个名字只是为了增加可读性。 我们也可以这么写:
/**
* 函数类型包含两部分:参数类型和返回值类型,
* 只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确
* @param x
* @param y
*/
let myAdd3: (baseValue: number, increment: number) => number =
function(x: number, y: number): number{ return x + y; }
只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确
推断类型
如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型:
**
* 推断类型
* @param x
* @param y
*/
let myAdd4: (baseValue: number, increment: number) => number =
function(x, y){ return x + y; }
可选参数与默认参数
可选参数
TypeScript里的每个函数参数都是必须的。 这不是指不能传递 null或undefined作为参数,而是说编译器检查用户是否为每个参数都传入了值。 编译器还会假设只有这些参数会被传递进函数。 简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致
/**
* TypeScript里的每个函数参数都是必须的
* 传递给一个函数的参数个数必须与函数期望的参数个数一致
* @param firstName
* @param lastName
*/
function buildName( firstName: string, lastName: string ){
return firstName + " " + lastName;
}
// let result1 = buildName("Bob"); // error
// let result2 = buildName("Bob", "Smith", "Sr."); // error
let result3 = buildName("zhang", "li"); // ok!
JavaScript里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是undefined。 在TypeScript里我们可以在参数名旁使用 ?实现可选参数的功能。 比如,我们想让last name是可选的:
/**
* JavaScript里,每个参数都是可选的,可传可不传
* 在TypeScript里我们可以在参数名旁使用 ?实现可选参数的功能, 比如,我们想让last name是可选的
* 可选参数必须跟在必须参数后面,如果我们想让first name是可选的,那么就必须调整它们的位置,把first name放在后面
* @param firstName
* @param lastName
*/
function buildName1( firstName: string, lastName?: string ){
if(lastName){
return firstName + " " + lastName;
}else{
return firstName;
}
}
let result1 = buildName1("Bob"); // ok
// let result2 = buildName1("Bob", "Smith", "sr."); // error
let result4 = buildName1("zhang", "li"); // ok
可选参数必须跟在必须参数后面。 如果上例我们想让first name是可选的,那么就必须调整它们的位置,把first name放在后面
默认参数
在TypeScript里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是undefined时。 它们叫做有默认初始化值的参数。 让我们修改上例,把last name的默认值设置为"Smith"
/**
* 在TypeScript里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是undefined时
* 它们叫做有默认初始化值的参数
* @param firstName
* @param lastName
*/
function buildName2(firstName: string, lastName = "Smith" ){
return firstName + " " + lastName;
}
let result2 = buildName2("Bob"); // ok "Bob Smith"
let result5 = buildName2("Bob",undefined); // ok "Bob Smith"
// let result6 = buildName2("Bob","Adama", "Sr."); // error
let result6 = buildName2( "zhang", "li"); // ok "zhang li"
与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面
/**
* 与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面
* 如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined值来获得默认值
* @param firstName
* @param lastName
*/
function buildName3(firstName = "will", lastName: string){
return firstName + " " + lastName;
}
// let result7 = buildName3("Bob"); // error
let result8 = buildName3("zhang", "li"); // ok "zhang li"
let result9 = buildName3(undefined, "Smith"); // ok "will Smith"
剩余参数
必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在JavaScript里,你可以使用 arguments来访问所有传入的参数
在TypeScript里,你可以把所有参数收集到一个变量里:
/**
* 剩余参数
* 剩余参数会被当做个数不限的可选参数,可以一个都没有,同样也可以有任意个
* @param firstName
* @param restOfName
*/
function buildName4(firstName: string, ...restOfName: string[]){
return firstName + " " + restOfName.join("-");
}
let employeeNames = buildName4("Bob", "Will", "Smith", "Haha", "Heihei");
console.log(employeeNames);
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName4;
let employeeName = buildName4("Bob", "Will", "Smith", "Haha", "Heihei");
console.log(employeeName);
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();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
可以看到createCardPicker是个函数,并且它又返回了一个函数。 如果我们尝试运行这个程序,会发现它并没有弹出对话框而是报错了。 因为 createCardPicker返回的函数里的this被设置成了window而不是deck对象。 因为我们只是独立的调用了 cardPicker()。 顶级的非方法式调用会将 this视为window。 (注意:在严格模式下, this为undefined而不是window)。
为了解决这个问题,我们可以在函数被返回时就绑好正确的this。 这样的话,无论之后怎么使用它,都会引用绑定的‘deck’对象。 我们需要改变函数表达式来使用ECMAScript 6箭头语法。 箭头函数能保存函数创建时的 this值,而不是调用时的值:
let 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};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.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 deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// 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 cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.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 means that addClickListener expects onclick to be a function that does not require a this type. Second, annotate your calling code with this:
class Handler {
info: string;
onClickBad(this: Handler, e: Event) {
// oops, used this here. using this callback would crash at runtime
this.info = e.message;
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // error!
指定了this类型后,你显式声明onClickBad必须在Handler的实例上调用。 然后TypeScript会检测到 addClickListener要求函数带有this: void。 改变 this类型来修复这个错误:
class Handler {
info: string;
onClickGood(this: void, e: Event) {
// can't use this here because it's of type void!
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 {
// 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方法根据传入参数的不同会返回两种不同的类型。 如果传入的是代表纸牌的对象,函数作用是从中抓一张牌。 如果用户想抓牌,我们告诉他抓到了什么牌。 但是这怎么在类型系统里表示呢。
方法是为同一个函数提供多个函数类型定义来进行函数重载。 编译器会根据这个列表去处理函数的调用。 下面我们来重载 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 {
// 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函数在调用的时候会进行正确的类型检查。
为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。
注意,function pickCard(x): any并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字。 以其它参数调用 pickCard会产生错误