目录

1. 简介

2. Object.getPrototypeOf()

3. super 关键字

4. 类的 prototype 属性和__proto__属性

4.1extends 的继承目标

4.2实例的 __proto__ 属性

5. 原生构造函数的继承

6. Mixin 模式的实现


1. 简介

Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多

class Point {
}
class ColorPoint extends Point {
}

上面代码定义了一个 ColorPoint 类,该类通过 extends 关键字,继承了 Point 类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一
样,等于复制了一个 Point 类。下面,我们在 ColorPoint 内部加上代码。

class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}

上面代码中, constructor 方法和 toString 方法之中,都出现了 super 关键字,它在这里表示父类的构造函数,用来新建父类的 this 对象。
子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进
行加工。如果不调用 super 方法,子类就得不到 this 对象。

class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}

另一个需要注意的地方是,在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,是基于对父类
实例加工,只有 super 方法才能返回父类实例。

class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正确
}
}

上面代码中,子类的 constructor 方法没有调用 super 之前,就使用 this 关键字,结果报错,而放在 super 方法之后就是正确的。
下面是生成子类实例的代码。

let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true

上面代码中,实例对象 cp 同时是 ColorPoint 和 Point 两个类的实例,这与 ES5 的行为完全一致。
最后,父类的静态方法,也会被子类继承。

class A {
static hello() {
console.log('hello world');
}
}
class B extends A {
}
B.hello() // hello world

上面代码中, hello() 是 A 类的静态方法, B 继承 A ,也继承了 A 的静态方法。

2. Object.getPrototypeOf()

Object.getPrototypeOf(ColorPoint) === Point
// tru

因此,可以使用这个方法判断,一个类是否继承了另一个类。

3. super 关键字

super 这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
第一种情况, super 作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次 super 函数。

class A {}
class B extends A {
constructor() {
super();
}
}

上面代码中,子类 B 的构造函数之中的 super() ,代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。
注意, super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B ,因此 super() 在这里相当于
A.prototype.constructor.call(this) 。

class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B

上面代码中, new.target 指向当前正在执行的函数。可以看到,在 super() 执行时,它指向的是子类 B 的构造函数,而不是父类 A 的构造函数。也就是
说, super() 内部的 this 指向的是 B 。
作为函数时, super() 只能用在子类的构造函数之中,用在其他地方就会报错。

class A {}
class B extends A {
m() {
super(); // 报错
}
}

上面代码中, super() 用在 B 类的 m 方法之中,就会造成句法错误。
第二种情况, super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();

上面代码中,子类 B 当中的 super.p() ,就是将 super 当作一个对象使用。这时, super 在普通方法之中,指向 A.prototype ,所以 super.p() 就相当于
A.prototype.p() 。
这里需要注意,由于 super 指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过 super 调用的

class A {
constructor() {
this.p = 2;
}
}
class B extends A {
get m() {
return super.p;
}
}
let b = new B();
b.m // undefined

上面代码中, p 是父类 A 实例的属性, super.p 就引用不到它。
如果属性定义在父类的原型对象上, super 就可以取到。

class A {}
A.prototype.x = 2;
class B extends A {
constructor() {
super();
console.log(super.x) // 2
}
}
let b = new B();

上面代码中,属性 x 是定义在 A.prototype 上面的,所以 super.x 可以取到它的值。
ES6 规定,通过 super 调用父类的方法时,方法内部的 this 指向子类

class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}
let b = new B();
b.m() // 2

上面代码中, super.print() 虽然调用的是 A.prototype.print() ,但是 A.prototype.print() 内部的 this 指向子类 B ,导致输出的是 2 ,而不是 1 。
也就是说,实际上执行的是 super.print.call(this) 。

由于 this 指向子类,所以如果通过 super 对某个属性赋值,这时 super 就是 this ,赋值的属性会变成子类实例的属性。

class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined
console.log(this.x); // 3
}
}
let b = new B();

上面代码中, super.x 赋值为 3 ,这时等同于对 this.x 赋值为 3 。而当读取 super.x 的时候,读的是 A.prototype.x ,所以返回 undefined 。
如果 super 作为对象,用在静态方法之中,这时 super 将指向父类,而不是父类的原型对象。

class Parent {
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}
class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}
myMethod(msg) {
super.myMethod(msg);
}
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2

上面代码中, super 在静态方法之中指向父类,在普通方法之中指向父类的原型对象。
注意,使用 super 的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

class A {}
class B extends A {
constructor() {
super();
console.log(super); // 报错
}
}

上面代码中, console.log(super) 当中的 super ,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。这
时,如果能清晰地表明 super 的数据类型,就不会报错

class A {}
class B extends A {
constructor() {
super();
console.log(super.valueOf() instanceof B); // true
}
}
let b = new B();

上面代码中, super.valueOf() 表明 super 是一个对象,因此就不会报错。同时,由于 super 使得 this 指向 B ,所以 super.valueOf() 返回的是一个 B
的实例。
最后,由于对象总是继承其他对象的,所以可以在任意一个对象中,使用 super 关键字。

var obj = {
toString() {
return "MyObject: " + super.toString();
}
};
obj.toString(); // MyObject: [object Object]

4. 类的 prototype 属性和__proto__属性

大多数浏览器的 ES5 实现之中,每一个对象都有 __proto__ 属性,指向对应的构造函数的 prototype 属性。Class 作为构造函数的语法糖,同时有
prototype 属性和 __proto__ 属性,因此同时存在两条继承链。
(1)子类的 __proto__ 属性,表示构造函数的继承,总是指向父类。
(2)子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的 prototype 属性。

class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

上面代码中,子类 B 的 __proto__ 属性指向父类 A ,子类 B 的 prototype 属性的 __proto__ 属性指向父类 A 的 prototype 属性。
这样的结果是因为,类的继承是按照下面的模式实现的。

class A {
}
class B {
}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 的实例继承 A 的静态属性
Object.setPrototypeOf(B, A);
const b = new B()

Object.setPrototypeOf 方法的实现。

Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}

因此,就得到了上面的结果

Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;

这两条继承链,可以这样理解:作为一个对象,子类( B )的原型( __proto__ 属性)是父类( A );作为一个构造函数,子类( B )的原型对象
( prototype 属性)是父类的原型对象( prototype 属性)的实例。

Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype

4.1extends 的继承目标

extends 关键字后面可以跟多种类型的值

class B extends A {
}

上面代码的 A ,只要是一个有 prototype 属性的函数,就能被 B 继承。由于函数都有 prototype 属性(除了 Function.prototype 函数),因此 A 可以是任
意函数。
下面,讨论三种特殊情况。
第一种特殊情况,子类继承 Object 类。

class A extends Object {
}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true

这种情况下, A 其实就是构造函数 Object 的复制, A 的实例就是 Object 的实例。
第二种特殊情况,不存在任何继承。

class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true

这种情况下, A 作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承 Function.prototype 。但是, A 调用后返回一个空对象(即
Object 实例),所以 A.prototype.__proto__ 指向构造函数( Object )的 prototype 属性。

第三种特殊情况,子类继承 null

class A extends null {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true

这种情况与第二种情况非常像。 A 也是一个普通函数,所以直接继承 Function.prototype 。但是, A 调用后返回的对象不继承任何方法,所以它的
__proto__ 指向 Function.prototype ,即实质上执行了下面的代码。

class C extends null {
constructor() { return Object.create(null); }
}

4.2实例的 __proto__ 属性

子类实例的 __proto__ 属性的 __proto__ 属性,指向父类实例的 __proto__ 属性。也就是说,子类的原型的原型,是父类的原型。

var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true

上面代码中, ColorPoint 继承了 Point ,导致前者原型的原型是后者的原型。
因此,通过子类实例的 __proto__.__proto__ 属性,可以修改父类实例的行为

p2.__proto__.__proto__.printName = function () {
console.log('Ha');
};
p1.printName() // "Ha"

上面代码在 ColorPoint 的实例 p2 上向 Point 类添加方法,结果影响到了 Point 的实例 p1

5. 原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。

Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
以前,这些原生构造函数是无法继承的,比如,不能自己定义一个 Array 的子类。

function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});

上面代码定义了一个继承 Array 的 MyArray 类。但是,这个类的行为与 Array 完全不一致

var colors = new MyArray();
colors[0] = "red";
colors.length // 0
colors.length = 0;
colors[0] // "red

之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过 Array.apply() 或者分配给原型对象都不行。原生构造函数会忽略 apply 方
法传入的 this ,也就是说,原生构造函数的 this 无法绑定,导致拿不到内部属性。
ES5 是先新建子类的实例对象 this ,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如, Array 构造函
数有一个内部属性 [[DefineOwnProperty]] ,用来定义新属性时,更新 length 属性,这个内部属性无法在子类获取,导致子类的 length 属性行为不正
常。
下面的例子中,我们想让一个普通对象继承 Error 对象。

var e = {};
Object.getOwnPropertyNames(Error.call(e))
// [ 'stack' ]
Object.getOwnPropertyNames(e)
// []

上面代码中,我们想通过 Error.call(e) 这种写法,让普通对象 e 具有 Error 对象的实例属性。但是, Error.call() 完全忽略传入的第一个参数,而是返
回一个新对象, e 本身没有任何变化。这证明了 Error.call(e) 这种写法,无法继承原生构造函数。
ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象 this ,然后再用子类的构造函数修饰 this ,使得父类的所有行为都可以继
承。下面是一个继承 Array 的例子。

class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined

上面代码定义了一个 MyArray 类,继承了 Array 构造函数,因此就可以从 MyArray 生成数组的实例。这意味着,ES6 可以自定义原生数据结构(比如
Array 、 String 等)的子类,这是 ES5 无法做到的。
上面这个例子也说明, extends 关键字不仅可以用来继承类,还可以用来继承原生的构造函数。因此可以在原生数据结构的基础上,定义自己的数据结
构。下面就是定义了一个带版本功能的数组。

class VersionedArray extends Array {
constructor() {
super();
this.history = [[]];
}
commit() {
this.history.push(this.slice());
}
revert() {
this.splice(0, this.length, ...this.history[this.history.length - 1]);
}
}
var x = new VersionedArray();
x.push(1);
x.push(2);
x // [1, 2]
x.history // [[]]
x.commit();
x.history // [[], [1, 2]]
x.push(3);
x // [1, 2, 3]
x.history // [[], [1, 2]]
x.revert();
x // [1, 2]

上面代码中, VersionedArray 会通过 commit 方法,将自己的当前状态生成一个版本快照,存入 history 属性。 revert 方法用来将数组重置为最新一次保
存的版本。除此之外, VersionedArray 依然是一个普通数组,所有原生的数组方法都可以在它上面调用。
下面是一个自定义 Error 子类的例子,可以用来定制报错时的行为。

class ExtendableError extends Error {
constructor(message) {
super();
this.message = message;
this.stack = (new Error()).stack;
this.name = this.constructor.name;
}
}
class MyError extends ExtendableError {
constructor(m) {
super(m);
}
}
var myerror = new MyError('ll');
myerror.message // "ll"
myerror instanceof Error // true
myerror.name // "MyError"
myerror.stack
// Error
// at MyError.ExtendableError
// ...

注意,继承 Object 的子类,有一个行为差异。

class NewObj extends Object{
constructor(){
super(...arguments);
}
}
var o = new NewObj({attr: true});
o.attr === true // false

上面代码中, NewObj 继承了 Object ,但是无法通过 super 方法向父类 Object 传参。这是因为 ES6 改变了 Object 构造函数的行为,一旦发现 Object 方
法不是通过 new Object() 这种形式调用,ES6 规定 Object 构造函数会忽略参数

6. Mixin 模式的实现

Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下

const a = {
a: 'a'
};
const b = {
b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}

上面代码中, c 对象是 a 对象和 b 对象的合成,具有两者的接口。
下面是一个更完备的实现,将多个类的接口“混入”(mix in)另一个类。

function mix(...mixins) {
class Mix {}
for (let mixin of mixins) {
copyProperties(Mix, mixin); // 拷贝实例属性
copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
}
return Mix;
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== "constructor"
&& key !== "prototype"
&& key !== "name"
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}

上面代码的 mix 函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可

class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}

总结

本博客源于本人阅读相关书籍和视频总结,创作不易,谢谢点赞支持。学到就是赚到。我是歌谣,励志成为一名优秀的技术革新人员。

欢迎私信交流,一起学习,一起成长。

推荐链接 其他文件目录参照

“睡服“面试官系列之各系列目录汇总(建议学习收藏)

“睡服”面试官系列第二十二篇之class的继承(建议收藏学习)相关推荐

  1. “睡服”面试官系列第十九篇之async函数(建议收藏学习)

    目录 1. 含义 2. 基本用法 3. 语法 3.1返回 Promise 对象 3.2Promise 对象的状态变化 3.3await 命令 3.4错误处理 3.5使用注意点 4. async 函数的 ...

  2. “睡服”面试官系列第二十篇之generator函数的异步应用(建议收藏学习)

    目录 1. 传统方法 2. 基本概念 2.1异步 2.2回调函数 2.3Promise 3. Generator 函数 3.1协程 3.2协程的 Generator 函数实现 3.3Generator ...

  3. “睡服”面试官系列第十八篇之generator函数的语法(建议收藏学习)

    目录 1简介 1.1基本概念 1.2yield 表达式 1.3与 Iterator 接口的关系 2. next 方法的参数 3. for...of 循环 4. Generator.prototype. ...

  4. “睡服”面试官系列第十六篇之Symbol(建议收藏学习)

    目录 1. 概述 2. 作为属性名的 Symbol 3. 实例:消除魔术字符串 4. 属性名的遍历 5. Symbol.for(),Symbol.keyFor() 6. 实例:模块的 Singleto ...

  5. “睡服”面试官系列第十五篇之对象的扩展(建议收藏学习)

    目录 1. 属性的简洁表示法 2. 属性名表达式 3. 方法的 name 属性 4. Object.is() 5. Object.assign() 5.1基本用法 5.2注意点 5.21.浅拷贝 5. ...

  6. “睡服”面试官系列第十四篇之数组的扩展(建议收藏学习)

    目录 1. 扩展运算符 1含义 1.2替代数组的 apply 方法 1.3扩展运算符的应用 1.3.1复制数组 1.3.2合并数组 1.3.3与解构赋值结合 1.3.4字符串 1.3.5实现了 Ite ...

  7. “睡服”面试官系列第十篇之module的语法(建议收藏学习)

    目录 1.概述 2. 严格模式 3. export 命令 4. import 命令 5. 模块的整体加载 6. export default 命令 7. export 与 import 的复合写法 8 ...

  8. “睡服”面试官系列第二篇之promise(建议收藏学习)

    目录 1promise的定义 2基本用法 3. Promise.prototype.then() 4. Promise.prototype.catch() 5. Promise.all() 6. Pr ...

  9. “睡服”面试官系列第二十三篇之修饰器(建议收藏学习)

    目录 1. 类的修饰 2. 方法的修饰 3. 为什么修饰器不能用于函数? 4. core-decorators.js 4.1@autobind 4.2@readonly 4.3@override 4. ...

最新文章

  1. Python 多进程、多线程启动
  2. 选择排序的基本原理及实现
  3. Java 8新特性终极指南
  4. spring boot 引用外部配置文件
  5. code block怎样导入整个文件夹_按需分配随时可用的在线开发环境:弹性容器+code-server踩坑记...
  6. Eclipse中使用自己的makefile管理工程
  7. NYOJ-14 会场安排问题(经典贪心,区间完全不覆盖模板)
  8. hashmap修改对应key的值_死磕 java集合之HashMap源码分析
  9. [Swift]LeetCode288. 唯一单词缩写 $ Unique Word Abbreviation
  10. 仅用一年时间,蓝巨人 IBM 如何开发出首台个人计算机?
  11. Autodesk 3DSMax 2019 安装注册说明
  12. mysql sql注入工具下载_超级SQL注入工具【SSQLInjection】
  13. 汇编指令lea取偏移地址
  14. 微信公众号(订阅号)如何开通付费功能?
  15. 笔记本连不上网(IPV4和IPV6无网络访问权限)解决方法
  16. VPS常用网络测试工具
  17. C#实现微信公众号群发消息(解决一天只能发一次的限制)
  18. [深入研究4G/5G/6G专题-8]: 测试-测试终端-高端无线CPE/Router的高通SDX55 5G NR芯片方案
  19. Mac OS下搭建Hadoop3.2.1
  20. 知乎热议: Java, Go和Python那个前景好?

热门文章

  1. 提交本地项目到github
  2. centos7修改服务器密码忘记,Centos7忘记root密码怎么修改
  3. python的opencv 车牌识别 开源_毕节进出口车牌识别系统怎么样
  4. json_decode php数组,json_decode转化为数组加true,json_encode和json_decode区别
  5. android是java_为什么大家都用JAVA写android程序
  6. linux中win文件转为unix,如何将文本文件从Windows转换为Unix
  7. java 绘图球的移动_求助在JFrame上绘制移动的小球
  8. java中迭代器要导包吗_java 中迭代器的使用方法详解
  9. windows 串口编程 c语言,windows下C语言版串口发送程序(基于VS2017)
  10. 把cpp编译为so_基于VSCode和CMake进行C/C++开发第三讲GCC编译器