文章使用的 TypeScript 版本为
3.9.x,后续会根据 TypeScript 官方的更新继续添加内容,如果有的地方不一样可能是版本报错的问题,注意对应版本修改即可。
前言
该文章是笔者在学习 TypeScript 的笔记总结,期间寻求了许多资源,包括 TypeScript 的官方文档等多方面内容,由于技术原因,可能有很多总结错误或者不到位的地方,还请诸位及时指正,我会在第一时间作出修改。
文章中许多部分的展示顺序并不是按照教程顺序,只是对于同一类型的内容进行了分类处理,许多特性可能会提前使用,如果遇到不懂的地方可以先看后面内容。
下面内容接 TypeScript 知识汇总(二)(3W 字长文)
8.TypeScript 中的模块
与 ES6 一样,TypeScript 也引入了模块化的概念,TypeScript 也可以使用 ES6 中的 export、export default 和 import 导出和引入模块类的数据,从而实现模块化
ES6 标准与 Common.js 的区别
- require: node 和 es6 都支持的引入
- export 和 import: ES6 支持的导出引入,在浏览器和 node 中也不支持(node 8.x 版本以后已经支持),需要 babel 转换,而且在 node 中会被转换为 exports,但是在 TypeScipt 中使用编译出来的 JS 代码可以在 node 中运行,因为会被编译为 node 认识的 exports
- module.exports 和 exports: 只有 node 支持的导出
注: ES6 的模块不是对象,import命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载
8.1 导出
8.1.1 导出声明
任何声明(比如变量、函数、类、类型别名或接口)都能够通过添加export关键字来导出
export interface StringValidator {
isAcceptable(s: string): boolean
}
export const numberRegexp = /^[0-9]+$/
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s)
}
}
8.1.2 导出语句
//上面的语句可以直接通过导出语句来写
const numberRegexp = /^[0-9]+$/
interface StringValidator {
isAcceptable(s: string): boolean
}
class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s)
}
}
export { ZipCodeValidator }
export { ZipCodeValidator as mainValidator } //as能够改变导出变量的名字,在外部接收时使用
8.1.3 默认导出
每个模块都可以有一个default导出,默认导出使用 default关键字标记,并且一个模块只能够有一个default导出。需要使用一种特殊的导入形式来导入 default导出。通过export default导出的值可以用任意变量进行接收
注:
-
类和函数声明可以直接被标记为默认导出,标记为默认导出的类和函数的名字是可以省略的
//ZipCodeValidator.ts export default class ZipCodeValidator { static numberRegexp = /^[0-9]+$/ isAcceptable(s: string) { return s.length === 5 && ZipCodeValidator.numberRegexp.test(s) } }import validator from './ZipCodeValidator' let myValidator = new validator() -
export default导出也可以是一个值//OneTwoThree.ts export default '123'import num from './OneTwoThree' console.log(num) // "123"
8.1.4 导出模块
TypeScript 提供了export =语法,export =语法定义一个模块的导出对象
注意:
- 这里的
对象一词指的是类、接口、命名空间、函数或枚举 - 若使用
export =导出一个模块,则必须使用 TypeScript 的特定语法import module = require("module")来导入此模块
//ZipCodeValidator.ts
let numberRegexp = /^[0-9]+$/
class ZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s)
}
}
export = ZipCodeValidator
import zip = require('./ZipCodeValidator')
// Some samples to try
let strings = ['Hello', '98052', '101']
// Validators to use
let validator = new zip()
// Show whether each string passed each validator
strings.forEach((s) => {
console.log(
`"${s}" - ${validator.isAcceptable(s) ? 'matches' : 'does not match'}`
)
})
8.2 导入
模块的导入操作与导出一样简单,可以使用以下 import形式之一来导入其它模块中的导出内容
import { ZipCodeValidator } from './ZipCodeValidator'
let myValidator = new ZipCodeValidator()
//可以对导入内容重命名
import { ZipCodeValidator as ZCV } from './ZipCodeValidator'
let myValidator = new ZCV()
//将整个模块导入到一个变量,并通过它来访问模块的导出部分
import * as validator from './ZipCodeValidator'
let myValidator = new validator.ZipCodeValidator()
//导入默认模块
//可以对导入内容重命名
import ZCV from './ZipCodeValidator'
let myValidator = new ZCV()
当然,也可以直接使用import导入一个不需要进行赋值的模板,该模板会自动进行内部的代码
import './my-module.js'
8.2.1 动态导入
import 的导入导出默认是静态的,如果要动态的导入导出可以使用 ES6 新增的import()函数实现类似require()动态导入的功能
注:
- 使用
import()函数返回的是 Promise 对象 - 如果是
commonjs格式的模块需要我们手动调用default()方法获得默认导出
async function getTime(format: string) {
const momment = await import('moment')
return moment.default().format(format)
}
// 使用async的函数本身的返回值是一个Promise对象
getTime('L').then((res) => {
console.log(res)
})
8.3 仅限类型导入和导出
import type { SomeThing } from './some-module.js'
export type { SomeThing }
该语法为 TypeScript 3.8 新增,像上面这样,只导入或导出某个特定类型,该声明仅用于类型注释,在运行时会被消除。
值得注意的是,类在运行时具有值,在设计时具有类型,并且使用上下文很敏感。使用导入类时,不能执行从该类扩展之类的操作,引入使用了import type后我们仅把其当作一个类型来使用。
import type { Component } from 'react'
interface ButtonProps {
// ...
}
class Button extends Component<ButtonProps> {
// ~~~~~~~~~
// error! 'Component' only refers to a type, but is being used as a value here.
// ...
}
8.4 export = 和 import = require()
CommonJS 和 AMD 的环境里都有一个exports变量,这个变量包含了一个模块的所有导出内容。CommonJS 和 AMD 的exports都可以被赋值为一个对象, 这种情况下其作用就类似于 es6 语法里的默认导出,即 export default语法了。虽然作用相似,但是 export default 语法并不能兼容 CommonJS 和 AMD 的exports
为了支持 CommonJS 和 AMD 的exports, TypeScript 提供了export =语法。export =语法定义一个模块的导出对象。 这里的对象一词指的是类,接口,命名空间,函数或枚举
而import module = require("module")也是 TypeScript 新增的一种导入格式,该格式的导入可以兼容所有的导入格式,但是注意如果是引入的 ES6 特有的导出会默认把导出的模块转换为对象(因为 module 只能够接受一个值,默认应该要获取到所有的导出),同时该对象会多一个__esModule值为true的属性(),而其他的所有属性会加载这个对象中
注: 即使使用的是export default在也会是同样的效果,不过会把默认导出添加到一个default属性上
注意:
export =在一个模块中只能使用一次,所以是与CommonJS一样基本都是用于导出一个对象出来ES6的import ... from ...的默认导出的语法不能作用在export =导出的对象,因为没有default对象,就像CommonJS的module.exports一样(虽然最后是转换为这个),而ES6的export default转换为CommonJS就是为其添加一个default属性- 若使用
export =导出一个模块,则必须使用 TypeScript 的特定语法import module = require("module")来导入此模块 - 除了
import module = require("module")导入ES6的模块有区别之外,在导入 CommonJS 和 AMD 效果类似,如果在都支持的模块中(UMD 模块为代表),该导入相当于是导入了 ES6 模块中的default
// ZipCodeValidator.ts
let numberRegexp = /^[0-9]+$/
class ZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s)
}
}
export = ZipCodeValidator
// Test.ts
import zip = require('./ZipCodeValidator')
// Some samples to try
let strings = ['Hello', '98052', '101']
// Validators to use
let validator = new zip()
// Show whether each string passed each validator
strings.forEach((s) => {
console.log(
`"${s}" - ${validator.isAcceptable(s) ? 'matches' : 'does not match'}`
)
})
8.4.1 生成模块代码
在之前说到的import module = require("module")的区别的原因是根据编译时指定的模块目标参数,编译器会生成相应的供 Node.js (CommonJS),Require.js (AMD),UMD,SystemJS或ECMAScript 2015 native modules (ES6)模块加载系统使用的代码。如:
-
SimpleModule.ts
import m = require('mod') export let t = m.something + 1 -
AMD / RequireJS SimpleModule.js
define(['require', 'exports', './mod'], function (require, exports, mod_1) { exports.t = mod_1.something + 1 }) -
CommonJS / Node SimpleModule.js
let mod_1 = require('./mod') exports.t = mod_1.something + 1 -
UMD SimpleModule.js
;(function (factory) { if (typeof module === 'object' && typeof module.exports === 'object') { let v = factory(require, exports) if (v !== undefined) module.exports = v } else if (typeof define === 'function' && define.amd) { define(['require', 'exports', './mod'], factory) } })(function (require, exports) { let mod_1 = require('./mod') exports.t = mod_1.something + 1 }) -
System SimpleModule.js
System.register(['./mod'], function (exports_1) { let mod_1 let t return { setters: [ function (mod_1_1) { mod_1 = mod_1_1 } ], execute: function () { exports_1('t', (t = mod_1.something + 1)) } } }) -
Native ECMAScript 2015 modules SimpleModule.js
import { something } from './mod' export let t = something + 1
8.4.2 可选的模块加载
有时候,你只想在某种条件下才加载某个模块。 在 TypeScript 里,使用下面的方式来实现它和其它的高级加载场景,我们可以直接调用模块加载器并且可以保证类型完全。
编译器会检测是否每个模块都会在生成的 JavaScript 中用到。 如果一个模块标识符只在类型注解部分使用,并且完全没有在表达式中使用时,就不会生成 require这个模块的代码。略掉没有用到的引用对性能提升是很有益的,并同时提供了选择性加载模块的能力
import a = require('./a') // 如果只写这句话是不会引入a模块的
console.log(a) // 必须要使用过才会真正引入
这种模式的核心是import id = require("...")语句可以让我们访问模块导出的类型。 模块加载器会被动态调用(通过 require),就像下面if代码块里那样。 它利用了省略引用的优化,所以模块只在被需要时加载。 为了让这个模块工作,一定要注意 import定义的标识符只能在表示类型处使用(不能在会转换成 JavaScript 的地方)
为了确保类型安全性,我们可以使用typeof关键字。 typeof关键字,当在表示类型的地方使用时,会得出一个类型值,这里就表示模块的类型
// 如下面这样就可以在node.js环境实现可选模块加载
declare function require(moduleName: string): any
import { ZipCodeValidator as Zip } from './ZipCodeValidator'
if (needZipValidation) {
let ZipCodeValidator: typeof Zip = require('./ZipCodeValidator')
let validator = new ZipCodeValidator()
if (validator.isAcceptable('...')) {
/* ... */
}
}
8.5 模块转换问题
TypeScript 中默认是将所有代码转换为CommonJS模块代码,相对于模块有不同的代码转换规则
-
ES6 模块:
-
import * as ... from ...,这种写法是最接近CommonJS中require的写法,将所有导出的模块装维一个对象,所以最后也会变为var ... = require('...') -
import {...} from ...,同上一种一样,不过相当于是用了取对象符 -
import ... from ...,因为这种写法是取出 export 的默认导出,而默认导出其实是模块的一个叫作default的属性,所以也是用了取对象符var ... = require('...').default注意: 这样导入的模块一般是需要对应
ES6的export default语法的,因为要获取default属性,而使用的CommonJS和export =的写法是直接导出一整个对象,如果不给这些导出的对象设置default属性会得到undefined -
export单独导入同CommonJS中的exports.xxx语法,只需要主要export default等同于exports.default = xxx
-
-
CommonJS 模块: 因为是转为这种语法的,所以没有兼容性可说
-
TypeScript 模块:
import ... = require('...'),等同于CommonJS的require语法,只是可以支持 AMD 模块,而原生的require是不支持的export =,等同于CommonJS的module.exports =
9.命名空间
在代码量较大的情况下,为了避免各种变量命名相冲突,可以将类似功能的函数、类、接口等放置到命名空间中
在 TypeScript 中的命名空间中的对象、类、函数等可以通过 export 暴露出来通过命名空间名.类名等来使用
注意: 这个暴露是暴露在命名空间外,不是将其在模块中暴露出去
命名空间和模块的区别:
- 命名空间: 内部模块,主要用于组织代码,避免命名冲突
- 模块: TypeScript 的外部模块的简称,侧重代码的复用,一个模块里可能会有多个命名空间
namespace Validation {
//通过namespace关键词创建一个命名空间
export interface StringValidator {
isAcceptable(s: string): boolean //类类型接口
}
const lettersRegexp = /^[A-Za-z]+$/
const numberRegexp = /^[0-9]+$/
export class LettersOnlyValidator implements StringValidator {
//要在外部使用必须导出
isAcceptable(s: string) {
//函数内部可以不导出
return lettersRegexp.test(s)
}
}
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s)
}
}
}
// Some samples to try
let strings = ['Hello', '98052', '101']
// 在外界就可以直接通过Validation.StringValidator访问命名空间内部导出的接口
let validators: { [s: string]: Validation.StringValidator } = {}
//上面接口的意思是一个对象,对象中的每个成员都是有isAcceptable接口方法的实例化对象
validators['ZIP code'] = new Validation.ZipCodeValidator()
validators['Letters only'] = new Validation.LettersOnlyValidator()
// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
console.log(
`"${s}" - ${
validators[name].isAcceptable(s) ? 'matches' : 'does not match'
} ${name}`
)
}
}
9.1 多文件中的命名空间
如果命名空间相同,多个文件内部的代码会合并到同一个命名空间中,其实就是使用var声明字重复定义变量,如果内部没有导出的变量依然只能在内部使用,而暴露的变量就会合并
注: 如果导出变量有重名,后面的文件会覆盖掉前面的
-
通过 export 和 import 进行使用
//module.ts export namespace A { interface Animal { name: string eat(): void } export class Dog implements Animal { name: string constructor(theName: string) { this.name = theName } eat(): void { console.log(this.name + '吃狗粮') } } }// A在JS中就被转换为了一个对象 import { A } from './module' let dog = new A.Dog('狗') //传入命名空间 dog.eat() -
通过三斜线指令引入
三斜线指令: 包含单个 XML 标签的单行注释,注释的内容会做为编译器指令使用,三斜线引用告诉编译器在编译过程中要引入的额外的文件
注意: 三斜线指令仅可放在包含它的文件的最顶端。 一个三斜线指令的前面只能出现单行或多行注释,这包括其它的三斜线指令。 如果它们出现在一个语句或声明之后,那么它们会被当做普通的单行注释,并且不具有特殊的涵义
这里只用
///<reference path=""/>,其余用法在 TypeScript 中文文档 查看/// <reference path="..." />指令是三斜线指令中最常见的一种,它用于声明文件间的 依赖,三斜线引用告诉编译器在编译过程中要引入的额外的文件,也就是会引入对应 path 的文件//Validation.ts namespace Validation { export interface StringValidator { isAcceptable(s: string): boolean } }//LettersOnlyValidator.ts /// <reference path="Validation.ts" /> namespace Validation { const lettersRegexp = /^[A-Za-z]+$/ export class LettersOnlyValidator implements StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s) } } }//ZipCodeValidator.ts /// <reference path="Validation.ts" /> namespace Validation { const numberRegexp = /^[0-9]+$/ export class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s) } } }/// <reference path="Validation.ts" /> /// <reference path="LettersOnlyValidator.ts" /> /// <reference path="ZipCodeValidator.ts" /> // Some samples to try let strings = ['Hello', '98052', '101'] // Validators to use let validators: { [s: string]: Validation.StringValidator } = {} validators['ZIP code'] = new Validation.ZipCodeValidator() validators['Letters only'] = new Validation.LettersOnlyValidator() // Show whether each string passed each validator for (let s of strings) { for (let name in validators) { console.log( `"${s}" - ${ validators[name].isAcceptable(s) ? 'matches' : 'does not match' } ${name}` ) } }
9.2 别名
别名是另一种简化命名空间操作的方法是使用import q = x.y.z给常用的对象起一个短的名字,不要与用来加载模块的import x = require('name')语法弄混了,这里的语法是为指定的符号创建一个别名
注: 可以用这种方法为任意标识符创建别名,也包括导入的模块中的对象
namespace Shapes {
export namespace Polygons {
export class Triangle {}
export class Square {}
}
}
import polygons = Shapes.Polygons //用polygons代替Shapes.Polygons,相当于C语言的define
let sq = new polygons.Square() // Same as "new Shapes.Polygons.Square()"
注意:并没有使用require关键字,而是直接使用导入符号的限定名赋值,与使用 var相似,但它还适用于类型和导入的具有命名空间含义的符号。 重要的是,对于值来讲, import会生成与原始符号不同的引用,所以改变别名的var值并不会影响原始变量的值
10.TypeScript 中的装饰器
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,属性或参数上,可以修改类的行为,通俗来讲装饰器就是一个方法,可以注入到类、方法、属性参数上来扩展类、属性、方法、参数的功能
装饰器已经是 ES7 的标准特性之一
常见的装饰器
- 类装饰器
- 属性装饰器
- 方法装饰器
- 参数装饰器
装饰器的写法
- 普通装饰器(无法传参)
- 装饰器工厂(可传参)
注意: 装饰器是一项实验性特性,因为装饰器只是个未来期待的用法,所以默认是不支持的,如果想要使用就要打开 tsconfig.json 中的experimentalDecorators,否则会报语法错误
命令行:
tsc --target ES5 --experimentalDecorators
tsconfig.json:
10.1 类装饰器
类装饰器在类声明之前被声明(紧跟着类声明),类装饰器应用于类构造函数,可以用来监视,修改或替换类定义,需要传入一个参数
10.1.1 普通装饰器
function logClass(target: any) {
console.log(target)
//target就是当前类,在声明装饰器的时候会被默认传入
target.prototype.apiUrl = '动态扩展的属性'
target.prototype.run = function () {
console.log('动态扩展的方法')
}
}
@logClass
class HttpClient {
constructor() {}
getData() {}
}
//这里必须要设置any,因为是装饰器动态加载的属性,所以在外部校验的时候并没有apiUrl属性和run方法
let http: any = new HttpClient()
console.log(http.apiUrl)
http.run()
10.1.2 装饰器工厂
如果要定制一个修饰器如何应用到一个声明上,需要写一个装饰器工厂函数。 装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用
注: 装饰器工厂是将内部调用的函数作为真正的装饰器返回的,所以装饰器工厂需要和函数用法一样通过()来调用,内部可以接收参数
function color(value: string) {
// 这是一个装饰器工厂
return function (target: any) {
//这是装饰器,这个装饰器就是上面普通装饰器默认传入的类
// do something with "target" and "value"...
}
}
function logClass(value: string) {
return function (target: any) {
console.log(target)
console.log(value)
target.prototype.apiUrl = value //将传入的参数进行赋值
}
}
@logClass('hello world') //可传参数的装饰器
class HttpClient {
constructor() {}
getData() {}
}
let http: any = new HttpClient()
console.log(http.apiUrl)
10.1.3 类装饰器重构构造函数
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数 ,如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明,通过这种方法我们可以很轻松的继承和修改原来的父类,定义自己的属性和方法
注意: 如果要返回一个新的构造函数,必须注意处理好原来的原型链
/*
通过返回一个继承的类实现一个类的属性和方法的重构,换句话说就是在中间层有一个阻拦,然后返回的是一个新的继承了父类的类,这个类必须有父类的所有属性和方法,不然会报错
*/
function logClass(target: any) {
// 返回一个继承原来类的新的类
return class extends target {
//可以当做是固定写法吧
apiUrl: string = '我是修改后的数据'
getData() {
console.log(this.apiUrl)
}
}
}
//重构属性和方法
@logClass
class HttpClient {
// 如果不在这声明TypeScript的检测器检测不出来,在下面的使用都会报错,可以使用接口的声明合并来消除
constructor(public apiUrl = '我是构造函数中的数据') {}
getData() {
console.log(123)
}
}
/*
interface HttpClient {
apiUrl: string
getData(): void
}
*/
let http: any = new HttpClient()
console.log(http.apiUrl) //我是修改后的数据
http.getData() //我是修改后的数据
10.1.4 装饰器求值
类中不同声明上的装饰器将按以下规定的顺序应用:
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员
- 参数装饰器应用到构造函数
- 类装饰器应用到类
10.2 方法装饰器
方法装饰器声明在一个方法的声明之前(紧靠着方法声明)
注意:
- 它会被应用到方法的属性描述符上,可以用来监视,修改或者替换方法定义
- 方法装饰器不能用在声明文件(
.d.ts),重载或者任何外部上下文(比如declare的类)中
方法装饰器被应用到方法的属性描述符上,可以用来监视,修改或替换方法定义,传入三个参数(都是自动传入的):
-
对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
-
成员的名字(只是个 string 类型的字符串,没有其余作用)
-
成员的属性描述符,是一个对象,里面有真正的方法本身
注: 如果代码输出目标版本小于
ES5,属性描述符将会是undefined
注意:如果方法装饰器返回一个值,它会被用作方法的属性描述符,如果代码输出目标版本小于ES5返回值会被忽略
function get(value: any) {
// PropertyDescriptor是TypeScript中内置的属性描述符的类型限定,包含了类型修辞符的所有属性
return function (target: any, methodName: string, desc: PropertyDescriptor) {
console.log(target) //HttpClient类
console.log(methodName) //getData方法名,一个字符串
console.log(desc) //描述符
console.log(desc.value) //方法本身就在desc.value中
target.url = 123 //也能改变原实例
}
}
class HttpClient {
public url: any | undefined
constructor() {}
@get('hello world')
getData() {
console.log(this.url)
}
}
let http = new HttpClient()
console.log(http.url) //123
function get(value: any) {
// PropertyDescriptor是TypeScript中内置的属性描述符的类型限定
return function (target: any, methodName: string, desc: PropertyDescriptor) {
let oMethod = desc.value
desc.value = function (...args: any[]) {
//因为用了方法装饰器,所以实际调用getData()方法的时候会调用desc.value来实现,通过赋值可以实现重构方法
//原来的方法已经赋值给oMethod了,所以可以改变
args = args.map(
//这个段代码是将传入的参数全部转换为字符串
(value: any): string => {
return String(value)
}
)
console.log(args) //因为方法重构了,所以原来的getData()中的代码无效了,调用时会打印转换后参数
/*
如果想依然能用原来的方法,那么写入下面的代码,相当于就是对原来的方法进行了扩展
*/
oMethod.apply(target, args) //通过这种方法调用可以也实现原来的getData方法
}
}
}
class HttpClient {
public url: any | undefined
constructor() {}
@get('hello world')
getData(...args: any[]) {
console.log(args) //[ '1', '2', '3', '4', '5', '6' ]
console.log('我是getData中的方法')
}
}
let http = new HttpClient()
http.getData(1, 2, 3, 4, 5, 6) //[ '1', '2', '3', '4', '5', '6' ]
function get(bool: boolean): any {
return (target: any, prop: string, desc: PropertyDescriptor) => {
// 通过返回值修改属性描述符
return {
value() {
return 'not age'
},
enumerable: bool
}
}
}
class Test {
constructor(public age: number) {}
@get(false)
public getAge() {
return this.age
}
}
const t = new Test(18)
console.log(t.getAge()) // not age,getAge()函数的值以及被修改了
for (const key in t) {
console.log(key) // 只有age属性,如果上面@get传入的是true就还有getAge()方法
}
10.3.1 属性描述符
在 ES5 之前,JavaScript 没有内置的机制来指定或者检查对象某个属性(property)的特性(characteristics),比如某个属性是只读(readonly)的或者不能被枚举(enumerable)的。但是在 ES5 之后,JavaScript 被赋予了这个能力,所有的对象属性都可以通过属性描述符(Property Descriptor)来指定
interface obj {
[key: string]: any
}
let myObject: obj = {}
Object.defineProperty(myObject, 'a', {
value: 2,
writable: true, // 可写
configurable: true, // 可配置
enumerable: true // 可遍历
})
// 上面的定义等同于 myObject.a = 2;
// 所以如果不需要修改这三个特性,我们不会用 `Object.defineProperty`
console.log(myObject.a) // 2
属性描述符的六个属性
-
value:属性值
-
writable:是否允许赋值,true 表示允许,否则该属性不允许赋值
interface obj { [key: string]: any } let myObject: obj = {} Object.defineProperty(myObject, 'a', { value: 2, writable: false, // 不可写 configurable: true, enumerable: true }) myObject.a = 3 // 写入的值将会被忽略 console.log(myObject.a) // 2 -
get:返回属性值的函数。如果为 undefined 则直接返回描述符中定义的 value 值
-
set:属性的赋值函数。如果为 undefined 则直接将赋值运算符右侧的值保存为属性值
注:
- 一旦同时使用了
get和set,需要一个中间变量存储真正的值。 set和writable:false是不能共存的。
- 一旦同时使用了
-
configurable:如果为 true,则表示该属性可以重新使用(
Object.defineProperty(...))定义描述符,或者从属性的宿主删除。缺省为truelet myObject = { a: 2 } Object.defineProperty(myObject, 'a', { value: 4, writable: true, configurable: false, // 不可配置! enumerable: true }) console.log(myObject.a) // 4 myObject.a = 5 // 因为最开始writable时true,所以不会影响到赋值 console.log(myObject.a) // 5 Object.defineProperty(myObject, 'a', { value: 6, writable: true, configurable: true, enumerable: true }) // TypeError注: 一旦某个属性被指定为
configurable: false,那么就不能从新指定为configurable: true了,这个操作是单向,不可逆的这个特性还会影响
delete操作的行为let myObject = { a: 2 } Object.defineProperty(myObject, 'a', { value: 4, writable: true, configurable: false, // 不可配置! enumerable: true }) delete myObject.a console.log(myObject.a) // 4 -
enumerable:如果为 true,则表示遍历宿主对象时,该属性可以被遍历到(比如
for..in循环中)。缺省为trueinterface obj { [key: string]: any } let myObject: obj = {} Object.defineProperty( myObject, 'a', // make `a` enumerable, as normal { enumerable: true, value: 2 } ) Object.defineProperty( myObject, 'b', // make `b` NON-enumerable { enumerable: false, value: 3 } ) console.log(myObject.b) // 3 console.log('b' in myObject) // true myObject.hasOwnProperty('b') // true // ....... // 无法被遍历到 for (let k in myObject) { console.log(k, myObject[k]) } // "a" 2 myObject.propertyIsEnumerable('a') // true myObject.propertyIsEnumerable('b') // false Object.keys(myObject) // ["a"] Object.getOwnPropertyNames(myObject) // ["a", "b"]可以看出,
enumerable: false使得该属性从对象属性枚举操作中被隐藏,但Object.hasOwnProperty(...)仍然可以检测到属性的存在。另外,Object.propertyIsEnumerable(..)可以用来检测某个属性是否可枚举,Object.keys(...)仅仅返回可枚举的属性,而Object.getOwnPropertyNames(...)则返回该对象上的所有属性,包括不可枚举的
注: Object 有专门操作属性的方法,在这里就不再多讲了
10.3 方法参数装饰器
参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。
注意: 参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如 declare的类)里
参数装饰器被表达式会在运行时当作函数被调用,可以使用参数装饰器为类的原型增加一些元素数据,传入三个参数(都是自动传入的):
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
- 方法的名字(只是个 string 类型的字符串,没有其余作用)
- 参数在函数参数列表中的索引
注:
- 参数装饰器只能用来监视一个方法的参数是否被传入
- 参数装饰器的返回值会被忽略
//这个装饰器很少使用
function logParams(value: any) {
return function (target: any, methodName: any, paramsIndex: any) {
console.log(target)
console.log(methodName) //getData
console.log(paramsIndex) //1,因为value在下面是第二个参数
}
}
class HttpClient {
public url: any | undefined
constructor() {}
getData(index: any, @logParams('hello world') value: any) {
console.log(index)
console.log(value)
}
}
let http: any = new HttpClient()
http.getData(0, '123') //我是修改后的数据
10.4 访问器装饰器
访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的属性描述符并且可以用来监视,修改或替换一个访问器的定义。
注意:
- 访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如
declare的类)里 - TypeScript 不允许同时装饰一个成员的
get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的
访问器装饰器表达式会在运行时当作函数被调用,传入下列 3 个参数(都是自动传入的):
-
对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
-
成员的名字
-
成员的属性描述符
注: 如果代码输出目标版本小于
ES5,Property Descriptor将会是undefined
注意: 如果访问器装饰器返回一个值,它会被用作方法的属性描述符。如果代码输出目标版本小于ES5返回值会被忽略
function configurable(value: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.configurable = value
}
}
class Point {
private _x: number
private _y: number
constructor(x: number, y: number) {
this._x = x
this._y = y
}
@configurable(false)
get x() {
return this._x
}
@configurable(false)
get y() {
return this._y
}
}
10.5 属性装饰器
属性装饰器声明在一个属性声明之前(紧靠着属性声明)。
注意: 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。
属性装饰器表达式在运行时当作函数被调用,传入两个参数(都是自动传入的):
- 对应静态成员来说是类的构造函数,对于实例成员来说是类的原型对象
- 成员的名字
注: 属性描述符不会做为参数传入属性装饰器,这与 TypeScript 是如何初始化属性装饰器的有关。 因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。 因此,属性描述符只能用来监视类中是否声明了某个名字的属性
function logProperty(value: string) {
return function (target: any, attr: string) {
//target为实例化的成员对象,attr为下面紧挨着的属性
console.log(target)
console.log(attr)
target[attr] = value //可以通过修饰器改变属性的值
}
}
class HttpClient {
@logProperty('hello world') //修饰器后面紧跟着对应要修饰的属性
public url: string | undefined
constructor() {}
getData() {
console.log(this.url)
}
}
let http: any = new HttpClient()
http.getData() //hello world
10.5.5 返回值总结
- 属性和方法参数装饰器的返回值会被忽略
- 访问器和方法装饰器的返回值都会被用做方法的属性描述符(低于
Es5版本会被忽略) - 类装饰器的返回值会返回一个新的构造函数
10.6 装饰器的执行顺序
我们可以对同一个对象使用多个装饰器,装饰器的执行顺序是从后往前执行的
-
书写在同一行上
@f @g x -
书写在多行上
@f @g x
在 TypeScript 里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:
- 由上至下依次对装饰器表达式求值。
- 求值的结果会被当作函数,由下至上依次调用。
简单的说就是: 如果是装饰器工厂修饰的(不是只有一个函数,是通过返回函数来实现),会从上到下按照代码的顺序先执行装饰器工厂生成装饰器,然后再从下往上执行装饰器
特别提醒: 如果方法和方法参数装饰器在同一个方法出现,参数装饰器先执行
function f() {
console.log('f(): evaluated')
return function (
target,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log('f(): called')
}
}
function g() {
console.log('g(): evaluated')
return function (
target,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log('g(): called')
}
}
class C {
@f()
@g()
method() {}
}
# 在控制台中打印
f(): evaluated
g(): evaluated
g(): called
f(): called
11.Mixins 混入
11.1 对象的混入
和 JS 一样,TypeScript 中混入对象也是使用Object.assign()方法来实现,不多最后的结果会多了一个交叉类型的类型定义,同时包含了所有混入对象的属性
interface ObjectA {
a: string
}
interface ObjectB {
b: string
}
let A: ObjectA = {
a: 'a'
}
let B: ObjectB = {
b: 'b'
}
let AB: ObjectA & ObjectB = Object.assign(A, B) // 及时左边没有类型定义也会自动被定义为交叉类型
console.log(AB)
11.2 类的混入
对于类的混入,我们需要理解下面这个例子:
// Disposable Mixin
class Disposable {
isDisposed: boolean
dispose() {
this.isDisposed = true
}
}
// Activatable Mixin
class Activatable {
isActive: boolean
activate() {
this.isActive = true
}
deactivate() {
this.isActive = false
}
}
class SmartObject implements Disposable, Activatable {
constructor() {
setInterval(() => console.log(this.isActive + ' : ' + this.isDisposed), 500)
}
interact() {
this.activate()
}
// Disposable
isDisposed: boolean = false
dispose: () => void
// Activatable
isActive: boolean = false
activate: () => void
deactivate: () => void
}
applyMixins(SmartObject, [Disposable, Activatable])
let smartObj = new SmartObject()
setTimeout(() => smartObj.interact(), 1000)
////////////////////////////////////////
// In your runtime library somewhere
////////////////////////////////////////
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
derivedCtor.prototype[name] = baseCtor.prototype[name]
})
})
}
代码里首先定义了两个类,它们将做为 mixins。 可以看到每个类都只定义了一个特定的行为或功能。 稍后我们使用它们来创建一个新类,同时具有这两种功能
// Disposable Mixin
class Disposable {
isDisposed: boolean
dispose() {
this.isDisposed = true
}
}
// Activatable Mixin
class Activatable {
isActive: boolean
activate() {
this.isActive = true
}
deactivate() {
this.isActive = false
}
}
然后我们需要创建一个类来使用他们作为接口进行限制。没使用extends而是使用implements。 把类当成了接口,仅使用 Disposable 和 Activatable 的类型而非其实现。 这意味着我们需要在类里面实现接口。 但是这是我们在用 mixin 时想避免的。
我们可以这么做来达到目的,为将要 mixin 进来的属性方法创建出占位属性。 这告诉编译器这些成员在运行时是可用的。 这样就能使用 mixin 带来的便利,虽说需要提前定义一些占位属性。
class SmartObject implements Disposable, Activatable {
constructor() {
setInterval(() => console.log(this.isActive + ' : ' + this.isDisposed), 500)
}
interact() {
this.activate()
}
// Disposable
isDisposed: boolean = false
dispose: () => void
// Activatable
isActive: boolean = false
activate: () => void
deactivate: () => void
}
创建帮助函数,帮我们做混入操作。 它会遍历 mixins 上的所有属性,并复制到目标上去,把之前的占位属性替换成真正的实现代码
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
derivedCtor.prototype[name] = baseCtor.prototype[name]
})
})
}
最后,把 mixins 混入定义的类,完成全部实现部分
applyMixins(SmartObject, [Disposable, Activatable])
总结
想了想,最后还是加个总结吧,第一次使用 TypeScript 其实是在 3.1 版本发行的时候,当初由于对强类型语言没有什么经验(当初只学了 C,也没有学习 Java 等),对当时的我来说,从 JavaScript 直接转向 TypeScript 是件非常困难的事,所以这个汇总笔记的时间跨度其实还是比较大的,中间经过不断修修补补,也算是对 TypeScript 有了一定的理解。到如今我的项目中也都是使用 TypeScript 进行开发,也算是不妄我这么长的笔记吧(笑)。
最后的最后,如今 TypeScript 已经成为了前端的一大趋势,掌握 TypeScript 也逐渐变成了前端开发者们的基本技能,花一点时间对 TypeScript 进行深入了解能够写出更加符合规范的代码,对项目的开发与维护都有着极大的作用。 如果你对 TypeScript 有着自己的看法或笔记存在的不完备的地方,欢迎在评论区留言。