Part1 · JavaScript【深度剖析】
TypeScript语言
文章说明:本专栏内容为本人参加【拉钩大前端高新训练营】的学习笔记以及思考总结,学徒之心,仅为分享。如若有误,请在评论区支出,如果您觉得专栏内容还不错,请点赞、关注、评论。共同进步!
本篇开始,梳理一些TypeScript语言的知识。包含快速上手、原始类型、作用域、数据类型等
一、TypeScript概述
TypeScript是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。
TypeScript 扩展了 JavaScript 的句法,所以任何现有的 JavaScript 程序可以不加改变的在 TypeScript下工作。TypeScript 是为大型应用之开发而设计,而编译时它产生 JavaScript 以确保兼容性。任何一种JavaScript运行环境都支持TypeScript开发。
-
缺点一:语言多了很多概念,提高学习成本。但其属于渐进式的,所以可以按照JavaScript标准语法来使用,在学习过程中了解到一个特性就可以使用一个特性。
-
缺点二:项目初期,TypeScript会增加一些时间成本,需要很多的类型声明。
二、TypeScript快速上手
-
安装TypeScript
yarn init --yes # 初始化项目目录 yarn add typescript --dev # 开发依赖
-
新建TypeScript文件,文件名01-getting-started.ts,其文件扩展名默认为**.ts**
// 可以完全按照 JavaScript 标准语法编写代码 const hello = (name: any) => { console.log(`Hello, ${name}`) } hello('TypeScript')
-
使用TypeScript编译上述文件
yarn tsc 01-getting-started.ts
编译后的文件01-getting-started.js:
"use strict"; // 可以完全按照 JavaScript 标准语法编写代码 var hello = function (name) { console.log("Hello, " + name); }; hello('TypeScript'); //# sourceMappingURL=01-getting-started.js.map
三、TypeScript配置文件
tsc不仅仅可以编译单个文件,还可以编译整个项目或者整个工程。在编译整个项目前,我们需要先给整个项目创建一个TypeScript的配置文件。
使用命令自动生成配置文件:
yarn tsc --init
在根目录下会多出tsconfig.json文件
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": ["ES2015", "DOM", "ES2017"], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "dist", /* Redirect output structure to the directory. */
"rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
.
.
.
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}
四、TypeScript原始类型
// 原始数据类型
const a: string = 'foobar'
const b: number = 100 // NaN Infinity
const c: boolean = true // false
// 在非严格模式(strictNullChecks)下,
// string, number, boolean 都可以为空
// const d: string = null
// const d: number = null
// const d: boolean = null
const e: void = undefined
const f: null = null
const g: undefined = undefined
五、TypeScript标准库声明
标准库就是内置对象多对应的声明,代码中使用内置对象就必须引用对应的标准库,否则TypeScript就会报错。
// Symbol 是 ES2015 标准中定义的成员,
// 使用它的前提是必须确保有对应的 ES2015 标准库引用
// 也就是 tsconfig.json 中的 lib 选项必须包含 ES2015
// "lib": ["ES2015", "DOM", "ES2017"],TypeScript把BOM和DOM都归结到一个标准库中,为DOM
const h: symbol = Symbol()
// Promise
// const error: string = 100
在上述**const h: symbol = Symbol()**中,如果配置文件中“target"为es5,那么会提示错误,因为,我们的symbol是es2015中新增的类型,所以需要将target改为es2015或以上才可以使用。
六、TypeScript中文错误消息
使TypeScript显示中文的错误消息,这样方便国人进行快速定位问题。
tsc --local zh-CN # 绝大多数会使用中文
VS Code中,搜索TypeScript local,将TypeScript:local设置为zh-CN,此时VS Code报出的错误提示也会是中文的。不过,并不推荐这样做,锻炼英语,之后更便捷的阅读官方文档,这才是我这种菜鸟程序员的挑战。
七、TypeScript作用域问题
不同文件中会有相同变量名的情况,此时TypeScript就会报出错误,重复定义变量。为解决这个办法,我们需要将变量放到不同的作用域或者使用exports导出,这样文件就作为一个模块,模块是有单独的作用域的。
八、TypeScript Object类型
TypeScript中的object并不单独指对象类型,而是泛指所有的非原始类型,也就是对象、数组、函数。
// Object 类型
export {} // 确保跟其它示例没有成员冲突
// object 类型是指除了原始类型以外的其它类型
const foo: object = function () {} // [] // {} 可以接受对象、数组、函数
// 如果需要明确限制对象类型,则应该使用这种类型对象字面量的语法,或者是「接口」
const obj: { foo: number, bar: string } = { foo: 123, bar: 'string' }
// 接口的概念后续介绍
九、TypeScript数组类型
TypeScript中定义数组的方式,与Flow中几乎完全一致。他有两种方式,第一种就是使用array泛型。第二种使用元素类型加方括号的形式。
// 数组类型
export {} // 确保跟其它示例没有成员冲突
// 数组类型的两种表示方式
const arr1: Array<number> = [1, 2, 3]
const arr2: number[] = [1, 2, 3]
TypeScript中使用强类型的优势:
// 案例 -----------------------
// 如果是 JS,需要判断是不是每个成员都是数字
// 使用 TS,类型有保障,不用添加类型判断
function sum (...args: number[]) {
return args.reduce((prev, current) => prev + current, 0)
}
// reduce回顾:第一个参数prev为累加的和,第二个参数为当前循环的项,函数体中写业务代码,默认值为0
sum(1, 2, 3) // => 6
一般情况下,数组应该保持同质,也就是说数组中的元素应该具有相同的类型。
十、TypeScript元组类型
元组是array的一种子类型,是定义数组的一种特殊类型。长度固定,各索引位置上的值具有固定的已知类型。与其他多数类型不同,声明元组时必须显示注解类型。这是因为,创建元组使用的语法与数组相同(都使用方括号),而TypeScript遇到方括号,推导出来的是数组的类型。
export {}
const tuple:[number, string] = [10,'Leo']
// const age = tuple[0]
// const name = tuple[1]
const [age, name] = tuple
// ---------------------
const entries: [string, number][] = Object.entries({
foo: 123,
bar: 456
})
const [key, value] = entries[0]
// key => foo, value => 123
十一、TypeScript枚举类型
枚举的作用是列举类型中包含的各个值。这是一种无序数结构,把键映射到值上。枚举可以理解为编译时键的固定访问对象,访问键时,TypeScript将检查指定的键是否存在。示例如下:
export {}
// 文章对象
const post = {
title: "TypeScript介绍",
content: "简单介绍一下TypeScript语言",
status: 2 // 1 0,
}
这里0表示草稿,1表示未发布,2代表已发布,为了防止出现其他值出现,我么可以使用枚举来限定其值。
枚举特点:
- 给一组数值起一个更好理解的名字
- 一个枚举中只会存在几个固定的值,并不会出现超出范围的可能性
// 用对象模拟枚举
const PostStatus = {
Draft: 0,
Unpublished: 1,
Published: 2
}
// 标准的数字枚举
enum PostStatus2 {
Draft = 0,
Unpublished = 1,
Published = 2
}
// 数字枚举,枚举值自动基于前一个值自增,如果不指定每个枚举类型的值,那么其值会从0自增
enum PostStatus3 {
Draft = 6,
Unpublished, // => 7
Published // => 8
}
// 字符串枚举,需要手动给每个成员明确一个初始化的值
enum PostStatus4 {
Draft = 'aaa',
Unpublished = 'bbb',
Published = 'ccc'
}
// 常量枚举,不会侵入编译结果
const enum PostStatus5 {
Draft,
Unpublished,
Published
}
然后根据上面的枚举值,我们可以将文章状态的写法进行如下转换:
const post = {
title: 'Hello TypeScript',
content: 'TypeScript is a typed superset of JavaScript.',
status: PostStatus.Draft // 3 // 1 // 0
}
// PostStatus[0] // => Draft
使用常量进行枚举时,编译ts文件后的js文件中会移出键值对形式的数组,改为在需要枚举的地方直接写入值,其他值则以注释的方式出现,如下为编译后的js文件:
"use strict";
// 枚举(Enum)
Object.defineProperty(exports, "__esModule", { value: true });
var post = {
title: 'Hello TypeScript',
content: 'TypeScript is a typed superset of JavaScript.',
status: 0 /* Draft */ // 3 // 1 // 0
};
// PostStatus[0] // => Draft
//# sourceMappingURL=07-enum-types.js.map
十二、TypeScript函数类型
函数的类型约束无外乎就是对函数的输入、输出进行类型限制,输入指的就是函数的参数,输出指的是函数的返回值。JavaScript中有两种函数声明方式,分别是函数声明和函数表达式。所以我们需要了解在这两种声明方式下,我们如何进行函数的类型约束。
- 函数声明式:
// 函数类型
export {} // 确保跟其它示例没有成员冲突
function func1 (a: number, b: number = 10, ...rest: number[]): string {
return 'func1'
}
// a类型为数字类型,b类型为数字类型,...rest为参数默认值类型为数字,:string为函数返回值约束,如果没有...rest,则实参和形参个数必须相同
function func0 (a: number, b?: number = 10, ...rest: number[]): string {
return 'func1'
}
// 在参数声明冒号前加问号?,表示这个参数为可选值,无论可选参数或参数默认值,都只能出现在参数最后面
func1(100, 200)
func1(100)
func1(100, 200, 300)
- 函数表达式:
const func2: (a: number, b: number) => string = function (a: number, b: number): string {
return 'func2'
}
十三、TypeScript任意类型
JavaScript本身是弱类型的语言,很多内置的API本身就支持接收任意类型的参数。而TypeScript又是基于JavaScript基础之上的,所以会在代码中用一个变量去接收任意一个类型的值。例:
function stringify (value: any) {
return JSON.stringify(value)
}
stringify('string')
stringify(100)
stringify(true)
any属于动态类型
let foo: any = 'string'
foo = 100
foo.bar() // 语法上不会报错,TypeScript不会对any进行类型检查,any 类型是不安全的
十四、TypeScript隐式类型推断
在TypeScript中,如果我们没有通过类型注解取标记一个变量的类型,TypeScript会根据变量的使用情况去推断变量的类型。这种特性叫:隐式类型推断
// 隐式类型推断
export {} // 确保跟其它示例没有成员冲突
let age = 18 // number
// age = 'string' // 语法上会出现错误
// 如果TypeScript无法对变量进行类型推断,那TypeScript就会对这个变量的类型注解为any
let foo
foo = 100
foo = 'string' // 变量赋值任意类型的值,语法上都不会报错
虽然在TypeScript中支持隐式类型推断,这种隐式类型推断可以帮我们简化代码,但我们仍然建议大家对每个变量添加类型注解,这样便于我们后续更加直观的理解代码。
十五、TypeScript类型断言
特殊情况下,TypeScript无法推断出变量的具体类型,开发者根据代码的使用情况,总是根据使用情况可以知道变量是什么类型的。
// 类型断言
export {} // 确保跟其它示例没有成员冲突
// 假定这个 nums 来自一个明确的接口
const nums = [110, 120, 119, 112]
const res = nums.find(i => i > 0)
// const square = res * res,TypeScript此时推断返回值为number或undefined,即无法找到大于0的数字
const num1 = res as number // 开发者告诉TypeScript:你相信我,我断言res为number类型
const num2 = <number>res // JSX 下不能使用
类型断言的方式有两种:
-
as关键词(推荐使用)
const num1 = res as number
-
变量前面使用<类型>进行断言
const num2 = <number>res // JSX 下不能使用
!注意:类型断言并不是类型转换
十六、TypeScript接口
Interfases接口,约定对象的结构,我们使用一个接口,就必须遵循这个接口全部的约定。TypeScript中,接口最直观的体现就是约定一个对象中应该有哪些成员,而且,成员的类型是固定的。
function printPost (post) {
console.log(post.title)
console.log(post.content)
}
此时该函数对于接受的参数post有一定的要求,也就是说post一定要有title和content属性,只不过这种要求为隐形的,并没有明确的表达,这时我们可以用接口明确的表达出来,此时我们可以定义一个接口:使用interface关键词。
interface Post {
title: string // 类型限定,可以使用逗号分隔多个成员,更标准的是使用分号,当然也可以省略
content: string
}
function printPost (post: Post) {
// 显示的要求传入的对象必须要有title和content成员
console.log(post.title)
console.log(post.content)
}
printPost({
title: 'Hello TypeScript',
content: 'A javascript superset'
})
接口中可以使用逗号分隔多个成员,更标准的是使用分号,当然也可以省略,根据自己团队习惯来
编译完成后的JavaScript文件,发现在js代码中并没有接口,也就是说TypeScript中的接口只是对代码进行类型约束的,在实际运行的阶段,接口并无意义。
"use strict";
// 接口
Object.defineProperty(exports, "__esModule", { value: true });
function printPost(post) {
console.log(post.title);
console.log(post.content);
}
printPost({
title: 'Hello TypeScript',
content: 'A javascript superset'
});
//# sourceMappingURL=12-interface-basic.js.map
对于接口中约定的成员,还有一些特殊的用法。
- 可选成员
// 可选成员、只读成员、动态成员
export {} // 确保跟其它示例没有成员冲突
// -------------------------------------------
interface Post {
title: string
content: string
subtitle?: string // 表示为可选成员,其实就是将subtitle类型标注为string|undefined
readonly summary: string
}
const hello: Post = {
title: 'Hello TypeScript',
content: 'A javascript superset',
summary: 'A javascript'
}
- 只读成员
// 可选成员、只读成员、动态成员
export {} // 确保跟其它示例没有成员冲突
// -------------------------------------------
interface Post {
title: string
content: string
readonly summary: string // readonly表示此成员只读不能修改,在summary初始化完成后不能修改
}
const hello: Post = {
title: 'Hello TypeScript',
content: 'A javascript superset',
summary: 'A javascript'
}
- 动态成员
interface Cache {
[prop: string]: string
}
const cache: Cache = {}
cache.foo = 'value1' // cache可以任意添加成员,只不过需要遵循成员必须为string类型
cache.bar = 'value2'
十七、TypeScript类的基本使用
类classes,可以用来描述一类具体事务的抽象特征,例如手机属于一种类型,特征是可以打电话、发短信,在这种类型下,又可以细分某智能手机、某非智能手机(具体事物)。ES6以前,JavaScript都是由函数+原型模拟实现类,ES6开始,JavaScript中有了专门的class,TypeScript中除了使用ES6中类的所有用法,它还额外添加一些用法功能。
// 类(Class)
export {} // 确保跟其它示例没有成员冲突
class Person {
name: string // = 'init name'
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
sayHi(msg: string): void {
console.log(`I am ${this.name}, ${msg}`)
}
}
在类的构造函数中,并不能直接使用this.name,需要在类中直接声明属性才可以使用。
十八、TypeScript类的访问修饰符
TypeScript中类的特殊用法,首先是类成员的访问修饰符,接着使用上面定义的person
// 类的访问修饰符
export {} // 确保跟其它示例没有成员冲突
class Person {
public name: string // = 'init name' 共有成员,默认的修饰符,加不加都一样,但建议加上
private age: number // 私有属性,只能在类内部访问
protected gender: boolean // 受保护的属性,
constructor(name: string, age: number) {
this.name = name
this.age = age
this.gender = true
}
sayHi(msg: string): void {
console.log(`I am ${this.name}, ${msg}`)
console.log(this.age)
}
}
class Student extends Person {
private constructor(name: string, age: number) {
super(name, age)
console.log(this.gender) 这里可以访问到projected修饰符修饰的属性
} 构造函数默认的修饰符为public,当它修饰符为private时,外部无法访问,使用静态方法可以访问
static create(name: string, age: number) {
return new Student(name, age) 使用静态方法创建对象实例
}
}
const tom = new Person('tom', 18)
console.log(tom.name)
// console.log(tom.age) 无法访问age属性,为私有属性
// console.log(tom.gender) 无法访问gender,不能再外部使用,与private的区别是,projected只允许在此类中访问成员
// const jack = Student.create('jack', 18) 使用静态方法创建对象实例
十九、TypeScript类的只读属性
对于类属性的修饰,还可以使用readonly进行修饰,只允许访问,不允许修改。
// 类的只读属性
export {} // 确保跟其它示例没有成员冲突
class Person {
public name: string // = 'init name'
private age: number
// 只读成员
protected readonly gender: boolean // readonly跟在访问修饰符后面,对于只读属性可以再类型声明的时候直接初始化,也可以在构造函数中初始化,二者选 // 其一
constructor(name: string, age: number) {
this.name = name
this.age = age
this.gender = true
}
sayHi(msg: string): void {
console.log(`I am ${this.name}, ${msg}`)
console.log(this.age)
}
}
const tom = new Person('tom', 18)
console.log(tom.name)
// tom.gender = false
二十、TypeScript类与接口
相比于类,接口的概念更抽象。使用前面说到的手机的例子,手机是一个类型,这个类型的实例都是可以打电话发短信,但是能够打电话的不仅仅只有手机,座机也可以打电话,但座机并不属于手机这个类目,而是一个单独的类目,因为它不能发短信。这种情况下,这两个类之间有公共的特征,公共的特征我们一般使用接口进行抽象。第一次使用可能会有一些吃力,多从生活的角度思考。
// 类与接口
export {} // 确保跟其它示例没有成员冲突
// 一个接口只实现一个方法
interface Eat {
eat(food: string): void
}
interface Run {
run(distance: number): void
}
// 人类
class Person implements Eat, Run {
eat(food: string): void {
console.log(`优雅的进餐: ${food}`)
}
run(distance: number) {
console.log(`直立行走: ${distance}`)
}
}
// 动物类
class Animal implements Eat, Run {
eat(food: string): void {
console.log(`呼噜呼噜的吃: ${food}`)
}
run(distance: number) {
console.log(`爬行: ${distance}`)
}
}
二十一、TypeScript抽象类
抽象类在某种程度上说与接口有点类似,他也是可以用来约束子类中必须要有某个成员。但是不同的是,抽象类可以办函一些具体的实现。
// 抽线类
export {} // 确保跟其它示例没有成员冲突
// abstract抽象类,在被abstract声明后,他只能被继承,不能被new一个实例对象
abstract class Animal {
eat(food: string): void {
console.log(`呼噜呼噜的吃: ${food}`)
}
abstract run(distance: number): void // 抽象方法,不需要方法体,当父类中有抽象方法时,子类必须实现这个方法
}
// dog继承自animal
class Dog extends Animal {
run(distance: number): void {
console.log('四脚爬行', distance)
}
}
const d = new Dog()
d.eat('狗粮')
d.run(100)
二十二、TypeScript泛型
泛型指我们在定义函数、接口或类的时候,没有去指定具体的类型,等到使用的时候再去指定具体类型的特征。以函数为例,泛型就是我们在声明这个函数的时候,不去指定具体的类型,只在函数调用的时候去传递一个类型。作用是极大程度的复用我们的代码。
// 泛型
export {} // 确保跟其它示例没有成员冲突
// 创建一个指定长度的数组,数组元素为数字,返回值为数字类型的数组
function createNumberArray(length: number, value: number): number[] {
// ES6中的fill方法,Array创建一个长度为length,元素类型为number的空数组,然后使用fill填充值
// Array为泛型类,在调用之前并不知道需要什么类型元素的数组,使用泛型参数进行声明元素类型<number>
const arr = Array<number>(length).fill(value)
return arr
}
function createStringArray(length: number, value: string): string[] {
const arr = Array<string>(length).fill(value)
return arr
}
function createArray<T>(length: number, value: T): T[] {
const arr = Array<T>(length).fill(value)
return arr
}
// const res = createNumberArray(3, 100)
// res => [100, 100, 100]
const res = createArray<string>(3, 'foo')
上述方法createNumberArray创建了一个数字类型的数组,但是当我们需要创建一个字符串类型的数组是,最笨的办法是再创建一个函数,更改其参数,这样就会造成冗余。更合理的办法就是使用泛型。将string或number变成一个参数createArray,一般泛型参数为大写T。在使用的时候,我们在T的位置传递想要的类型,这样就可以同时满足上述两个需求了。
二十三、TypeScript类型声明
在实际开发过程中,我们会经常使用到一些第三方的npm模块,而这些npm模块并不一定都是使用TypeScript编写的。所以他提供的成员就不会有强类型的体验。
yarn add lodash
// 类型声明
// camelCase将字符串转换为驼峰格式
import { camelCase } from 'lodash'
import qs from 'query-string' //
qs.parse('?key=value&key2=value2')
// declare function camelCase (input: string): string // 如果我们使用的第三方模块没有对应的类型声明模块,我们需要declare手动进行类型声明
const res = camelCase('hello typed')
在直接导入时,会报错,无法找到类型声明文件import { camelCase } from 'lodash’
目前很多第三方模块会提供类型声明的模块,我们安装@types-lodash。并且越来越多的模块在内部会直接支持类型声明。比如上面query-string模块。
今日分享就到了这里,上面很多的概念性问题,要完全的理解并使用这些新的知识,需要很长一段时间。多用、多查、多做!
下面一节我们讲一讲JavaScript的性能优化问题双十一大家买了什么好东西,可以再下面评论区一块分享哦~
记录:2020/11/11