通过原型继承理解ES6 extends 如何实现继承

前言

第一次接触到 ES6 中的 class 和 extends 时,就听人说这两个关键字不过是语法糖而已。它们的本质还是 ES3 的构造函数,原型链那些东西,没有什么新鲜的,只要理解了原型链等这些概念自然就明白了。这话说的没错,但是这些继承的实现是否是我们想的那样呢,今天让我们来用原型链解释下 ES6 extends 如何实现的继承。

结论

这里先上结论,如果有理解不对的地方,欢迎在留言指出;如果有不理解的地方可以看完结论后继续阅读,如果阅读完后有难以理解指出也欢迎留言讨论。

  1. extends 的继承通过两种方式完成了三类值的继承
  2. 构造函数设置的属性通过复制完成继承
  3. 实例方法通过实例原型之间的原型链完成继承
  4. 构造函数的静态方法通过构造函数之间的原型链完成继承

属性通过复制完成继承

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 属性。原始类型的值的复制好理解,直接拷贝值就好,那么引用类型的复制是深拷贝,还是浅拷贝,或者说仅仅是对象引用的拷贝呢?

构造函数对象值的继承,比想象中要复杂一点,根据代码实践(暂未查看标准),得出结论,引用类型的继承主要分为两种情况:

  1. 字面量定义的对象属性是深拷贝
  2. 变量赋值对象属性是引用复制

字面量定义的对象属性是深拷贝

这里的字面量定义的对象属性指的是,指的是直接在构造函数中通过 {} 的形式定义的对象直接赋值给 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 的构造函数定义的属性值的继承是通过复制继承的。第二点,副职的方式主要分为以下几重情形:

  1. 原始类型直接复制值到子类对象
  2. 引用类型的值如果值是直接在构造函数中定义的(包括字面量直接赋值给属性和在构造函数内定义的变量然后变量赋值给属性),那么其会被深拷贝到子类对象上
  3. 在构造函数外定义的变量,其值是引用类型,构造函数中将该变量赋值给对象的某个属性,该属性会被通过引用复制的方式拷贝到子类对象上

实例方法的继承

实例方法的继承比较好理解,通过原型链原型链继承的,只不过这个链的形式是一个比较直接的链。这条链的大概就像下面这个图

没错,图上那条红色的线就是 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

用关系图来表达上面的这些等式会更容易理解

转载请注明出处!

原文地址:https://www.cnblogs.com/sujinqu/p/11503663.html

时间: 2024-10-08 00:09:08

通过原型继承理解ES6 extends 如何实现继承的相关文章

【ES6】更易于继承的类语法

和其它面向对象编程语言一样,ES6 正式定义了 class 类以及 extend 继承语法糖,并且支持静态.派生.抽象.迭代.单例等,而且根据 ES6 的新特性衍生出很多有趣的用法. 一.类的基本定义 基本所有面向对象的语言都支持类的封装与继承,那什么是类? 类是面向对象程序设计的基础,包含数据封装.数据操作以及传递消息的函数.类的实例称为对象. ES5 之前通过函数来模拟类的实现如下: // 构造函数 function Person(name) { this.name = name; } //

[JavaScript原型继承理解一]

转:http://www.cnblogs.com/harolei/p/3740354.html 对于JavaScript的继承和原型链,虽然之前自己看了书也听了session,但还是一直觉得云里雾里,不禁感叹JavaScript真是一门神奇的语言.这次经过Sponsor的一对一辅导和自己回来后反复思考,总算觉得把其中的精妙领悟一二了. 1. JavaScript创建对象 在面向对象语言中,通常通过定义类然后再进行实例化来创建多个具有相同属性和方法的对象.但是在JavaScript中并没有类的概念

04面向对象编程-02-原型继承 和 ES6的class继承

1.原型继承 在上一篇中,我们提到,JS中原型继承的本质,实际上就是 "将构造函数的原型对象,指向由另一个构造函数创建的实例". 这里,我们就原型继承的概念,再进行详细的理解.首先回顾一下之前的一个示例,Student构造函数 和 原型链: function Student(props) { this.name = props.name || 'Unnamed'; } Student.prototype.hello = function () { alert('Hello, ' + t

通过JavaScript原型链理解基于原型的编程

零.此文动机 用了一段时间的Lua,用惯了Java C++等有Class关键字的语言,一直对Lua的中的面向对象技术感到费解,一个开源的objectlua更是看了n遍也没理解其中的原理,直到看到了Prototype-based programming 一.什么是基于原型的编程 基于原型的编程是面向对象编程的一种形式,通过复制已经存在的原型对象来实现面向对象,无与基于类的编程较大的区别是没有Class关键字,但是有类的概念.基于原型的编程也可以理解成基于实例的编程. 基于原型的系统可以在程序运行时

js重新讲解继承,es5的一些继承,es6继承的改变 ----------由浅入深

es5 利用原型公有私有继承 function Parent(name) { this.name = name } Parent.prototype.home = '北京'; function Child() { this.age = 8; } //将父类的实例绑定在子类的原型上 Child.prototype = new Parent('aa'); //实例化原型 let child = new Child(); //这时候继承父类的公有私有属性 console.log(child.home+

es5继承和es6类和继承

es6新增关键字class,代表类,其实相当于代替了es5的构造函数 通过构造函数可以创建一个对象实例,那么通过class也可以创建一个对象实列 /* es5 创建一个person 构造函数 */ function person (name,age) { this.name = name this.age = age } /* 定义原型链上的方法sayholle */ /* 为什么要将方法定义在原型上,定义在原型上的方法,所有的实例对象都共享 不会出现没实列一个对象都重新创建一个这个方法 */

JS面向对象组件 -- 继承的其他方式(类式继承、原型继承)

继承的其他形式: •类式继承:利用构造函数(类)继承的方式 •原型继承:借助原型来实现对象继承对象 类 : JS是没有类的概念的 , 把JS中的构造函数看做的类 要做属性和方法继承的时候,要分开继承. function Aaa(){ //父类 this.name = "小明"; } Aaa.prototype.showName = function(){ alert( this.name ); }; function Bbb(){ //子类 } Bbb.prototype = new

谈谈我对JS原型的理解

昨天阿里实习的第一次电面,也是我人生中的第一次电面,问了很多问题.结果还行吧,算是进入了下一轮.虽然不知道姓名,但还是要感谢面我的那个前辈.好吧,言归正传,为什么要写这篇关于原型的博文呢?因为电面时被问到了.当时有点紧张,感觉回答的很不理想,也许是自己还没有牢固的掌握吧!所以今天就写一写我对原型的理解,顺便理一下自己的思路. 首先,JS没有类继承机制,它是靠原型机制实现继承的,两种方式孰优孰劣,在此不做评判(知识量不足╮(╯▽╰)╭) 先上代码解释这一机制: var people = { nam

PHP面向对象三大特点学习(充分理解抽象、封装、继承、多态)

PHP面向对象三大特点学习 学习目标:充分理解抽象.封装.继承.多态 面象对向的三大特点:封装性.继承性.多态性 首先简单理解一下抽象:我们在前面定义一个类的时候,实际上就是把一类事物共有的属性和行为提取出来,形成一个物理模型(模版),这种研究问题的方法称为抽象 一.封装性 封装就是把抽取出来的数据和对数据的操作封装在一起,数据被保护在内部,程序的其他部分只有被授权的操作(方法)才能对数据进行操作. php提供了三种访问控制修饰符 public 表示全局,本类内部,类外部,子类都可以访问 pro