通过原型继承理解ES6 extends 如何实现继承
前言
第一次接触到 ES6 中的 class 和 extends 时,就听人说这两个关键字不过是语法糖而已。它们的本质还是 ES3 的构造函数,原型链那些东西,没有什么新鲜的,只要理解了原型链等这些概念自然就明白了。这话说的没错,但是这些继承的实现是否是我们想的那样呢,今天让我们来用原型链解释下 ES6 extends 如何实现的继承。
结论
这里先上结论,如果有理解不对的地方,欢迎在留言指出;如果有不理解的地方可以看完结论后继续阅读,如果阅读完后有难以理解指出也欢迎留言讨论。
- extends 的继承通过两种方式完成了三类值的继承
- 构造函数设置的属性通过复制完成继承
- 实例方法通过实例原型之间的原型链完成继承
- 构造函数的静态方法通过构造函数之间的原型链完成继承
属性通过复制完成继承
class 实例对象属性的继承是通过复制达到继承效果的,这里的属性指的是通过构造函数的 this 定义的属性。
class Person {
constructor(name) {
this.maxage = 100;
this.name = name
}
}
class Programmer extends Person {
constructor(name) {
super(name);
this.job = 'coding'
}
}
const personA = new Person('xiaoming')
const programmerB = new Programmer('xiaozhi')
console.log(personA.hasOwnProperty('maxage'));
console.log(programmerB.hasOwnProperty('maxage'));
以上代码的打印结果都是true,这个结果就证明了对象的 extends 继承的属性是通过复制继承的,而不是通过原型链完成的属性继承。
我们将以上代码中得到的两个实例对象打印出来,可以得到如下图结果

根据打印结果可以更直观的看到两个实例对象上均有 maxage 属性。原始类型的值的复制好理解,直接拷贝值就好,那么引用类型的复制是深拷贝,还是浅拷贝,或者说仅仅是对象引用的拷贝呢?
构造函数对象值的继承,比想象中要复杂一点,根据代码实践(暂未查看标准),得出结论,引用类型的继承主要分为两种情况:
- 字面量定义的对象属性是深拷贝
- 变量赋值对象属性是引用复制
字面量定义的对象属性是深拷贝
这里的字面量定义的对象属性指的是,指的是直接在构造函数中通过 {} 的形式定义的对象直接赋值给 this 的某个属性。代码形如
class A {
constructor() {
this.obj = {
name: 'obj',
secondObj: {
name: 'secondObj'
}
}
}
}
示例代码中,obj 属性是直接通过 {} 定义的一个对象。
class Person {
constructor(name) {
this.maxage = 100;
this.name = name;
this.obj = {
name: 'obj',
secondObj: {
name: 'secondObj'
}
};
}
eat() {
console.log('eat food')
}
}
class Programmer extends Person {
constructor(name) {
super(name);
this.job = 'coding'
}
coding() {
console.log('coding world')
}
}
const personA = new Person('xiaoming')
const programmerB = new Programmer('xiaohei')
console.log(personA.obj === programmerB.obj)
console.log(personA.obj.secondObj === programmerB.obj.secondObj)
上述代码的运行结果如下

Person 的实例对象上定义了一个 obj 属性,该属性被 Programmer 的实例对象继承,通过对比这两个属性的值,我们知道他们并不相等,这首先排除了是引用复制的可能(如果是引用复制,这里两个属性应该指向同一个对象,也就是其存储的内存地址应该是致的,但是这里得到的结果应该是等式不成立)。通过实例对象属性 obj 中的 secondObj 属性的比较,排除了这是浅拷贝,由此我们可以得出在代码示例的场景中引用类型的继承是通过深拷贝完成的。
变量赋值对象属性是引用复制
按理来说我们得出上一小节的结论应该基本就可以确定 extends 继承是如何处理引用类型的值的继承了,但是事实是到这里并没有结束。
考虑如下代码,这段代码和上一小节的代码区别不大,有变化的地方是,这是在外部定义了一个变量,变量的值是对象,然后将变量赋值给了了 obj 属性。
let obj = {
name: 'obj'
}
class Person {
constructor(name) {
this.maxage = 100;
this.name = name;
this.obj = obj;
}
eat() {
console.log('eat food')
}
}
class Programmer extends Person {
constructor(name) {
super(name);
this.job = 'coding'
}
coding() {
console.log('coding world')
}
}
const personA = new Person('xiaoming')
const programmerB = new Programmer('xiaohei')
console.log(personA.obj === programmerB.obj);
运行结果如下

从代码运行结果中不难看出,这里出现了变化,通过变量赋值定义的对象属性,是通过引用复制完成继承的。下面我们来看看对象变量被定义在构造函数中然后再赋值给对象的属性是否还是这样的结果。
class Person {
constructor(name) {
const innerObj = {
name: 'obj'
}
this.maxage = 100;
this.name = name;
this.obj = innerObj;
}
eat() {
console.log('eat food')
}
}
class Programmer extends Person {
constructor(name) {
super(name);
this.job = 'coding'
}
coding() {
console.log('coding world')
}
}
const personA = new Person('xiaoming')
const programmerB = new Programmer('xiaohei')
console.log(personA.obj === programmerB.obj);
运行结果如下

没错当你把变量定义在构造函数中,然后来赋给 this 的属性的时候,是通过深拷贝来继承的。神奇不,同样是变量,只是变量定义的作用域不一样,连继承方式都变了,具体为什么要这么做,我现在还不太清楚,改日查下标准,有知道的同学还望评论区不吝赐教。
小结
这节有点长,需要个小结总结下我们得到的结论。首先,extends 的构造函数定义的属性值的继承是通过复制继承的。第二点,副职的方式主要分为以下几重情形:
- 原始类型直接复制值到子类对象
- 引用类型的值如果值是直接在构造函数中定义的(包括字面量直接赋值给属性和在构造函数内定义的变量然后变量赋值给属性),那么其会被深拷贝到子类对象上
- 在构造函数外定义的变量,其值是引用类型,构造函数中将该变量赋值给对象的某个属性,该属性会被通过引用复制的方式拷贝到子类对象上
实例方法的继承
实例方法的继承比较好理解,通过原型链原型链继承的,只不过这个链的形式是一个比较直接的链。这条链的大概就像下面这个图

没错,图上那条红色的线就是 programmerB 这个实例对象继承 eat 方法的方式,是不是和想的不一样。这条链还是比较好理解的,具有继承关系的构造函数的 prototype 之间有一条原型链,而每个实例对象的原型又是其构造函数的 prototype,这样一来就产生了图中红色线条实例方法的原型链。两个 class 的实例对象之间没有什么关系。
如果对上面的图存在疑问运行下面这段代码,运行结果会证明图是没有问题的。
class Person {
constructor(name) {
const innerObj = {
name: 'obj'
}
this.maxage = 100;
this.name = name;
this.obj = innerObj;
}
eat() {
console.log('eat food')
}
}
class Programmer extends Person {
constructor(name) {
super(name);
this.job = 'coding'
}
coding() {
console.log('coding world')
}
}
const personA = new Person('xiaoming')
const programmerB = new Programmer('xiaohei')
console.log(personA.__proto__ === Person.prototype);
console.log(programmerB.__proto__ === Programmer.prototype);
console.log(Programmer.prototype.__proto__ === Person.prototype);
console.log(programmerB.__proto__.__proto__ === Person.prototype);
静态方法的继承
相比实例方法的继承,静态方法的继承要简单的多,就是一条简单的原型链,具有继承关系的两个 class 之间存在一条原型链。如下图这样

这个关系图就没什么多说了,有疑问的同学可以随便写段验证下。
继承关系图
这里假定 class B extends A,那么关于原型 class 之间的原型继承可得出如下等式。
B.proto === A
b.proto === B.prototype
a.proto === A.prototype
B.prototype.proto === A.prototype
b.proto.proto === A.prototype
用关系图来表达上面的这些等式会更容易理解

转载请注明出处!
通过原型继承理解ES6 extends 如何实现继承的更多相关文章
- 前端知识体系:JavaScript基础-原型和原型链-理解 es6 中class构造以及继承的底层实现原理
理解 es6 中class构造以及继承的底层实现原理 原文链接:https://blog.csdn.net/qq_34149805/article/details/86105123 1.ES6 cla ...
- 【ES6】更易于继承的类语法
和其它面向对象编程语言一样,ES6 正式定义了 class 类以及 extend 继承语法糖,并且支持静态.派生.抽象.迭代.单例等,而且根据 ES6 的新特性衍生出很多有趣的用法. 一.类的基本定义 ...
- 深入理解es6(下)
一.symbol javascript基本数据类型: null.undefined.number.boolean.string.symbol ES6 引入了一种新的原始数据类型Symbol,表示独一无 ...
- extends:类似于java中的继承特征,extends="struts-default"
extends:类似于java中的继承特征,extends="struts-default"就是继承struts-default.xml,它里面定义了许多跳转类型.拦截器等一些常用 ...
- 关于JS对象原型prototype与继承,ES6的class和extends · kesheng's personal blog
传统方式:通过function关键字来定义一个对象类型 1234567891011 function People(name) { this.name = name}People.prototype. ...
- ES6 extends继承及super使用读书笔记
extends 继承 extends 实现子类的继承 super() 表示父类的构造函数, 子类必须在 constructor中调用父类的方法,负责会报错. 子类的 this 是父类构造出来的, 再在 ...
- javascript实现继承3种方式: 原型继承、借用构造函数继承、组合继承,模拟extends方法继承
javascript中实现继承的三种方式:原型继承.借用构造函数继承.混合继承: /* js当中的继承 js中 构造函数 原型对象 实力对象的关系: 1 构造函数.prototype = 原型对象 2 ...
- js原型链理解(2)--原型链继承
1.原型链继承 2.constructor stealing(构造借用) 3.组合继承 js中的原型链继承,运用的js原型链中的__proto__. function Super(){ this.se ...
- 通过一段代码理解es6继承;
class animal{ constructor(props){ this.name = 'xiaoniao' || props.name } eat(){ console.log(this.nam ...
随机推荐
- 记录用友T+接口对接的心酸历程
前言:公司的业务主要是对接财务系统做单据传输或者凭证处理的,难免少不了和各大财务软件做数据对接,其中当然是必须通过接口来传递数据了.于是乎,用友T+的版本来了,对接的工作自然是我来做,可没想到就是这样 ...
- Fork 多进程 模拟并行访问web service获取响应时间差
#include <ros/ros.h> #include <iostream> #include <string> #include <cstring> ...
- python开发基础--思维导图
开始学习python,相当于零基础 非自学,自学的痛苦不想体会和尝试,毕竟不会很友好,知乎上看到很多说自学的好处啊噼里啪啦的.嗯,说的很对,但是我偏不听,略略略.锻炼我的自学能力,这还需要锻炼吗,百度 ...
- 关于JSP页面的静态包含和动态包含
JSP中有两种包含:静态包含:<%@include file="被包含页面"%> 和 动态包含:<jsp:include page="被包含页面&quo ...
- js及jquery常用代码
1.获取屏幕尺寸 document.documentElement.scrollWidth; document.documentElement.scrollHeight; $(window).widt ...
- Winform改变Textbox边框颜色
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; usin ...
- RedHat 6.5换源
https://wenku.baidu.com/view/5b87fb42c77da26924c5b03b.html
- Python模块之requests,urllib和re
目录 一.爬虫的步骤 二.使用Jupyter 三.爬虫请求模块之urllib 四.爬虫请求模块之requests 五.爬虫分析之re模块 一.爬虫的步骤 1.发起请求,模拟浏览器发送一个http请求 ...
- Entity Framework 6.0 入门系列 第一篇
Entity Framework 6.0 入门系列 第一篇 好几年前接触过一些ef感觉不是很好用,废弃.但是 Entity Framework 6.0是经过几个版本优化过的产物,性能和功能不断完善,开 ...
- Container及其内部进程监控剖析
目前市场上的虚拟化技术种类很多,例如moby(docker).LXC.RKT等等.在带来方便应用部署和资源充分利用的好处的同时,如何监控相应Container及其内部应用进程成为运维人员不可避免遇到的 ...