函数
状态: 初稿
介绍
函数是 JavaScript 应用最基本的构件.
JavaScript 开发者用函数建立抽象层, 模拟类, 实施信息隐藏, 实行模块管理等.
尽管 TypeScript 原生支持类, 名字空间, 模块, 函数依然要承担起描述怎么做事的职责.
TypeScript 也为标准 JavaScript 函数增加了许多易于使用的特性.
函数
如同 JavaScript, 你可以创建命名函数和匿名函数.
你可以根据需要从中选择 — 是在创建一套 API, 还是创建作为参数的一次性函数…
以一个例子快速回顾这两种函数风格:
1 | // Named function |
函数可以引用定义在函数体外部的变量.
我们称那种情形为函数捕获了这个变量.
理解这个体系的原理(及应用该技术利弊)超出了本文讨论的范围, 但我们认为作为 JavaScript 或 TypeScript 学习者, 至少要掌握它们的运行方式.
1 | let z = 100; |
函数与类型注解
为函数定类型
让我们为先前定义的函数加上类型注解:
1 | function add(x: number, y: number): number { |
我们可以注明函数每个参数和返回值的类型.
多数情况, 你可以省去返回值类型不写, TypeScript 有能力结合参数列表和返回表达式推断返回值的类型.
写出函数类型
我们已经了解如何定下函数参数和返回值类型, 现在可以写出函数自身的类型.
1 | let myAdd: (x: number, y: number) => number = |
函数类型由两部分构成: 参数列表声明和返回值类型声明.
两部分都是写出一个完整函数类型所需的.
类型中的参数列表声明即函数的参数列表, 为每个参数定下名称和类型.
这里的名字是为了提高可读性, 并无实际作用.
可将上例改写成:
1 | let myAdd: (baseValue: number, increment: number) => number = |
无论参数名是否匹配, 只要参数类型跟函数两两一致, 函数类型就被认为是有效的(译注: 还应当考虑返回值类型).
第二部分是函数返回值类型声明.
同样为了可读性, 我们用胖箭头 =>
连接参数列表声明和返回值类型声明.
如前所述, 函数返回值是一个函数类型的关键组成部分, 如果你的函数没有返回值, 就要以 void
指出.
注意, 只有参数列表和返回值类型对一个函数类型起决定作用.
函数捕获的变量不反映在函数类型中.
实际上, 被捕获的变量是一个函数的隐藏状态, 用户不可感知.
推断类型
通过这些例子, 你会注意到赋值语句只要等号有一端类型是确定的, TypeScript 就能弄清楚另一端的类型:
1 | // myAdd has the full function type |
这叫做上下文类型推断, 一种类型推断形式.
帮助减轻你维护类型信息的负担.
可选参数, 参数默认值
TypeScript 对照参数列表, 认为每个参数都是函数所需的.
在调用函数时, 编译器检查用户是否为每个参数提供了值, 即使是 null
, undefined
.
TypeScript 还认为除此之外, 函数不需要其他参数.
简单来说, 函数实参数量与形参数量应严格匹配.
1 | function buildName(firstName: string, lastName: string) { |
对 JavaScript 而言, 每个参数都是可选的, 只要用户觉得合理, 就可以留空.
留空参数默认值是 undefined
.
要在 TypeScript 中利用该特性, 你需要参数名后面加上问号?
, 以表明它是”可选”的.
举个例子, 我们把上例最后一个参数 lastName
指定为可选:
1 | function buildName(firstName: string, lastName?: string) { |
可选参数只能出现在所有必选参数之后.
上例, 如果我们想让第一个参数 firstName
“可选”, 而不是第二个参数 lastName
, 需要改变两个参数的顺序, 把 firstName
置于最后.
我们可以为参数指定默认值, 如果用户留空一个参数, 或传递 undefined
值, 函数参数都会得到默认值.
该参数称作默认初始化的参数.
再次修改上例, 把 "Smith"
作为参数 lastName
的默认值.
1 | function buildName(firstName: string, lastName = "Smith") { |
排在所有必选参数身后的默认初始化参数按可选参数对待, 与可选参数一样, 调用时允许留空.
这也说明在函数类型中, 不区分可选参数和尾部默认参数, 所以:
1 | function buildName(firstName: string, lastName?: string) { |
和
1 | function buildName(firstName: string, lastName = "Smith") { |
的函数类型都是 (firstName: string, lastName?: string) => string
.
函数类型不保存默认值, 它只体现默认参数”可选”的那一面.
与一般可选参数不同, 默认初始化的参数可以出现在必选参数之前.
对于这样的默认参数, 用户需要显式传入 undefined
使默认值生效.
我们只为 firstName
参数添加默认值:
1 | function buildName(firstName = "Will", lastName: string) { |
变长参数
“必选”, “可选”, “默认”都是在讨论单个参数.
有时, 一个函数最终需要的参数数量是不确定的, 或者你就是想以一个组接受多个参数.
在 JavaScript 中, 你可以直接访问每个函数都有的 arguments
变量.
借助 TypeScript, 你可以把这些参数收集到一个变量中:
1 | function buildName(firstName: string, ...restOfName: string[]) { |
变长参数相当于不限数量的可选参数.
要填充一个变长参数, 实参的数量可以是 0 个; 也可以是任意多个.
TypeScript 为变长参数(以省略号 ...
指示)创建一个数组, 存储所有元素, 以便你在函数中使用.
书写函数类型时, 我们也用省略号表示变长参数:
1 | function buildName(firstName: string, ...restOfName: string[]) { |
this 指针
在 JavaScript 中学习如何使用 this
可以说是一条充满仪式感的道路.
要掌握它的超集 TypeScript, 我们同样需要学习 this
的用法, 最好还能指出哪些 this
用法是不当的.
保险起见, TypeScript 也有技术找出 this
的不当使用.
如果你想了解 JavaScript 中 this
的工作原理, 可以阅读 Yehuda Katz 的 Understanding JavaScript Function Invocation and “this”.
鉴于这篇文章已经详细阐述 this
的内部原理, 我们在这里仅做必要说明.
this 和箭头函数
在 JavaScript 中, this
是一个函数被调用时创建的变量.
这造就了它强大而灵活的特性, 而代价是你必须时刻留意函数是在何种上下文中被调用的.
这是极大的负担, 特别是你把一个函数当成返回值返回或当成一个参数传递时.
我们看一个例子:
1 | let deck = { |
首先注意到 createCardPicker
是一个返回函数的函数.
运行上例, 我们希望弹出一个消息框, 而事实上, 它会报错.
原因是 createCardPicker
返回的那个函数里面的 this
指向 window
, 而不是 deck
对象.
根本原因是 15 行, 我们独立地调用 cardPicker()
.
一个顶层非方法调用的 this
总是指向 window
.
(注: 在严格模式下, this
指向 undefined
).
我们可以确保在作为返回值的函数在返回前已经与正确的 this
绑定, 以修复该问题.
这样, 无论这个函数之后怎么被调用, 它看到的都是最初的 desk
对象.
要实现该想法, 我们修改函数表达式, 运用 ECMAScript 6 箭头语法.
箭头函数 捕获 函数创建处而不是调用处的 this
:
1 | let deck = { |
如果你打开 --noImplictThis
编译器选项, TypeScript 还能做得更好.
它会指出 this.suits[pickedSuit]
中的 this
是 any
类型.
this 作为参数
没错, this.suits[pickedSuit]
中的 this
的确是 any
.
因为 this
来自一个对象字面量内部的函数表达式.
要消除 any
, 可以显式增加 this
参数.this
参数是个先于参数列表其他所有参数的伪参数.
1 | function f(this: void) { |
先为我们的例子添加一些接口, Card
, Deck
. 使类型更清晰, 也有利于重用.
1 | interface Card { |
现在 TypeScript 知道 createCardPicker
希望自己在 Deck
对象上调用.
同时也说明 this
的类型变成了 Deck
, --noImplicitThis
不再报错.
回调与 this
你传递一个函数给库, 库在正确地时机调用它, 这个调用就叫回调, 回调中的 this
同样容易出错.
假设库以调用普通函数的方式调用回调函数, this
会是 undefined
.this
再加上一些技巧可以避免回调中的 this
错误.
首先, 库作者需要正确书写回调函数类型, 为它增加 this
参数:
1 | interface UIElement { |
this: void
表示 addClickLisener
希望 onclick
是一个不依赖 this
的函数.
其次, 为你的回调函数加上 this
参数.
1 | class Handler { |
上例, 你显式注明 onClickBad
必须在 Handler
对象上调用.
而 addClickListener
要求它参数(回调函数)有 this: void
.
我们改变 this
的类型:
1 | class Handler { |
这里 onClickGood
的 this
类型是 void
, 可以合法传递给 addClickListener
.
当然, 既然 this
是 void
, 你就不能再访问 this.info
.
如果你想同时做到两点(1. 能调用addClickListener
, 2. 能使用 this
):
1 | class Handler { |
我们再次请出箭头函数, 它捕获外层 this
, 不受 this: void
限制.
其局限性是 TypeScript 为每个 Handler
对象创建一个箭头函数.
作为对比, 普通方法是附加到 Handler
类的原型上的, 只会被创建一次.
所有 Handler
对象共享这些方法.
重载
我们知道, JavaScript 是一种动态语言.
一个函数根据函数参数的类型, 返回不同类型的结果, 是很稀松平常的事情.
1 | let suits = ["hearts", "spades", "clubs", "diamonds"]; |
pickCard
根据参数的类型返回两种不同类型的值.
如果用户传入的参数代表一副扑克, 这个函数会挑选一张卡片.
如果用户挑选卡片, 我们告诉他们挑选的是哪一张.
那么, 怎么用类型系统来描述?
答案是为一个函数提供多个函数签名, 组成一个重载列表.
编译器将查询重载列表解析每个函数调用.
下面, 我们为 pickCard
创建一个重载列表, 以描述这个函数怎么根据不同的参数返回不同的返回值.
1 | let suits = ["hearts", "spades", "clubs", "diamonds"]; |
有了重载列表, 每个 pickCard
函数调用都经过了类型检查.
编译器循序 JavaScript 底层一种类似流程选择合适重载检查调用正确性.
它从第一个重载开始, 遍历重载列表, 看当前参数列表是否匹配.
如果它找到一个匹配, 选取它作为最佳重载.
为配合这一机制, 通常我们按照最常用-最不常用原则手动排序重载列表.
注意函数实现 function pickCard(x): any
不是重载列表的一部分, 这个函数只有两个有效重载: 一个接受 object
, 一个接收 number
.
以其他任何形式调用 pickCard
都会引发错误.