【JavaScript】面向对象继承

2023-12-27 12:07:46

一、前言

??对象编程很重要的一方面,就是对象的继承。A对象通过继承B对象,就能直接拥有B对象的属性和方法。这对于代码的复用是非常有用的。
??大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript语言的继承不通过class(ES6引入了class语法),而是通过“原型对象”(prototype)实现。

二、问题是最好的老师

在正题开始之前,先问自己两个问题:
1. 继承解决了什么问题?为什么要使用继承?
2. 如何实现继承?

1、为什么要使用继承

通过以下图片我们可以看出不同的类,存在着大量的重复代码,这样导致难以维护。所以需要进行封装,需要继承
在这里插入图片描述

2、如何实现继承

function Person(name,age){
  this.name=name
  this.age=age
}
Person.prototype.say=function(){
  console.log(this.name,"hello")
}

//继承
function Student(name,age,grade){
  // Person.call(this,name,age)
  Person.apply(this,[name,age])
  this.grade=grade
}

//原型继承
Student.prototype = new Person()
//在继承的基础上增加方法
Student.prototype.printGrade = function(){
  console.log(this.grade,100)
}

上面代码,存在两个类(Person,Student),那么二者在内存中的表现如何:

在这里插入图片描述

类与类之间相互独立,毫无相关。那么应该怎么产生关联呢?

在这里插入图片描述

上图表示,需要创建一个中间对象,那么改中间对象的作用:

  1. 子类的函数原型对象指向中间对象
  2. 中间对象的原型指向父类的函数原型对象

最后的结果就是子类和父类间接挂钩,继承也就在此诞生。好啦,问了自己两个问题并且搞清楚之后,正题就可以开始啦。

三、面向对象继承方式

1、借用构造函数继承

子类直接调用父类函数

function Person(name,age){
  this.name=name
  this.age=age
}
Person.prototype.say=function(){
  console.log(this.name,"hello")
}

//继承
function Student(name,age,grade){
  //call、apply都可以借用父类的代码
  // Person.call(this,name,age)
  Person.apply(this,[name,age])
  this.grade=grade
}

1)、优点

解决了传递参数的问题

2)、缺点

仅仅继承了父类的属性,并没有继承原型上的方法

2、原型链继承

将子类的函数原型(prototype)指向父类的函数原型(prototype),这就是原型链继承

Student.prototype = Person.prototype 

是以上这种形式的吗?当然不是,这样写的话,针对一个子类也还行,但是存在多个子类。都是使用了同一个原型(即父类的原型),那么子类与子类之间就会相互影响。那么该如何写呢?

//原型继承
Student.prototype = new Person()
//在继承的基础上增加方法
Student.prototype.printGrade = function(){
  console.log(this.grade,100)
}

//拆分来写
var obj = new Person()
Student.prototype = obj

创建一个实例对象obj,obj就是所谓的中间对象,那么子类的原型指向它,子类与子类之间就不会相互影响。

1)、完整代码

function Person(name,age){
  this.name = name
  this.age = age
}
Person.prototype.say=function(){
  console.log(this.name,"hello")
}

//继承
function Student(name,age,grade){
  // Person.call(this,name,age)
  Person.apply(this,[name,age])
  this.grade = grade
}

//原型继承
Student.prototype = new Person()
//在继承的基础上增加方法
Student.prototype.printGrade = function(){
  console.log(this.grade,100)
}
var obj = new Student("xiaoming",18,100)
console.log(obj)

obj.say()
obj.printGrade()

2)、缺点

  1. 参数不好传递(子类的name不好传递到父类中)
  2. 子类的实例对象类型错误,继承的name属性也没有显示

类型错误的原因:Student.prototype.constructor.name,由于本身丢失,中间对象又没有constructor属性,就会寻找到Person的函数原型上,所以类型为Person

3、组合继承(原型链+借用构造函数)

&emps;&emps;为了解决原型链继承中存在的问题,开发人员提供了一种新的技术:constructor stealing(有很多名称:借用构造函数或者称之为经典继承或者称之为伪造对象)

借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数

  • 因为函数可以在任意的时刻被调用,因此通过call()和apply()方法也可以在新创建的对象上执行构造函数
function Person(name,age){
  this.name=name
  this.age=age
}
Person.prototype.say=function(){
  console.log(this.name,"hello")
}

//继承
function Student(name,age,grade){
  // Person.call(this,name,age)
  Person.apply(this,[name,age])
  this.grade=grade
}

//原型继承
Student.prototype = new Person()
//在继承的基础上增加方法
Student.prototype.printGrade = function(){
  console.log(this.grade,100)
}

var obj = new Student("xiaoming",99,11)
console.log(obj)

obj.say()
obj.printGrade()

1)、优点

  1. 解决了传递参数的问题
  2. 解决了子类的类型不对的问题(指定子类的constructor)

2)、缺点

  1. 构造函数会被调用两次:一次在创建子类型原型对象的时候,一次在创建子类型实例的时候
  2. 父类型的属性(示例:name属性)会有两份:一份在中间对象中,一份在子类型示例中

4、原型式继承

&emps;&emps;这种模式要从道格拉斯·克罗克福德(Douglas Crockford,著名的前端大师,JSON的创立者)。在2006年写的一篇文章说起:Prototypal Inheritance in JavaScript(在JS中使用原型式继承)

怎么实现继承?需要中间对象,最开始我们就已经提及了。

那么是不是可以自己定义一个对象,使其_proto_指向另外一个对象呢?

可以定义一个函数,该函数接受一个对象作为参数,函数体内部创建一个新的对象,使其新对象的原型指向参数对象,然后返回新的对象。实现类似功能的函数,被称为原型式继承函数

1)、实现函数的三种方式

①、借用构造函数原型的指向(最早使用的方法)
 function createObject1(obj) {
     function Fn() {}
     Fn.prototype = obj  
     return new Fn()
 }
②、利用Object.setPrototypeOf可以改变原型
 function createObject2(obj) {
   var newObj = {};
   Object.setPrototypeOf(newObj, obj);  // newObj的原型指向obj
   return newObj;
 }
③、利用Object.create(),本质上内部代码实现跟方式一的实现是一样的 (最新版本ECMAScript提供)
const newObj = Object.create(obj)

既然有了原型式继承函数,那么就可以实现对象与对象之间的继承,类与类之间的继承了

2)、对象与对象之间的继承

 const Person = {
   name: "父类",
   play: function () {
     console.log("play~~");
   },
 };
 const obj = Object.create(Person);
 console.log(obj); // {}
 console.log(obj.__proto__); // { name: '父类', play: [Function: play] }

3)、类与类之间的继承

①、替换到原型链继承的一行代码
 Student.prototype = new Person()
 ?
 // 修改成:
 Student.prototype = Object.create(Person.prototype)
②完整代码
function Person(name,age){
  this.name=name
  this.age=age
}
Person.prototype.say=function(){
  console.log(this.name,"hello")
}

//继承
function Student(name,age,grade){
  // Person.call(this,name,age)
  Person.apply(this,[name,age])
  this.grade=grade
}

//原型继承
Student.prototype = Object.create(Person.prototype)
//在继承的基础上增加方法
Student.prototype.printGrade = function(){
  console.log(this.grade,100)
}

var obj = new Student("小明",18,100)
console.log(obj)

obj.say()
obj.printGrade()

4)、优点

创建中间对象简单,容易实现继承

5)、缺点

必须与其他方式的继承才能实现完成的继承

5、ES6-class继承

1)、class继承

ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比ES5的通过修改原型链实现继承要清晰和方便很多。

ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Person.apply(this)。ES6的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法,然后再用子类的构造函数修改this))
需要注意:class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的

//父类
class Person{
  //构造函数
  constructor(name,age){
    this.name = name
    this.age = age
  }

  say(){
    console.log(this.name,"hello")
  }
}

//子类
//extends 原型继承
class Student extends Person{
  constructor(name,age,grade){
    //super 超类
    super(name,age) //Person.call(this,name,age)
    this.grade = grade
  }
  say(){
    //如果想要先打印父类中的say方法,就需要使用super去调用父类中的say方法即可
    super.say()
    console.log(this.name,"您好")
  }
}
var obj = new Student("小明",100,100)
console.log(obj)
obj.say()

2)、优点

代码量少,思路清晰,便于理解

3)、缺点

浏览器兼容性的问题。ie10及以下都不支持。

ES6中使用到了super关键字,欢迎大家对[ES6]Class继承-super关键字这篇博客提出需要改善的建议

四、总结

在JavaScript中,面向对象继承有多种实现方式,每种方式都有其优缺点。选择合适的继承方式取决于具体的需求和场景。通过本文的介绍,希望各位读者能够更好的理解JavaScript中的面向对象继承,并能够灵活运用于实际开发中。有任何问题,可以通过评论进行交流~!

文章来源:https://blog.csdn.net/weixin_45490023/article/details/135236132
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。