隔壁小孩也能看懂的 7 种 JavaScript 继承实现

本文首发自腾讯IMWEB社区:imweb.io/javascript

JavaScript 没有类,原型链来实现继承

由于我在学校接触的第一门语言是cpp,是一个静态类型语言,而且实现面向对象直接就有class关键字,并且只讲了面向对象一种设计思想,致使我一直很难理解javascript语言的继承机制。java

JavaScript没有”子类“和”父类“的概念,也没有”类“(class)和”实例“(instance)的区分,全靠”原型链“(prototype chain)实现继承。c++

学的时候就很想吐槽,费了这么大的劲去模拟类,那js干吗不一开始就设计class关键字而是最开始仅将class做为保留字呢?(ES6以后有了class关键字,是原型的语法糖)es6

当时我一直怀疑,“js没有class是一种设计缺陷吗?”web

原来,JavaScript设计之初,设计里面全部的数据类型都是对象(object),最开始,JavaScript只想被设计成一种简易的脚本语言,设计者JavaScript里面都是对象,必需要有一种机制将全部对象联系起来,但若是引入“类”(class)的概念,那么就太“正式”了,增长了上手难度。编程

要实现继承,但又不想用类,那该怎么办呢?bash

JavaScript 的设计者Brendan Eich发现,能够像c++和Java语言中使用new命令生成实例。数据结构

因而new命令被引入到JavaScript,用来从原型对象生成一个实例对象。可是JavaScript没有“类”,原型对象该如何表示呢?编程语言

这时,他想到c++和java使用new命令时,都会调用“类”的构造函数(constructor),因而他作了个简化设计,在JavaScript中,new命令后面跟的不是类而是构造函数。函数

用构造函数生成实例对象,有一个缺点就是没法共享属性和方法。

每个实例对象,都有本身的属性和方法的副本。这不只没法作到数据共享,也是极大的资源浪费。

考虑到这一点,brendan Eich决定为构造函数设置一个prototype属性

这个属性包含一个prototype对象(是的,prototype属性的值是prototype对象),全部的实例对象须要共享的属性和方法,都放在这个对象里面,那些不须要共享的属性和方法,就放在构造函数里。

实例对象一旦建立,将自动引用prototype对象的属性和方法,也就是说,实例对象的属性和方法,分红两种,一种是本地的,另外一种是引用的。

因为全部的实例对象共享同一个prototype对象,那么从外界看起来,prototype对象就好像是实例对象的原型,而实例对象则好像"继承"了prototype对象同样。

若是没了解过c++、java或者其余的编程语言,我相信你看完上面这段内容应该会看睡着了吧!好的,咱们仍是直接来看看代码吧~

原型链继承

//原型链继承

// 父类
// 拥有属性 name
function parents(){
    this.name = "JoseyDong";
}

// 在父类的原型对象上添加一个getName方法
parents.prototype.getName = function(){
    console.log(this.name);
}

//子类
function child(){
}

//子类的原型对象 指向 父类的实例对象
child.prototype = new parents()

// 建立一个子类的实例对象,若是它有父类的属性和方法,那么就证实继承实现了
let child1 = new child();

child1.getName(); // => JoseyDong
复制代码

在只有一个 子类实例对象的时候,咱们貌似看不出什么问题。然而在实际场景中,咱们会建立不少实例对象来继承父类,毕竟继承得越多,被复写的代码量就越多嘛~

//原型链继承

// 父类
// 拥有属性 name
function parents(){
    this.name = ["JoseyDong"];
}

// 在父类的原型对象上添加一个getName方法
parents.prototype.getName = function(){
    console.log(this.name);
}

//子类
function child(){
}

//子类的原型对象 指向 父类的实例对象
child.prototype = new parents()

// 建立一个子类的实例对象,若是它有父类的属性和方法,那么就证实继承实现了
let child1 = new child();

child1.getName(); // => ["JoseyDong"]

// 建立一个子类的实例对象,在child1修改name前实现继承
let child2 = new child();

// 修改子类的实例对象child1的name属性
child1.name.push("xixi");

// 建立子类的另外一个实例对象,在child1修改name后实现继承
let child3 = new child();

child1.getName();// => ["JoseyDong", "xixi"]
child2.getName();// => ["JoseyDong", "xixi"]
child3.getName();// => ["JoseyDong", "xixi"]
复制代码

当不少时候,咱们的实例对象里的值是会虽具体场景而改变的。好比这个时候,咱们的child1除了joseydong之外,她的朋友又给她取了个新名字xixi,咱们改变了child1的name值。而child一、child二、child3是三个独立的个体,可是最后发现三个孩子都有了新名字!

这就表示,原型链继承里面,使用的都是同一个内存里的值,这样修改该内存里的值,其余继承的子类实例里的值都会变化。

这可不是咱们想要的效果,毕竟只有child1被赋予了新名字。而且,若是我想经过子类实例对象传递参数给父类,也是作不到的。

借用构造函数

// 构造函数继承


function parents(){
    this.name = ["JoseyDong"];
}

// 在子类中,使用call方法构造函数,实现继承
function child(){
    parents.call(this);
}

let child1 = new child();
let child2 = new child();

child1.name.push("xixi");

let child3 = new child();

console.log(child1.name);// => ["JoseyDong", "xixi"]
console.log(child2.name);// => ["JoseyDong"]
console.log(child3.name);// => ["JoseyDong"]
复制代码

咱们使用构造函数的方法,就只修改了child1的名字,而child2和child3的name属性并无受影响~

同时,因为call()支持传递参数,咱们也能够在child中向parent传参啦~

// 构造函数实现继承
//子类向父类传参

function parents(name){
    this.name = name;
}

//call方法支持传递参数
function child(name){
    parents.call(this,name)
}

let child1 = new child("I am child1");

let child2 = new child("I am child2");

console.log(child1.name);// => I am child1
console.log(child2.name);// => I am child2
复制代码

好了,如今咱们经过构造函数实现继承弥补了用原型链实现继承的缺点,同时也是经过构造函数实现继承的优势:

1.避免了引用类型的属性被全部实例共享

2.能够在child中向parent传参

可是,这种方式也有缺点,由于方法都在构造函数中定义,每次建立实例都会建立一遍方法。

组合继承

咱们发现,经过原型链实现的继承,都是复用同一个属性和方法;经过构造函数实现的继承,都是独立的属性和方法。因而咱们大打算利用这一点,将两种方式组合起来:经过在原型上定义方法实现对函数的复用,经过构造函数的方式保证每一个实例都有它本身的属性

下面我再举个栗子,让你们感觉下组合继承的好处~

//组合继承

// 偶像练习生大赛开始报名了
// 初赛,咱们找了一类练习生
// 这类练习生都有名字这个属性,但名字的值不一样,而且都有爱好,而爱好是相同的
// 只有会唱跳rap的练习生才可进入初赛
function student(name){
    this.name = name;
    this.hobbies = ["sing","dance","rap"];
}

// 咱们在student那类里面找到更特殊的一类进入复赛
// 固然,咱们已经知道初赛时有了name属性了,而不一样练习生名字的值不一样,因此使用构造函数方法继承
// 同时,咱们想再让练习生们再介绍下本身的年龄,每一个子类还能够本身新增属性
// 固然啦,具体的名字年龄就由每一个练习生实例来定
// 类只告诉你,有这个属性

function greatStudent(name,age){
    student.call(this,name);
    this.age = age;
}

// 而你们的爱好值都相同,这个时候用原型链继承就好啦
// 每一个对象都有构造函数,原型对象也是对象,也有构造函数,这里简单的把构造函数理解为谁的构造函数就要指向谁
// 第一句将子类的原型对象指向父类的实例对象时,同时也把子类的构造函数指向了父类
// 咱们须要手动的将子类原型对象的构造函数指回子类
greatStudent.prototype = new student();
greatStudent.prototype.constructor = greatStudent;

// 决赛 kunkun和假kunkun进入了决赛
let kunkun = new greatStudent('kunkun','18');
let fakekun = new greatStudent('fakekun','28');

// 有请两位选手介绍下本身的属性值
console.log(kunkun.name,kunkun.age,kunkun.hobbies) // => kunkun 18 ["sing", "dance", "rap"]
console.log(fakekun.name,fakekun.age,fakekun.hobbies) // => fakekunkun 28 ["sing", "dance", "rap"]

// 这个时候,kunkun选手说本身还有个隐藏技能是打篮球
kunkun.hobbies.push("basketball");

console.log(kunkun.name,kunkun.age,kunkun.hobbies) // => kunkun 18 ["sing", "dance", "rap", "basketball"]
console.log(fakekun.name,fakekun.age,fakekun.hobbies)// => fakekun 28 ["sing", "dance", "rap"]

// 咱们能够看到,假kunkun并无抄袭到kunkun的打篮球技能
// 而且若是这个时候新来一位选手,从初赛复赛闯进来的一匹黑马
// 能够看到黑马并无学习到kunkun的隐藏技能
let heima = new greatStudent('heima','20')
console.log(heima.name,heima.age,heima.hobbies) // => heima 20 ["sing", "dance", "rap"]
复制代码

能够看到,组合继承避开了原型链继承和构造函数继承的缺点,结合了二者的优势,成为了javascript中最经常使用的继承方式。

原型式继承

这种继承的思想是将传入的对象做为建立的对象的原型。

function createObj(o){
  function F(){};
  F.prototype = o;
  return new F();
}
复制代码

咱们来实现下原型式继承,看看会不会有什么问题

// 原型式继承

function createObj(o){
    function F(){};
    F.prototype = o;
    return new F();
}

let person = {
    name:'JoseyDong',
    hobbies:['sing','dance','rap']
}

let person1 = createObj(person);
let person2 = createObj(person);

console.log(person1.name,person1.hobbies) // => JoseyDong ["sing", "dance", "rap"]
console.log(person2.name,person2.hobbies) // => JoseyDong ["sing", "dance", "rap"]

person1.name = "xixi";
person1.hobbies.push("basketball");

console.log(person1.name,person1.hobbies) //xixi ["sing", "dance", "rap", "basketball"]
console.log(person2.name,person2.hobbies) //JoseyDong ["sing", "dance", "rap", "basketball"]
复制代码

这个时候咱们发现,修改了person1的hobbies的值,person2的hobbies的值也变了。

这是由于包含引用类型的属性值始终会共享相应的值,这点跟原型链继承同样~

而修改了person1.name的值,person2.name的值并未发生改变,并非由于person1和person2有独立的name值,而是由于person1.name = "xixi"这条语句是给person1实例对象添加了一个name属性,而它的原型对象上name值并无被修改,因此person2的name没有变化。由于咱们找对象上的属性时,老是先找实例对象,没有找到的话再找原型对象上的属性。实例对象和原型对象上若是有同名属性,老是先取实例对象上的值。

ESMAScript5新增了Object.create()方法规范化了原型式继承~

寄生式继承

建立一个仅用于封装继承过程的函数,该函数在内部以某种形式来作加强对象,最后返回对象。

//寄生式继承

function createObj(o){
    let clone = Object.create(o);
    clone.sayName = function(){
        console.log('hi');
    }
    return clone
}

let person = {
    name:"JoseyDong",
    hobbies:["sing","dance","rap"]
}

let anotherPerson = createObj(person);
anotherPerson.sayName(); // => hi
复制代码

固然,用寄生式继承来为对象添加函数,和借用构造函数模式同样,每次建立对象都会建立一遍方法。

寄生组合式继承

前面咱们说了,组合继承是javascript最经常使用的继承模式。这里咱们先来回顾下组合式继承的代码:

//组合继承

function student(name){
    this.name = name;
    this.hobbies = ["sing","dance","rap"];
}

function greatStudent(name,age){
    student.call(this,name);
    this.age = age;
}

greatStudent.prototype = new student();
greatStudent.prototype.constructor = greatStudent;

let kunkun = new greatStudent('kunkun','18');
复制代码

组合继承最大的缺点是最调用两次父构造函数

一次是设置子类实例的原型的时候:

greatStudent.prototype = new student();
复制代码

一次是在建立子类型实例的时候:

let kunkun = new greatStudent('kunkun','18');
复制代码

在这个例子中,若是咱们打印一下kunkun这个对象,咱们就会发现greatStudent.prototype和kunkun都有一个属性为hobbies。

这其实就是实例对象和原型对象上的属性值重复了,而再找属性值的时候,在实例对象上找到了属性值就不会在原型对象上找了,而这部分原型对象上的值就实打实的浪费了存储空间。

那么咱们该如何精益求精,避免这一次重复调用呢?

若是咱们不使用greatStudent.prototype = new student(),而是直接让greatStudent.prototype访问到student.prototype呢?

看看如何实现:

// 寄生组合式继承

function student(name){
    this.name = name;
    this.hobbies = ["sing","dance","rap"];
}

function greatStudent(name,age){
    student.call(this,name);
    this.age = age;
}

//关键的三步 实现继承
// 使用F空函数当子类和父类的媒介 是为了防止修改子类的原型对象影响到父类的原型对象
let F = function(){};
F.prototype = student.prototype;
greatStudent.prototype = new F();

let kunkun = new greatStudent('kunkun','18');
console.log(kunkun);
复制代码

打印结果:

能够看到,kunkun实例的原型对象上再也不有hobbies属性了。

最后,咱们封装下这个继承方法:

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function prototype(child, parent) {
    let prototype = object(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}

// 当咱们使用的时候:
prototype(Child, Parent);
复制代码

引用《JavaScript高级程序设计》中对寄生组合式继承的夸赞就是:

这种方式的高效率体现它只调用了一次 Parent 构造函数,而且所以避免了在 Parent.prototype 上面建立没必要要的、多余的属性。与此同时,原型链还能保持不变;所以,还可以正常使用 instanceof 和 isPrototypeOf。开发人员广泛认为寄生组合式继承是引用类型最理想的继承范式。

总而言之就是,这种js实现继承的方式是最佳的。

ES6实现继承

然而,ES6以后经过extends关键字实现了继承。

// ES6 

class parents {
    constructor(){
        this.grandmather = 'rose';
        this.grandfather = 'jack';
    }
}

class children extends parents{
    constructor(mather,father){
    //super 关键字,它在这里表示父类的构造函数,用来新建父类的 this 对象。
        super();
        this.mather = mather;
        this.father = father;
    }
}

let child = new children('mama','baba');
console.log(child) // =>
// father: "baba"
// grandfather: "jack"
// grandmather: "rose"
// mather: "mama"
复制代码

子类必须在 constructor 方法中调用 super方法,不然新建实例时会报错。这是由于子类没有本身的this 对象,而是继承父类的 this 对象,而后对其进行加工。

只有调用 super 以后,才可使用 this 关键字,不然会报错。这是由于子类实例的构建,是基于对父类实例加工,只有 super 方法才能返回父类实例。

ES5 的继承实质是先创造子类的实例对象 this,而后再将父类的方法添加到 this 上面(Parent.call(this))。

ES6 的继承机制实质是先创造父类的实例对象 this (因此必须先调用 super() 方法),而后再用子类的构造函数修改 this。

es6实现继承的核心代码以下:

function _inherits(subType, superType) {
  subType.prototype = Object.create(superType && superType.prototype, {
    constructor: {
      value: subType,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
  if (superType) {
    Object.setPrototypeOf 
    ? Object.setPrototypeOf(subType, superType) 
    : subType.__proto__ = superType;
  }
}
复制代码

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

除此以外,ES6 能够自定义原生数据结构(好比Array、String等)的子类,这是 ES5 没法作到的。

相关文章
相关标签/搜索