javascript 面向对象编程,多种继承方式

面向对象编程有三个特点: 继承,封装,多态

1.继承

ES6之后,一般就是类的继承。在类之前是构造函数的继承。

构造函数:静态属性和方法,原型对象属性和方法,构造函数内this定义的属性和方法。

对于静态属性的继承。

for(let key in SuperClass) {
  if(SuperClass.hasOwnProperty(key)) {
    SubClass[key] = SuperClass[key];
  }
}

其他的属性和方法的继承方法有如下:

1. 原型链继承-子类原型对象

将父类的实例赋值给子类的原型对象。

优点

父类的属性和方法都能复用

缺点:

1. 子类实例化时可以向父类构造函数传参进行初始化

2.父类上的引用类型的属性会被子类的实例共享。一个修改,引起其他实例访问的时候是修改后的属性。

语法:

SubClass.prototype = new SuperClass();
// 重新指定构造函数属性,否则它指向SuperClass
SubClass.prototype.constructor = SubClass;

示例:

function SuperClass() {
  this.superValue = [1];
}
SuperClass.prototype.getSuperValue = function() {
  console.log('super');
}

function SubClass() {
  this.subValue = [2];
}
// 父类的实例既包含父类构造函数内的属性和方法,也包含原型对象上的属性和方法
SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass;

// 缺点1,公共属性superValue是引用类型,实例访问属性的时候访问的是原型对象上的属性
const instance1 = new SubClass();
instance1.superValue.push('a'); 
console.log(instance1.superValue); //[1,'a']
instance1.getSuperValue(); //super 可以访问原型链上的方法

//在instance2未进行任何操作时访问superValue
const instance2 = new SubClass();
console.log(instance2.superValue); // [1,'a']

2. 构造函数继承-创建即继承

在子类构造函数中调用父类的构造函数。将父类构造函数上的属性和方法复制到子类实例中。

优点:

1. 实例化子类时可以给父类构造函数传参进行初始化

2. 父类上的引用类型的属性不会被子类的实例共享。

缺点:

1. 无法继承原型链上的方法

2.如果想要继承原型链上的方法,必须将原型链上的方法放到构造函数中。违背了“代码共享”的原则。

语法:

function SubClass(props) {
  SuperClass.call(this, props);// 构造函数继承
}

示例:

function SuperClass(id) {
  this.id = id;
  this.books = ['js', 'html'];
}
SuperClass.prototype.getSuperBooks = function() {
  console.log(this.books);
}

function SubClass(props) {
  SuperClass.call(this, props);// 构造函数继承!
}

const instance1 = new SubClass(11);
instance1.books.push('css');
console.log(instance1.books);// ['js', 'html', 'css']
console.log(instance1.id); // 11 -向父类构造函数传参并初始化
// 未继承原型链上的方法
instance1.getSuperBooks(); // ❌instance1.getSuperBooks is not a function

const instance2 = new SubClass(12);
console.log(instance2.books); // ['js', 'html'] 不受其他实例影响
console.log(instance2.id); // 12

3. 组合继承-原型链+构造函数

融合了原型链继承和构造函数继承的优点。

缺点:

父类的构造函数调用了两次,浪费性能

语法:

function SubClass(props) {
  SuperClass.call(this, props); // 第二次调用父类构造函数
}
SubClass.prototype = new SuperClass(); // 第一次调用父类构造函数
SubClass.prototype.constructor = SubClass;

示例:

function SuperClass(name) {
  console.log(`super call->${name}`);
}

function SubClass(name) {
  // 将构造函数内部的属性和方法直接绑定到子类的实例上
  SuperClass.call(this, name);
}
SubClass.prototype = new SuperClass(['proto', 'Call']);//第一次
SubClass.prototype.constructor = SubClass;

const instance1 = new SubClass(['proto', 'Call']);// 第二次

// 运行结果⚠️字符串模版将数组转为字符串
// super call-> proto,Call
// super call-> new,Call

4. 寄生式组合继承

在组合继承的继承上,加入寄生式继承。

优点:

解决了组合继承调用两次父类构造函数的问题。

语法:

function SubClass(props) {
  SuperClass.call(this, props);
}
const obj = Object.create(SuperClass.prototype);
obj.constructor = SubClass;
SubClass.prototype = obj;

示例:

function SuperClass(props) {
  console.log('super call-->'+props);
}
SuperClass.prototype.getName =function() {
  console.log('superClass-prototype-fn');
}

function SubClass(props) {
  SuperClass.call(this, props);
}
const obj = Object.create(SuperClass.prototype);
obj.constructor = SubClass;
SubClass.prototype = obj;

const instance = new SubClass('newCall');
instance.getName();

// 运行结果
// super call-->newCall  只调用一次
// superClass-prototype-fn  能够访问原型对象上的方法

5. 多重继承(Mixin模式)

即一个构造函数继承多个构造函数。

function Child() {
    Father1.call(this);
    Father2.call(this);
}
Child.prototype = Object.create(Father1.prototype);
Object.assign(Child.prototype, Father2.prototype);
Object.prototype.constructor = Child;

6. 类的继承

1. 子类的实例this的生成基于父类的实例,所以必须先调用super(),获取父类实例。之后才能使用this。

2. 类的继承还会继承静态属性和方法

class Father {}
class Child extends Father{ constructor(props) { super(props); // =Father.prototype.constructor.call(this,props) } }
// super的用法:
1)作为父类构造函数,只能用在构造函数中
2)作为原型对象,在普通函数中:super.xxx();
   super相当于父类的原型对象(super.prototype.xxx.call(this)),里面的this指向子类实例。
   取不到子类的实例上面的属性和方法!!
3)作为父类,在静态方法中使用super,相当于父类,里面的this指向子类。

类的继承的实现原理:

Object.setPrototypeOf(Child, Father); // 静态方法
Object.serPrototypeOf(Child.prototype, Father.prototype); // 原型对象上的方法

可推导出:

Child.__proto__ === Father;
Object.getPrototypeOf(Child) === Father;
Child.prototype.__proto__ === Father.prototype;
Object.getPrototypeOf(Child.prototype) === Father.prototype;

7. 类的多重继承

本质上是将多个被继承类的属性遍历复制到目标。

function mix(...mixins) {// 多个类
  class Mix {
    constructor() {
      for (let mixin of mixins) {
        copyProperties(this, new mixin()); // 拷贝实例属性
      }
    }
  }

  for (let mixin of mixins) {
    copyProperties(Mix, mixin); // 拷贝静态属性
    copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
  }

  return Mix;
}

function copyProperties(target, source) {// 将被继承类的属性和方法复制到this上
  for (let key of Reflect.ownKeys(source)) {
    if ( key !== 'constructor'
      && key !== 'prototype'
      && key !== 'name'
    ) {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}

2. 封装

ES5中没有模块的概念,可以模拟实现将代码封装成模块。

1. ES5封装

将方法和属性封装在构造函数中。

2. ES6封装

将方法和属性封装在类中。

3. 多态

同一个函数的多种调用形式。根据传参不同,实现逻辑不同。

function sum(...args) {
  const len = args.length;
  if(len === 1) {
    return 'no need'
  } else if(len === 2) {
    return args.reduce((a,b) => a+b);
  } else if(len === 3) {
    return args.reduce((a,b) => a*b)
  } else {
    return 'end'
  }
}
console.log(
  '\r\n', sum(1), 
  '\r\n', sum(1,2),
  '\r\n', sum(1,2,3),
  '\r\n', sum(),
);
// 结果如下:
no need
3
6
end