JS对象进阶-实现继承的3种形式

上一篇介绍了创建对象的5种模式,本篇介绍对象实现继承的3种形式。继承简单说就是在原有对象基础上稍作改动,得到一个新的对象,这个新对象可以拥有原对象的属性和方法。JS实现继承的3种方式:类式继承、class继承和拷贝继承。

JS这门语言和其他面向对象的语言不同,它并不支持类和类继承特性,只能通过其他方法定义并关联多个相似对象。虽然ES6增加了class关键字,但只是构造函数的语法糖而已,并不是意味着JS中是有类的。

类式继承

类式继承本质是通过构造函数实例化对象,然后用原型链把实例对象关联起来。

原型链继承

原型链继承本质是通过超类型的实例重写子类型的原型对象。

// 超类
function Super(){
  this.name = 'li'
}
Super.prototype.sayName = function(){
  return this.name
}

// 子类
function Sub(){}
// Sub继承Super
Sub.prototype = new Super()
Sub.prototype.constructor = Sub

var s1 = new Sub()
console.log(s1.sayName()) // 'li'

原型链继承有两个问题:第一是如果包含引用类型值会被所有实例共享;第二是在创建子类型实例时不能向超类型传递参数,因为会影响所有对象实例。

function Super() {
  this.hobbies=['编程']
}

Super.prototype.sayHobby=function() {
  return this.hobbies
}

function Sub() {}

Sub.prototype=new Super() 
Sub.prototype.constructor=Sub;

var s1=new Sub();
var s2=new Sub();
s1.hobbies.push('音乐')

console.log(s1.sayHobby()) // ['编程','音乐']
console.log(s2.sayHobby()) // ['编程','音乐']

借用构造函数继承

借用构造函数继承(constructor stealing)也叫伪类继承或经典继承。它的思想是在子类型构造函数内部调用超类型构造函数,并通过apply()或call()方法使子类型实例继承超类型的属性和方法。

function Super() {
  this.hobbies=['编程']
}

function Sub() {
  Super.call(this)
}

var s1=new Sub();
var s2=new Sub();
s1.hobbies.push('音乐')

console.log(s1.hobbies) // ['编程','音乐']
console.log(s2.hobbies) // ['编程']

使用这种方式不仅可以解决引用类型值共享的问题,而且可以在子类型构造函数中向超类型构造函数传递参数。

function Super(hobbies) {
  this.hobbies=hobbies
}

function Sub() {
  Super.call(this,['编程'])
  this.school='Tsinghua'
}

var s1=new Sub();
console.log(s1.hobbies) // ['编程']
console.log(s1.school) // 'Tsinghua'

但是使用这种方式,方法和属性都要在构造函数中定义,导致函数无法复用。

组合继承

组合继承(combination inheritance)也叫伪经典继承。它是原型链继承和借用构造函数继承的组合,取两者的优势。组合继承的思路是使用原型链实现对原型属性和方法的继承,使用构造函数实现对超类型实例属性的继承。这样不仅实现了函数的复用,而且保证了每个实例都有自己的属性。

function Super(name) {
  this.name=name;
  this.school='Tsinghua';
  this.hobbies=['编程','音乐'];
}

Super.prototype.sayName=function() {
  return this.name;
}

function Sub(name, age) {
  // 继承超类属性
  Super.call(this, name); 
  // 定义子类属性
  this.age=age;
}

// 继承超类方法
Sub.prototype=new Super();
Sub.prototype.constructor=Sub;
// 定义子类方法
Sub.prototype.sayAge=function() {
  return this.age;
}

var p1=new Sub('li', 10);
var p2=new Sub('wang', 20);

console.log(p1.school, p1.sayName(), p1.sayAge()) // 'Tsinghua' 'li' 10
console.log(p2.school, p2.sayName(), p2.sayAge()) // 'Tsinghua' 'wang' 20

p1.hobbies.push('跑步');
console.log(p1.hobbies, p2.hobbies); // ["编程", "音乐", "跑步"]  ["编程", "音乐"]

组合模式似乎很完美,但是它也有一个问题,那就是它会调用两次超类型的构造函数。一次是在子类构造函数内部,一次是在创建子类型原型的时候。每次调用子类型都会得到超类型的属性,但得不到超类型的方法,只有两次调用配合才能继承超类型的方法。

function Super(name) {
  this.name=name;
  this.school='Tsinghua';
  this.hobbies=['编程','音乐'];
}

Super.prototype.sayName=function() {
  return this.name;
}

function Sub(name, age) {
  // 第二次调用 Sub.prototype又得到了name、school和hobbies属性,并覆盖上次得到的值
  Super.call(this, name); 
  // 定义子类属性
  this.age=age;
}

// 第一次调用 Sub.prototype得到了name、school和hobbies属性
Sub.prototype=new Super();
Sub.prototype.constructor=Sub;
Sub.prototype.sayAge=function() {
  return this.age;
}

寄生组合继承

寄生组合继承解决了组合继承调用两次的问题,寄生组合继承和组合继承类似,它是寄生式继承和构造函数继承的组合。它的思路是通过构造函数实现对实例属性的继承,通过Object.create()方法实现对原型属性和方法的继承。

function Super(name) {
  this.name=name;
  this.school='Tsinghua';
  this.hobbies=['编程', '音乐'];
}

Super.prototype.sayName=function() {
  return this.name;
}

function Sub(name, age) {
  Super.call(this, name);
  this.age=age;
}

Sub.prototype=Object.create(Super.prototype);
Sub.prototype.constructor=Sub;
Sub.prototype.sayAge=function() {
  return this.age;
}

寄生组合继承是认可度最高的继承方式。

ES6 class

第二种继承方式是使用ES6中的class,它思路上和类式继承一样,只不过隐藏了很多类式继承的细节。如果使用ES6的class改写上面的示例,代码会精简很多。

class Super {
  constructor(name) {
    this.name = name;
    this.school = 'Tsinghua';
    this.hobbies = ['编程','音乐'];
  }
  sayName() {
    return this.name;
  }
}

class Sub extends Super {
  constructor(name, age) {
    super(name);
    this.age = age;
  }
  sayAge() {
    return this.age
  }
}

var p1=new Sub('li', 10);
var p2=new Sub('wang', 20);

console.log(p1.school, p1.sayName(), p1.sayAge()) // 'Tsinghua' 'li' 10
console.log(p2.school, p2.sayName(), p2.sayAge()) // 'Tsinghua' 'wang' 20

p1.hobbies.push('跑步');
console.log(p1.hobbies, p2.hobbies); // ["编程", "音乐", "跑步"]  ["编程", "音乐"]

拷贝继承

拷贝继承不需要改变原型链,它的思路是通过对象深拷贝将超类型的属性和方法复制给子类型。拷贝继承解决了引用类型值被实例共享的问题,所以可以不适用构造函数实现对象的继承。JQuery使用的就是拷贝继承。

关于深拷贝和浅拷贝移步此文

// 深拷贝函数
function extend(obj, cloneObj) {
  var isObj = obj instanceof Object;
  if(!isObj) {return false;}
  var cloneObj = cloneObj || {};

  for (var i in obj) {
    if (typeof obj[i] === 'object') {
      cloneObj[i] = (obj[i] instanceof Array) ? [] : {};
      arguments.callee(obj[i], cloneObj[i]);
    } else {
      cloneObj[i] = obj[i];
    }
  }
  return cloneObj;
}

var Super = {
  init: function(value){
    this.value = value
  },
  sayName: function(){
    return this.name
  },
  hobbies: ['编程'],
    school: 'Tsinghua'
}

var s1 = extend(Super);
var s2 = extend(Super);
s1.hobbies.push('音乐');

console.log(s1.hobbies,s2.hobbies) // ["编程", "音乐"] ["编程"]

构造函数的拷贝组合继承

如果构造函数和拷贝继承组合使用,则可以拷贝继承超类型的引用类型值和方法,利用构造函数定义子类型的属性值。

// 深拷贝函数
function extend(obj, cloneObj) {
  var isObj = obj instanceof Object;
  if(!isObj) {return false;}
  var cloneObj = cloneObj || {};

  for (var i in obj) {
    if (typeof obj[i] === 'object') {
      cloneObj[i] = (obj[i] instanceof Array) ? [] : {};
      arguments.callee(obj[i], cloneObj[i]);
    } else {
      cloneObj[i] = obj[i];
    }
  }
  return cloneObj;
}

function Super(name){
  this.name = name;
  this.school = 'Tsinghua';
  this.hobbies = ['编程','音乐'];
}

Super.prototype.sayName = function() {
  return this.name
}

function Sub(name,age){
  Super.call(this,name);
  this.age = age;
}

Sub.prototype = extend(Super.prototype);

var p1=new Sub('li', 10);
var p2=new Sub('wang', 20);

console.log(p1.school, p1.sayName()) // 'Tsinghua' 'li'
console.log(p2.school, p2.sayName()) // 'Tsinghua' 'wang'

p1.hobbies.push('跑步');
console.log(p1.hobbies, p2.hobbies); // ["编程", "音乐", "跑步"]  ["编程", "音乐"]
posted @ 2021-09-29 11:31  wmui  阅读(186)  评论(0编辑  收藏  举报