以JS基础知识来解释JS的继承

168 阅读5分钟

假如有一天,你的男朋友在学习JS的时候折磨你,如何以她所知道的JS基础知识,来讲解JS的继承?

JS的继承有什么用,为什么有继承这种概念?

function Animal(name) {
    this.name = name;
}
Animal.prototype.eat = function() {
    console.log(this.name + ' eat')
}

function Cat(name) {
    this.name = name;
    this.subname = 'cat ' + name;
}
Cat.prototype.eat = function() {
    console.log(this.name + ' eat')
}

假如需要定义一个Animal类,同时要定义一个和Animal类有相同的属性和方法的Cat类。

就可以让Cat类继承Animal类的属性和方法,如果更改了Animal类的方法,Cat也会继承更改后的方法和属性。

可以让你减少很多代码工作量。

new一个对象的时候做了什么?

先回顾一下,new一个对象时做了什么,以下是一个简单的例子

function Person (name,age) {
    this.name = name;
    this.age = age;
}
Person.prototype.getName = function(){
    console.log('my name is ' + this.name);
}
 
let person = new Person("xiaohei",18);
console.log(person);
person.getName();//my name is xiaohei

使用关键字new创建新实例对象经过了以下几步:

  1. 创建一个新对象
  2. 将新对象的_proto_指向构造函数的prototype对象(让实例对象可以访问原型对象上的方法)
  3. 将构造函数的作用域赋值给新对象 (将this指向新对象)
  4. 执行构造函数中的代码(将属性添加到实例对象上面)
  5. 返回新的对象
function Person (name,age) {
    this.name = name;
    this.age = age;
}
Person.prototype.getName = function(){
    console.log('my name is ' + this.name);
}
function createPerson(name,age){
    let obj = {};// 创建一个新对象
    obj.__proto__ =  Person.prototype;//将新对象的_proto_指向构造函数的prototype对象
    Person.call(obj,name,age,sex);//改变this指向,并执行函数内代码
    return obj;//返回对象
}
let person = new Person("xiaohei", 18);
let person2 = createPerson("xiaobai", 19);
console.log(person,person2);

//  person = {
//    name: 'xiaohei',
//    age: 18,
//    __proto__: Person.prototype,
//  }

//  person2 = {
//    name: 'xiaobai',
//    age: 19,
//    __proto__: Person.prototype,
//  }

new一个对象,就等于调用了createPerson方法来创建对象

JS的几种继承

类式继承

function Animal(name) {
    this.name = name;
    this.age = 18;
}
Animal.prototype.eat = function(){
    console.log(this.name +' eat')
}
function Cat(name) {
    this.subname = 'cat '+ name;
}
Cat.prototype = new Animal();
let catA = new Cat('xiaohei');
console.log(catA.name); //undefined
console.log(catA.age); //18
console.log(catA.subname); //'cat xiaohei'
catA.eat(); //undefined eat

以上就是一个类式继承的例子,把Cat.prototype指向一个Animal的实例对象

Cat类继承了Animal类的eat方法,Cat类的实例对象catA有age属性,但是name属性为undfined

下面用伪代码来分析一下,以上代码做了什么

`Cat.prototype = new Animal();`

//模拟new Animal()的过程
let obj = {};
obj.__proto__ = Animal.prototype;
Animal.call(obj); // 由于没有传入参数,等于执行了obj.name = undefined
Cat.prototype = obj;

//可得到Cat.prototype为以下结构对象
 Cat.prototype = {
     name: undefined, //要从构造函数Animal()传参
     age: 18,
     __proto__: {
         eat() {
            console.log(this.name +' eat')
        }
     }
}

`let catA = new Cat('xiaohei');`

//模拟new Cat('xiaohei')的过程
let obj = {};
obj.__proto__ = Cat.prototype;
Cat.call(obj, 'xiaohei'); //obj.subname = 'cat '+ 'xiaohei'
catA = obj;

//可得到catA为以下结构对象
 catA = {
     subname: 'cat xiaohei',
     __proto__: {
         name: undefined, //要从构造函数Animal()传参
         age: 18,
         __proto__: {
             eat() {
                console.log(this.name +' eat')
            }
         }
    }
}

通过 Cat.prototype = new Animal();

let catA = new Cat('xiaohei');

得到的catA对象没有name属性

name属性需要从构造函数Animal()传入参数,而类式继承的过程中调用Animal()构造函数是没有传入参数

catA对象的name和age属性都是在__proto__上的,也就是共有的属性,如果是引用类型的属性,修改了会影响到其他实例

以下是一个引用类型属性共享的例子

function Animal() {
    arr: [1,2,3]
}
function Cat() {}
Cat.prototype = new Animal();
let catA = new Cat();
let catB = new Cat();

`可得到catA catB为以下结构对象
 {
     __proto__: {
         arr: [1,2,3],
         __proto__: {}
    }
}`
catA.arr.push(666); //修改了__proto__上的引用类型的属性
console.log(catB.arr); //[1,2,3,666]

以上就是类式继承的特点,会有两个问题:

  1. 父构造函数不能传参
  2. 引用类型属性共享

构造器继承

function Animal(name) {
    this.name = name;    
}
Animal.prototype.eat = function () {
    console.log(this.name + ' eat');
}
function Cat(name) {
    Animal.call(this, name);
    this.subname = 'cat '+ name;
}
let cat = new Cat('xiaohei');
console.log(cat.name, cat.subname); //xiaohei  cat xiaohei
cat.eat(); //cat.eat is not a function

以上就是构造器继承,在构造函数Cat()中执行父构造函数Animal(),实例化时将父构造函数的属性复制到自身上,但是会有问题:

  1. 不会继承父构造函数原型上的方法

组合继承

组合继承将类式继承和构造器继承组合在一起,实现属性和方法的继承

function Animal(name) {
    this.name = name;
    this.age = 22;
}
Animal.prototype.eat = function () {
    console.log(this.name + ' eat');
}
function Cat(name) {
    Animal.call(this, name);
    this.subname = 'cat ' + name;
}
Cat.prototype = new Animal();
let cat = new Cat('xiaohei');
console.log(cat.name); //xiaohei
console.log(cat.age); //22
console.log(cat.subname); //cat xiaohei
cat.eat(); //xiaohei eat

用伪代码来分析一下

`Cat.prototype = new Animal();`

//用伪代码模拟new Animal()
let obj = {};
obj.__proto__ = Animal.prototype;
Animal.call(obj); // 没有传入参数,等于执行了 this.name = undefined; thia.age = 22
Cat.prototype = obj;

//可得到Cat.prototype为以下结构的对线
Cat.prototype = {
    name: undefined, //没有传入参数
    age: 22,
    __proto__: {
        eat() {
            console.log(this.name + ' eat');
        }
    }
}

`let cat = new Cat('xiaohei');`

//用伪代码模拟new Cat('xiaohei')
let obj = {};
obj.__proto__ = Cat.prototype;
Cat.call(obj,'xiaohei');
let cat = obj;

//可得到Cat.prototype为以下结构的对象
let cat = {
    name: 'xiaohei',
    age: 22,
    subname: 'cat xiaohei',
    __proto__: {
        name: undefined, // 没有传入参数
        age: 22,
        __proto__: {
            eat() {
                console.log(this.name + ' eat');
            }
        }
    }
}

以上就是得到的cat实例对象的结构,Cat类继承了Animal类的属性和方法

但组合继承有两个问题:

  1. 实例化cat时,执行了两次父构造函数的方法
  2. cat对象的__proto__里面的属性没有意义

寄生组合继承

function Animal(name) {
    this.name = name;
    this.age = 20;
}
Animal.prototype.eat = function() {
    console.log(this.name + ' eat');
}
function Cat(name) {
    Animal.call(this,name);
    this.subname = 'cat ' + name;
}
function inherit(subClass,superClass){
    function F(){};
    F.prototype = superClass.prototype;
    subClass.prototype = new F();
    subClass.prototype.constructor = subClass;
}
inherit(Cat,Animal);
let cat = new Cat('xiaohei');
console.log(cat); //{name: 'xiaohei', age: 20, subname: 'cat xiaohei'}
cat.eat(); //xiaohei eat

经过inherit()方法,使用空的构造函数F()去实例化对象,这样就不会像组合继承那样复制父类的属性,并能复制父类原型上的方法

惯例,上伪代码~

`inherit(Cat,Animal);`

//用伪代码模拟inherit(Cat,Animal)
function F(){}
F.prototype = Animal.prototype;
let obj = {};
obj.__proto__ = F.prototype;
F.call(obj);
Cat.prototype = obj;
Cat.prototype.constructor = Cat;

//可得到Cat.prototype为以下结构的对象
Cat.prototype = {
    __proto__: {
        eat(){
            console.log(this.name + ' eat');
        }
    }
}

`let cat = new Cat('xiaohei');`

//用伪代码模拟 new Cat('xiaohei');
let obj = {};
obj.__proto__ = Cat.prototype;
//Cat.call(obj,'xiaohei');
Animal.call(obj,'xiaohei');
obj.subname = 'cat ' + 'xiaohei';
let cat = obj;
//可得到cat为以下结构的对象
let cat = {
    name: 'xiaohei',
    age: 20,
    subname: 'cat xiaohei',
    __proto__: {
         __proto__: {
            eat(){
                console.log(this.name + ' eat');
            }
        }
    }
   
}

以上的继承方式没有上面那些方式的缺点,能够把父类的属性和方法全部继承,一般认为是最好的继承方式

extends关键字

extends就是寄生组合继承的一种语法,在react中经常会用到

class Animal {
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    eat(){
        console.log(this.name + ' eat')
    }
}
class Cat extends Animal {
    constructor(name,age){
        super(name,age); // 相当于Animal.call(this,name,age)
        this.subname = 'cat ' + name;
    }
}
var cat = new Cat('xiaohei',18);

// cat为以下结构的对象,和寄生组合继承得到的对象是一样的结构
`let cat = {
    name: 'xiaohei',
    age: 18,
    subname: 'cat xiaohei',
    __proto__: {
         __proto__: {
            eat(){
                console.log(this.name + ' eat');
            }
        }
    }
}`

Cat类可以继承Animal类的所有属性和方法,实例化后和寄生组合继承得到的对象是一样的结构