【js进阶2】原型和继承

JavaScript | 2020-07-22 14:31:22 1102次 5次

一、概念

构造函数、原型、原型链三者紧密相联,先看如下代码:

//构造函数
function Person(name){
    this.name = name
    this.init()
}

Person.prototype.init = function(){
    console.log(this.name, '==')
}

let person1 = new Person('person1')
console.log(person1)

创建一个构造函数 Person,其原型 Person.prototype 上有一个 init 方法,实例化之后打印这个对象可以看到如下信息:

Person {name: "person1"}
    name: "person1"
    __proto__:
        init: ƒ ()
        constructor: ƒ Person(name)
        __proto__: Object

其中通过原型链 __proto__ (其实是内部方法,不过后来被开放出来,但是还是不要直接使用,Object.getPrototypeOf(obj))可以找到原型上的方法 init,他们的关系表面上大致如此,放出我珍藏多年的一张图:

1.png

这张图也解释了如下的问题:

Object instanceof Function 	// true
Function instanceof Object 	// true
Object instanceof Object 	// true
Function instanceof Function // true

继续补充一下,对象实例的 constructor 属性指向创建此实例的构造器,最上面的写法没有什么问题,看下这个写法:

function Person(name){
    this.name = name
    this.init()
}
Person.prototype = {
    init(){
        console.log(this.name)
    }
}

let person1 = new Person('person1')

Person {name: "person1"}
    name: "person1"
    __proto__:
        init: ƒ init()
            __proto__: Object

此时因为直接将 prototype 指向一个全新的对象,constructor 则丢失了,这种情况的写法需要手动指定回来:

Person.prototype.constructor = Person


二、继承的实现

现在继承有了 es6 的方式,很少再会按照以前的写法去用,但是还是需要掌握一下,大概有如下几种方式,下面逐个看下,并进行一些优缺点分析。

先定义个基类,实例上和原型上各定义一个方法:

// 基类
function Person(name) {
  // 属性
  this.name = name;
  this.hobbies = ['play'];
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉!');
  }
}
// 原型方法
Person.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

1.原型链继承

将父类的实例作为子类的原型:

function Teacher(name){
    this.name = name
}
Teacher.prototype = new People();
Teacher.prototype.contructor = Teacher;
Teacher.prototype.name = '不会生效';

var tea = new Teacher('nini');
var tea1 = new Teacher('nana');

tea.hobbies.push('sing')

console.log(tea.hobbies, tea1.hobbies);  // [ 'play', 'sing' ] [ 'play', 'sing' ]
console.log(tea.name, tea1.name); //nini  nana
tea.eat('饭') //nini正在吃:饭
tea.sleep()   //nini正在睡觉!
console.log(tea instanceof People); //true 
console.log(tea instanceof Teacher); //true

创建一个 Teacher 类继承自 Person,其中注意 Teacher 中实例属性上有个 name,原型上也修改 name 但是不会生效,因为先查找实例上的属性方法,找不到再去查找原型上的方法和属性。

优点

        1. 实例是子类的实例,也是父类的实例

        2. 可以访问父类中所有属性和方法(实例和原型上都可以)

缺点

        1. 无法给父类传参 Teacher.prototype = new People();

        2. 多个实例对父类【引用类型】的操作会被篡改。例如:hobbies被共享


2.构造函数继承

复制父类的实例给子类:

function Teacher(name){
    People.call(this, name)
}

var tea = new Teacher('nini');
var tea1 = new Teacher('nana');

tea.hobbies.push('sing')

console.log(tea.hobbies, tea1.hobbies);  // [ 'play', 'sing' ] [ 'play' ]
console.log(tea.name, tea1.name); //nini  nana
//tea.eat('饭') //报错
tea.sleep()   //nini正在睡觉!
console.log(tea instanceof People); //false 这里是false,证明不是父类的实例
console.log(tea instanceof Teacher); //true

优点

        1. 可以往父类传参

        2. 多个实例对父类【引用类型】的操作不会被篡改,this隔离

缺点

        1. 无法访问父类中原型上的方法和属性。例如:eat方法报错

        2. 多个实例的创建,会初始化多次的 父类 副本。例如:People.call会随着子类实例创建被多次调用


3.组合继承

针对第二种方法的缺点,通过组合第一种的方式来解决,可以实现访问父类原型链上的方法:

function Teacher(name){
    People.call(this, name)
}
Teacher.prototype = new People();
Teacher.prototype.constructor = Teacher;

var tea = new Teacher('nini');
var tea1 = new Teacher('nana');

tea.hobbies.push('sing')

console.log(tea.hobbies, tea1.hobbies);  // [ 'play', 'sing' ] [ 'play' ]
console.log(tea.name, tea1.name); //nini  nana
//tea.eat('饭') //nini正在吃:饭
tea.sleep()   //nini正在睡觉!
console.log(tea instanceof People); //true 
console.log(tea instanceof Teacher); //true

此时看着彷佛完美,但是打印下 tea 实例,发现原型上也有一份实例:

Teacher {name: "nini", hobbies: Array(2), sleep: ƒ}
  hobbies: (2) ["play", "sing"]
  name: "nini"
  sleep: ƒ ()
   __proto__: People
    constructor: ƒ Teacher(name)
    hobbies: ["play"]
    name: undefined
    sleep: ƒ ()
     __proto__: Object


优点

        1. 可以往父类传参

        2可以继承实例属性/方法,也可以继承原型属性/方法

        3. 多个实例对父类【引用类型】的操作不会被篡改,this隔离

缺点

        1. 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)


4.寄生组合式继承

继续上一个方式改进,问题很明显,就是原型上存在多份实例,那么需要考虑如何避免,我们知道在原型链继承的时候会生成实例,但是构造函数继承只会继承实例,而不会继承原型上的方法,所以仍然在组合的基础上只需要把原型链继承那里的实例继承给切断就好,有两种方式实现:

// 创建一个没有实例方法的类
var Super = function(){};
//相当于只要父类的原型方法
Super.prototype = Person.prototype;
//因为 super 中没有实例方法
Teacher.prototype = new Super();

明白这个思路之后,就是只想获取父类原型上方法,更好的方式是使用 Object.create,省区了创建空函数的步骤,其实 create的原理就是类似上面的方式通过空函数:

Teacher.prototype = Object.create(Person.prototype)

看下完整实现:

function Teacher(name){
    People.call(this, name)
}
Teacher.prototype = Object.create(Person.prototype)
Teacher.prototype.constructor = Teacher;

var tea = new Teacher('nini');
var tea1 = new Teacher('nana');

tea.hobbies.push('sing')

console.log(tea.hobbies, tea1.hobbies);  // [ 'play', 'sing' ] [ 'play' ]
console.log(tea.name, tea1.name); //nini  nana
//tea.eat('饭') //nini正在吃:饭
tea.sleep()   //nini正在睡觉!
console.log(tea instanceof People); //true 
console.log(tea instanceof Teacher); //true

可以将继承方式封装为一个函数方便使用:

function inheritPrototype(subType, superType){
  var prototype = Object.create(superType.prototype);
  //修正指向
  prototype.constructor = subType;
  subType.prototype = prototype;
}

Teacher.prototype = Object.create(Person.prototype)这句就可以改为
inheritPrototype(Teacher, People) //直接传入子类和父类,实现继承关系

优点

        1. 完美的实现了继承,避免以上所有缺点

缺点

        1. 暂无


5.实例继承

继承相关的掌握如上四个就已经足够了,剩下的这些方法没啥意思,作为了解即可。实例继承就是在子类中直接实例化父类,返回这个实例:

function Teacher(name){
    return new People(name)
}
//这里修改也没用了
Teacher.prototype.constructor = Teacher;

// 注意这里可以new  也可以直接调用  效果一样
var tea = new Teacher('nini');
var tea1 = new Teacher('nana');

tea.hobbies.push('sing')

console.log(tea.hobbies, tea1.hobbies);  // [ 'play', 'sing' ] [ 'play' ]
console.log(tea.name, tea1.name); //nini  nana
tea.eat('饭') //nini正在吃:饭
tea.sleep()   //nini正在睡觉!
console.log(tea instanceof People); //true
console.log(tea instanceof Teacher); //false

优点

        1. 可传参,可在父类实例上扩展属性方法

缺点

        1. 实例是父类的实例,子类的 prototype 失去了作用


6.拷贝继承

针对上一个方法继续改进,通过拷贝父类实例的方法到子类原型上:

function Teacher(name){
    let s = new People(name)
	for(let k in s){
        Teacher.prototype[k] = s[k];
     }
}

var tea = new Teacher('nini');
var tea1 = new Teacher('nana');

tea.hobbies.push('sing')

console.log(tea.hobbies, tea1.hobbies);  // [ 'play', 'sing' ] [ 'play', 'sing' ]
console.log(tea.name, tea1.name); //nini  nana
tea.eat('饭') //nini正在吃:饭
tea.sleep()   //nini正在睡觉!
console.log(tea instanceof People); //false
console.log(tea instanceof Teacher); //true

优点

        1. 可传参,可在父类实例上扩展属性方法

缺点

        1. 引用类型数据被共享

        2. 效率比较低,拷贝父类所有属性方法


7.extends

通过 es6 class extends 继承的方式更加符合常规的代码编写习惯,比如以上几种方式真的是很奇葩的写法:

class A {
  constructor() {
    this.x = 1;
  }
  test(){console.log('父类方法')}
  static print() {
    console.log(this.x);
  }
}
class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  static m() {
    super.print();
  }
}
let b = new B()
b.test() // 父类方法
//静态方法也可以继承
B.x = 3;
B.m() // 3
B.print() // 3

虽然现在的代码中很少用到这些,除了 class 之外,但是想要深入了解 js 这些原型和继承还是需要好好斟酌的。

5人赞

分享到: