js 中每个类都有三个部分:
- 构造函数内的,这是供实例化对象复制用的
- 构造函数外的,直接通过点语法添加的,这是供类使用的,实例化对象访问不到
- 类的原型上的,实例化对象可以通过其原型链间接访问到,供所有实例化对象共用
然而,继承中所涉及的不仅仅是一个对象,并且 js 也没有继承这一现有的机制,那它如何实现的呢?
原型
在 JavaScript 中,每个构造函数都拥有一个prototype属性,它指向构造函数的原型对象,这个原型对象中有一个 construtor 属性指回构造函数;每个实例都有一个__proto__属性,当我们使用构造函数去创建实例时,实例的__proto__属性就会指向构造函数的原型对象。
1.类式继承
// 声明父类
function SuperClass() {
this.superValue = true;
}
// 父类添加共有方法
SuperClass.prototype.getSuperValue = function () {
return this.superValue;
};
// 声明子类
function SubClass() {
this.subValue = false;
}
// 继承父类
SubClass.prototype = new SuperClass();
// 为子类添加共有方法
SubClass.prototype.getSubValue = function () {
return this.subValue;
};
var instance1 = new SubClass();
console.log(instance.getSuperValue()); // true
console.log(instance.getSubValue()); // false
通常说这种继承方式有两个缺点:
- 由于子类通过其原型 prototype 对父类实例化,继承了父类。所以父类中的共有属性如果是引用类型,就会在子类中被所有实例共用,因此一个子类的实例在修改从父类构造函数中继承来的共有属性时,就会直接影响到其他的子类。
- 由于子类实现的继承是通过其原型 prototype 对父类的实例化实现的,因此在创建子类实例的时候,无法向父类构造函数中传递参数,因而在实例化父类的时候也无法对父类构造函数内的属性进行初始化。
2.构造函数继承
// 声明父类
function SuperClass(id) {
this.books = ["js", "html", "css"];
this.id = id;
}
SuperClass.prototype.showBooks = function () {
console.log(this.books);
};
// 声明子类
function SubClass(id) {
SuperClass.call(this, id);
}
var instance1 = new SubClass(10);
var instance2 = new SubClass(11);
instance1.books.push("设计模式");
console.log(instance1.books); // ['js', 'html', 'css', '设计模式']
console.log(instance1.id); // 10
console.log(instance2.books); // ['js', 'html', 'css']
console.log(instance2.id); // 11
console.log(instance1.showBooks()); // TypeError
缺点:SuperClass.call(this, id);是构造函数式继承的精华,由于这种类型的继承没有涉及原型 prototype,所以父类的原型方法不会被子类继承。而如果想被子类继承就必须要放在构造函数中,这样创建出来的每个实例就会单独拥有一份而不能共用,这样就违背了代码复用原则。
3.组合继承
// 声明父类
function SuperClass(name) {
this.books = ["js", "html", "css"];
this.name = name;
}
SuperClass.prototype.getName = function () {
console.log(this.name);
};
// 声明子类
function SubClass(name, time) {
SuperClass.call(this, name);
this.time = time;
}
SubClass.prototype = new SuperClass();
subClass.prototype.getTime = function () {
console.log(this.time);
};
var instance1 = new SubClass("js book", 2014);
instance1.books.push("设计模式");
console.log(instance1.books); // ['js', 'html', 'css', '设计模式']
instance1.getName(); // js book
instance2.getTime(); // 2014
var instance2 = new SubClass("css book", 2011);
console.log(instance2.books); // ['js', 'html', 'css']
instance2.getName(); // css book
instance2.getTime(); // 2011
缺点:在使用构造函数继承时执行了一遍父类的构造器,而在实现子类原型的类式继承时又调用了一遍父类构造函数,因此父类构造函数调用了两遍。
4.原型式继承
function inheritObject(o) {
// 声明一个过渡函数对象
function F() {}
// 过渡对象的原型继承父对象
F.prototype = o;
// 返回过渡对象的实例,该实例的原型继承了父对象
return new F();
}
var person = {
name: 'ming',
hobby: ['read', 'game', 'bike']
};
var p1 = inheritObject(person);
console.log(p1.name); // ming
它是对类式继承的一种封装,类式继承中的问题在这里也会存在,不过这种方式由于 F 过渡类的构造函数中无内容,所以开销比较小,使用起来比较方便。如果你觉得有必要可以将 F 过渡类缓存起来,不必每次创建一个新过渡类 F。当然这种顾虑也是不必要的,随着这种思想的深入,后来就出现了 Object.create()方法。Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
5.寄生式继承
function inheritObject(o) {
// 声明一个过渡函数对象
function F() {}
// 过渡对象的原型继承父对象
F.prototype = o;
// 返回过渡对象的实例,该实例的原型继承了父对象
return new F();
}
// 声明基对象
var book = {
name: "js",
alikeBook: ["css", "html"],
};
function createBook(obj) {
// 通过原型继承方式创建新对象
var o = new inheritObject(obj);
// 拓展新对象
o.getName = function () {
console.log(name);
};
// 返回拓展后的新对象
return o;
}
寄生式继承是对原型继承的第二次封装,并且在第二次封装的过程中对继承的对象进行了拓展,这样新创建的对象不仅具有父类中的属性和方法而且还添加新的属性和方法,而这种思想的作用也是为了寄生组合式继承模式的实现。使用寄生式继承来为对象添加函数,会由于不能做到函数服用而降低效率;这一点与构造函数模式类似。
6.寄生组合式继承
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。背后的思路是:不必为了指定子类型的原型而调用父类的构造函数,我们所需要的无非就是父类原型的一个副本。本质上,就是使用寄生式继承来继承超类型的原型,然后在将结果指定给子类型的原型。
6.1 方法一:
function inheritObject(o) {
// 声明一个过渡函数对象
function F() {}
// 过渡对象的原型继承父对象
F.prototype = o;
// 返回过渡对象的实例,该实例的原型继承了父对象
return new F();
}
function inheritPrototype(subClass, superClass) {
// 复制一份父类的副本保存在变量中
var p = inheritObject(superClass.prototype);
// 修正因为重写子类原型导致子类的constructor属性被修改
p.constructor = subClass;
subClass.prototype = p;
}
// 声明父类
function SuperClass(name) {
this.name = name;
this.colors = ["red", "orange", "blue"];
}
// 父类添加共有方法
SuperClass.prototype.getName = function () {
console.log(this.name);
};
// 声明子类
function SubClass(name, time) {
// 构造函数式继承
SuperClass.call(this, name);
// 子类新增属性
this.time = time;
}
// 寄生式继承父类原型
inheritPrototype(SubClass, SuperClass);
// 子类新增原型方法
SubClass.prototype.getTime = function () {
console.log(this.time);
};
6.2 方法二:
// 声明父类
function SuperClass(name) {
this.name = name;
this.colors = ["red", "orange", "blue"];
}
// 父类添加共有方法
SuperClass.prototype.getName = function () {
console.log(this.name);
};
// 声明子类
function SubClass(name, time) {
// 构造函数式继承
SuperClass.call(this, name);
// 子类新增属性
this.time = time;
}
// 继承父类原型(区别于方法一之处)
SubClass.prototype = Object.create(SuperClass.prototype);
SubClass.prototype.constructor = SubClass;
// 子类新增原型方法
SubClass.prototype.getTime = function () {
console.log(this.time);
};
7.ES6继承
ECMAScript 2015 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。类语法不会为 JavaScript 引入新的面向对象的继承模型。 ——MDN
class Animal {
// 构造函数,里面写上对象的属性
constructor(props) {
this.name = props.name || 'Unknown';
}
// 方法写在后面
eat() { // 父类共有的方法
console.log(this.name + " will eat pests.");
}
}
//class继承
class Bird extends Animal {
// 构造函数
constructor(props,myAttribute) {
// 调用实现父类的构造函数
super(props); // 相当于获得父类的this指向
this.type = props.type || "Unknown";
this.attr = myAttribute; // 自己的私有属性
}
fly() { // 自己私有的方法
console.log(this.name + " are friendly to people.");
}
myattr() { // 自己私有的方法
console.log(this.type + '---' + this.attr);
}
}
// 通过new实例化
var myBird = new Bird({ name: '小燕子', type: 'Egg'},'Bird');
myBird.eat();
myBird.fly();
myBird.myattr();