typescript

TS 笔记六 函数 关键字this

2022-03-03  本文已影响0人  合肥黑

参考https://github.com/zhongsp/TypeScript

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

参考js红宝书笔记十一 第十章 函数 闭包

一、JS中的函数

通过下面的例子可以迅速回想起这两种JavaScript中的函数:

// Named function
function add(x, y) {
    return x + y;
}

// Anonymous function
let myAdd = function(x, y) { return x + y; };

在JavaScript里,函数可以使用函数体外部的变量。 当函数这么做时,我们说它‘捕获’了这些变量。 至于为什么可以这样做以及其中的利弊超出了本文的范围,但是深刻理解这个机制对学习JavaScript和TypeScript会很有帮助。

let z = 100;

function addToZ(x, y) {
    return x + y + z;
}
二、为函数定义类型
function add(x: number, y: number): number {
    return x + y;
}

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

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

三、参数
1.可选参数

JavaScript里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是undefined。

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

function buildName(firstName: string, lastName: string) {
    return firstName + " " + lastName;
}
// error, too few parameters
let result1 = buildName("Bob"); 

// error, too many parameters
let result2 = buildName("Bob", "Adams", "Sr."); 

// ah, just right
let result3 = buildName("Bob", "Adams");         

在TypeScript里我们可以在参数名旁使用?实现可选参数的功能。 可选参数必须跟在必须参数后面。

function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}
2.参数默认值

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

function buildName(firstName: string, lastName = "Smith") {
    return firstName + " " + lastName;
}
3.剩余参数

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

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

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

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号(...)后面给定的名字,你可以在函数体内使用这个数组。

四、this
1.JS中的this

如果你以前没用过JS,有一件事可能会令你惊讶:JS中的每个函数都有this变量,而不局限于类中的方法。以不同的方式调用函数,this的值也不同,这极易导致代码脆弱、难以理解。鉴于此,很多团队禁止在类方法以外使用this。如果你也想这么做,开启eslint的no-invalid-this规则。

以下参考
js红宝书笔记十一 第十章 函数 闭包
js es6 => arrow function箭头函数

window.color = 'red'; 
let o = { 
 color: 'blue' 
}; 
function sayColor() { 
 console.log(this.color); 
} 
sayColor(); // 'red' 
o.sayColor = sayColor; 
o.sayColor(); // 'blue' 

定义在全局上下文中的函数 sayColor()引用了 this 对象。这个 this 到底引用哪个对象必须到函数被调用时才能确定。因此这个值在代码执行的过程中可能会变。如果在全局上下文中调用sayColor(),这结果会输出"red",因为 this 指向 window,而 this.color 相当于 window.color。而在把 sayColor()赋值给 o 之后再调用 o.sayColor(),this 会指向 o,即 this.color 相当于o.color,所以会显示"blue"。

此时将回调函数写成箭头函数就可以解决问题。这是因为箭头函数中的 this 会保留定义该函数时的上下文。

window.color = 'red'; 
let o = { 
 color: 'blue' 
}; 
let sayColor = () => console.log(this.color); 
sayColor(); // 'red' 
o.sayColor = sayColor; 
o.sayColor(); // 'red' 

在es5中有很多方法解决这个问题,但是一般比较常用的方法就是,在调用上面声明个变量,一般叫做self或者vm,然后把这个用在函数中

let obj = {
    name: "asim",
    sayLater: function () {
        let self = this; // Assign to self
        console.log(self);
        setTimeout(function () {
            console.log(`${self.name}`); // Use self not this
        }, 1000);
    }
};

但是在es6中,我们有更好的方式解决这个问题,如果我们使用箭头函数,那箭头函数里面的this的值和外面的值是一样的

let obj = {
    name: "asim",
    sayLater: function () {
        console.log(this); // `this` points to obj
        setTimeout(() => {
            console.log(this); // `this` points to obj
            console.log(`${this.name}`); // `this` points to obj
        }, 1000);
    }
};
obj.sayLater();
2.Typescript的This

参考
详解Typescript里的This

this可以说是Javascript里最难理解的特性之一了,Typescript里的 this 似乎更加复杂了,Typescript里的 this 有三中场景,不同的场景都有不同意思。

3.TS this 类型: 用于支持链式调用

参考《TypeScript编程》第5.3节
实现ES6中 Set数据结构的简化版,有以下两个操作:

let set = new Set;
set.add(1).add(2).add(3);
set.has(2);//true
set.has(4);//false

这里要支持链式调用,大概这样的:

class Set{
   has(value:number):boolean{}
   add(value:number):Set{}
}

这样做是可以的,但是如果想定义Set的子类呢?

class MutableSet extends Set{
   delete(value:number):boolean{}
   add(value:number):MutableSet{}
}

这就需要又写一遍add去覆盖,其实可以在父类Set中返回this就不用这么麻烦了:

class Set{
   has(value:number):boolean{}
   add(value:number):this{}
}
4.TS中this做为对象方法调用,这一点和JS中的this表现一致

这也是绝大部分 this 的使用场景,当函数作为对象的 方法调用时,this 指向该对象

const obj = {
  name: "yj",
  getName() {
    return this.name // 可以自动推导为{ name:string, getName():string}类型
  },
}
obj.getName() // string类型

这里有个坑就是如果对象定义时对象方法是使用箭头函数进行定义,则 this 指向的并不是对象而是全局的 window,Typescript 也自动的帮我推导为 window

const obj2 = {
  name: "yj",
  getName: () => {
    return this.name // check 报错,这里的this指向的是window
  },
}
obj2.getName() // 运行时报错
5.TS中this在普通函数中使用

即使是通过非箭头函数定义的函数,当将其赋值给变量,并直接通过变量调用时,其运行时 this 执行的并非对象本身

const obj = {
  name: "yj",
  getName() {
    return this.name
  },
}
const fn1 = obj.getName
fn1() // this指向的是window,运行时报错

很不幸,上述代码在编译期间并未检查出来,我们可以通过为getName添加this的类型标注解决该问题

interface Obj {
  name: string
  // 限定getName调用时的this类型
  getName(this: Obj): string
}
const obj: Obj = {
  name: "yj",
  getName() {
    return this.name
  },
}
obj.getName() // check ok
const fn1 = obj.getName
fn1() // check error

这样我们就能报保证调用时的 this 的类型安全.

关于这个作用,《TypeScript编程》第4.1.4节这样描述,此时this不是常规的参数,而是保留字,是函数签名的一部分。

6.TS中this在构造函数中使用
class People {
  name: string
  constructor(name: string) {
    this.name = name // check ok
  }
  getName() {
    return this.name
  }
}

const people = new People("yj") // check ok

这里面还是有坑的,具体可以参考原文:详解Typescript里的This

7.call 和 apply 调用

call 和 apply 调用没有什么本质区别,主要区别就是 arguments 的传递方式,不分别讨论。和普通的函数调用相比,call 调用可以动态的改变传入的 this, 幸运的是 Typescript 借助 this 参数也支持对 call 调用的类型检查

interface People {
  name: string
}
const obj1 = {
  name: "yj",
  getName(this: People) {
    return this.name
  },
}
const obj2 = {
  name: "zrj",
}
const obj3 = {
  name2: "zrj",
}
obj1.getName.call(obj2)
obj1.getName.call(obj3) // check error
五、函数重载

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会产生错误。

1.重载签名

参考
TypeScript中函数重载写法,你在第几层!

TypeScript中的函数重载让我们定义以多种方式调用的函数。使用函数重载需要定义重载签名:一组带有参数和返回类型的函数,但没有主体。这些签名表明应该如何调用该函数。

// 重载签名
function greet(person: string): string;
function greet(persons: string[]): string[];

// 实现签名
function greet(person: unknown): unknown {
 if (typeof person === 'string') {
 return `Hello, ${person}!`;
 } else if (Array.isArray(person)) {
 return person.map(name => `Hello, ${name}!`);
 }
 throw new Error('Unable to greet');
}
2.参考如何看待Typescript中的重载(Overload)? - 贺师俊的回答 - 知乎

第一,常见的静态类型语言中的overload是发生在编译时的,编译器可以清楚的将每一处同名函数调用对应到你写的不同的函数实现。也就是,同名只是一个(让程序员看到的)假象。如你写了fun(x),有两个实现fun(x: string)、fun(x: int),真正编译后的程序里实际会有两个函数,假设记做fun_string和fun_int,而每个fun(x)调用会被自动替换成fun_string(x)或fun_int(x)。有没有可能编译器无法确定替换成哪一个?当然有可能,这个时候编译器就报错了嘛,意思是你代码写错啦!

第二,JavaScript是动态类型,所以是没有上面这种意义上的overload的。但JS程序员可以在运行时判断类型,也就是 function fun(x) { if (typeof x === 'string') ... else/* assume x is int */ ... }。TypeScript 的『overload』只是允许给这样的函数标注多个类型。某轮说这是『绕过编译器类型检查』,是有问题的。这不是绕过,把函数参数标记为 (x: any) 才叫『绕过』。不过因为函数的具体实现只有一个,代码本身会比上面那种overload要麻烦一些,比如说为了检测类型偶尔你需要自己实现一些 type guard。至于说[下标函数]也不能自己写,这个很傻逼』,我估计某轮指的是 operator overload,然而很多语言都不允许(比如 java)。所以单单骂 TS/JS 有点扯。

第三,TS理论上当然是可以实现传统的 overload 的,比如直接生成两个函数,fun1、fun2。问题是从TS与JS的互操作性上来说,这事情就比较麻烦,比如一个js项目用了ts的库,我不能直接写fun,而得写fun1、fun2。本来 overload 就是希望给程序员提供便利,但现在就并没有什么卵用。其实像java之类有『真』重载的语言编译到js,或直接和js互操作,都有类似的问题。早在二十年前rhino里就有这问题——你在js里要指定到底调用的是哪一个java的重载方法是非常烦人的。特别是构造器,一般函数你说编译成fun_string、fun_int也就算了,但构造器呢?相当棘手。

上一篇下一篇

猜你喜欢

热点阅读