一文详解 JS 中常见继承方式
原型链继承
原型链
首先每个构造函数(其实是每个函数都有)都有一个原型对象,同时原型有一个属性回指构造函数,而实例内部有一个指针 [[prototype]]
(__ptoto__
) 指向原型;
原型又可以看做是另一个构造函数的实例,那么对于原型来说,它也有一个内部指针 [[prototype]]
(即 __ptoto__
)指向它的原型;
那么这个原型链什么时候到头呢?
实际上,任何函数的默认原型都是一个Object的实例,所以原型链的尽头就是 Object 原型的原型,即 Object.prototype.__ptoto__
,我们可以用 Object.getPrototypeOf(Object.prototype)
获取,它的值为 null
;
原型链继承代码
原型链继承就是基于这个思想:通过把自己的原型指向另外一个构造函数的实例对象,从而"继承"这个对象上的属性、方法
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
// 让 SubType 这个构造函数的原型指向 SuperType 的实例
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true
这个例子中实现继承的关键,是 SubType
没有使用默认原型,而是将其替换成了一个新的对象。这个新的对象恰好是 SuperType
的实例。这样一来,SubType
的实例不仅能从 SuperType
的实例中继承属性和方法,而且还与 SuperType
的原型挂上了钩。
问题
- 原型中包含的引用值会在所有实例间共享;
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {}
// 继承SuperType
SubType.prototype = new SuperType();
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red, blue, green, black"
let instance2 = new SubType();
console.log(instance2.colors); // "red, blue, green, black"
- 子类型在实例化时不能给父类型的构造函数传参,所有类型的默认值都是相同的,不能够定制化;
借用构造函数继承
就是在子类构造函数中调用父类构造函数,使得子类的实例上也能拥有父类的相关属性。
来看下面的例子:
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
//继承SuperType
SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red, blue, green, black"
let instance2 = new SubType();
console.log(instance2.colors); // "red, blue, green"
子类可以向父类传递参数:
function SuperType(name){
this.name = name;
this.sayName = function() {
console.log(this.name);
};
}
function SubType() {
// 继承SuperType并传参
SuperType.call(this, "Nicholas");
// 实例属性
this.age = 29;
}
let instance = new SubType();
console.log(instance.name); // "Nicholas";
console.log(instance.age); // 29
问题
- 方法都在构造函数中定义,每次创建实例都会创建一遍方法。
- 因为没有使用原型链,所以子类不能访问父类原型上定义的方法。
组合继承
组合模式综合了原型链模式和借用构造函数的模式;是 JavaScript 中最常用的继承模式。
总的来说就是:使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age){
// 调用父类构造函数 继承属性
SuperType.call(this, name);
this.age = age;
}
// 调用父类构造函数 通过原型链 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
};
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // "red, blue, green, black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red, blue, green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27
问题
父类构造函数始终会被调用两次:一次在是创建子类原型时调用(用来继承方法),另一次是在子类构造函数中调用(用来继承属性)。
原型式继承
其实也就是 Object.create()
的模拟实现:
function createObj(o) {
// 创建一个临时构造函数
function F(){}
// 将这个构造函数的原型指向传入的对象
F.prototype = o;
// 返回这个构造函数的实例
return new F();
}
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
// yetAnotherPerson 修改引用类型的值,会导致 person 的值也被修改
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // "Shelby, Court, Van, Rob, Barbie"
原型式继承非常适合:你需要在两个对象之间共享属性,但是又不想创建一个构造函数,通过实例化这个构造函数来实现共享属性的时候。
问题
属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。
寄生式继承
创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。
function createAnother(original){
let clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"
问题
跟借用构造函数模式一样,每次创建对象都会创建一遍方法。
寄生式组合继承
组合继承其实也存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次:一次在是创建子类原型时调用,另一次是在子类构造函数中调用。
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
// 关键的三步
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
var child1 = new Child('kevin', '18');
console.log(child1);
封装后:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function prototype(child, parent) {
var prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
// 当我们使用的时候:
prototype(Child, Parent);
ES6 继承
ES6 中的继承使用 extends
关键字可以创建一个类,该类继承自另一个类;子类可以继承父类的属性和方法,并且还可以添加自己的属性和方法:
class Parent {
constructor(name) {
this.name = name;
}
sayHello() {
console.log("Hello, " + this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类的构造函数
this.age = age;
}
sayAge() {
console.log("I am " + this.age + " years old.");
}
}
const child = new Child("Alice", 10);
child.sayHello(); // 输出:Hello, Alice
child.sayAge(); // 输出:I am 10 years old.
在上面的例子中,Parent
是一个父类,Child
是一个子类。Child
通过 extends
关键字继承了 Parent
的属性和方法。
在 Child
的 constructor
中,通过 super
关键字调用了父类的构造函数,以便初始化父类的属性。然后,Child
可以使用父类的方法 sayHello
,并且还可以添加自己的方法 sayAge
。
总的来说,ES6 引入了 class
和 extends
关键字,使得继承更加简洁和易于理解。
以上就是 JS 中常见的几种继承方式,小伙伴们都学会了吗~