学习自:http://cavszhouyou.top/JavaScript%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E4%B9%8B%E7%BB%A7%E6%89%BF.html
继承方式
原型链
在 ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
Son.prototype = new Father("父亲");
Son.prototype.constructor = Son;
缺点:
-
包含引用类型的原型属性会被所有实例属性共享,容易造成属性的修改混乱。
-
在创建子类型的实例时,不能向父类型的构造函数中传递参数。
1.借用构造函数
借用构造函数的思想主要是在子类型的构造函数中调用父类型构造函数。如下面的例子所示
function Father(name) {
this.name = name;
this.colors = ["红", "黄", "蓝", "绿"];
}
// 借用构造函数,并非真实继承,所以 无法获取 Father 原型上的函数、属性
Father.prototype.saySth = "你好啊";
function Son(name) {
Father.call(this, name);
}
let f1 = new Son("儿子");
console.log(f1);
优点:可以在子类型构造函数中向父类型构造函数添加参数
缺点:和构造函数模式一样的问题,所有的方法都在构造函数中定义,因此就无法做到函数的复用。而且父类型的原型中定义的方法,对于子类型而言也是不可见的。
基于以上问题,借用构造函数的技术也是很少单独使用的。
2.组合继承
组合继承指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。这种方法的主要思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。如下面的例子所示:
function Father(name) {
this.name = name;
this.colors = ["绿色", "黄色", "蓝色", "红色"];
}
Father.prototype.SaySth = function () {
console.log(`我的名字是${this.name},我喜欢${this.colors}`);
};
function Son(name, age) {
Father.call(this, name); // 继承属性(借用构造函数)
this.age = age;
}
Son.prototype = new Father("父亲"); // (原型链) 让 子类 原型指向 父类实例,如此之后 son.__proto__ = father ,而 father 的__proto__ 为Father.prototype
Son.prototype.constructor = Son;
/*
// Son.prototype = Father.prototype;
不能指向 父类原型的原因是:
上述这种方式,在将 Son.prototype.constructor 指向Son的话,同时会改变 Father.prototype.constructor
这样会让 父类原型的constructor指向了 子类构造函数,这就本末倒置了
*/
let son = new Son("儿子", 18);
son.SaySth();
console.log(son);
优点:组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且,instanceof 和 isPropertyOf() 也能够用于识别基于组合继承创建的对象。
缺点:调用了两次父类的构造函数,导致子类的proto中增添了不必要的父类的实例对象中的所有属性。
3.原型式继承
原型式继承的主要思路是可以基于已有的对象创建新的对象,同时还不必因此创建自定义类型。如下面的例子所示
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
简单来说这个函数的作用就是,传入一个对象,返回一个原型对象为该对象的新对象。
ECMAScript 5 中新增了 Object.create() 方法规范了原型式继承。这个方法接收两个参数,一个是将被用作新对象原型的对象,一个是为新对象定义额外属性的对象(可选)。
注意第二个参数的格式与 Object.defineProperties() 方法的第二个参数格式相同。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。在第二个参数为空的情况下,该方法与 object() 方法的行为相同。
优点:可以实现基于一个对象的简单继承,不必创建构造函数
缺点:与原型链中提到的缺点相同,一个是传参问题,一个是属性共享的问题。
使用场合:没必要构建构造函数,仅仅是想模拟一个对象的时候
4.寄生式继承
寄生式继承的思路是,创建一个仅用于封装继承过程的函数,该函数在内部以某种方式增强对象,最后返回这个对象。如下面的例子所示
function createAnother(original) {
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function () {
// 某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
var person = {
name: "james",
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"
优点: 在主要考虑对象而不是自定义类型和构造函数的情况下,实现简单的继承。
缺点:使用该继承方式,在为对象添加函数的时候,没有办法做到函数的复用。
5.寄生式组合继承
前面我们提到过了组合继承的缺点,由于调用了两次父类的构造函数,导致子类的原型对象中增添了不必要的父类的实例对象中的所有属性。
寄生式组合继承就是用来解决这个问题,它与组合继承不同的地方主要是,在继承原型时,我们继承的不是父类的实例对象,而是原型对象是父类原型对象的一个实例对象,这样就解决了子类的原型对象中增添了不必要的父类的实例对象中的所有属性的问题。
我们可以封装继承原型时函数如下:
function Father(name) {
this.name = name;
this.colors = ["红", "黄", "蓝", "绿"];
}
Father.prototype.saySth = "你好啊";
function Son(name) {
// 借用构造函数,并非真实继承,所以 无法获取 Father 原型上的函数、属性
Father.call(this, name);
}
/* ================== 和组合继承 相比多了的步骤 ======================== */
/* 主要是为了让 Son的原型不会有 Father构造函数里的属性,所以用Father的原型创建了一个只有原型而没有构造函数属性的 实例 f1 */
function MyObject(o) {
function F() {}
F.prototype = o;
return new F();
}
let f1 = MyObject(Father.prototype); // 返回的f1是 F的实例,且F的构造函数没有任何属性,实例f1只有Father的原型属性,方法
/* ======================================================== */
Son.prototype = f1;
Son.prototype.constructor = Son;
let s1 = new Son("儿子");
console.log(s1, Father.prototype);
优点:效率高,避免了在 Son.prototype 上创建不必要的属性。与此同时还能保持原型链不变,开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
总结
-
原型链 : 会使得 子类都共享 原型上的属性、方法,且创建子实例时无法向父类构造函数传参
-
借用构造函数 : 不算是继承的继承,只是改变了 this 指针,让 son 的 tshi 指针调用了 Father 的构造函数,所以可以得到 Father 的 构造函数上的属性、方法,但无法获取 Father 原型上的属性、方法。
-
组合继承 : 结合上述 2 种方式,虽然即得到了原型上的属方,又获得了构造函数里的属方,但是调用了 2 次父类构造函数,原型链上会产生父类实例的属方,借用构造函数也会产生父类实例的属方。虽然借用构造函数可以改变其属性值(且该属方才是子类真正的属方),但多出来的父类实例属方确实多余的
-
原型式继承 : 一种为了后面的继承方式的工具……该继承方式的构造函数里没有任何属方,但是其实例的 proto 指向 原型。故为原型式继承。(和原型链继承的区别:原型链继承 prototype=new Father();会把其 Father 的实例的属方一起继承过来)
-
寄生式继承 :在原型式继承的基础上,增强了其返回的对象
-
寄生式组合继承 : 利用 原型式继承 的构造函数里没有任何属方的特性,将子类的 prototype 赋值给 原型式继承 返回的实例,子类的构造函数里还是用 借用构造法Father.call(this),如此既获得了原型属方,又获得了构造函数里的属方,又不会有重复的父类属方