继承

71 阅读7分钟

Object和Function的关系

Object、Function和其他对象的关系可以归纳为四点:

  1. 一切对象最终都是继承自Object对象,Object对象直接继承自根对象Null
  • 一切对象的原型链最终都是.... → Object.prototype → null。例如定义一个num变量var num = 1,则num的原型链为x → Number.prototype → Object.prototype → null; 定义一个函数对象fnfunction fn() {},则fn的原型链为fn → Function.prototype → Object.prototype → null;等等...
  • 一切对象都包含有Object的原型方法,Object的原型方法包括了toString、valueOf、hasOwnProperty等等,在js中不管是普通对象,还是函数对象都拥有这些方法,下面列出了几个例子,大家可以自行去举例验证:
  1. 一切函数对象(包括Object对象)都直接继承自Function对象
    函数对象包括了Function、Object、Array、String、Number,还有正则对象RegExp、Date对象等等,它们在js中的构造源码都是function xxx() {[native code]);,Function其实不仅让我们用于构造函数,它也充当了函数对象的构造器,甚至它也是自己的构造器。
  • 一切对象都继承自Object对象是因为一切对象的原型链最终都是.... → Object.prototype → null,包括Function对象,只是Function的原型链稍微绕了一点,Function的原型链为Function → Function.prototype → Object.prototype → null,它与其它对象的特别之处就在于它的构造器为自己,即直接继承了自己,最终继承于Object,上面的原型链可以在浏览器验证:
  • Object继承自Function,Object的原型链为 Object → Function.prototype → Object.prototype → null,原型链又绕回来了,并且跟第一点没有冲突。可以说Object和Function是互相继承的关系。
  1. Object对象直接继承自Function对象
  2. Function对象直接继承自己,最终继承自Object对象

继承的实现方式

在实现继承之前,让我们回顾一下原型链的概念:每一个实例对象都有一个__proto__属性(隐式原型),在js内部用来查找原型链,对象有一个prototype属性(显式原型),用来显示修改对象的原型,实例.proto = 对象.prototype = 原型。原型链的特点就是通过实例的__proto__属性去查找原型的属性,从子类一直向上查找对象原型的属性,继而形成一个查找链即原型链。

原型链继承

核心:将父类的实例作为子类的原型,即sub.prototype = new super,这样连通了子类-子类原型-父类。

function Super() {
  this.flag = true;
  this.arr = ['1'];
}
// 为了提高复用性,方法绑定在父类原型属性上
Super.prototype.getFlag = function() {
  return this.flag;
}
function Sub(subFlag) {
  this.subFlag = subFlag;
}
// 实现继承
Sub.prototype = new Super(); // 缺陷1,这虽然实现了继承,但是切断了Sub.prototype.constructor与Sub的关系,相当于给Sub.prototype重新赋值了
Sub.prototype.constructor = Sub;  // 解决缺陷1
// 给子类添加子类持有的方法,注意顺序要在继承之后
Sub.prototype.getSubFlag = function() {
  return this.subFlag;
}
// 构造实例
let sub1 = new Sub(false);
let sub2 = new Sub(true);
sub1.arr.push('2');
console.log(sub1.getSubFlag());
console.log(sub1.getFlag());
console.log(sub1.arr);
console.log(sub2.getSubFlag());
console.log(sub2.getFlag());
console.log(sub2.arr);
// 执行结果
// false
// true
// [ '1', '2' ]
// true
// true
// [ '1', '2' ]

构造函数继承

核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(在构造子类构造函数内部使用call或apply来调用父类的构造函数)

function Super() {
  this.flag = true;
  this.arr = ['1'];
  this.getFlag = function() {
    return this.flag;
  }
}
Super.prototype.sayFlag = function() {
  return 'hi,' + this.flag;
}
function Sub() {
  Super.call(this); // 如果父类可以接收参数,这里也可以直接传递
  // Super.apply(this, arguments); // 这个地方的继承可以使用apply和call来实现
}
let obj1 = new Sub();
obj1.flag = false;
obj1.arr.push('2');
let obj2 = new Sub();
console.log(obj1.flag);
console.log(obj2.flag);
console.log(obj1.arr);
console.log(obj2.arr);
console.log(obj1.getFlag());
console.log(obj2.getFlag());
// console.log(obj2.sayFlag());
// 执行结果
// false
// true
// [ '1', '2' ]
// [ '1' ]
// false
// true
// 报错:TypeError: obj2.sayFlag is not a function

组合继承

利用构造函数和原型链的方法,可以比较完美的实现继承

// 第一个例子
function Super() {
  this.flag = true;
}
Super.prototype.getFlag = function() {
  return this.flag;    // 继承方法
}
function Sub() {
  this.subFlag = false;
  Super.call(this);   // 继承属性
}
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;
let obj = new Sub();
Super.prototype.getSubFlag = function() {
  return this.subFlag;
}
console.log(obj.flag);
console.log(obj.getFlag());
console.log(obj.getSubFlag());
//执行结果:
// true
// true
// false


// 第二个例子
function Fu(name) {
  this.name = name;
  this.arr = ['1'];
  this.sayName = function() {
    console.log(this.name);
  }
}
Fu.prototype.sayHi = function() {
  console.log('hi, ', this.name);
}
function Zi(name) {
  Fu.apply(this, arguments);
  // this.name = name;    // 如果父构造函数没有参数name,则这里需要赋值
}
Zi.prototype = new Fu();
Zi.prototype.constructor = Zi;
let z1 = new Zi('儿子1');
z1.sayName();
z1.sayHi();
z1.arr.push('2');
let z2 = new Zi('儿子2');
console.log(z1.arr);
console.log(z2.arr);
// 执行结果
// 儿子1
// hi,  儿子1
// [ '1', '2' ]
// [ '1' ]

原型式继承

核心:直接利用es5的object.create方法
object.create原理

  • 创建一个构造函数,构造函数的原型指向对象
  • 调用new操作符创建实例,并返回这个实例,
  • 本质上是一个浅拷贝
let parent = {
  name: 'parent',
  share: [1, 2, 3],  // 父类的属性全部被子类共享
  log: function() {
    return this.name;
  }
}
let child = Object.create(parent);

寄生继承

核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免组合继承的缺点。

function Fu(name) {
  this.name = name;
  this.arr = ['1'];
  this.sayName = function() {
    console.log(this.name);
  };
}
Fu.prototype.sayHi = function() {
  console.log('Hi, ', this.name);
};
function Zi(name) {
  Fu.apply(this, arguments);
  this.name = name;
}
Zi.prototype = Object.create(Fu.prototype);  // Object.create(Fu.prototype)是对Fu.prototype的浅拷贝,而不是调用Fu的构造函数
// Zi.prototype = create(Fu.prototype);
Zi.prototype.constructor = Zi;
let z1 = new Zi('儿子1');
console.log(z1);
z1.sayName();
z1.sayHi();

// Object.create(Fu.prototype)相当于下面的函数,先初始化一个F构造函数,F的原型指向obj原型,返回F的构造函数
function create(obj) {
  let F = function() {};
  F.prototype = obj;
  return new F();
}

// 执行结果
// Zi { name: '儿子1', arr: [ '1' ], sayName: [Function (anonymous)] }
// 儿子1
// Hi,  儿子1

寄生组合式继承

揉杂了原型链式、构造函数式、组合式、原型式、寄生式而形成的一种方式:主要是解决了组合继承的唯一缺点:多次调用Parent

function Parent(name, friends) {
  this.name = name;
  this.friends = friends;
}

Parent.prototype = {
  constructor: Parent,  // 需要手动绑定conatructor
  share: [1, 2, 3],
  log: function() {
    return this.name
  }
}

function Child(name, friends, gender) {
  Parent.call(this, name, friends);
  this.gender = gender;
}

function proto(child, parent) {
  let clonePrototype = Object.create(parent.prototype);
  child.prototype = clonePrototype;
  child.prototype.constructor = child;
}

proto(Child, Parent);

es6 class

class Parent {
  constructor(name, friends) {   // 该属性在构造函数上,不共享
    this.name = name;
    this.friends = friends;
  }
  log() { // 该方法在原型上,共享
    return this;
  }
}
Parent.prototype.share = [1, 2, 3];   // 原型上的属性,共享
class Child extends Parent {
  constructor(name, friends, gender) {
    super(name, friends);
    this.gender = gender;
  }
}

各自的优缺点

结合上面实现方式,很容易看出他们各自的优缺点。

原型链继承

优点:

  1. 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  2. 父类新增原型方法/原有属性,子类都能访问到
    缺点:
  3. 切断了Sub.prototype.constructor与Sub的关系
  4. 原型链上的引用类型的数据会被所有实例共享

构造函数继承

优点:

  1. 解决了原型继承中的子类实例共享父类引用属性的问题。
  2. 创建子类实例时,可以向父类传递参数
  3. 可以实现多继承(call多个父类对象)
    缺点:
  4. 实例并不是父类的实例,只是子类的实例
  5. 只能继承父类的实例属性和方法,不能继承原型属性和方法
  6. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

组合继承

优点:

  1. 弥补了构造函数继承的缺陷,可以继承实例属性/方法,也可以继承原型原型/方法
  2. 既是子类的实例也是父类的实例
  3. 不存在引用属性共享问题
  4. 可传参
  5. 函数可复用
    缺点:
  6. 调用了两次父类的继承,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

原型式继承

优点:父类方法可以复用
缺点:

  1. 父类引用属性全部被共享
  2. 子类不可传递参数给父类

寄生继承

特点:使用到了Object.create(Fu.prototype)实现原型链的浅拷贝
优点:解决了原型链继承和构造函数继承的缺点

继承的应用场景

JS继承的话主要用于面向对象的编程中,试用场景的话还是以单页面应用或者JS为主的开发里,因为如果只是在页面级的开发中很好会用到JS继承的方式,与其说继承,还不如直接写个函数来的简单有效一些。
想用继承的话最好是那种主要以JS为主开发的大型项目,比如说单页面的应用或者写JS框架,前台的所有的东西都用JS来完成,整个站的跳转,部分逻辑,数据处理等大部分使用JS来做,这样面向对象的编程才有存在的意义和价值。

开源项目(如node)中应用原理继承的案例

参考开源项目的源码