前言
最近面试的时候被问到了关于typescript的问题,答得稀烂,故在此重新梳理知识体系巩固一下。
TypeScript的在线编辑器推荐
TypeScript Playground、playcode.io、stackblitz.com、codesandbox.io
es6类的概念以及和构造函数的联系
在es6以前,js是通过构造函数生成实例对象,以一种基于原型的方式实现,这和传统的面向对象语言不一样,因此也增加了开发者的理解成本。而es6之后,引入了类的概念,它必须由new调用,以class关键字定义,但是它的绝大部分功能依然可以用es5去实现,只是写法更贴近传统面向对象语言基于类的写法,Js的class依然有一些特性没有实现,他本质上还是构造函数,因此也可以把它看成一个语法糖。
使用 getter 和 setter 可以改变属性的赋值和读取行为。
es6类的写法:
class Point{
constructor(x,y){
this.x = x;
this.y = y;
}
notStatic(){
console.log('not static func');
}
static dips(){
console.log('static func');
}
}
如果上述代码想要用构造函数去实现:
function Point(x,y){
this.x = x;
this.y = y;
Point.dips = function(){
console.log('static func');
}
}
Point.prototype.notStatic= function(){
console.log('not static func');
}
//或者静态方法/属性在函数体外写
Point.dips = function(){
console.log('static func');
}
验证一下结果:
let point1 = new Point(1,2)
console.log(JSON.stringify(point1))
//'{"x":1,"y":2}'
point1.dips()
//Error: point1.dips is not a function
Point.prototype.notStatic()
//"not static func"
point1.notStatic()
//"not static func"
Point.dips()
//"static func"
Point.notStatic()
//Error: Point.notStatic is not a function
总结一下就是:类中若非显式的定义在this上,则都是定义在类的原型上,而如果用了static修饰符,则是定义在该类本身,不会被实例对象继承,而是以类.静态属性/方法的方式访问。
另外,私有属性/方法用#实现,继承可以让子类extends父类,或者在子类中用super调用父类的方法,当然在子类中重写父类的方法也是可以的。
特别地,这里给出一个例子
class MyPromise{
#result;
#state;
constructor(executor){
this.#state = 'pending'
this.x=1
console.log(JSON.stringify(this))
/*constructor中的this指向实例对象,this.resolve会沿着原型链
找到原型上的resolve函数*/
executor(this.resolve,this.reject)
this.resolve(2)
}
resolve(value){
console.log(value)
console.log(JSON.stringify(this))
// this.#state = 'fulfilled'
// this.#result = value
}
reject(value){
this.#state = 'rejected'
this.#result = value
}
then(onFulfilled,onRejected){
if(this.#state==='fulfilled'){
onFulfilled(this.#result)
}
if(this.#state==='rejected'){
onRejected(this.#result)
}
}
}
const data = new MyPromise((resolve,reject)=>{
//相当于window调用
resolve(1)
})
> '{"x":1}' 7 line
> 1 13
> undefined 14
> 2 13
> '{"x":1}' 14
至于js中类和构造函数的关系,我们可以看以下这一段来自阮一峰es6网站的一段示例:
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
上面代码表明,类的数据类型就是函数,类本身就指向构造函数。
使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。
构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。
因此,在类的实例上面调用方法,其实就是调用原型上的方法。
唯一存在差异的就是:类的内部所有定义的方法,都是不可枚举的,而es5则相反。
ts类与es6类的差异
ts的类本质也是构造函数+原型链。它和es6中的类的概念差异不大,但是它会支持面向对象的所有特性。
它包含以下模块:
- 字段(类里面声明的遍历,表示对象的有关数据)
- 构造函数(类实例化时会调用)
- 方法
它还支持许多修饰符:
- public 可以自由访问类内的成员
- private 私有 只可以在该类内访问
- protected 受保护的 只能在该类或者子类内访问,不能在实例对象中访问
- readonly 只读 必须在类内声明时或者构造函数内初始化
- static 在类本身定义的方法或属性,不会被实例对象继承
- abstract 抽象 用于定义抽象类和抽象类里的抽象方法【也就是没有具体的实现细节】
抽象类一般用作其他派生类的基类,不能被实例化
和接口不同的是,接口只有属性和抽象方法【必须由子类具体实现】,而抽象类可以有方法的具体实现细节
接口和类的区别
接口是一系列抽象方法声明的集合,只声明但不实现;而类可以声明并实现方法。
类可以用implements关键字去实现接口;接口之间也可以继承;接口甚至也可以继承类。
那么就会有一个问题,既然接口能做的类也能做,那直接用类不行吗?为什么还需要接口?↓
当我们想用某个函数统一处理所有具有某个方法的对象作为入参时,接口就显得很重要。
如果用类写:
class Essay{
getContent(){
return 'Essay!';
}
}
function print(obj: Essay): void {
console.log(obj.getContent());
}
let essay1 = new Essay();
print(essay1);
但是这样就只能处理Essay类的对象。
当然也可以让每个类都具有getContent方法的实现,或者继承公共父类去重写这个方法也可以。
class Essay {
getContent() {
return 'Essay!';
}
}
class EssayChild1 {
getContent() {
return 'EssayChild1!';
}
}
class EssayChild2 {
getContent() {
return 'EssayChild2!';
}
}
function print(obj:Essay): void {
console.log(obj.getContent());
}
let essay = new Essay();
let essay1 = new EssayChild1();
let essay2 = new EssayChild2();
print(essay);
print(essay1);
print(essay2);
//Essay!
//EssayChild1!
//EssayChild2!
当然用接口约束函数参数类型,然后用类去实现接口的方法也是可以的:
interface Essay {
getContent() :string;
}
class EssayChild1 implements Essay{
getContent() {
return 'EssayChild1!';
}
}
class EssayChild2 implements Essay{
getContent() {
return 'EssayChild2!';
}
}
function print(obj:Essay): void {
console.log(obj.getContent());
}
let essay1 = new EssayChild1();
let essay2 = new EssayChild2();
print(essay1);
print(essay2);
//EssayChild1!
//EssayChild2!
因此,在某些应用场景中,也可以直接把类当作接口使用。
泛型
在定义类、接口或函数时无法直接确定具体类型时,就可以使用泛型。尤其是当碰到需要入参和出参的类型一样的场景时,泛型就可以发挥很大作用,而如果直接用any,就相当于关闭了ts类型检查的优势,如果同时写很多个类型的函数,那么代码的重复性又会很高。
使用方法:
function test<T, K>(a: T, b: K): K{
return b;
}
test<number>(10)
//不指定类型也可以,它会根据入参自动推断类型
类中也可以使用:
class MyClass<T>{
prop: T;
constructor(prop: T){
this.prop = prop;
}
}
除此之外,也可以对泛型的范围进行约束
interface MyInter{
length: number;
}
function test<T extends MyInter>(arg: T): number{
return arg.length;
}
直接把泛型当成一个数据类型去使用就行。
类型别名type和接口的区别
一、表示范围
接口只能用来声明对象类型;而类型别名只是为类型创建一个新名称,可以定义基本类型的别名,也可以用来声明联合类型,如 type paramType = number | string,元组类型,如type arrType = [string, string, number];
二、是否可重复声明
接口可重复声明,ts会将其合并;而type重复声明会报错
三、继承和实现
接口可以被其他接口继承,也可以被类实现
一些小的知识点
当属性是可有可无时,用可选属性?
[propName:String]:any代表后面可以加任意及任意多的属性
类型断言:变量 as 类型 直接告诉编译器变量的类型
ts新增的类型还有:
新增类型 | 例子 | 描述 |
---|---|---|
字面量 | 其本身 | 限制变量的值就是该字面量的值 |
any | * | 任意类型 |
unknown | * | 类型安全的any |
void | 空值(undefined) | 没有值(或undefined) |
never | 没有值 | 不能是任何值 |
tuple | [4,5] | 元组,固定长度数组 |
enum | enum{A, B} | 枚举 |
ts中的两种文件格式
- .ts
- .d.ts
.ts文件:
1.既包含类型信息又可执行代码。
2.可以被编译为.js文件,然后,执行代码。
3.用途∶编写程序代码的地方。
.d.ts 文件:
1.只包含类型信息的类型声明文件。
2.不会生成.js 文件,仅用于提供类型信息。
3.用途∶为JS提供类型信息。
引入第三方库的时候,不像js环境了,必须要引入其对应的类型声明文件。
在vue中使用typescript
我是直接用的vue-cli脚手架去vue create一个项目,然后选择带有typescript的预设。