JS 继承

2023-09-20 21:24:50

一、继承是什么?

继承(inheritance)是面向对象软件技术当中的一个概念。
如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”

  • 继承的优点:

继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码

在子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能

虽然JavaScript并不是真正的面向对象语言,但它天生的灵活性,使应用场景更加丰富

关于继承,我们举个形象的例子:

定义一个类(Class)叫汽车,汽车的属性包括颜色、轮胎、品牌、速度、排气量等

class Car{
    constructor(color,speed){
        this.color = color
        this.speed = speed
        // ...
    }
}

由汽车这个类可以派生出“轿车”和“货车”两个类,在汽车的基础属性上,为轿车添加一个后备厢、给货车添加一个大货箱

// 货车
class Truck extends Car{
    constructor(color,speed){
        super(color,speed)
        this.Container = true // 货箱
    }
}

这样轿车和货车就是不一样的,但是二者都属于汽车这个类,汽车、轿车继承了汽车的属性,而不需要再次在“轿车”中定义汽车已经有的属性

在“轿车”继承“汽车”的同时,也可以重新定义汽车的某些属性,并重写或覆盖某些属性和方法,使其获得与“汽车”这个父类不同的属性和方法

class Truck extends Car{
    constructor(color,speed){
        super(color,speed)
        this.color = "black" //覆盖
        this.Container = true // 货箱
    }
}

从这个例子中就能详细说明汽车、轿车以及卡车之间的继承关系

二、继承实现的方式

2.1 原型链继承

原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针

function Person() {
  this.name = 'aaa';
  this.age = 18;
  this.friends = [];
}
// 原型上添加方法
Person.prototype.eat = function () {
  console.log(this.name + 'eating');
}


function Student() {
  this.sno = 111;
}
// 给student构造函数上继承一个父类
Student.prototype = new Person();
// 原型添加方法
Student.prototype.studying = function () {
  console.log(this.name + 'studying');
}

上面代码看似没问题,实际存在潜在问题


let stu = new Student()
let stu2 = new Student()

// 弊端一: 打印实例,无法获取到继承到数据
console.log(stu);

// 弊端二: 给父类friends添加kobe,stu和stu2都会打印出kobe,原因为给父类添加的数据,索引两个实例都会在父类中查找到
stu.friends.push('kobe')  // 修改值 会修改原型上的 friends
// stu.name = 'xyh'          // 赋值 不会修改原型上的name,而是在实例上创建name
console.log(stu.friends);
console.log(stu2.friends);

// 弊端三: 不能传递参数
let stu3 = new Student('jlc', 19, ['wyc'], 333)
console.log(stu3);

解决方式:

我们只需要在student构造函数中添加这行代码即可

function Student(name, age, friends, sno) {
	Person.call(this, name, age, friends); // 继承父类的方法  将student中的this传递给person中去继承属性
	this.sno = sno;
}

更新后的完整代码:

function Person(name, age, friends) {
  this.name = name;
  this.age = age;
  this.friends = friends;
}
Person.prototype.eat = function () {
  console.log(this.name + 'eating');
}


function Student(name, age, friends, sno) {
  Person.call(this, name, age, friends); // 继承父类的方法  将student中的this传递给person中去继承属性
  this.sno = sno;
}
// 给 人  赋值给 学生构造函数
Student.prototype = new Person();

Student.prototype.studying = function () {
  console.log(this.name + 'studying');
}

let stu = new Student('wyc',19, ['xyh'], 111)
let stu2 = new Student('xyh',16, ['wyc'], 222)

// 解决弊端一: 打印实例,无法获取到继承到数据
console.log(stu);

// 解决弊端二: 给父类friends添加kobe,stu和stu2都会打印出kobe,原因为给父类添加的数据,索引两个实例都会在父类中查找到
stu.friends.push('kobe')  // 修改值 会修改原型上的 friends
// stu.name = 'xyh'          // 赋值 不会修改原型上的name,而是在实例上创建name
console.log(stu.friends);
console.log(stu2.friends);

// 解决弊端三: 不能传递参数
let stu3 = new Student('jlc', 19, ['wyc'], 333)
console.log(stu3);

2.2 原型式继承

新对象的原型指向 obj 对象

三种实现:

let obj = {
  name: 'wyc',
  age: 19,
  friends: ['xhy']
}

// 实现方式一
function createObject(o) {
  var newObj = {}
  Object.setPrototypeOf(newObj, o)
  return newObj;
}
// 实现方式二
function createObject2(o) {
  function foo() {}
  foo.prototype = o;
  let newObj = new foo()
  return newObj;
}
// let fn = createObject2(obj);
// 实现三
let fn = Object.create(obj)
let fn1 = Object.create(obj)


// 向基本数据类型添加数据
fn.name = 'xyh'
// 向引用数据类型添加数据
fn.friends.push('jlc')

console.log(fn);
console.log(fn.__proto__);
console.log(fn1);
console.log(fn1.__proto__);

这种继承方式的缺点也很明显,因为Object.create方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能

在这里插入图片描述

2.3 构造函数继承

function Parent(){
  this.name = 'parent1';
}

Parent.prototype.getName = function () {
  return this.name;
}

function Child(){
  Parent.call(this);
  this.type = 'child'
}

let child = new Child();
console.log(child);  // 没问题
console.log(child.__proto__);  // 原型上并没有getName方法
console.log(child.getName());  // 会报错

在这里插入图片描述
可以看到,父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法

相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法

2.4 组合继承

function Parent3 () {
  this.name = 'parent3';
  this.play = [1, 2, 3];
}

Parent3.prototype.getName = function () {
  return this.name;
}
function Child3() {
  // 第二次调用 Parent3()
  Parent3.call(this);
  this.type = 'child3';
}

// 第一次调用 Parent3()
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);  // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'

这种方式看起来就没什么问题,方式一和方式二的问题都解决了,但是从上面代码我们也可以看到Parent3 执行了两次,造成了多构造一次的性能开销

2.5 寄生式继承

寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法

let parent5 = {
    name: "parent5",
    friends: ["p1", "p2", "p3"],
    getName: function() {
        return this.name;
    }
};

function clone(original) {
    let clone = Object.create(original);
    clone.getFriends = function() {
        return this.friends;
    };
    return clone;
}

let person5 = clone(parent5);

console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]

其优缺点也很明显,跟上面讲的原型式继承一样

2.6 寄生组合式继承

寄生组合式继承,借助解决普通对象的继承问题的Object.create 方法,在前面几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式

function clone (parent, child) {
    // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;
}

function Parent6() {
    this.name = 'parent6';
    this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
    return this.name;
}
function Child6() {
    Parent6.call(this);
    this.friends = 'child5';
}

clone(Parent6, Child6);

Child6.prototype.getFriends = function () {
    return this.friends;
}

let person6 = new Child6();
console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6}
console.log(person6.getName()); // parent6
console.log(person6.getFriends()); // child5

可以看到 person6 打印出来的结果,属性都得到了继承,方法也没问题

2.7 extends 方法

文章一开头,我们是使用ES6 中的extends关键字直接实现 JavaScript的继承

class Person {
  constructor(name) {
    this.name = name
  }
  // 原型方法
  // 即 Person.prototype.getName = function() { }
  // 下面可以简写为 getName() {...}
  getName = function () {
    console.log('Person:', this.name)
  }
}
class Gamer extends Person {
  constructor(name, age) {
    // 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
    super(name)
    this.age = age
  }
}
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法

利用babel工具进行转换,我们会发现extends实际采用的也是寄生组合继承方式,因此也证明了这种方式是较优的解决继承的方式

更多推荐

Qt5开发及实例V2.0-第九章-Qt文件及磁盘处理

Qt5开发及实例V2.0-第九章-Qt文件及磁盘处理第9章Qt5文件及磁盘处理9.1读写文本文件9.1.1QFile类读写文本9.1.2QTextStream类读写文本9.2读写二进制文件9.3目录操作与文件系统9.3.1文件大小及路径获取实例9.3.2文件系统浏览9.4获取文件信息9.5监视文件和目录变化本章相关例程

基于Java+SpringBoot+vue前后端分离校园周边美食探索分享平台设计实现

博主介绍:✌全网粉丝30W+,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌🍅文末获取源码联系🍅👇🏻精彩专栏推荐订阅👇🏻不然下次找不到哟2022-2024年最全的计算机软件毕业设计选题

Neutron — DHCP Agent 实现原理

目录文章目录目录DHCPDHCP协议格式DHCP报文类型DHCP协议流程DHCPAgent关键配置dnsmasq服务进程高可用方案DHCPDHCP(DynamicHostConfigurationProtocol,动态主机配置协议)用于当Host加入一个L3网络的时候动态的从一个IPPool中为Host租用一个IP地址

系统架构设计师(第二版)学习笔记----需求工程

【原文链接】系统架构设计师(第二版)学习笔记----需求工程文章目录一、需求定义1.1需求包含的内容1.2软件需求的3个不同层次1.3需求工程的阶段1.4需求管理的主要内容二、需求获取2.1需求获取的基本步骤2.2需求获取方法2.3需求讨论会参与人员2.4专题讨论会的优点三、需求变更3.1需求变更管理过程3.2需求变更

上海亚商投顾:沪指震荡调整 两市成交金额跌破6000亿

上海亚商投顾前言:无惧大盘涨跌,解密龙虎榜资金,跟踪一线游资和机构资金动向,识别短期热点和强势个股。一.市场情绪三大指数昨日集体调整,创业板指续创3年多以来新低。ST板块继续走强,*ST柏龙、ST恒久等十余股涨停。华为产业链午后活跃,捷荣技术涨停,股价创出历史新高。减肥药概念股逆势走强,翰宇药业20cm涨停。脑机接口概

Django(18):中间件原理和使用

目录概述Django自带中间件Django的中间件执行顺序自定义中间件函数使用类其它中间件钩子函数process_viewprocess_exceptionprocess_template_response如何使用这3个钩子函数?全局异常处理小结概述中间件(middleware)是一个镶嵌到Django的request

网络爬虫-----http和https的请求与响应原理

目录前言简介HTTP的请求与响应浏览器发送HTTP请求的过程:HTTP请求主要分为Get和Post两种方法查看网页请求常用的请求报头1.Host(主机和端口号)2.Connection(链接类型)3.Upgrade-Insecure-Requests(升级为HTTPS请求)4.User-Agent(浏览器名称)5.Ac

Spring Cloud Gateway快速入门(一)——网关简介

文章目录前言一、什么是网关1.1gateway的特点1.2为什么要使用gateway二、使用Nginx实现网关服务什么是网关服务?为什么选择Nginx作为网关服务?如何使用Nginx实现网关服务?1.安装Nginx2.配置Nginx3.启动Nginx4.测试网关服务总结代码编写三、使用Gateway实现网关服务什么是网

【Java 基础篇】Java后台线程和守护线程详解

在Java多线程编程中,有两种特殊类型的线程:后台线程(DaemonThread)和守护线程(DaemonThread)。这两种线程在一些特定的场景下非常有用,但也需要谨慎使用。本文将详细介绍后台线程和守护线程的概念、特性、用法,以及注意事项。什么是后台线程和守护线程?后台线程(DaemonThread)后台线程是一种

数组和指针笔试题解析之【指针】

目录🍂笔试题1:🍂笔试题2:🍂笔试题3:🍂笔试题4:🍂笔试题5:🍂笔试题6:🍂笔试题7:🍂笔试题8:🍂笔试题1:intmain(){inta[5]={1,2,3,4,5};int*ptr=(int*)(&a+1);printf("%d,%d",*(a+1),*(ptr-1));return0;}运行结

CTF 全讲解:[SWPUCTF 2022 新生赛]webdog1__start

文章目录参考环境题目learning.php信息收集isset()GET请求查询字符串全局变量$_GETMD5绕过MD5韧性脆弱性md5()弱比较隐式类型转换字符串连接数学运算布尔判断相等运算符MD5绕过科学计数法前缀0E与0e绕过start.php信息收集头部检索f14g.php信息收集探秘F1l1l1l1l1lag

热文推荐