当前位置: 首页 > 工具软件 > sparrow-js > 使用案例 >

理解JavaScript概念系列--继承

廖琪
2023-12-01

思考一下

  1. 什么是JavaScript继承?
  2. 怎么实现JavaScript继承?
  3. JavaScript继承原理是什么?

面向对象编程(OOP)语言

面向对象编程(OOP)语言的三个个主要特性分别是“封装”,“继承”和“多态”。
封装: 将属性和方法放到一个“类”中的过程。
继承: 让子类拥有父类的属性和方法的过程。
多态: 多种(使用)形态,主要有重写(覆盖)和重载。

重写(覆盖)是子类对继承了来自父类中允许访问的方法的实现过程进行重写编写,即外壳不变核心改变。我们先看一下Java中如何实现继承和重写。

class Animal {
    public void move() {
        System.out.println("I am an Animal, I can move!");
    }
}
class Bird extends Animal {
    public void move() {
        System.out.println("I am a bird, I can fly!");
    }
}

public class BirdTest {
    public static void main(String args[]) {
        Animal animal = new Animal();
        animal.move();
        Animal bird = new Bird();
        bird.move();
    }
}

复制代码

运行结果

I am an Animal, I can move!
I am a bird, I can fly!
复制代码

重载(overloading)是指在一个类里面有多个方法(函数),他们的方法(函数)名相同,而参数不同。下面再看一下Java中实现方法的重载。

public class Person {
    public void say() {
        System.out.println("hello ");
    }
    public void say(name) {
        System.out.println("hello " + name);
    }
}

public static void main(String args[]) {
    Person person = new Person();
    person.say();
    person.say("world");
}
复制代码

运行结果

hello 
hello world!
复制代码

Java中实现类的继承以及方法的重写和重载是很方便的,因为这是语言本身天然的特点。JavaScript同样被认为是面向对象编程语言,但是在es6出现之前,类的“封装”,“继承”和“多态”特性方面的实现却和Java有着很大的差别。

下面我们看一下如何在JavaScript中实现OOP语言的三大特性。

JavaScript中的类

es5中确切的来说是没有“类”的概念,通常把构造函数当成面向对象编程语言中的“类”。函数与new结合使用时会被当成构造函数,其他情况下就是普通函数。

// es5
function Animal(name) {
    this.name = name;
}
Animal.prototype.sayName = function() {
    return this.name;
}
var animal = new Animal("Tom");
var name = animal.sayName(); // Tom
复制代码

es6中新增了class关键词,在语法方面有了“类”的概念,实质是对es5的构造函数封装的一层语法糖。

// es6
class Animal {
    constructor(name) {
        this.name = name;
    }
    sayName() {
        return this.name;
    }
}
var animal = new Animal("Jack");
var name = animal.sayName(); // Jack

typeof Animal; // "function"
Animal.prototype.constructor === Animal; // true
复制代码

es6中通过class定义的“类”,本质上就是es5中的构造函数。

JavaScript类之间的继承

在《JavaScript高级程序设计》这本书中,作者介绍了es5中6种实现js继承的方式。下面我们逐个分析一下实现过程。
1. 原型链继承

核心: 将父类的实例赋值给子类的prototype

function Animal(name) {
    this.name = name;
    this.weight = '20kg';
};
Animal.prototype.getName = function() { return this.name};
Animal.prototype.food = ['rice', 'wheat'];

function Bird() {};
// 实现原型链继承
Bird.prototype = new Animal("sparrow");
// 这个时候已经将Bird.prototype指向了新的对象实例,那么原有对象上的constructor属性已经无法访问,需手动为其添加constructor属性
Bird.prototype.constructor = Bird; 
// 此时Bird已经继承了Animal里面的属性和方法
var bird1 = new Bird();
var bird2 = new Bird();
var name = bird1.getName(); // "sparrow"
bird1.food; // 'rice', 'wheat'
bird1.food.push("fruit");
bird2.food; // 'rice', 'wheat', 'fruit'
复制代码

原型链继承过程很简单,看下面图示。

从上面例子可以看出原型链继承存在很多问题,比如:

  1. 所有子类实例的原型都共享同一个父(超)类实例的属性和方法,如果任何一个子类实例修改了原型链上属性的值,其他子类实例也会受到影响。
  2. 不能将参数传递到父类(构造函数)中。
  3. 无法实现多继承。

思考:var birdObj= new Bird("sparrow");这样可以为对象实例的name属性赋值吗?

在上面原型链继承中肯定是不行的,我们可以通过借用构造函数来实现。

2. 借用构造函数

核心: 使用call和apply借用其他构造函数(父类)的成员,等于是复制父类的实例属性给子类。

function Animal(name) {
    this.name = name;
    this.weight = '20kg';
};
Animal.prototype.getName = function() { return this.name};
Animal.prototype.food = ['rice', 'wheat'];

function Bird(name) {
    Animal.call(this, name)
}
var bird = new Bird("sparrow");
bird.name; // "sparrow"
bird.weight; // '20kg'
// bird.getName(); TypeError: bird1.getName is not a function
复制代码

借用构造函数继承过程也很简单,看下面图示。


借用父类构造函数继承特点是:

  1. 子类的实例可以拥有父类内部的属性,不能获得父类prototype上的属性。
  2. 创建子类实例时可以传递参数。
  3. 可以实现多继承(call多个父类)。
  4. 每个子类都有父类属性的副本,无法实现函数复用。

如果将原型链继承和借用构造函数继承结合在一起,那么就会发挥二者之长,这种方式就是组合继承。
3. 组合继承
核心: 使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现实例属性的继承。

// 
function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
    alert(this.name)
}
// 
funtion SubType(name, age) {
    SuperType.call(this, name); // 第二次调用SuperType
    this.age = age;
}
//
SubType.prototype = new SuperType(); // 第一次调用SuperType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
    alert(this.age);
}

var instance1 = new SubType('Jack', 23);
var instance2 = new SubType('Tom', 28);
instance1.colors.push('black');
console.log(instance1.colors); // 'red','blue','green','black'
console.log(instance2.colors); // 'red','blue','green'
instance1.sayName(); // 'Jack'
instance2.sayName(); // 'Tom'
复制代码

组合继承得到的子类实例对象之间的属性和方法相互不影响,是JavaScript中最常用的继承模式。此外,instanceofisPrototypeOf()能够用于识别基于组合继承创建的对象。
4. 原型式继承
核心: 借助原型基于已有的对象创建新对象,同时不必因此创建自定义类型

function object(o) {
    function F() {};
    F.prototype = o;
    return new F();
}
var person = {
    name: 'Jack',
    friends: ['rose', 'tomas']
}
var person1 = object(person);
// 等价于es5中出现的Object.create() 
// var person1 = Object.create(person);
var person2 = object(person);
console.log(person1.name); // Jack 此时访问的是F.prototype即person对象上面的name
console.log(person2.name); // Jack 此时访问的是F.prototype即person对象上面的name
console.log(person1.friends); // 'rose', 'tomas'
console.log(person2.friends); // 'rose', 'tomas'
person1.friends.push('van');
console.log(person2.friends); // 'rose', 'tomas','van'
person1.name = 'Rob'; // 此时为person1对象添加name属性,不影响其他实例
console.log(person2.name); // Jack 此时访问的是F.prototype即person对象上面的name
person1.__proto__.name = 'Bluse'; // F.prototype即person对象上面的name已经被修改
console.log(person.name); // 'Blues'
console.log(person2.name); // 'Blues'
复制代码

特点:
包含引用类型值的属性始终都会被所有实例共享,在实例上操作该属性也会影响到其他实例。

5. 寄生式继承
核心: 在原型继承基础上做了一层封装来增强对象

function createAnother(original) {
    var clone = object(original); // 通过调用函数创建一个新对象
    clone.sayHi = function() { // 以某种方式增强这个对象
        alert('hi')
    }
    return clone;
}
复制代码

6. 寄生组合式继承
核心: 将父类原型的副本赋值给子类的原型

// 核心函数
function inheritPrototype(subType, superType) {
    subType.prototype = object(superType.prototype);
    subType.prototype.constructor = subType;
}

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function() {
    alert(this.name);
}
function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
    alert(this.age);
}
复制代码

特点:
只调用了一次SuperType构造函数,避免了在SubType.prototype上面创建不必要的、多余的属性。可以正常使用instanceofisPrototypeOf();

es6中引入class关键词,从此JavaScript中正式有了“类”的概念,同时通过使用extends关键词可以很方便的实现继承,更加接近于Java继承模式。

Class的继承

// 父类
class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    getLocation() {
        return 'x:'+this.x+';y:'+this.y;
    }
}
// 子类
class ColorPoint extends Point {
    constructor(x, y, color) {
        super(x, y); // 调用父类的constructor(x, y)
        this.color = color;
    }
}

var cp = new ColorPoint(1.1, 2.2, 'red');
console.log(cp.getLocation()); // 'x:1.1;y:2.2'

cp instanceof ColorPoint; // true
cp instanceof Point; // true
复制代码

看下面Class继承图示。

需要注意的几个地方:
1.如果子类中没有定义 constructor方法,那么这个方法会被默认添加。
2.如果子类中有定义 constructor方法,那么必须结合 super()使用,且 super()需要出现在 this.xx之前。
3.使用Class结合extends实现继承和es5的继承在本质上有些不一样。在es5中每个对象实例都有 __proto__属性,指向对应构造函数的 prototype属性。Class作为构造函数的语法糖同时拥有 __proto__属性和 prototype属性,因此同时存在两条继承链:

子类的__proto__属性表示构造函数的继承,总是指向父类。
子类prototype属性的__proto__属性表示方法的继承,总是指向父类的prototype属性

到此,从JavaScript中最基础/原始的继承到es6中的继承已经介绍完了。类之间实现继承之后还有一些问题需要我们思考,比如:

Js中如何实现重写和重载?

JavaScript中若存在重名(变量)函数,那么下面将会被覆盖上面。所以实现函数的重写只需要在子类中重写定义一个与父类相同的函数名即可。那么如何在持有父类中函数逻辑的情况下添加新逻辑呢?

// es5
function Person(name) {
    this.name = name;
}
Person.prototype.introSelf = function() {
    console.log('My name is '+ this.name);
}

function Student(name, age) {
    Person.call(this, name);
    this.age = age;
    this.introSelf = function() {
        // 通过使用call把父类中的方法逻辑在子类中调用一遍
        Person.prototype.introSelf.call(this); 
        console.log("I am " + this.age + " years old!");
    }
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
var p1 = new Person('Jack');
p1.introSelf(); // My name is Jack
var s1 = new Student('LiMing', 18);
s1.introSelf(); // My name is LiMing, I am 18 years old!

//es6
class Person {
    constructor(name) {
        this.name = name;
    }
    introSelf() {
        console.log('My name is '+ this.name);
    }
}

class Student extends Person {
    constructor(name, age) {
        super(name);
        this.age = age;
    }
    introSelf() {
        // 通过使用call把父类原型中的方法逻辑在子类中调用一遍
        Person.prototype.introSelf.call(this);
        console.log("I am " + this.age + " years old!");
    }
}
var p1 = new Person('Jack');
p1.introSelf(); // My name is Jack
var s1 = new Student('LiMing', 18);
s1.introSelf(); // My name is LiMing, I am 18 years old!
复制代码

关于JavaScript实现重载有一篇文章总结的很好,请看JavaScript中的函数重载(Function overloading)

现在,你知道文章开头那几个问题怎么回答了吗?

总结

JavaScript主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样,子类型就能够访问超类型的所有属性和方法。原型链的问题是对象实例共享所有继承的属性和方法,因此不适宜单独使用。解决这个问题的技术是借用构造函数,即在子类型构造函数的内部调用超类型的构造函数。这样就可以做到每个实例都具有自己的属性,同时还能保证只使用构造函数模式来定义类型。使用最多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。
原型式继承,可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制。而复制得到的副本还可以得到进一步改造。
寄生式继承,与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。
寄生组合式继承,集寄生式继承和组合继承的有点于一身,是实现基于类型继承的最有效方式。
Class继承,ES6中提供新的继承方式,简单且方便,相对于基于原型链的继承在本质上稍有改变,即在类上面添加了__proto__属性。

 类似资料: