简单来一场js 面向对象编程的谈话—— js 继承方式

203 阅读8分钟

前言

  在面向对象的思考方式的课程中,有提到,“万物皆是对象” ⾯向对象是⼀种思考⽅式,是对现实世界的⼀种解释,就像⼒学三⼤定律。函数式编程也是⼀种思考⽅式,就像计算机的逻辑基础与或⾮,没有任何⽅式可以完全描述整个世界,没有万能理论。

⾯向对象三⼤特性:

  • 封装:封装属性和⾏为、降低耦合、隐藏内部细节
  • 继承:利⽤不同层级的抽象概念,复⽤同类的属性和⾏为
  • 多态:同类⾏为的不同表现,⾏为⼀致,内部实现不⼀致

  对于面向对象编程(OOP),其实它就是用抽象方式创建的基于现实世界模型的一种编程模式。在今天,由于OOP 在编程中表现出更好的灵活性和可维护性,故很多流行的编程语言都支持OOP。而接下来会就 Javascript以及参考的一些文章来整理一下本人对面向对象的理解(暂不讨论ECMAScript 6)

  Javascript 的面向对象编程和大多数的其他语言的面向对象编程是不太一样的的,比如说提到java 或者 c# ,大家可能会马上想到面向对象的两个概念:类和实例

  • :类是对象的类型模板,例如,定义Student类来表示学生,类本身是一种类型,Student表示学生类型,但不表示任何具体的某个学生;
  • 实例:实例是根据类创建的对象,例如,根据Student类可以创建出xiaoming、xiaohong、xiaojun等多个实例,每个实例表示一个具体的学生,他们全都属于Student类型。

  但是,在javascript 里面,并不是通过类和实例来实现面向对象编程,而是利用原型(prototype)来实现面向对象。

一、封装

其实在js 里面函数即是一个对象,可以基于函数封装一个对象。示例,封装一个person 对象

// 封装 person
const Person = function (name, age) {
    // 属性
    this.name = name;
    this.age = age;
    
    // 方法
    // 跑步
    this.run = function () {
         console.log(`${this.name}在跑步`);
    }
    // 唱歌
    this.sing = function () {
        console.log(`${this.name}在唱歌`);
    }
}
// 实例化 小明
const xiaoming = new Person('小明', 5);
xiaoming.run(); // 小明在跑步
// 实例化 玲玲
const lingling = new Person('玲玲', 10);
lingling.sing(); // 玲玲在唱歌

二、继承

  在js 中,继承的实现主要是利用了原型(prototype),所谓继承关系不过是把一个对象的原型指向另一个对象而已,最终形成一条原型链。 JavaScript的原型链和Java的Class区别就在,它没有“Class”的概念,所有对象都是实例。

概念:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针,让这个原型对象(子的原型)等于要继承的引用类型(父)的实例,由于引用类型(父)的实例包含一个指向(父)原型对象的内部指针,以此类推,层层递进,便构成实例与原型的链条,即原型链。

1、原型链

  • 概念:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针,让这个原型对象(子的原型)等于要继承的引用类型(父)的实例,由于引用类型(父)的实例包含一个指向(父)原型对象的内部指针,以此类推,层层递进,便构成实例与原型的链条,即原型链。
  • 基本思想:利用原型让(子)引用类型继承(父)引用类型的属性和方法

基本模式的代码(参考红宝书):

默认原型:所有函数的默认原型都是Object实例,所以都会继承toString()valueOf()等默认方法

确定原型和实例的关系方法:

(1)通过使用instanceof()

如:alert(instance instanceOf Object); //true

(2)通过使用isPrototypeOf()

如:alert(Object.prototype.isPrototyOf(instance)); //true

注意点:

  • (1)给原型添加方法一定要放在替换原型的语句之后
  • (2)在通过原型链实现继承时,不能使用对象字面量创建原型方法

原型链存在的问题

  • (1)子类型的所有实例都可以共享父类型的属性
  • (2)子类型的实例无法在不影响所有对象的情况下,给父类型的构造函数传递参数

2、借用构造函数继承(伪造对象或经典继承)

基本思想:在子类型构造函数的内部调用超类型构造函数(通过使用apply()和call()方法也可以在(将来)新创建的对象上执行构造函数);

代码(参考红宝书):

存在问题:方法都在构造函数中定义,函数复用变得没有意义

补充:通过call()或者appla()调用父类型时,可以进行传参

3、组合继承(伪经典继承,原型链和借用构造函数技术组合)

基本思想:使用原型链实现对原型属性和方法的继承(主要想继承方法),而通过借用构造函数来实现对实例属性的继承(子类型的实例内部存在同名属性,从而对父类型的同名属性进行屏蔽);最后同时避免了原型链会被继承时会共享同一个父类型属性和借用构造函数的函数复用的缺陷

代码(参考红宝书):

缺点:

  两次调用父类构造函数:(第一次是在创建子类原型的时候,第二次是在子类构造函数内部);从而造成子类继承父类的属性,一组在子类实例上,一组在子类原型上(即在子类原型上创建不必要的多余的属性)

4、原型式继承

基本思想:借助原型可以基于已有的的对象创建新对象,即如在一个函数Object内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型(实际上执行了浅复制),最后返回这个临时类型的新实例。

代码(参考红宝书):

补充:

ES5新增的Object.create()方法拥有和object()方法同样的效果,但是Object.create()方法接受两个参数:

  • 一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性(会覆盖作新对象原型的对象上的同名属性)的对象

举例子如下:

5、寄生式继承

基本思想:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象

代码(参考红宝书):

缺点:不能做到函数复用导致降低效率

6、寄生组合式继承

  • 概念:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
  • 基本思想:没有必要为了指定子类型的原型而调用超类型的构造函数(函数复用),我们需要的是超类型原型的副本。
  • 本质:使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

代码(参考红宝书):

三、多态

多态(Polymorphism)是指由继承而产生的相关的不同的类,其对象对同一消息会做出不同的响应;实际上是不同对象作用与同一操作产生不同的效果。

多态的思想实际上是把“想做什么”和“谁去做“分开,多态的最根本好处在于,你不必再向对象询问“你是什么类型”而后根据得到的答 案调用对象的某个行为——你只管调用该行为就是了,其他的一切多态机制都会为你安 排妥当。

示例:比较说存在一个动物类,分别创建一个小鸡,一个小狗,动物可以发出叫声,但是他们的叫声其实是不一样的,但是可以对该方法进行重载实现多态

const Animal = function(name) {

this.animalName = name

this.makeSounds = function() {

Console.log(`${this.animalName}在叫`);
}
}
// 实例化小狗
const dog = new Animal('小狗');

// 重写小狗的叫声
dog.makeSounds = function () {

console.log(`${this.animalName}:汪汪汪`);
};
dog.makeSounds(); // 小狗:汪汪汪



// 实例化小狗
const chick = new Animal('小鸡');

// 重写小狗的叫声
chick.makeSounds = function () {

console.log(`${this.animalName}:吱吱吱`);
}
chick.makeSounds(); // 小鸡:吱吱吱

总结

⽤⾯向对象的⽅式思考问题、让代码更加灵活稳定 ⽤合乎现实的⽅式定义对象、让设计更加容易理解 ⽤接⼝约束不同对象的相同⾏为、逻辑清晰可扩展

参考文章