函数是JavaScript应用程序的基础。 它帮助你实现抽象层,模拟类,信息隐藏和模块。 在TypeScript里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义 行为 的地方。 TypeScript为JavaScript函数添加了额外的功能,让我们可以更容易地使用。
在 TypeScript 中,函数是用于执行特定任务并可能返回结果的代码块。
函数类型
可以为函数定义类型来明确函数的参数类型和返回值类型。
// 有名字的函数
function sum(a: number, b: number): number {
return a + b;
}
// 匿名函数
let mySum = function(x: number, y: number): number { return x + y; };
这里明确了 sum
函数接受两个 number
类型的参数 a
和 b
,并返回一个 number
类型的值。
书写完整函数类型
函数类型包含两部分:参数类型和返回值类型。 当写出完整函数类型的时候,这两部分都是需要的。
在函数和返回值类型之前使用( =>
)符号。
返回值类型是函数类型的必要部分,如果函数没有返回任何值,必须指定返回值类型为 void
而不能留空。
let mySum: (x: number, y: number) => number =
function(x: number, y: number): number { return x + y; };
在这段代码中:
let mySum: (x: number, y: number) => number
这部分定义了一个变量mySum
,并指定了它的类型为接受两个number
类型参数并返回number
类型值的函数类型。- 然后通过
function(x: number, y: number): number { return x + y; }
为mySum
变量赋值了一个具体的函数实现。
以参数列表的形式写出参数类型,为每个参数指定一个名字和类型:
let mySum: (a: number, b: number) => number =
function(x: number, y: number): number { return x + y; };
在这段代码中,虽然函数定义中的参数名 a
、b
与函数实现中的参数名 x
、y
不同,但它们的类型都是 number
,这是匹配的。
TypeScript 在类型检查时关注的是参数的类型,而不是参数的名称。只要参数的类型符合函数类型定义的要求,代码就可以通过类型检查。
在定义函数的类型时,只考虑参数的类型和返回值的类型。
函数中使用的捕获变量不会体现在类型里。 实际上,这些变量是函数的隐藏状态并不是组成API的一部分。
这是因为这些捕获变量属于函数的内部实现细节,对于使用该函数的外部代码来说,并不需要知道这些内部的状态。函数的 API 主要是通过其公开的参数和返回值来与外部进行交互和通信。
示例:
let outerValue = 10;
function myFunction(x: number): number {
let innerValue = 5;
return x + outerValue + innerValue;
}
在上述 myFunction
函数中,outerValue
是捕获的外部变量,innerValue
是函数内部的变量,它们都不会体现在函数的类型 (x: number) => number
中。
推断类型
如果在赋值语句的一边指定了类型,但是另一边没有类型的话,TypeScript编译器会自动识别出类型:
let mySum = function(x: number, y: number): number { return x + y; };
在这个例子中,mySum
的类型会被推断为 (x: number, y: number) => number
。
可选参数和默认参数
JavaScript里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是undefined
。
TypeScript里的每个函数参数都是必须的。传递给一个函数的参数个数必须与函数期望的参数个数一致。
在 TypeScript 中,可选参数和默认参数为函数提供了更灵活的参数设置方式。
可选参数
通过在参数名后面添加 ?
来标记参数为可选参数。调用函数时,可以选择不传递该参数。
示例:
function printInfo(name: string, age?: number) {
console.log(`Name: ${name}, Age: ${age ?? 'no age'}`);
}
printInfo("张三"); // "Name: 张三, Age: no age"
printInfo("李四", 25); // "Name: 李四, Age: 25"
printInfo("王二", 0); // "Name: 王二, Age: 0"
可选参数必须跟在必须参数后面。
这是为了确保在调用函数时,必需的参数能够按照正确的顺序被传递,而不会因为可选参数的存在导致参数传递的混乱。
默认参数
可以为参数指定一个默认值,如果调用函数时没有传递该参数或传递的值是undefined
时,就会使用默认值。
function printInfo(name: string, age: number = 18) {
console.log(`Name: ${name}, Age: ${age}`);
}
printInfo("张三"); // "Name: 张三, Age: 18"
printInfo("李四", 25); // "Name: 李四, Age: 25"
printInfo("王二", undefined); // "Name: 王二, Age: 18"
当一个参数在所有必需参数之后并且有默认初始化值时,它就具有了可选的性质。
在 printInfo
函数中,age
参数具有默认值 18
,所以在调用函数时可以省略它,就像可选参数一样。
与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined
值来获得默认值。
示例:
function printInfo(name: string = "张三", age: number) {
console.log(`Name: ${name}, Age: ${age}`);
}
printInfo(undefined, 18); // "Name: 张三, Age: 18"
printInfo("李四", 25); // "Name: 李四, Age: 25"
// 假如只传入 age
printInfo(25); // Error: Expected 2 arguments, but got 1.
剩余参数
必要参数、默认参数、可选参数有个共同点:它们表示某一个参数。
有时,想同时操作多个参数,或者并不知道会有多少参数传递进来。 在JavaScript里,你可以使用 arguments
来访问所有传入的参数。
在 TypeScript 中,剩余参数用于表示不确定数量的参数。它使用 ...
语法来定义。
剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。
function sumAll(numbers: number[], ...restNum: number[]) {
let total = 0;
for (const num of numbers) {
total += num;
}
for (const num of restNum) {
total += num;
}
return total;
}
// 没有剩余参数
console.log(sumAll([1, 2, 3])); // 6
// 有 3 个剩余参数
console.log(sumAll([1, 2, 3], 4, 5, 6)); // 21
在为带有剩余参数的函数定义类型时,也需要正确地表示剩余参数的部分。
实现 sumAll
函数的类型定义:
let myFunction: (numbers: number[], ...restNum: number[]) => number = sumAll;
// 参数名字可以不一样,但是参数的类型必须一样
let myFunc: (nums: number[], ...rest: number[]) => number = sumAll;
this和箭头函数
在普通函数中,this
的值取决于函数的调用方式。如果是作为对象的方法被调用,this
指向该对象;如果是独立调用,非严格模式下,this
通常指向全局对象(在浏览器环境中是 window
,在 Node.js
环境中是 global
)。严格模式下, this
为 undefined
。
非严格模式下:
// 作为对象的方法被调用
let obj = {
method: function() {
console.log(this); // 这里的 this 指向 obj 对象
}
};
obj.method();
// 独立调用
function standalone() {
console.log(this); // 在浏览器中,这里的 this 指向 window 对象
}
standalone();
严格模式下:
// 作为对象的方法被调用
let strictObj = {
strictMethod: function() {
"use strict";
console.log(this); // 这里的 this 指向 strictObj 对象
}
};
strictObj.strictMethod();
// 独立调用
function strictStandalone() {
"use strict";
console.log(this); // 这里的 this 是 undefined
}
strictStandalone();
可以在浏览器中执行代码来验证结果。
箭头函数中的 this
继承自其定义时所在的上下文,不会因为调用方式的不同而改变。
let obj = {
method: () => {
// "use strict";
console.log(this); // 在浏览器中,这里的 this 指向 window 对象
}
};
obj.method();
这段代码中,无论是严格模式还是非严格模式,this
都指向 window
对象。
箭头函数 method
是在对象 obj
中定义的,由于是箭头函数,this
并不指向 obj
,而是继承了外部(即全局)的 this
值。在浏览器环境中,全局的 this
就是 window
对象。
使 this
指向obj
:
let obj = {
width: 10,
height: 10,
method() {
return () => {
return console.log(this)
}
}
};
obj.method()();
// 把obj.method()赋值给method变量
let method = obj.method();
method()
由上图运行结果可知,箭头函数里的 this
指向obj
。此时,this
的 类型 是 any
。
this
参数
在 TypeScript 中,this
可以被显式地作为参数添加到函数的参数列表中,用于更明确地控制和处理函数内部的 this
指向。
提供一个显式的 this
参数, 让this
的类型不是any
:
interface OBJ {
width: number;
height: number;
method(this: OBJ): () => number;
}
let obj: OBJ = {
width: 10,
height: 10,
method(this: OBJ) {
return () => {
return this.width + this.height
}
}
};
console.log(obj.method()()); // 20
let method = obj.method();
console.log(method); // 20
这段代码中,this
是 OBJ
类型的。不论如何调用method()
,this
始终指向OBJ
。
this
参数在回调函数里
当把一个函数作为回调传递给库函数,并且在回调函数中使用了 this
时,确实经常会遇到 this
指向错误或为 undefined
的问题。
这是因为函数的调用方式会影响 this
的指向。如果回调函数被库函数以普通函数的方式调用,而不是作为对象的方法调用,那么 this
就不会指向预期的对象,通常会是 undefined
。
例如,假设有一个库函数 doAsyncOperation
,它接受一个回调函数:
function doAsyncOperation(callback: () => void) {
// 模拟异步操作完成后调用回调
setTimeout(callback, 1000);
}
然后有一个类 MyClass
,其中有一个方法想要在回调中使用 this
:
class MyClass {
data: string = "Some data";
performOperation() {
doAsyncOperation(this.someCallback);
}
someCallback() {
console.log(this.data); // undefined
}
}
let myC = new MyClass();
myC.performOperation();
执行myC.performOperation()
语句, doAsyncOperation
内部以普通函数的方式调用 callback
,当 doAsyncOperation
调用回调函数时,this
不是指向 MyClass
的实例,而是 undefined
,导致无法访问 data
属性。回调函数的输出结果为undefined
。
因为在 setTimeout
内部,如果传递的回调函数是一个普通函数,那么 this
的指向取决于运行环境和调用方式。
在非严格模式下,如果是在浏览器环境中,this
通常指向 window
对象;在 Node.js
环境中,this
通常指向 global
对象。
在严格模式下,this
的值会是 undefined
。
把someCallback
改为箭头函数:
class MyClass {
data: string = "Some data";
performOperation() {
doAsyncOperation(this.someCallback);
}
someCallback = () => {
console.log(this.data); // "Some data"
}
}
let myC = new MyClass();
myC.performOperation();
在上述代码中,someCallback
被定义为箭头函数。
箭头函数不会创建自己的 this
上下文,而是继承外层(即定义它的 MyClass
实例)的 this
上下文。
当 doAsyncOperation
函数中的异步操作完成并调用 callback
(即 this.someCallback
)时,由于 someCallback
是箭头函数,它所使用的 this
仍然是其定义时所在的 MyClass
实例的 this
。
所以,在 someCallback
函数内部,能够正确访问到 MyClass
实例的属性 data
,并将其打印出来,即输出结果为 “Some data” 。
或者:
class MyClass {
data: string = "Some data";
performOperation() {
doAsyncOperation(() => this.someCallback());
}
someCallback() {
console.log(this.data); // "Some data"
}
}
let myC = new MyClass();
myC.performOperation();
在 doAsyncOperation(() => this.someCallback());
中,使用了一个立即执行的箭头函数。
箭头函数不会创建自己的 this
上下文,而是继承外层(即定义它的 MyClass
实例)的 this
上下文。
在这个箭头函数内部,this
正确地指向了 MyClass
的实例。
上面这个例子比较简单,再举一个复杂一点的例子。
例如,假设有一个库函数 UIElement
,库函数的作者要指定 this
的类型:
interface UIElement {
addClickListener(onclick: (this: void, e: CustomEvent, params?: {[propName: string]: any}) => void): void;
}
有一个Handler
类,处理自定义事件以及在不同类型的回调函数中处理数据和 this
的指向。
class Handler {
info: string = "";
onClickGood(this: void, e: CustomEvent) {
// 通过指定 this: void ,表明该回调函数在被调用时不期望或不依赖于特定的 this 上下文。
console.log(this); // undefined
console.log("clicked!");
}
onClickGood2 = (e: CustomEvent) => {
// 箭头函数,this 指向 Handler
this.info = e.detail.message;
};
doOther (this: void, e: CustomEvent, params?: {[propName: string]: any}) {
// 在这里根据传入的 params 进行相应的处理
// 如果要操作this,就使用箭头函数。
console.log(e.detail.message);
console.log(params);
};
}
let h = new Handler();
let uiElement: UIElement = {
addClickListener(onclick) {
const event = new CustomEvent("click", { detail: { message: "Hello" } });
onclick(event);
}
};
uiElement.addClickListener(h.onClickGood);
uiElement.addClickListener(h.onClickGood2);
let uiElementDo: UIElement = {
addClickListener(onclick) {
const event = new CustomEvent("click", { detail: { message: "输出用户的输入" } });
let word = prompt("输入你想说的话", "");
let params = {
word: word
}
onclick(event, params);
}
};
uiElementDo.addClickListener(h.doOther)
函数重载
在 TypeScript 中,函数重载是指可以为同一个函数定义多个不同的签名(参数类型和返回类型的组合)。
示例:
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
if (typeof a === 'number' && typeof b === 'number') {
return a + b;
}
if (typeof a ==='string' && typeof b ==='string') {
return `${a} ${b}`;
}
throw new Error('Invalid parameters');
}
console.log(add(1, 2)); // 3
console.log(add('hello', 'world')); // "hello world"
在上述示例中,add
函数有两个不同的函数签名:一个接受两个数字参数并返回一个数字,另一个接受两个字符串参数并返回一个字符串。
函数的实现部分根据传入参数的类型来决定执行不同的逻辑。