LayaBox学习小札-05 TypeScript语言的function


请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com

1 函数的声明定义方式

和JavaScript一样,TypeScript函数可以创建有具名函数和匿名函数。如下代码所示:

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

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

在JavaScript里,函数可以使用函数体外部的变量。 当函数这么做时,即认为它‘捕获’了这些变量。 这个C语言里,在一个函数中访问一个全局变量近似:

let z = 100;

function addToZ(x, y) {
    return x + y + z;
}

近似于C语言中的:

extern int z = 100;
int addTo2(int x, int y )
{
    return x + y + z;
}

2 “函数类型”变量

和lua语言一样,TypeScript语言也把function视为first class。也就是说可以把“函数”视为一种变量类型,可以把一个具名或者匿名的函数赋值给“类型变量”,如下代码所示:

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

// 把匿名函数赋值给函数类型变量myAdd
let myAdd = function(x: number, y: number): number { return x + y; };

// 把具名函数multiply赋值给函数类型变量myMultiply
let myMultiply = multiply;

console.log(myAdd(10,20)); // 打印出30
console.log(myMultiply(10,20)); // 打印出200

在上面的声明一个“函数类型”变量myAdd和myMultiply时,实质上是利用类型推导的方式简化了声明,完整的“函数类型”变量声明格式如下:
let 函数类型变量名 : (参数1名:参数1类型,参数2名:参数2类型 ... ) => 函数返回值类型。
示例代码如下:

// 显式地声明一个函数类型变量的方式格式是:
// let 函数类型变量名 : (参数1名:参数1类型,参数2名:参数2类型 ... ) => 函数返回值类型
let myAdd :(x:number,y:number) => number =
function(x:number,y:number):number{return x+y;}

3 函数的可选参数和参数默认值

TypeScript里的每个函数参数都是必须的。编译器检查用户是否为每个参数都传入了值。编译器还会假设只有这些参数会被传递进函数。简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。而在JavaScript里,每个参数都是可选的,可传可不传。没传参的时候,它的值就是undefined。 在TypeScript中使用了函数的可选参数机制实现了JavaScript语言的同等效果。在函数参数名后加上“?”即可选参数的功能。 比如,想让last name是可选的可以如下做法:

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

let result1 = buildName("Bob");                  // 错误,参数个数过少
let result2 = buildName("Bob", "Adams", "Sr.");  // 错误,参数个数过多
let result3 = buildName("Bob", "Adams");         // 正确

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

let result4 = buildName2("Ken");  // 正确,lastName可以不传参
let result5 = buildName2("Jim","Green"); // 正确,lastName也可以传参

TypeScript的函数的默认值参数的规则和C++语言的规则相同,也是要把默认值参数放在最后。

4 函数的剩余参数

必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在JavaScript里,你可以使用arguments来访问所有传入的参数。在TypeScript里,你可以把所有参数收集到一个变量里:剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号( ...)后面给定的名字,你可以在函数体内使用这个数组。例如有以下的typescript使用了剩余参数:

function buildName(firstName: string, ...restOfName: string[]) 
{
    // restOfName.join(" ")表示把restOfName数组中的每一项字符串用空格符连接成一个新字符串
    return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
let button = document.createElement('button');
button.textContent = "Say Hello";
button.onclick = function ()
{
    alert(employeeName);
}

document.body.appendChild(button);

对应的javascript代码如下:

// javascript中不需要在声明中指定到底要传入多少个参数,未明确声明的参数都会依次汇集到arguments数组中
function buildName(firstName) {
    var restOfName = [];
    // arguments是javascript中保留的一个参数数组。
    for (var _i = 1; _i < arguments.length; _i++) {
        restOfName[_i - 1] = arguments[_i];
    }
    return firstName + " " + restOfName.join(" ");
}

var employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
var button = document.createElement('button');
button.textContent = "Say Hello";
button.onclick = function () {
    alert(employeeName);
};
document.body.appendChild(button);

5 this和箭头函数

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

下面看一个例子:

let deck = // deck是一个interface
{
    suits: ["hearts", "spades", "clubs", "diamonds"], //suits是一个数组
    cards: Array(52), // cards是一个数组对象
    createCardPicker: function() { //createCardPicker是一个函数类型属性
        return function() { // 函数内部又定义一个闭包函数,此闭包返回一个interface类型变量
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);
            // 这里的this不是deck,而是window
            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 () => { // 使用箭头语法明确地指定this就是deck
            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.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) { // 明确指明本函数的this就是Deck
        return () => { // 明确指明本函数的this就是Deck
            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不会报错了。

7 this参数在回调函数里

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

interface UIElement {
    // 声明监听器接口,因为不知道onClick会是一个怎样的对象的成员方法,所以this要声明为void
    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;
    // 回调函数要明确地传入this指针进去
    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就是Handler的实例对象
}

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

一句话,JavaScript的this,要随着运行时的上下文环境,去确定具体的指代值。

上一章
下一章
返回首页