JS高级
深度透析JS原型
1、为什么使用原型
原型上所有的方法和属性都可以被构造函数【实际开发原型主要共享方法和所有实例公用引用属性】的实例共享。那为什么要共享呢? 先来看一个案例 【先不用管什么是原型】
function QQUsers (QQNo_, QQAge_, QQMark_) {
this.QQNo = QQNo_;//QQ号
this.QQAge = QQAge_;//Q龄
this.QQMark = QQMark_;//QQ标签
//引用对象类型=引用类型=对象类型=引用数据类型
// 数组也是一种引用数据类型
this.commonfriends = ['骑驴看海', '大漠上的英雄', '坚实的果子', '小草']//共同好友
// 方法也是一种引用数据类型
this.show = function () {
console.log(`QQ号:${this.QQNo},QQ龄:${this.QQAge},QQ标注:${this.QQMark}`)
console.log(`共同的好友是:${this.commonfriends}`);
}
}
// 对象也叫实例(instance)
// QQZhangSan叫做对象变量 对象是等号右边通过new出来的一个实例 而且是运行期间才在堆中开辟对象的内存空间
let QQZhangSan = new QQUsers("37834522", 15, "王阳明传人")
let QQLisi = new QQUsers("30424232", 10, "袁隆平的徒弟")
2、没有用原型会有什么问题?
所有 QQUser 实例都有相同的好友属性,好友属性用 commonfriends 英文表示,所有 QQUser 对象都有相同的 show 方法。但我们发现每一个 QQUser 实例都单独分配一个 commonfriends 属性空间和 show 方法空间,浪费了大量内存空间。
那怎么解决这个问题呢?答案:使用原型
3、认识函数+原型定义
-
3.1 函数也是一个
对象,当真正开始执行函数,执行环境【开发时为浏览器或控制台】会为函数分配一个函数对象变量空间和函数对象空间,函数对象变量用函数名表示,存在栈空间中,函数对象空间是在堆中开辟的一个内存空间,这个空间中有一个默认的prototype属性,这个prototype属性就是一个原型对象属性【也叫对象变量】。 -
3.2函数和构造函数的区别
当通过 new 函数()时,此刻这个函数就是构造函数【 日后会演变成TS 类的构造器】
- 3.3
原型对象定义
原型【 prototype ] 是定义函数由 JS 自动分配给函数的一个可以被所有构造函数的实例对象变量共享的对象变量【也叫对象属性】
4、如何访问原型对象空间上的属性和方法
(1)构造函数所有实例对象都可以访问原型对象(prototype)空间上的属性和方法。每一个实例都有默认的 __proto__ 属性,这个 __proto__ 属性指向原型对象(prototype)空间。
(2)关于__proto__:new 在创建新对象的时候,会赋予新对象一个属性指向构造函数的 prototype 对象空间,这个属性就是 __proto__
(3) 可以直接通过构造函数.prototype 对象属性来访问原型对象空间上的属性和方法
5、构造函数的实例相关
- 5.1 实例访问原型对象空间上的属性和方法
(1)构造函数实例访问一个属性和方法,首先从实例空间中查找。
当执行环境执行 new 构造函数()时,构造函数中通过 this 定义的属性和方法会分配在这个空间中。如果找到该属性和方法,就停止查找;如果没有找到,就继续在该实例的原型(__proto__)对象空间中去查找该属性和方法,实例的原型(__proto__)对象默认指向构造函数的原型对象(prototype)空间。
(2)实例正是借助自身的__ proto __对象属性 来查找原型对象(prototype)空间中的属性和方法,有点像儿子去和爸爸要他没有的东西一样。 - 5.2 增加或修改原型对象的属性或方法后, 所有的实例或叫对象立即可以访问的到 【但创建实例后再覆盖原型除外】
- 5.3 高频面试题:创建实例后再覆盖原型,实例对象无法访问到,为什么?
function QQUsers (QQNo_, QQAge_, QQMark_) {
this.QQNo = QQNo_;//QQ号
this.QQAge = QQAge_;//Q龄
this.QQMark = QQMark_;//QQ标签
}
// 初次定义的原型对象(假设为A)
QQUsers.prototype.commonfriends = ['骑驴看海', '大漠上的英雄', '坚实的果子', '小草']
QQUsers.prototype.show = function () {
console.log(`QQ号:${this.QQNo},QQ龄:${this.QQAge},QQ标注:${this.QQMark}`)
console.log(`共同的好友是:${this.commonfriends}`);
}
// 修改前实例化
let QQZhangSan = new QQUsers("37834522", 15, "王阳明传人")
let QQLisi = new QQUsers("30424232", 10, "袁隆平的徒弟")
//QQUsers.prototype.commonfriends.push("大树");
console.log("1-QQZhangSan.commonfriends",QQZhangSan.commonfriends);
console.log("1-QQLisi.commonfriends", QQLisi.commonfriends);
// 修改原型对象(假设为B)
QQUsers.prototype = {
commonfriends: ["abc", "bcd", '骑驴看海']
}
// 修改后实例化
let QQWangWu = new QQUsers("30424232", 10, "袁隆平的徒弟")
console.log("QQUsers.prototype:", QQUsers.prototype)
console.log("2-QQZhangSan.commonfriends:", QQZhangSan.commonfriends)
console.log("2-QQLisi.commonfriends:", QQLisi.commonfriends)
console.log("1-QQWangWu.commonfriends", QQWangWu.commonfriends);
console.log("QQUsers.prototype.commonfriends:", QQUsers.prototype.commonfriends)
输出结果:
分析:
- 初次定义时,构造函数
QQUsers的原型对象(prototype)、实例对象QQZhangSan、QQLisi的原型对象(__proto__)都是指向的是同一个内存空间(假设名字是A),并且有commonfriends和show属性。
// A
prototype.commonfriends = ['骑驴看海', '大漠上的英雄', '坚实的果子', '小草']
prototype.show = function () {
console.log(`QQ号:${this.QQNo},QQ龄:${this.QQAge},QQ标注:${this.QQMark}`)
console.log(`共同的好友是:${this.commonfriends}`);
}
- 修改后,构造函数
QQUsers的原型对象(prototype)已经更改了(假设名字是B)
// B
QQUsers.prototype = {
commonfriends: ["abc", "bcd", '骑驴看海']
}
- 实例化
QQWangWu,它的原型对象(__proto__)此时指向的是B。
思考题
console.log(QQZhangSan.show())
console.log(QQZhangSan.__proto__.show())
console.log(QQWangWu.show())
console.log(QQWangWu.__proto__.show())
TS高级
环境搭建
配置TS+TS热启动
- 初始化
npm init --yes出现 package.json - 安装 typescript
//全局
npm i typescript -g
//本地
npm i typescript -D
npm i typescript -D 是 npm install typescript --save-dev的缩写
3. tsc --init 生成tsconfig.json文件
4. 修改 tsconfig.json 中的配置
“outDir: "./dist" --outDir是ts编译后生成js文件保存的目录
"rootDir": "./src", --rootDir是自己编写的ts源文件所在的目录
- 编译src目录以及子目录下的ts文件:在src当前目录下输入
tsc命令即可。
会把src目录以及子目录下的ts文件全部编译成js文件,并全部输出到dist目录中 - 安装 ts-node
//全局
npm i ts-node -g
//本地
npm i ts-node -D
ts-node让node能直接运行ts代码,无需使用tsc将ts代码编译成js代码。【ts-node包装了node,它可以直接的运行ts代码】
7. 安装nodemon工具 【自动检测工具】
//全局
npm install -g nodemon
//本地
npm i nodemon -D
nodemon作用:nodemon可以自动检测到目录中的文件更改时通过重新启动应用程序来调试基于node.js的应用程序。
// src/index.ts
console.log("abc")
let count: number = 12
console.log("count:", count)
console.log("count2:", count)
- 在package.json中配置自动检测,自动重启应用程序
"scripts": {
"start": "nodemon --watch src/ -e ts --exec ts-node ./src/index.ts"
},
nodemon --watch src/表示检测目录是package.json同级目录src-e ts表示nodemon 命令准备将要监听的是ts后缀的文件--exec ts-node ./src/index.ts表示检测到src目录下有任何变化 都要重新执行index.ts文件- 此时可启动项目 npm run start 控制台会有打印输出
Parcel打包支持浏览器运行TS文件
- 安装Parcel打包工具:
npm install parcel-bundler --save-dev - 增加index.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="src/index.ts"></script>
</head>
<body>
</body>
</html>
- 在package.json中给npm添加启动项,支持启动parcel工具包
"scripts": {
"start-parcel": "parcel ./index.html"
},
- 启动parcel工具包
npm run start-parcel
TS类
什么是类
定义:类就是拥有相同属性和方法的一系列对象的集合,类是一个摸具,是从这该类包含的所有具体对象中抽象出来的一个概念,类定义了它所包含的全体对象的静态特征(属性)和动态特征(方法)。
TS哪些技能基于类
TypeScript 类是 OOP【面型对象编程】 的技术基石,包括类、属性封装、继承、多态、抽象、泛型。紧密关联的技术包括方法重写,方法重载,构造器,构造器重载,类型守卫,自定义类型守卫,静态方法、属性,关联引用属性,多种设计模式等。
class Person {
public name: string
public age: number
public phone: string
// constructor(){}//无参构造器
constructor(name_: string, age_: number, phone_: string) {
//有参构造器
this.name = name_
this.age = age_
this.phone = phone_
}
doStep() {}
}
// 给实例赋值
let zhangSanPerson = new Person("zhangSan", 23, "134123123");
在给实例赋值的过程中经过了三步:
- 在堆中为类的实例分配一个空间;
- 调用对应的构造器,并且把构造器中的各个参数值赋值给实例属性。如果是无参数,自动匹配无参数的构造器;
- 把实例赋值给实例变量。
注意:类的实例的方法在原型对象上
原因:在原型对象上方法就可以共用
类的引用属性
定义
如果类中的属性的类型是引用类型,那么这个属性就是引用属性。
引用属性的数据类型一般有数组、函数、类、对象类型、对象数组类型、集合类(Set,Map,自定义 集合类)。
真实应用场景
- ES6的Set方法:数组的二次包装,包含一个数组的引用属性
- Promise:底层类中就采用了函数类型的引用属性
- 二次封装应用场景:Set 集合虽好,但不能使用 get(index) 直接取值,这也造成了取值不方便,如果我们自己动手封装一个包含了 add、get、remove、delete、query 的集合类【ArrayList】,这时也需要借助数组引用属性 。
TS4 类的属性新式赋值
一般,正常的赋值过程
class Person {
public name: string
public age: number
constructor(name_: string, age_: number) {
this.name = name_
this.age = age_
}
}
在ts中,有一种方法,给构造器的参数加上public,这个参数就变成了一个属性:
class Person {
constructor(public name: string, public age: number) {}
}
这种写法,主要包含了两个步骤:
- 定义了一个属性
- 默认构造函数会给这个属性赋值
小技巧
如果类的某个属性只声明,而没有赋值,为了避免报错的方案:
TS4之前的解决办法是写一个包含有undefined的联合类型。比如:
public age: number |undefinedTS4的解决方法是:
public age!:number
TS静态属性的九大规则
TS类有中有静态属性和静态方法,他们统称为静态成员。
静态成员=静态属性+静态方法
- 外部调用TS类的静态成员: 类名直接调用静态成员。即:
类名.静态属性、类名.静态方法 - TS类中的静态方法调用其他静态成员:
使用 this 来获取静态成员 - 静态方法不可以访问类中原型对象上的方法和属性
- 实例变量不可以访问静态成员
- 一个静态方法改变了某个静态属性,其他静态方法或类外部任何地方访问这个属性都会发生改变
- 静态成员保存在内存哪里?何时分配的内存空间呢?
任何一个 TS 类中的静态成员存储在
内存的静态区,运行一个 TS 类,TS首先会为静态成员开辟内存空间,静态成员的内存空间分配的时间要早于实例空间的分配,也就是任何一个实例创建之前 TS 就已经为静态成员分配好了空间。但一个静态方法或静态属性只会分配一个空间,只要当前服务器不重启或控制台程序还没有结束之前【如果是开发期间临时测试,一般用控制台】,那么静态方法或者是静态属性就一直存在内存空间,无论调用多少次这个静态方法或静态属性,都是调用的同一块空间。总结静态方法,两点:
总结1: 无论你是否创建实例,创建多少个实例,是否调用该静态方法或静态属性,TS都会为这个静态方法或静态属性分配内存空间,注意:静态成员和实例无关。
总结2: 一旦为静态方法或静态属性分配好空间,就一直保存到内存中,直到服务器重启或者控制台程序执行结束才被释放。
彩蛋: new 一个 TS 类的方法可以吗?能在TS 类外部使用 prototype为TS类增加方法或属性吗?
虽然在 JS 中可以 new 一个类【构造函数】内部定义的对象方法或静态方法,但TS已经屏蔽了去new 一个类中的方法【 JS 可以,会当成一个构造函数】,TS 类可以访问 prototype 原型对象属性,但无法在 prototype 原型对象属性增加新的方法或属性,这么做,就是让我们只能在类的内部定义方法,防止回到 ES5 从前非面向类和对象的而写法。【但是可以覆盖类上已经存在的方法】
- 静态方法或属性和原型对象空间上的方法或属性有何区别?
原型对象空间上的方法和属性用来提供给该类的所有实例变量共用的方法或属性,没有实例和实例变量,原型上的属性和方法就没有了用武之地,而静态方法或静态属性属于类,可以通过类来直接访问。任何一个实例创建之前 TS 就已经为静态成员分配好了空间。但一个静态方法或静态属性只会分配一个空间,而每一个对象都有自己独立的空间。
- 静态方法是否可以接受一个实例变量来作为方法的参数?
可以,静态方法内部不能通过this来访问实例属性和方法,但可以通过调用静态方法时把实例变量传递给静态方法来使用。比如:我们把 js 的 Object 构造函数想象成一个 TS 类【实际 TS 类编译后的 JS 文件中就变成了一个构造函数】。Object 类就拥有大量的静态方法,例如:apply,call,bind,keys等,现在我们来关注静态方法是否可以接受对象变量作为方法的参数,我们以Object.keys方法为例 【Object类的keys方法用来获取给定对象的自身可枚举属性组成的数组】。
// 我们现在把 Object 构造函数看成一个 Object 类,创建 Object 类的对象。
let obj = new Object({ username: "wangwu", age: 23 })//1
let obj2 = { username: "wangwu", age: 23 }// 2是1的简写
// 把 obj 实例变量传递给 keys静态方法,obj实例变量作为 keys 静态方法的参数
Object.keys(obj2)
- 何时应该把一个方法定义成静态方法或属性定义为静态属性呢?【应用】
- 应用1:单件设计模式就是静态方法和静态属性很好的应用场景之一。 当外部不能创建实例,就只能借助类内部的静态方法来获取类的对象;这时肯定不能把这个方法定义成原型对象属性上的方法,只能定义为类的静态方法,因为如果定义成原型对象属性的方法,就会导致外部无法被访问,因为外部根本不能创建对象,也就无法访问原型对象属性上的方法。而静态方法要访问的属性就只能是静态属性了,这也是静态属性的应用时机。
- 应用2: 当类中某个方法没有任何必要使用任何实例属性时,而且使用了实例属性反而让这个方法的逻辑不正确,那既如此,就应该禁止这个方法访问任何实例属性和其他的实例方法,这时就应该把这个方法定义为静态方法。
- 应用3:当一个类中某个方法只有一个或者 1-2个 实例属性,而且更重要的是,你创建这个类的实例毫无意义,我们只需要使用这个类的一个或者多方法就可以了,那么这个方法就应该定义为静态方法。常见的工具类中的方法通常都应该定义为静态方法。
可以结合单件设计模式理解
TS 函数重载与方法重载
TS函数重载的重要性
假设有这样一个真实案例:
有一个获取微信消息发送接口消息查找函数,根据传入的参数从数组中查找数据,如果入参为数字, 就认为消息 id,然后从从后端数据源中找对应 id 的数据并返回,否则当成类型,返回这一类型的全部消息。
type MessageType = "image" | "audio" | string //微信消息类型
type Message = {
id: number
type: MessageType
sendMessage: string
}
let messages: Message[] = [
{
id: 1,
type: "image",
sendMessage: "你好啊,今晚咱们一起去三里屯吧",
},
{
id: 2,
type: "audio",
sendMessage: "朝辞白帝彩云间,千里江陵一日还",
},
{
id: 3,
type: "audio",
sendMessage: "你好!张无忌",
},
{
id: 4,
type: "image",
sendMessage: "刘老根苦练舞台绝技!",
},
{
id: 5,
type: "image",
sendMessage: "今晚王牌对王牌节目咋样?",
},
]
根据需求,可以编写这样子的一个函数:
function getMessage(value: number | MessageType): Message | undefined | Array<Message> {
if (typeof value === "number") {
return messages.find((msg) => return value === msg.id)
} else {
return messages.filter((msg) => value === msg.type)
}
}
let audioArr = getMessage("audio");
let msg = getMessage(1)
console.log(audioArr)
console.log(msg);
结果也验证了没有问题:
但是如果,我们想获取某一条消息的某一项时,会报错:
根据错误提示,可以知道,是返回结果的类型不确定性导致的。在函数编写时,定义的返回类型是一个联合类型
Message | undefined | Array<Message>,所以在获取某条消息下的某个属性时,要先告诉TS,具体返回的是什么类型:
// 写明返回的是Message类型就不会报错
let msg = (<Message>getMessage(1)).sendMessage
从这里可以看出:这里的方法结构不明细,分工也不明确。而函数重载,恰好可以解决这个问题。 函数重载的三个优势:
- 结构分明:让 代码可读性,可维护性提升许多。
- 各司其职,自动提示方法和属性:每个重载签名函数完成各自功能,输出取值时不用强制转换就能出现自动提示,从而提高开发效率。
- 更利于功能扩展
TS函数重载的定义与规则
TS 的函数重载比较特殊,学习函数重载,先要了解什么是函数签名。
1、函数签名 [ function signature ]:函数签名=函数名称+函数参数+函数参数类型+返回值类型。在 TS 函数重载中,包含了实现签名和重载签名,实现签名和重载签名都是一种函数签名。
在 TS 函数重载中,实现签名会包含函数体,而重载签名没有函数体。
2、不完整模糊的 TS 函数重载定义:一组具有相同名字、不同参数列表的和返回值无关的函数。
3、完整的函数重载定义:包含了以下规则的一组函数就是TS函数重载。
- 规则1: 由一个实现签名+ 一个或多个重载签名合成。
- 规则2:: 外部调用重载定义的函数时,只能调用重载签名,不能调用实现签名,这看似矛盾的规则,其实 是TS 的规定:实现签名下的函数体是给重载签名编写的,实现签名只是在定义时起到了统领所有重载签名的作用,在执行调用时就看不到实现签名了。(函数在编译阶段只会调用重载签名,运行阶段才会调用实现签名。也就是说,在vscode编译器中,当按住ctrl,点击函数名字时,只会跳到重载签名的位置,而不是实现签名的位置)
- 规则3: 调用重载签名函数时,会根据传递的参数来判断你调用的是哪一个函数
- 规则4: 只有一个函数体。只有实现签名配备了函数体,所有的重载签名都只有签名,没有配备函数体。
- 规则5: 关于参数类型规则完整总结如下: 实现签名参数个数可以少于重载签名的参数个数,但实现签名如果准备包含重载签名的某个位置的参数 ,那实现签名就必须兼容所有重载签名该位置的参数类型【联合类型或 any 或 unknown 类型的一种】。
- 规则6: 关于重载签名和实现签名的返回值类型规则完整总结如下:
必须给重载签名提供返回值类型,TS 无法默认推导。
提供给重载签名的返回值类型不一定为其执行时的真实返回值类型。可以为重载签名提供真实返回值类型,也可以提供 void 或 unknown 或 any 类型,如果重载签名的返回值类型是 void 或 unknown 或 any 类型,那么将由实现签名来决定重载签名执行时的真实返回值类型。 当然为了调用时能有自动提示+可读性更好+避免可能出现了类型强制转换,强烈建议为重载签名提供真实返回值类型。
不管重载签名返回值类型是何种类型【包括泛型】,实现签名都可以返回 any 类型 或 unknown类型,当然一般我们两者都不选择,让 TS 默认为实现签名自动推导返回值类型。
有了以上的基础,就可以使用函数重载优化代码
// 重载签名
function getMessage(id: number): Message;
// 重载签名
function getMessage(type: MessageType): Array<Message>
// 实现签名
function getMessage(value: number | MessageType): Message | undefined | Array<Message> {
if (typeof value === "number") {
return messages.find((msg) => value === msg.id)
} else {
return messages.filter((msg) => value === msg.type)
}
}
// 函数在编译阶段只会调用重载签名,运行阶段才会调用实现签名
// 也就是说,当按住ctrl,点击函数名字时,只会跳到重载签名的位置,而不是实现签名的位置
let audioArr = getMessage("audio");
let msg = getMessage(1)
此时,audioArr和msg两个变量都有清晰的类型。
TS 方法重载
方法签名: 和函数签名一样,方法签名 = 方法名称 + 方法参数 + 方法参数类型 + 返回值类型四者合成。
/**
* 1.对现有的数组进行封装,让数组增删改变得更加好用
* 2.提供get方法 remove方法 显示方法【add方法】
* 其中需求中的remove方法有两个,我们用方法重载来实现
*
*/
class ArrayList {
//第一步:定义一个引用属性【数组】
constructor(public element: Array<object>) {}
// 第二步:根据索引来查询数组中指定元素
get(index: number) {
return this.element[index]
}
// 第三步: 显示方法
show() {
this.element.forEach((ele) => {
console.log(ele)
})
}
// 重载签名
remove(value: number): number
remove(value: object): object
remove(value: any): any {
this.element = this.element.filter((ele, index) => {
//如果是根据数字【元素索引】去删除元素,remove方法返回的是一个数字
if (typeof value === "number") {
return value !== index
} else {
// 如果是根据对象去删除元素,remove方法返回的是一个对象
return value !== ele
}
})
return value
}
}
let stuOne = { name: "wnagwu", age: 23 }
let stuTwo = { name: "lisi", age: 39 }
let stuThree = { name: "liuqi", age: 31 }
let arrayList = new ArrayList([stuOne, stuTwo, stuThree]);
console.log(arrayList, "arrayList")
arrayList.remove(0)
console.log(arrayList)
TS类的单件设计模式
定义
简明定义1:一个类对外有且仅有一个实例,这种编码方案就是单件设计模式。
完整定义1: 如果某个类对外始终只提供一个实例,并且在该类的内部提供了一个外部访问该实例的方法或该实例属性,那么这种编写代码方案就是单件设计模式。
完整定义2: 如果一个类的任何外部通过访问类提供的某个方法或某个属性始终只能获取该类一个实例,但如果该类提供了多个外部可以访问的方法或属性,那么外部就能访问到该类的多个不同的实例,但从实际开发来看,绝大多数情况的应用场景,我们对外都只提供一个唯一的可以访问的方法或属性,这样就保证了实例为单个,类的这种编写代码的方案就是单件设计模式。
真实应用场景
- 比如 Vuex,React-Redux 中的全局状态管理容器 store 对象在整个项目被设计成唯一的对象【实例】,把 store 对象所在 的类设计成单件设计模式将是最好的设计方案
- 一般前端项目需要进行客户端本地数据存储时,都会考虑使用 localStorage,localStorage只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份 localStorage 数据。那封装 localStorage设计成一个单件设计模式类就再合适过了。
class MyLocalStorage {
// 静态属性和实例属性是类中两大成员
static localstorage: MyLocalStorage//引用静态属性
private constructor() {
console.log("这是TS的单件设计模式的静态方法的构造器");
}
// 提供一个外部访问的方法,
// 通过这个方法用来提供外部得到一个对象的方法
// 1. 带static关键字的方法就是一个静态方法
// 2. 静态方法和实例无关,外部的实例变量不能调用静态方法和静态属性,
// 3. 外部可以通过类名来调用
// 静态方法不可以访问实例属性或实例方法
public static getInstance() {
if (!this.localstorage) {//如果静态对象属性指向实例
console.log("我是一个undefined的静态属性,用来指向一个实例空间的静态属性")
this.localstorage = new MyLocalStorage()
}
return this.localstorage
}
// 保存key-value
public setItem(key: string, value: any) {
localStorage.setItem(key, JSON.stringify(value))
}
public getItem(key: string) {
let value = localStorage.getItem(key)
return value != null ? JSON.parse(value) : null;
}
}
设计模式的两种实现方式
一、饿汉式单件设计模式
定义:无论你是否用到了实例,一开始就建立这个唯一的实例。
实现步骤:
第一步:把构造器设置为私有的,不允许外部来创建类的实例。
第二步: 建立一个静态引用属性,同时把这个静态引用属性直接指向一个对象【 new MyLocalStorage()】。
第三步:外部调用第二步提供的静态方法来获取一个对象
class MyLocalStorage {
// 实例属性【实例的基本类型属性和实例的引用属性】
// 静态属性【静态的基本类型属性和静态的引用属性】
static localstorage: MyLocalStorage = new MyLocalStorage();
static count: number = 3
private constructor() {
console.log("这是TS的单件设计模式的静态方法的构造器");
}
public setItem(key: string, value: any) {
localStorage.setItem(key, JSON.stringify(value))
}
public getItem(key: string) {
let value = localStorage.getItem(key)
return value != null ? JSON.parse(value) : null;
}
}
二、懒汉式单件设计模式
1、定义:需要使用对象时才创建对象,它是按需创建。
实现步骤:
第一步:把构造器设置为私有的,不允许外部来创建类的实例
第二步: 至少应该提供一个外部访问的方法或属性,外部可以通过这个方法或属性来得到一个实例。所以应该把这个方法设置为静态方法
第三步:外部调用第二步提供的静态方法来获取一个实例
class MyLocalStorage {
// 静态属性和实例属性是类中两大成员
static localstorage: MyLocalStorage//引用静态属性
private constructor() {
console.log("这是TS的单件设计模式的静态方法的构造器");
}
// 提供一个外部访问的方法,
// 通过这个方法用来提供外部得到一个对象的方法
// 1. 带static关键字的方法就是一个静态方法
// 2. 静态方法和实例无关,外部的实例变量不能调用静态方法和静态属性,
// 3. 外部可以通过类名来调用
// 静态方法不可以访问实例属性或实例方法
public static getInstance() {
if (!this.localstorage) {//如果静态对象属性指向实例
console.log("我是一个undefined的静态属性,用来指向一个实例空间的静态属性")
this.localstorage = new MyLocalStorage()
}
return this.localstorage
}
// 保存key-value
public setItem(key: string, value: any) {
localStorage.setItem(key, JSON.stringify(value))
}
public getItem(key: string) {
let value = localStorage.getItem(key)
return value != null ? JSON.parse(value) : null;
}
}
继承(JS & TS)
在讲TS继承之前,先来讲讲JS继承的几种方式。
JS继承-原型链继承
1、原理
子类的原型对象属性是父类的一个实例。Son.prototype = new Parent()
function Parent(name,age){
this.name=name
this.age=age
}
function Son(favor,sex){
this.favor=favor // 兴趣爱好
this.sex=sex
}
Son.prototype=new Parent("好好的",23) // 98
let sonObj=new Son("篮球","男")
console.log('sonObj:', sonObj)
原型链继承实现的本质是改变子类构造函数的原型对象变量(Son.prototype)的指向。 即:Son.prototype = new Parent( )
那么 Son.prototype对象变量可以访问 Parent 对象空间的属性和方法。所以顺着 __proto__属性 ,Son类也可以访问 Parent 类 的原型对象空间中的所有属性和方法。
原型链继承查找属性和方法的规则: 子对象首先在自己的对象空间中查找要访问的属性或方法,如果找到,就输出,如果没有找到,就沿着子对象中的__proto__属性指向的原型对象空间中去查找有没有这个属性或方法,如果找到,就输出,如果没有找到,继续沿着原型对象空间中的__proto__查找上一级原型对象空间中的属性或方法,直到找到Object.prototype原型对象属性指向的原型对象空间为止,如果再找不到,就输出null。
2、原型链继承容易遗忘的constructor属性
原型链继承而来的函数原型对象prototype容易遗忘constructor属性。
根据上述代码,打印console.log('sonObj:', sonObj)的结果如下:
可以看出,Son.prototype对象缺少属性constructor,因此需要加上:
Son.prototype.constructor = Son
...
Son.prototype=new Parent("好好的",23) // 98
// 遗忘的constructor
Son.prototype.constructor = Son
let sonObj=new Son("篮球","男")
3、原型链继承的常见疑问
Son.prototype= Parent.prototype 这样作为原型链继承的模式和 Son.prototype=new Parent (...) 又有什么区别呢?
function Parent(name,age){
this.name=name
this.age=age
}
function Son(favor,sex){
this.favor=favor // 兴趣爱好
this.sex=sex
}
Son.prototype = Parent.prototype;
Son.prototype.constructor = Son
let parent = new Parent("王五", 23);
console.log("parent:", parent)
- Son.prototype和Parent.prototype会指向同一个原型对象
Parent.prototype - Parent.prototype的
constructor属性,会指向Son构造函数,而不是Parent构造函数,这违背了Parent原型对象空间中的constructor属性必须指向Parent构造函数对象空间的原则。
4、原型链继承的不足
不能通过子类构造函数向父类构造函数传递参数。
比如,在上述案例中,在let sonObj=new Son("篮球","男")代码中,想要直接把Parent构造函数需要的参数也传入是没办法做到的。
JS继承-借用构造函数继承
1、原理
子类的内部借助 apply() 和 call () 方法调用并传递参数给父类,在父类构造函数中为当前的子类对象变量增加属性。
apply():第一个参数是调用对象,第二个参数是传递的数组的形式的参数。 call():第一个参数是调用对象,后面的一系列参数是传递的多个参数。
function Parent(name, age) {
this.name = name
this.age = age
console.log("this:", this)
console.log("this.name:", this.name)
}
Parent.prototype.friends = ["xiaozhang", "xiaoli"]
Parent.prototype.eat = function () {
console.log(this.name + " 吃饭");
}
function Son(name, age, favor, sex) {
this.favor = favor // 兴趣爱好
this.sex = sex
Parent.call(this, name, age)// TS继承中使用super
}
let sonobj2 = new Son("lisi", 34, "打篮球", "男");
console.log("sonobj2:", sonobj2)
console.log("sonobj2.friends:", sonobj2.friends);//undefined
2、不足
借用构造函数实现了子类构造函数向父类构造函数传递参数,但没有继承父类原型的属性和方法,无法访问父类原型上的属性和方法。
JS继承-组合继承
组合继承 = 借用构造函数继承+原型链继承
// 组合继承
function Parent(name, age) {
this.name = name
this.age = age
console.log("this.name:", this.name)
}
Parent.prototype.friends = ["xiaozhang", "xiaoli"]
Parent.prototype.eat = function () {
console.log(this.name + " 吃饭")
}
function Son(name, age, favor, sex) {
this.favor = favor // 兴趣爱好
this.sex = sex
Parent.call(this, name, age) // TS继承中使用super
}
Son.prototype = new Parent("temp", 3)
Son.prototype.constructor = Son
let sonobj2 = new Son("lisi", 34, "打篮球", "男")
console.log('sonobj2:', sonobj2)
1、优势
- 具备借用构造函数的优点:子类可以向父类传递参数
- 具备原型链继承的优点:继承父类原型的属性和方法
2、不足
调用了两次父类构造函数。一次是在创建子类型的时候,一次是在子类型的构造函数内部
new 父类() 构造函数带来问题:
- 进入
父类构造函数为属性赋值,分配内存空间,浪费内存; - 赋值导致效率下降一些,关键是
new 父类()赋的值无意义,出现代码冗余
JS继承-寄生组合式继承
寄生组合继承模式=借用构造函数继承+寄生继承
定义
子类的原型对象(prototype) 不再指向父类new 出来的实例空间,而用父构造函数的原型对象属性“克隆”了一个对象。再让子类的原型对象指向这个新实例,很好的避免了借用构造函数+原型链继承调用了两次父类构造函数为属性赋值的不足。
function People(name, sex, phone) {
this.name = name;
this.sex = sex;
this.phone = phone;
}
People.prototype.doEat = function () {
console.log(this.name + "吃饭...")
}
function ChinesePeople(name, sex, phone, national) {
People.call(this, name, sex, phone)
this.national = national
}
// 第一步: 创建一个寄生构造函数
function Middle() {
this.count = 23
}
// 组合继承
// ChinesePeople.prototype = new People("temp", 3)
// ChinesePeople.prototype.constructor = ChinesePeople
// 第二步:创建父类原型对象的副本
Middle.prototype = People.prototype
// 第三步:创建一个寄生构造函数的实例
let middle = new Middle();
// 第四步:子类的原型对象属性指向寄生构造函数的实例
ChinesePeople.prototype = middle
// 第五步:子类原型对象的constructor属性指向子类构造函数
ChinesePeople.prototype.constructor = ChinesePeople
let chinesePeopleTwo = new ChinesePeople("王海", "男", "1111", "汉族");
let chinesePeopleOne = new ChinesePeople("约克夏", "女", "1111", "傣族");
console.log("chinesePeopleOne:", chinesePeopleOne);
console.log("chinesePeopleTwo:", chinesePeopleTwo);
通过借用Middle构造函数,子类能继承父类的方法与属性,实现过程分析:
- 1、
ChinesePeople.prototype = middle,说明ChinesePeople.prototype对象是构造函数Middle的一个实例 - 2、每一个实例对象都有一个
__proto__属性,指向构造函数的原型对象,因此ChinesePeople.prototype.__proto__ = Middle.prototype - 3、在第二步中:
Middle.prototype = People.prototype,说明Middle.prototype对象和People.prototype对象指向同一个对象空间。 - 4、结合2跟3,可推导出:
ChinesePeople.prototype.__proto__ == People.prototyp是成立的,也就是说,ChinesePeople.prototype(子类)对象是People.prototype(父类)对象的实例。 - 5、因为
chinesePeopleOne是构造函数ChinesePeople的实例,所以:chinesePeopleOne.__proto__指向ChinesePeople.prototype - 6、结合4跟5,
chinesePeopleOne.__proto__.__proto__ = People.prototype - 7、因此实现了子类继承父类的方法与属性
优化版本一:
// 寄生组合继承 = 借用构造函数继承+寄生继承
function People(name, sex, phone) {
this.name = name;
this.sex = sex;
this.phone = phone;
}
People.prototype.doEat = function () {
console.log(this.name + "吃饭...")
}
function ChinesePeople(name, sex, phone, national) {
People.call(this, name, sex, phone)
this.national = national
}
// 寄生继承
function _extend(parent, son) {
// 第一步:创建一个寄生构造函数
function Middle() {
// 第五步:子类原型对象的constructor属性指向子类构造函数
this.constructor = son
}
// 第二步:寄生构造函数的原型对象指向父类的原型对象
Middle.prototype = parent.prototype
// 第三步:创建一个寄生构造函数的实例
return new Middle()
}
let middle = _extend(People, ChinesePeople)
// 第四步:子类的原型对象属性指向寄生构造函数的实例
ChinesePeople.prototype = middle
let chinesePeopleTwo = new ChinesePeople("王海", "男", "1111", "汉族");
let chinesePeopleOne = new ChinesePeople("约克夏", "女", "1111", "傣族");
console.log("chinesePeopleOne:", chinesePeopleOne);
console.log("chinesePeopleTwo:", chinesePeopleTwo);
优化版本二:(使用Object.create 不通用)
Object.create() 方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)。
结合Object.create()的定义可知,Object.create(parent.prototype)相当于实现了这几步:
- 默认创建了一个寄生构造函数
- 该寄生构造函数的原型对象是
parent.prototype - 返回了一个新的对象,该新对象是该寄生构造函数的实例
function _extend(parent, son) {
// 第一步:创建一个寄生构造函数
function Middle() {}
// 第二步:寄生构造函数的原型对象指向父类的原型对象
Middle.prototype = parent.prototype
// 第三步:创建一个寄生构造函数的实例
return new Middle()
}
但是,Object.create()创建的新对象的原型对象,缺失了constructor属性。因此,使用Object.create()方法的寄生组合继承的完整代码如下:
// 寄生组合继承 = 借用构造函数继承+寄生继承
function People(name, sex, phone) {//父类 【父构造函数】
this.name = name;
this.sex = sex;
this.phone = phone;
}
People.prototype.doEat = function () {
console.log(this.name + "吃饭...")
}
function ChinesePeople(name, sex, phone, national) {//ChinesePeople子类【子构造函数】
People.call(this, name, sex, phone)
this.national = national;//民族
}
function _extends(parent) {//继承
// 不通用 增加额外属性不太灵活
let middle = Object.create(parent.prototype, {
count: {
writable: true,
value: 23
}
})
return middle;
}
const middle = _extends(People);
ChinesePeople.prototype = middle
//需要额外增加子构造函数指向的原型对象空间中的constructor属性
ChinesePeople.prototype.constructor = ChinesePeople
let chinesePeopleTwo = new ChinesePeople("王海", "男", "1111", "汉族");
let chinesePeopleOne = new ChinesePeople("约克夏", "女", "1111", "傣族");
console.log("chinesePeopleOne:", chinesePeopleOne);
console.log("chinesePeopleTwo:", chinesePeopleTwo);
优势
寄生组合继承既沿袭了借用构造函数+原型链继承两个优势,而且解决了借用构造函数+原型链继承调用了两次父类构造函数为属性赋值的不足。寄生组合继承模式保留了借用构造函数继承,寄生组合继承模式使用寄生继承代替了原型链继承。
TS继承
TS继承使用super,super只能出现在子类中。
spuer用法
super的两种用法:
- 子类传递给父类构造函数的参数:在子类的构造函数中使用
super就表示用来调用父类构造函数(传递给父类构造函数的参数)。super 编译成 JS 源码后 可以看到:采用 JS 原型中的借用构造函数来实现 - 在子类
重写的方法中调用父类同名方法:super.重写的方法
class Vechile {
static count: number = 3
public brand: string // 品牌
public vechileNo: string // 车牌号
public days: number // 租赁天数
public total: number = 0 // 支付的租赁总费用
public deposit: number // 押金
constructor( brand_: string, vechileNo_: string, days_: number, deposit_: number) {
this.brand = brand_
this.vechileNo = vechileNo_
this.days = days_
this.deposit = deposit_
console.log("constructor Vechile=>this.brand:", this.brand)
}
public calculateRent() {
console.log("calculateRent来自Vechile=>this.brand:", this.brand)
console.log(this.brand + " 车牌号为:" + this.vechileNo + "开始被租")
return 0
}
}
class Car extends Vechile {
// public brand: string = "nobrand"
public type: string //车的型号
constructor(
brand_: string,
vechileNo_: string,
days_: number,
deposit_: number,
type_: string
) {
// JS源码是 Vechile.call(this,brand_, vechileNo_, days_, total_, deposit_)
super(brand_, vechileNo_, days_, deposit_)
this.type = type_
}
// 根据车的型号来获取租用一天该型号车的租金
public getPriceByType() {
let rentMoneyByDay: number = 0 //每天的租金
if (this.type === "普拉多巡洋舰") {
rentMoneyByDay = 800
} else if (this.type === "凯美瑞旗舰版") {
rentMoneyByDay = 400
} else if (this.type === "威驰智行版") {
rentMoneyByDay = 200
}
return rentMoneyByDay
}
public calculateRent() {
// 方法重写 [override]
// 相当于是寄生组合继承模式 middle()
super.calculateRent() // JS源码: Vechile.prototype.calculateRent.call(this)
console.log("Car:", Car.count)
console.log("this.brand:", this.brand)
return this.days * this.getPriceByType()
}
}
let car = new Car("普拉多", "京3A556", 3, 100000, "凯美瑞旗舰版")
console.log(car.calculateRent())
错误用法:当子类和父类有同名属性时,可以在子类中用 super 来获取父类同名属性吗?【不能】【一般要避免在子类,父类属性名同名】
spuer+方法重写
1、条件:一定发生在继承的子类中
2、位置: 子类中重写父类的方法
3、应用场景:当父类中方法的实现不能满足子类功能需要或不能完全满足子类功能需要时,就需要在子类中进行重写
4、方法重写给继承带来的好处: 让所有的子类共用父类中方法已经实现了一部分功能的代码,使得父类方法在各个子类中得到了复用。
5、定义规则:
- 和父类方法
同名 参数和父类相同,如果是引用类型的参数,需要依据具体类型来定义。- 父类方法的访问范围
(访问修饰符)必须小于子类中方法重写的访问范围(访问修饰符)。同时父类方法不能是private。
super继承编译的JS源码
1、setPrototypeOf 使用 和 Object.create区别
-
Object.setPrototypeOf(obj,prototype):接收两个参数:obj(要设置原型的对象)、prototype(该对象的新原型。就是把第二参数(原型对象)设置为第一个参数(新对象)的原型对象。 -
Object.create(proto, propertiesObject):用于创建一个新对象,使用第一个参数(proto)来作为新创建对象的原型。第二个参数是增加这个新对象的属性。
在讲这两个区别之前,先回忆之前的寄生组合继承
// 通用方法
// 寄生组合继承 = 借用构造函数继承+寄生继承
function People(name, sex, phone) {
this.name = name;
this.sex = sex;
this.phone = phone;
}
People.prototype.doEat = function () {
console.log(this.name + "吃饭...")
}
function ChinesePeople(name, sex, phone, national) {
People.call(this, name, sex, phone)
this.national = national
}
// 寄生继承
function _extend(parent, son) {
// 第一步:创建一个寄生构造函数
function Middle() {
// 第五步:子类原型对象的constructor属性指向子类构造函数
this.constructor = son
}
// 第二步:寄生构造函数的原型对象指向父类的原型对象
Middle.prototype = parent.prototype
// 第三步:创建一个寄生构造函数的实例
return new Middle()
}
let middle = _extend(People, ChinesePeople)
// 第四步:子类的原型对象属性指向寄生构造函数的实例
ChinesePeople.prototype = middle
let chinesePeopleTwo = new ChinesePeople("王海", "男", "1111", "汉族");
let chinesePeopleOne = new ChinesePeople("约克夏", "女", "1111", "傣族");
console.log("chinesePeopleOne:", chinesePeopleOne);
console.log("chinesePeopleTwo:", chinesePeopleTwo);
1、使用setPrototypeOf方法改造寄生组合继承:
// 寄生组合继承 = 借用构造函数继承+寄生继承
function People(name, sex, phone) {//父类 【父构造函数】
this.name = name;
this.sex = sex;
this.phone = phone;
}
People.prototype.doEat = function () {
console.log(this.name + "吃饭...")
}
function ChinesePeople(name, sex, phone, national) {//ChinesePeople子类【子构造函数】
People.call(this, name, sex, phone)
this.national = national;//民族
}
ChinesePeople.prototype.getHukou = function () {
console.log("Hukou");
}
// Object.setPrototypeOf
function _extends(son, parent) {//寄生继承
// 不通用 增加额外属性不太灵活
/**
*
* Object.setPrototypeOf
* 接收两个参数:obj(要设置原型的对象)、
* prototype(该对象的新原型。
* 就是把第二参数(原型对象)设置为第一个参数(新对象)的原型对象。
*
*/
return Object.setPrototypeOf(son.prototype, parent.prototype)
}
_extends(ChinesePeople, People);// 没有借用构造函数,保留了原来的原型对象空间
let chinesePeopleTwo = new ChinesePeople("王海", "男", "1111", "汉族");
let chinesePeopleOne = new ChinesePeople("约克夏", "女", "1111", "傣族");
console.log("chinesePeopleOne:", chinesePeopleOne);
console.log("chinesePeopleTwo:", chinesePeopleTwo);
// chinesePeopleTwo.__proto_.__proto__ = ChinesePeople.prototype.__proto__
console.log(chinesePeopleTwo.getHukou()) // 输出Hukou
/**
* console.log(chinesePeopleTwo.getHukou())分析:
*
* 1、chinesePeopleTwo调用getHukou()方法时,会从chinesePeopleTwo.__proto__查找
* 2、而chinesePeopleTwo.__proto__ = ChinesePeople.prototype
* 3、并且ChinesePeople.prototype刚好有getHukou方法
*
*/
代码分析:
- 1、根据方法
_extends(son, parent)函数和Object.setPrototypeOf()方法可知,被设置的对象是son.prototype,它的新原型是parent.prototype。也就是说,son.prototype是一个对象,,而所有的对象都是Object的一个实例,都有一个__proto__属性,并且这个属性指向parent.prototype,等价于son.prototype.__proto__ == parent.prototype; - 2、 结合
_extends(ChinesePeople, People)可知,ChinesePeople.prototype__proto__ == People.prototype成立 - 3、由2可知,没有改变
ChinesePeople的原型对象空间,而是改变的ChinesePeople.prototype这个对象的原型空间 - 因此,由
Object.setPrototypeOf()方法编写的寄生继承,子类的原型对象空间没有被改变,改变的是子类原型对象下的__proto__属性。所以,在寄生继承之前,子类上挂载的方法,实例化之后,仍然保留。
2、使用Object.create方法改造寄生组合继承:
// 寄生组合继承 = 借用构造函数继承+寄生继承
function People(name, sex, phone) {//父类 【父构造函数】
this.name = name;
this.sex = sex;
this.phone = phone;
}
People.prototype.doEat = function () {
console.log(this.name + "吃饭...")
}
function ChinesePeople(name, sex, phone, national) {//ChinesePeople子类【子构造函数】
People.call(this, name, sex, phone)
this.national = national;//民族
}
ChinesePeople.prototype.getHukou = function () {
console.log("Hukou");
}
// Object.create改造寄生继承
function _extends(parent) {//继承
// 不通用 增加额外属性不太灵活
/**
* Object.create
* 用于创建一个新对象
* 使用第一个参数(proto)来作为新创建对象的原型。第二个参数是增加这个新对象的属性。
*/
let middle = Object.create(parent.prototype, {
count: {
writable: true,
value: 23
}
}
// middle.count = 23 middle__proto__ = parent.prototype
return middle;
}
const middle = _extends(People);
ChinesePeople.prototype = middle
//需要额外增加子构造函数指向的原型对象空间中的constructor属性
ChinesePeople.prototype.constructor = ChinesePeople
let chinesePeopleTwo = new ChinesePeople("王海", "男", "1111", "汉族");
let chinesePeopleOne = new ChinesePeople("约克夏", "女", "1111", "傣族");
console.log("chinesePeopleOne:", chinesePeopleOne);
console.log("chinesePeopleTwo:", chinesePeopleTwo);
console.log(chinesePeopleTwo.getHukou()) // 输出报错
/**
*
* 由上面分析可知:
* 1、chinesePeopleTwo.__proto__ == ChinesePeople.prototype
* 2、ChinesePeople.prototype = People.prototype
* 3、根据查找规则,chinesePeopleTwo调用getHukou,从ChinesePeople.prototype查找,
* 而此时,ChinesePeople.prototype已经改变了空间,改成了People.prototype。
* People.prototype无此方法,所以报错
*
*/
代码分析:
- 1、根据
Object.create(parent.prototype)方法,会返回一个对象,该对象的原型是parent.prototype,也就是说,该对象的__proto__属性指向的就是parent.prototype - 2、结合本案例,
_extends(People)方法返回的middle对象,因此middle.__proto__ = People.prototype - 3、结合2和根据代码段
ChinesePeople.prototype = middle可知,ChinesePeople.prototype = People.prototype,改变了ChinesePeople的原型对象空间 - 因此
Object.create()方法编写的寄生继承,子类的原型对象空间被改变,(son.prototype = parent.prototype)。也就是说,在寄生继承之前,子类上挂载的方法,实例化之后,不能保留。
2、ES6之前的静态成员继承实现
// 共同代码段
function RootClass() {}
RootClass.rootName = "root-name"
function People(name, sex, phone) {//父类 【父构造函数】
this.name = name;
this.sex = sex;
this.phone = phone;
}
People.__proto__ = RootClass
People.count = 300;// 静态属性 相当于TS类中static属性
People.commonDescribe = function () {// 静态方法 相当于TS继承中static方法
console.log("需要守法")
}
People.prototype.doEat = function () {
console.log(this.name + "吃饭...")
}
let people = new People("wangw", 23, "111");
console.log("people:", people)
// 函数以对象形式呈现时,上面自有属性就是静态属性,上面自有方法就是静态方法
for (let key in People) {//自有属性 还会查找__proto__指向的对象空间【这里是rootClass函数对象空间】中自有属性
console.log("key:", key);
// count rootName 是静态属性
// commonDescribe 是静态方法
}
说明:
for-in除了查找自有属性,还会查找__proto__指向的对象空间中自有属性- 函数以对象形式呈现时,上面自有属性就是
静态属性,上面自有方法就是静态方法
实现子类ChinesePeople继承父类People
function ChinesePeople (name, sex, phone, national) {//ChinesePeople子类
People.call(this, name, sex, phone)
this.national = national;//民族
}
1、第一种实现方式:
...
// 共同代码段
...
// 第一种实现
for (let key in People) {//自有属性 还会查找__proto__指向的对象空间【这里是rootClass函数对象空间】中自有属性
if (Object.prototype.hasOwnProperty.call(People, key)) {//要求返回true的条件是本构造函数的自有属性 不会查找__proto__指向的对象空间【这里是rootClass函数对象空间】中自有属性
//console.log("key:", key);//静态属性和静态方法
ChinesePeople[key] = People[key]//子类ChinesePeople继承父类People的静态属性和静态方法
}
}
let chinesePeopleTwo = new ChinesePeople("王海", "男", "1111", "汉族");
console.log("chinesePeopleTwo:", chinesePeopleTwo);
2、第二种方式
...
// 共同代码段
...
// 第二种实现方式
Object.keys(People).forEach((key) => {
ChinesePeople[key] = People[key]
})
let chinesePeopleTwo = new ChinesePeople("王海", "男", "1111", "汉族");
console.log("chinesePeopleTwo:", chinesePeopleTwo);
3、第三种方式
// 第三种实现方式
ChinesePeople.__proto__ = People
4、第四种方式
//ES6 第四种实现方式
Object.setPrototypeOf(ChinesePeople, People)
// 最终建立的关系是ChinesePeople.__proto__ = People
5、子类继承父类静态属性与方法的总结
module.exports = extendStatics = (function (Son, Parent) {
function getStaticExtendsWithForIn(Son, Parent) {
for (let key in Parent) {
if (Object.prototype.hasOwnProperty.call(Parent, key)) {
Son[key] = Parent[key]
}
}
}
function getStaticExtendsWithObjectkeys(Son, Parent) {
Object.keys(Parent).forEach((key) => {
Son[key] = Parent[key]
})
}
function getStaticExtendsWithProto(Son, Parent) {
Son.__proto__ = Parent;
}
// let MyextendStatics = function (Son, Parent) {
// MyextendStatics = Object.setPrototypeOf || getStaticExtendsWithForIn ||
// getStaticExtendsWithObjectkeys || getStaticExtendsWithProto
// return MyextendStatics(Son, Parent)
// }
// return function (Son, Parent) {
// let MyextendStatics = Object.setPrototypeOf || getStaticExtendsWithForIn ||
// getStaticExtendsWithObjectkeys || getStaticExtendsWithProto
// return MyextendStatics(Son, Parent)
// }
// return MyextendStatics
return function (Son, Parent) {
return (Object.setPrototypeOf || getStaticExtendsWithForIn ||
getStaticExtendsWithObjectkeys || getStaticExtendsWithProto)(Son, Parent)
}
}())
function People(name, sex, phone) {//父类 【父构造函数】
this.name = name;
this.sex = sex;
this.phone = phone;
}
People.count = 300;
function ChinesePeople(name, sex, phone, national) {//ChinesePeople子类【子构造函数】
People.call(this, name, sex, phone)
this.national = national;//民族
}
extendStatics(ChinesePeople, People)
console.log("ChinesePeople.count:", ChinesePeople.count)
3、TS继承的JS_extends 方法源码
// 手写优化后源码:
var _extends = (this.extends_) || (function () {
function getExendsStatics2(son, parent) {
son.__proto__ = parent
}
function getExendsStatics3(son, parent) {
for (let key in parent) {
if (Object.prototype.hasOwnProperty.call(parent, key)) {
son[key] = parent[key]
}
}
// 等价
//继承父类的静态属性和方法
// Object.keys(parent).forEach(function (son) {
// Child[key] = Father[key];
// });
//return Object.setPrototypeOf(son, parent)
}
var extendsStatics = function (son, parent) {
extendsStatics = Object.setPrototypeOf || getExendsStatics2 || getExendsStatics3
return extendsStatics(son, parent)
}
var _extends = function (son, parent) {
// 子类继承父类静态属性和静态方法
extendsStatics(son, parent)
// 寄生继承
function middle() {
this.constructor = son
}
if (parent) {
middle.prototype = parent.prototype
//son.prototype = parent === null ? Object.create(null) : new middle()
son.prototype = new Middle();
} else {
son.prototype = Object.create(null)
}
}
return _extends;
})()
TS类型守卫
TS类型断言
TS 类型断言定义: 把两种能有重叠关系的数据类型进行相互转换的一种 TS 语法,把其中的一种数据类型转换成另外一种数据类型。类型断言和类型转换产生的效果一样,但语法格式不同。
TS 类型断言语法格式: A 数据类型的变量 as B 数据类型。A 数据类型和 B 数据类型必须具有重叠关系。
1、种类
- 1.1 如果 A,B 如果是类并且有继承关系
extends 关系:无论 A,B 谁是父类或子类, A 的对象变量可以断言成 B 类型,B 的对象变量可以断言成A类型 。但注意一般在绝大多数场景下都是把父类的对象变量断言成子类。
class People {
public uerName!: string
public age!: number
public address!: string
public phone!: string
constructor() {}
eat() {}
step() {
console.log("People=>step")
}
}
class Stu extends People {
public uerName!: string
public age!: number
public address!: string
constructor(
uerName: string,
age: number,
address: string,
public phone: string
) {
super()
this.address = address
}
study() {}
}
let people = new People()
// let result = <Stu>people // 类型转换 父类可以断言成子类
let result = people as Stu // 类型断言 父类可以断言成子类
result.study
- 1.2 如果 A,B 如果是类,但没有继承关系
两个类中的任意一个类的所有的 public 实例属性【不包括静态属性】,加上所有的 public 实例方法和另一个类的所有 public 实例属性加上所有的 public 实例方法 完全相同或是另外一个类的子集,则这两个类可以相互断言,否则这两个类就不能相互断言。
-
1.3 如果 A 是类,B 是接口,并且 A 类实现了 B 接口【implements】 ,则 A 的对象变量可以断言成 B 接口类型,同样 B 接口类型的对象变量也可以断言成A类型 。
-
1.4 如果 A 是类,B 是接口,并且 A 类没有实现了 B 接口,则断言关系和第2项的规则完全相同。
-
1.5 如果 A 是类,B 是 type 定义的数据类型 ,并且有 A
类实现了 B type定义的数据类型【implements】,则 A 的对象变量可以断言成 B type 定义的对象数据类型,同样 B type 定义的对象数据类型的对象变量也可以断言成 A 类型 。
type People = {
username: string, age: number, address: string, phone: string
}
class Stu implements People {
public username!: string
public age!: number;
public address!: string
public phone!: string
constructor(username: string, age: number, address: string) {
// super(username, age);
this.address = address;
}
}
let people: People = { username: "wangwu", age: 23, address: "11", phone: "111" }
let result = people as Stu;//正确
let stu = new Stu("wangwu", 23, "北京")
stu as People;// 正确
- 1.6 如果 A 是类,B 是 type 定义的数据类型,并且 A 类没有实现 B type定义的数据类型,则断言关系和第2项的规则完全相同。
type People = {
username: string, age: number, address: string, phone: string
}
class Stu {
public username!: string
public age!: number;
public address!: string
constructor(username: string, age: number, address: string) {
this.address = address;
}
}
let people: People = { username: "wangwu", age: 23, address: "11", phone: "111" }
let result = people as Stu;//正确
let stu = new Stu("wangwu", 23, "北京")
stu as People;// 正确
-
1.7 如果 A 是一个函数上参数变量的联合类型,例如 string |number,那么在函数内部可以断言成 string 或number 类型。
-
1.8 多个类组成的联合类型,可以断言成其中任意一种数据类型。
例如:let vechile: Car | Bus | Trunck。 vechile 可以断言成其中任意一种数据类型。 例如 vechile as Car, vechile as Bus , vechile as Trunck 。
- 1.9 任何数据类型都可以转换成 any 或 unknown 类型
2、常见错误
类型“unknown”不能作为索引类型使用
TS类型守卫
准备:new 底层实现
function Person(phone,age){
this.age=age; //age实例属性
this.phone=phone; //phone实例属性
this.showone=function(){} //showone实例方法
}
Person.prototype.doEat=function(){
console.log("电话:",this.phone)
}
let person=new Person("12344",23)
// new 一个实例对象的底层3步
// 1.创建一个 Object 对象
var obj={}
// 2.让新创建的对象的 __proto__ 变量指向构造函数的原型对象空间
obj.__proto__= Person.prototype;
// 3.借用构造函数,为 新创建的对象变量增加属性
Person.apply(obj,["12344",23]);
1、定义
类型守卫定义: 在语句的块级作用域【if语句内或条目运算符表达式内】缩小变量的一种类型推断的行为。
类型守卫产生时机:TS 条件语句中遇到下列条件关键字时,会在语句的块级作用域内缩小变量的类型,这种类型推断的行为称作类型守卫 ( Type Guard )。类型守卫可以帮助我们在块级作用域中获得更为需要的精确变量类型,从而减少不必要的类型断言。
- 类型判断:
typeof - 属性或者方法判断:
in - 实例判断:
instanceof - 字面量相等判断:
==,===,!=,!==
2、typeof局限性
2.1 typeof 作用
typeof 用来检测一个变量或一个对象的数据类型。
2.2 typeof 检测的范围
“string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" 等数据类型。
2.3 typeof 的局限性
typeof null结果为 object- typeof 来检测一个数组变量,
typeof []结果显示的是 object - ypeof 来检测一个 Set 变量,Map 变量,结果显示的是依然是 object
2.4 typeof 的替代方案
Object.prototype.toString.callObject.prototype.toString.call([])输出[object Array]Object.prototype.toString.call(null)输出[object null]Object.prototype.toString.call(Set类型的变量)输出[object Set]Object.prototype.toString.call(Map类型的变量)输出[object Map]
2.5 typeof 的替代方案依然无法解决的问题
就是无法获取一个自定义的类的实例变量或构造函数的对象变量的真正创建类型,答案是使用 instanceof 来解决。
3、instanceof 原理
instanceof 格式: 对象变量 instanceof 类名或函数名
instanceof 的主要作用: instanceof 帮助我们准确的判断一种自定义函数或类创建的对象变量的数据类型。
instanceof 执行后 返回 true 的几种条件【符合一个即可】:
对象变量.__proto__ = 类名或函数名.prototype
如果 instanceof 关键字 左边对象变量的
__proto__属性指向的原型对象空间= 右边类名或函数名的prototype对象属性指向的原型对象空间,那么返回 true。
对象变量.__proto__.(多层__proto__).__proto__ = 类名或函数名.prototype
instanceof 左边对象变量
__proto__的1到多个上一级__proto__指向的原型对象空间,等于右边类名或函数名的 prototype 对象属性指向的原型对象空间,那么也返回 true,按照这个查找规律,会一直找到Object.prototype对象属性指向的原型对象空间为止。
TS多态
1、多态的定义
父类的对象变量可以接受任何一个子类的对象,从而用这个父类的对象变量来调用子类中重写的方法而输出不同的结果。
// TS多态
class People {
public name!: string
public eat() {
console.log("People父类的eat")
}
}
class AmericanPeople extends People {
// 美国人
public phone!: string
public eat() {
console.log("用叉子吃饭...")
}
}
class ChinesePeople extends People {
//中国人
public eat() {
console.log("用筷子吃饭...")
}
}
class TuzhuPeople extends People {
// 土族人
public eat() {
console.log("用手抓吃饭...")
}
}
// 父类的对象变量people可以接受任何一个子类的对象,
// 例如可以接受AmericanPeople,ChinesePeople,TuzhuPeople子类对象
let people: People = new AmericanPeople()
// 从而用这个父类的对象变量来调用子类中重写的方法而输出不同的结果.
people.eat()// 用叉子吃饭...
people = new ChinesePeople()
people.eat()// 用筷子吃饭...
people = new TuzhuPeople()
people.eat()//用手抓吃饭...
export {}
2、产生多态的条件
- 1.必须存在继承关系
- 2.必须有方法重写
3、多态的好处
利于项目的扩展,从局部满足了 开闭原则--对修改关闭,对扩展开放。
abstract class Vechile {
constructor() {}
public calculateRent() {}
}
// 子类Car类 独有属性为type_
class Car extends Vechile {
constructor() {
super()
}
//方法重写
public calculateRent() {}
public checkIsWeigui(isOverWeight: boolean) {}
}
class Bus extends Vechile {
constructor() {
super()
}
//方法重写
public calculateRent() {}
public checkIsOverNum(isOverWeight: boolean) {}
}
class Customer {
// 给vechile 声明时 不用写联合类型,直接写父类 便于扩展
rentVechile(vechile: Vechile) {
if (vechile instanceof Car) {
/**
*vechile instanceof Car的判断分析
*
* 编译期间,只是判断语法是否正确,达到类型守卫的目的
* 运行期间,根据实际的vechile.__proto__ 是否等于 Car.prototype或者vechile的多级__proto__与Car.prototype是否相等
*/
vechile.checkIsWeigui(true)
} else if (vechile instanceof Bus) {
vechile.checkIsOverNum(true)
}
// 用这个父类的对象变量来调用子类中重写的方法而输出不同的结果.
return vechile.calculateRent()
}
}
let cust = new Customer()
let car = new Car()
console.log(cust.rentVechile(car))
export {}
4、多态的局限性
无法直接调用子类独有方法,必须结合instanceof类型守卫来解决
TS抽象类
1、定义
一个在任何位置都不能被实例化的类就是一个抽象类
什么样的类可以被定义为抽象类
从宏观上来说,任何一个实例化后毫无意义的类都可以被定义为抽象类。 比如:我们实例化一个玫瑰花类的对象变量,可以得到一个具体的 玫瑰花 实例对象,但如果我们实例化一个 Flower 类的对象变量,那世界上有一个叫 花 的对象吗?很明显没有,所以 Flower 类 可以定义为一个抽象类,但玫瑰花可以定义为具体的类。
2、抽象类的特点
- 可以包含只有方法声明的方法(抽象方法:只有方法体,没有方法实现的方法)
- 不能直接实例化一个抽象类,只能通过子类来实例化
- 可以包含实现了具体功能的方法
- 可以包含构造器
3、好处
- 提供统一名称的抽象方法,提高代码的可维护性
抽象类通常用来充当父类,当抽象类把一个方法定义为抽象方法,那么会强制在所有子类中实现它,防止不同子类的同功能的方法命名不相同,从而降低项目维护成本。
- 防止实例化一个实例化后毫无意义的类
TS自定义守卫
1、定义
格式:
function 函数名( 形参:参数类型【参数类型大多为any】) : 形参 is A类型 =boolean+类型守卫能力{
return true or false
}
理解:返回布尔值的条件表达式赋予类型守卫的能力, 只有当函数返回 true 时,形参被确定为 A 类型
/**
* 判断是否是字符串的自定义守卫方法
*/
//function isString(str: any): boolean {// 如果直接写boolean,无法调用string数据类型的相关方法
function isString(str: any): str is string {
return typeof str === "string"
}
function isFunction(data: any): data is Function {
return typeof data === "function"
}
在编译期间,TS 识别自动守卫中的类型并不是根据
自定守卫函数内部的运行时代码来识别的;当TS 编译器在编译期间遇到 包含自定义守卫方法的if语句的内部时【能进入内部就一定为true], 这时编译器会认为str is string成立,会自动把 参数 str 的类型范围缩小为定义时的string 。 TS编译器底层这样识别自定义守卫的类型。
2、在vue3中的应用
- ref()
- unref()
TS4新特性
const数据被修改
之前,如果给一个引用类型的数据声明用的const,数据仍然可被修改,不会报错:
const arr=[10,30,40,"abc"]
arr=[100,30,40,"abc"]
在TS4里面,解决的办法是:
const arr = [10, 30, 40, "abc"] as const
可变元组
// 元组标签
let [username, age, ...rest]: [name_: string, age_: number, ...rest: any[]] = ["wangwu", 23,
"海口海淀岛四东路3号", "133123333", "一路同行,一起飞", 23, "df"]
console.log("username:", username) //wangwu
console.log("age:", age) //23
console.log("rest:", rest)//[ '海口海淀岛四东路3号', '133123333', '一路同行,一起飞', 23, 'df' ]
可以用 rest接收没有被枚举出来的数据类型
深入可变元组
- 可变元组可以在任何位置
let [username, age, ...rest]: [name_: string, age_: number,
...rest: any[], descri_: string] = ["wangwu", 23,
"海口海淀岛四东路3号", "133123333", 23, "weixin", 3, "str"]
name_: string, age_: number以及descri_: string是固定的,中间的类型不确定,但是数组解构([username, age, ...rest])只能放在数组的最后。
- 元组退化成数组
let constnum3 = [10, 30, 40, 60, "abc"] as const
// 把元组退化成"数组"
let [x3, ...y3]: readonly [any, ...any[]] = constnum3
- 函数中的应用
const arr = [10, 30, 40, "abc"] as const
//类型“readonly any[]”中的索引签名仅允许读取
function showArr(arr: readonly any[]) {
//arr[0] = 100;// 会报错
console.log(arr)
}
TS泛型
泛型
1、定义
泛型一种参数化数据类型,具有以下特点的数据类型叫泛型 :
特点一:定义时不明确,使用时必须明确成某种具体数据类型的数据类型。【泛型的宽泛】
特点二:编译期间进行数据类型安全检查的数据类型。【泛型的严谨】
特别注意:
- 类型安全检查发生在
编译期间 - 泛型是
参数化的数据类型,使用时明确化后的数据类型就是参数的值。
2、好处
好处1: 编译期对类上调用方法或属性时的泛型类型进行安全检查(类型安全检查),不符合泛型实际参数类型(泛型实参类型) 就编译通不过,防止不符合条件的数据增加进来。
好处2: 一种泛型类型被具体化成某种数据类型后,该数据类型的变量获取属性和方法时会有自动提示,这无疑提高代码开发效率和减少出错率。
TS泛型类
1、定义
格式:class 类名<泛型形参类型>{}
泛型形参类型一般有两种表示:
-
- A-Z 任何一个字母
-
- 语义化的单词来表示
- 绝大多数情况,泛型都是采用第一种形式表示
2、详解Object不能代替类上的泛型(面试题)
- 编译期间 object 无法进行类型安全检查,而泛型在编译期间可以进行类型安全检查
- object 类型数据无法接受非 object 类型的变量,只能接受 object 类型的变量,泛型能轻松做到
- object 类型数据获取属性和方法时无自动提示,泛型有自动提示
一种泛型类型被具体化成某种数据类型后,该数据类型的变量获取属性和方法时会有自动提示,提高代码开发效率和减少出错率,但在 object 类型的变量无法获取数据类型的属性和方法,降低了体验感和开发效率
3、详解any不能代替类上的泛型(面试题)
- 编译期间 any 无法进行类型安全检查,而泛型在编译期间可以进行类型安全检查
- any 类型可以获取任意数据类型的任何属性和任意方法而不会出现编译错误导致潜在错误风险,而泛型却有效的避免了此类问题发生
any 是所有类型的父类
- any 类型数据获取属性和方法时无自动提示,泛型有自动提示
TS泛型约束
定义
泛型约束简单点说就是把泛型的具体化数据类型范围缩小。
extends的理解
比如T extends object :extends 表示具体化的泛型类型只能是 object 类型,某个变量如果能断言成 object 类型【变量 as object】,那么这个变量的类型就符合 T extends object 。就是说该变量的类型可以是T的具体化类型。
T extends {} 和 T extends object 区别?
- 在ts中,
{}可以接受除了null和undefined之外的任意类型数据 - 如果只是想把泛型变成对象类型, 用
test<object>, 如果想把泛型变成接受除了undefined和null之外的任意其他类型用test<{}> T extends object用于对象类型的泛型场景,平时用的最多T extends {}和T={}一样,没有区别了。可以接受除了null和undefined之外的任意类型的数据- 不具体化的泛型
<T>,在编译阶段是unknow类型,如果是作为索引签名会报错。比如:作为对象的key会报错。
keyof的理解
keyof:表示获取一个类或者一个对象类型或者一个接口类型的所有属性名key组成的联合类型。如果类或者对象类型或者接口上只有一个属性,那么就是一个单一的属性名的类型。
<T extends object, K extends keyof T>
TS泛型接口
interface List<T> {
add(ele: T): void;
get(index: number): T;
size(): number;
remove(value: T): T;
}
TS泛型类+TS泛型接口
class ArrayList<T> implements List<T> {
public array: Array<T>
constructor() {
this.array = [];
}
public index: number = 0;
size() {
return this.index ? this.index : 0
}
// 往数组中添加元素
public add(ele: T) {
//console.log("this.kk * 3:", this.kk * 3);
this.checkIndex();
this.array[this.index++] = ele;
}
public checkIndex() {
if (this.index < 0) {
throw new Error("数组下标不能为零");
}
}
// 第二步:根据索引来查询数组中指定元素
get(index: number): T {
return this.array[index];
}
// 第三步: 显示方法
show() {
this.array.forEach((ele) => {
console.log(ele);
})
}
remove(value: number): number
remove(value: T): T
//remove(value: number | object): number | object {
remove(value: any): any {
this.array = this.array.filter((ele, index) => {
//如果是根据数字【元素索引】去删除元素,remove方法返回的是一个数字
if (typeof value === "number") {
return value !== index
} else {
// 如果是根据对象去删除元素,remove方法返回的是一个对象
return value !== ele
}
})
return value;
}
}
优点:
- 降低代码管理成本,提供统一属性和方法命名。
- 可以从整体上快速通读类的共同方法和属性。
- 新增相同功能类时,可以快速搭建类的方法。
- 和多态结合增加了项目的扩展性和简洁度,对开发大中项目有好处
泛型函数
泛型函数
泛型函数格式1: 函数名<定义的泛型类型>(参数中可以使用泛型类型):返回值也可以是泛型类型
泛型函数格式2:函数名<定义的泛型类型1,定义的泛型类型2>(参数中可以使用泛型类型):返回值也可以是泛型类型
泛型函数带来的好处
泛型函数可以在调用返回后得到返回值的具体数据类型,从而可以有自动方法和属性的提示和错误的编译提示
function quickSort<T>(arr: Array<T>): Array<T> {
if (arr.length < 2) { return arr }
var left: Array<T> = [];
var right: Array<T> = [];
var mid = arr.splice(Math.floor(arr.length / 2), 1)[0];
console.log("mid:", mid)
for (var i = 0; i < arr.length; i++) {
if (arr[i] < mid) {
left.push(arr[i]);
} else {
right.push(arr[i])
}
}
return quickSort(left).concat(mid, quickSort(right))
}
let strArr: Array<string> = ["cba", "kkdf", "ndf", "bcdf", "dfd", "cdf"]
let strArrSort = quickSort(strArr)
console.log("strArrSort:", strArrSort)
泛型函数重载
- 在编译阶段,对传入的参数有约束作用
- 返回时,有相应的返回值类型
// 函数重载签名
export function ref<T extends object>(value: T): ToRef<T>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
// 函数实现签名
export function ref(value?: unknown) {
return createRef(value)
}
let str = ref<number>(123)
泛型工厂函数
泛型工厂函数定义:一个可以创建任意类对象的通用泛型函数
1、通用函数类型
// 通用函数类型
type commonFunc = (...args: any) => any
// 用接口定义函数类型 跟上面那个效果一样
interface commonFuncInter {
(...args: any): any
}
2、工厂函数类型
工厂函数类型定义:代表任意一个类的构造函数【等价JS的构造函数】的函数类型
// 工厂函数类型
type constructor = new (...args: any) => any
在TS不能直接new一个函数来创建实例对象