文章目录
概要
这阵子在整理JS的7种继承方式,发现很多文章跟视频,讲解后都不能让自己理解清晰,索性自己记录一下,希望个位发表需要修改的意见,共勉之。
部分文章总结出来的数据可能是错误的,网络上太多数据了,有些不严谨,当然我也可能是其中一人,如果有问题,欢迎提出来,共同进步。
javascript 7种继承 文章导航
由于避免篇幅过长导致阅读疲倦等原因,这边进行内容拆解成专栏,并且添加了导航。
:)
文章名 | |
---|---|
1 | new操作符的原理以及实现 |
2 | [[Prototype]] 与 proto 与 .prototype |
3 | 1. 原型链继承分析 |
4 | 2. 构造函数继承分析 |
5 | 3. 组合式继承分析 |
6 | 4. 原型式继承分析 |
7 | 5. 寄生式继承分析 |
8 | 6. 寄生组合式继承 |
9 | 7. class继承 |
继承的进化史
JS的7种并不是逐级进化的(个人觉得~),可以参考下图:
class继承
JS一开始的初衷就是为了方便开发者,当初的开发也比较简单就是单纯的页面,随着浏览器不断地发展,现在功能越来越多了,所以,才有7种继承的进化史。
class 是es6新增的特性,避免了开发者去使用繁杂的原型链继承,构造函数继承,组合式继承,原型式继承,寄生式继承,寄生组合式继承。
比如本人,一上来就用class…无奈面试官可不理会你这些哈哈:)
mdn参考文献:此处 文献中的概念跟大学学习C++的类,有点不一样~~~
JS一开始设计就是开发一些简单的页面,本文尽量按照面向对象的类来讲解,因为不知道哪天ES又升级了呢?
由于篇幅过长,就拿几个重点来讲解
1. 类声明与严格模式
- 函数声明和类声明之间的一个重要区别在于,函数声明会提升,类声明不会。你首先需要声明你的类,然后再访问它,否则类似以下的代码将抛出ReferenceError:
- 即使你未设置 “use strict” , class 体内部的代码总是在严格模式下执行,不能重复声明重复定义。
let person = new Parent(); // error~ 未声明 ReferenceError
class Parent(){}
...
class Parent(){} // error~ 重复定义
2. 类的实现
一个类的类体是一对花括号/大括号 {} 中的部分。这是你定义类成员的位置,如方法或构造函数。
这里大概了解一下,类的类体中有什么内容,下文再继续讲解:
- 构造函数
- get,set 存取器
- 方法
- 静态方法
class Rectangle {
// constructor
constructor(height, width) {
this.height = height;
this.width = width;
}
// Getter
get area() {
return this.calcArea()
}
// Method
calcArea() {
return this.height * this.width;
}
// Static 下面有
}
const square = new Rectangle(10, 10);
console.log(square.area);
// 100
3. 类的静态方法
类的静态方法,存放在类中,而不是实例后的对象
static 关键字用来定义一个类的一个静态方法。调用静态方法不需要实例化 (en-US) 该类,但不能通过一个类实例调用静态方法。静态方法通常用于为一个应用程序创建工具函数。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static displayName = "Point";
// 用来计算Point实例化后的2个对象之间的距离
static distance(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.hypot(dx, dy);
}
}
const p1 = new Point(5, 5);
const p2 = new Point(10,10);
p1.displayName;
// undefined
p1.distance;
// undefined
console.log(Point.displayName);
// "Point"
console.log(Point.distance(p1, p2));
// 7.0710678118654755
4. get,set 存取器
class Parent {
constructor(name) {
console.warn("Parent 实例化了...");
this._name = name;
}
sayName() {
console.log("我的名字:", this._name);
}
get name() {
return this._name;
}
set name(name) {
this._name = name;
}
}
let p = new Parent("钟先生", 31);
console.log("原先的名字:", p.name);
p.name = "钟先生超级帅";
console.log("通过class set 修改后的名字:", p.name);
5. 类中的公有继承以及私有继承
关于类这个概念本文章就不过多描述,一个是篇幅有限,一个是本人也没研究透彻,类是前人提出的概念,划分出3种继承:
- 公有继承:公有继承是最普遍且默认的继承方式,它在派生类中派生出的成员变量和成员函数都是公有的,可以被派生类的对象使用和访问。公有继承方式体现了“is-a”的关系,即派生类是基类的一种扩展或特化。
在JS说人话就是可以通过对象
.操作符进行操作。
- 保护继承:保护继承是介于公有继承和私有继承之间的一种继承方式。它在派生类中派生出的成员变量和成员函数都是保护的,不能被派生类的对象直接访问,但是可以在派生类的内部和派生类的子类中进行访问。保护继承方式体现了“derived-from”的关系,即派生类是基类的派生和扩展,但不希望外部使用派生类中的成员。
JS应用暂时不知道。。。
- 私有继承:私有继承是一种较为特殊的继承方式,它在派生类中派生出的成员变量和成员函数都是私有的,不能被派生类的对象直接访问,只能在派生类的内部进行访问和调用。私有继承体现了“实现的细节隐藏”的原则,只有派生类的内部可以直接访问和调用被继承类中的私有成员。
在JS说人话就是不能通过.操作符直接修改,只能通过实例对应的类方法跟get/set存取器进行修改
。
小插曲:
JavaScript有一个非强制的但很有用的编程惯例
,就是对所以私有变量或函数名加一个下划线(_
)作为前缀,以标识他们是私有的,仅用于提醒开发人员。
该操作是非强制性的,对于一些实例对象的属性名还是可以通过
.
操作符修改。
这里只是标识作用,用于提醒开发者,并不能限制开发者直接修改其属性。
但是class继承可以划分出,公有属性以及私有属性。
公有属性
可以通过对象.
操作符修改,也可以通过类方法以及get、set存取器操作。
私有属性:使用私有字段,可以按以下方式细化定义。
仅仅可以通过类方法以及get、set存取器操作。
6. 使用 super 调用超类
super 关键字用于调用对象的父对象上的函数。
class Cat {
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name + ' makes a noise.');
}
}
class Lion extends Cat {
speak() {
super.speak();
console.log(this.name + ' roars.');
}
}
// 打印2条数据
// 比如 threejs 中就有过这段代码
7. Mix-ins / 混入
抽象子类或者 mix-ins 是类的模板。一个 ECMAScript 类只能有一个单超类,所以想要从工具类来多重继承的行为是不可能的。子类继承的只能是父类提供的功能性。因此,例如,从工具类的多重继承是不可能的。该功能必须由超类提供。
一个以超类作为输入的函数和一个继承该超类的子类作为输出可以用于在 ECMAScript 中实现混合:
有点类似寄生模式,通过工厂模式,扩展出你的 target 类。
var calculatorMixin = (Base) =>
class extends Base {
calc() {}
};
var randomizerMixin = (Base) =>
class extends Base {
randomize() {}
};
使用 mix-ins 的类可以像下面这样写:
class Foo {}
class Bar extends calculatorMixin(randomizerMixin(Foo)) {}
源码: 类的继承
类的继承,主要通过关键字extends
这边弄的稍微复杂了,但是没办法,东西很多,很多情况要对比,还没有跟寄生组合式继承对比呢~~~
- 父类:Parent,要被继承的类,可以理解为人。
- 子类:Children,要继承父类的类,可以理解为不同的工种,但是还是一个人,所以继承了父类的一些基础属性。
- 实例:person1 ,person2,实例化的子类,一般实例化后数据就分开了。
父类属性:
- 构造函数中的 _name:公共属性,每个人都有名字
基础数据类型
,这里加上_
,指示是私有变量,告诉开发人员不要随意修改,但是还是可以修改的… - 构造函数中的方法 syaName1:公共方法,实例化后放在了对象上了
- 类中的方法sayName:公共方法,实例化后放在了原型对象上了
- get、set存取器 name:class 的存储器,用于设置跟读取类中的属性的方法,不可以跟其他属性重名,这里用来修改_name
- 静态方法 static compareNmae:在类中的方法,实例对象没有的,可以通过Parent.compareNmae()调用,一般是对比实例对象用的。
子类属性:
- #job:私有变量,只能构造函数跟get、set存取器才能修改。
- sayJob:子类中公共方法,实例化后放在了原型对象上了,私有变量,类中方法可以操作。
- get、set存取器 job:私有变量,存取器可以操作。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue路由中history模式的实现</title>
</head>
<body>
<h1>Vue路由中history模式的实现</h1>
<button id="myBtn">改变url</button>
<script>
class Parent {
constructor(name) {
// 公共属性,可以通过 .操作符修改
this._name = name;
// 放在了实例上
this.sayName1 = function () {
console.log(`我的名字1是:${this._name}`);
};
}
// 公共方法-放在原型链上
sayName() {
console.log(`我的名字是:${this._name}`);
}
get name() {
return this._name;
}
// 当然类中内部修改也是可以的
set name(name) {
this._name = name;
}
// 静态方法,一般用于比较2个实例,这里比较谁的名字长
static compareName(a, b) {
console.log(`${a.name}, 名字长度为${a.name.length}`);
console.log(`${b.name}, 名字长度为${b.name.length}`);
console.log(
(a.name.length > b.name.length ? a.name : b.name) + " 的名字长"
);
return a.name.length > b.name.length;
}
}
console.log("=========父类=========");
let p = new Parent("Penk");
p.sayName();
p.name = "Penk是个码农";
p.sayName();
class Children extends Parent {
// 私有变量
// 1. 构造函数
// 2. 内部方法或者get、set读取器
#job = "没工作";
// 构造函数
constructor(name, job) {
super(name);
this.#job = job;
}
// 内部方法
sayJob() {
console.log(`我叫${this.name},我的工作是:${this.#job}`);
}
// 读取器
get job() {
return this.#job;
}
set job(j) {
this.#job = j;
}
}
console.log("=========子类=========");
let person = new Children("钟先生", "程序员");
person.sayName();
person.name = "钟先生超级帅";
person.sayName();
// 直接修改,因为是公共属性
person._name = "钟先生";
person.sayName();
person.sayJob();
person.job = "扫地大叔1111111";
person.sayJob();
// 无效,根本读不到该值...
person["#job"] = "程序员";
person.sayJob();
// 静态方法比较长度
let person2 = new Children("刘备", "主公");
Parent.compareName(person, person2);
console.log(p);
console.log(person);
</script>
</body>
</html>
效果图
父类实例化对象,通过class 存取器修改_name属性
console.log("=========父类=========");
let p = new Parent("Penk");
p.sayName();
p.name = "Penk是个码农";
p.sayName();
子类,实例化对象,通过存取器修改,也通过了对象属性直接修改。
console.log("=========子类=========");
let person = new Children("钟先生", "程序员");
person.sayName();
person.name = "钟先生超级帅";
person.sayName();
// 直接修改,因为是公共属性
person._name = "钟先生";
person.sayName();
这边重复打印数据,是因为子类重写的sayName方法中,通过super 调用了父类的sayName
打印了子类的sayJob方法,因为#job是私有属性,只能通过读取器job进行修改:
person.sayJob();
person.job = "扫地大叔1111111";
person.sayJob();
// 无效,根本读不到该值...
person["#job"] = "程序员";
person.sayJob();
调用了类中的静态方法,静态方法一般不是用于对对象的操作,这边的功能是比较2个名字的长短,虽然有点不实用…
// 静态方法比较长度
let person2 = new Children("刘备", "主公");
Parent.compareName(person, person2);
小结
class 不仅仅是简化了之前讲解的6种继承方式,也更加严谨,提供了更多的更像面向对象变成的方法(读取器,静态方法等)。但是不同的项目需要进行技术调研,class或许不是最优的解决方案,但一定是最优雅的。