JavaScript 中的继承
继承是面向对象中只要的概念,分为接口继承、实现继承。继承接口其实就是继承方法签名,而实现继承是继承实际的方法。在JavaScript中因为函数没有签名,所以实现继承是 ES 唯一的继承方式,其主要是基于原型链来实现的。
1. 原型链继承
将父类的实例作为子类的原型
代码语言:javascript复制function Father() {
this.property = true
}
Father.prototype.getFatherValue = function () {
return this.property
}
function Son() {
this.sonProperty = false;
}
// 继承 Father
Son.prototype = new Father();
Son.prototype.getSonValue = function (){
return this.sonProperty;
}
let son1 = new Son();
console.log(son1.getFatherValue()) // true
console.log(son1 instanceof Son) // true
console.log(son1 instanceof Father) // true
创建 Father
的实例,并赋值给 Son
的原型 Son.prototype
。实现了 Son 继承 Father,
子类如需覆盖父类的方法,或者增加父类的方法,必须在原型赋值之后再添加到原型上。
优点
在父类的原型上新增的方法和属性,子类都能访问到
子类实例对象的原型链上既能找到子类构造函数的 prototype,也能找到到父类构造函数的 prototype
代码语言:javascript复制console.log(son1 instanceof Son) // true
console.log(son1 instanceof Father) // true
缺点
- 原型中包含的引用值会在所有实例间共享(修改一个实例的引用类型属性,其他所有实例上的改属性都会跟这变)
- 子类型在实例化时不能给父类型的构造函数传参
- 为子类新增属性和方法,不能在构造函数中
function Father() {
this.color = ['white']
}
function Son() {
this.sonProperty = false;
}
// 继承
Son.prototype = new Father();
let instance = new Son();
instance.color.push('blue');
console.log(instance.color) // [ 'white', 'blue' ]
let son2 = new Son();
console.log(son2.color) // [ 'white', 'blue' ]
2. 借用构造函数继承
代码语言:javascript复制我们可以通过 call 函数设置 this 的指向,在一个类中执行了一个类的构造函数,来得到需要的所有属性
function Father(name) {
this.name = name;
}
Father.prototype.say = function say() {
console.log(this.name)
}
function Son() {
// 继承 Father 并传参
Father.call(this, "AndyHu");
// 实例属性
this.age = 29;
}
let son1 = new Son();
console.log(son1.name); // "AndyHu";
console.log(son1.age); // 29
console.log(son1.say()) // 报错,找不到方法
console.log(son1 instanceof Son) // true
console.log(son1 instanceof Father) // false
优点:
- 插件子类实例时,可以向父类传递参数
- 可以通过 call 实现多继承
缺点:
- 只能继承父类的实例属性,不能继承父类原型上的属性、方法。
- 子类实例对象的原型链上只能找到子类构造函数的 prototype,找不到父类构造函数的 prototype
console.log(son1 instanceof Son) // true
console.log(son1 instanceof Father) // false
// 或者这样表示
// 构造函数的 prototype 属性是否出现在某个实例对象的原型链上
console.log(son1.__proto__ === Son.prototype) // true
console.log(son1.__proto__.__proto__ === Father.prototype) // false
3. 组合式继承
代码语言:javascript复制原型链继承和借用构造函数继承的缺点都比较明显,那我们可以综合原型链继承、借用构造函数继承的优点组合继承。思路是使用原型链继承原型上的属性和方法,借用构造函数继承实例属性。
function Father(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
Father.prototype.sayName = function () {
console.log(this.name);
};
function Son(name, age) {
// 继承属性
Father.call(this, name);
this.age = age;
}
// 继承方法
Son.prototype = new Father();
Son.prototype.sayAge = function () {
console.log(this.age);
};
let son1 = new Son("Nicholas", 29);
son1.colors.push("black");
console.log(son1.colors); // "red,blue,green,black"
son1.sayName(); // "Nicholas";
son1.sayAge(); // 29
// 构造函数指向被修改了,指向 Father
console.log(son1.constructor) // [Function: Father]
let son2 = new Son("Greg", 27);
console.log(son2.colors); // "red,blue,green"
son2.sayName(); // "Greg";
son2.sayAge(); // 27
优点
可以继承实例上和原型上的属性和方法
子类型在实例化时可以给父类型的构造函数传参
子类实例对象的原型链上既能找到子类构造函数的 prototype,也能找到到父类构造函数的 prototype
代码语言:javascript复制console.log(son1 instanceof Son) // true
console.log(son1 instanceof Father) // true
// 或者这样表示
// 构造函数的 prototype 属性是否出现在某个实例对象的原型链上
console.log(son1.__proto__ === Son.prototype) // true
console.log(son1.__proto__.__proto__ === Father.prototype) // true
缺点
- 会调用两次父类构造函数
- 需要修复构造函数指向
4. 原型式继承
通过原型的方式给父类实例对象添加属性、方法,并直接作为子类实例。
代码语言:javascript复制道格拉斯·克罗克福德(Douglas Crockford)在一篇文章中介绍了一种新的继承方式,严格意义上没有构造函数,不适用自定义类型,通过原型实现对象之间的信息共享。文中给出了一个函数 function object(o) { function F() {} F.prototype = o; return new F(); }
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
let person = {
name: "AndyHu",
friends: ["Alex", "KangKang"]
};
const person2 = object(person)
person2.friends.push('Jack')
person2.name= 'AndyWang'
console.log(person.friends) // [ 'Alex', 'KangKang', 'Jack' ]
console.log(person2.friends) // [ 'Alex', 'KangKang', 'Jack' ]
console.log(person.name) // AndyHu
console.log(person2.name) // AndyWang
本质上,object() 是对传入的对象进行了一次浅复制。
优点
- 感觉没啥优点吧,只能说非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合的情况
缺点
- 原型中包含的引用值会在所有实例间共享(修改一个实例的引用类型属性,其他所有实例上的改属性都会跟这变)
Object.create() 方法规范化原型式继承
代码语言:javascript复制let person = {
name: "AndyHu",
friends: ["Alex", "KangKang"]
};
const person2 = Object.create(person)
person2.friends.push('Jack')
person2.name= 'AndyWang'
console.log(person.friends)
console.log(person2.friends)
console.log(person.name)
console.log(person2.name)
Object.create()
的第二个参数和 Object.defineProperties()
的第二个参数一样,可以描述新增的属性的configurable
(可更改、删除属性)、enumerable
(可枚举)、value
(初始化值)、writable
(可通过赋值运算符更改属性值)。
5. 寄生式继承
代码语言:javascript复制给父类实例添加属性和方法,作为子类实例。(与原型式继承类似)
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
function createAnother(original) {
let clone = object(original)
clone.sayHi = function() {
console.log("hi")
}
return clone;
}
let person = {
name: "AndyHu",
friends: ["Alex", "KangKang"]
};
let anotherPerson = createAnother(person);
console.log(anotherPerson.sayHi()) // hi
优点
- 和原型式继承类似,适用只在乎对象间共享信息,而不在乎类型和构造函数的情况。
缺点
- 实例是父类的实例,不是子类的实例
6. 寄生式组合继承
代码语言:javascript复制组合继承的一个缺点是创建子类实例会执行两次构造函数,一次是在创建子类原型上调用了一次,另一次实在子类构造函数中调用。 那我们可以这样想:不通过调用父类构造函数来创建子类原型,而是借鉴寄生式继承的方式取得父类的一个副本,赋值给子类原型。这样结合寄生式继承和组合继承的优点来实现继承的方式叫做寄生式组合继承,算是一种比较完美的方式了!
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值对象
}
function Father(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
Father.prototype.sayName = function () {
console.log(this.name);
};
function Son(name, age) {
// 继承属性
Father.call(this, name);
this.age = age;
}
// 继承
// Son.prototype = new Father();
// 通过拷贝父类的方式赋值给子类原型来实现继承
inheritPrototype(Son,Father)
Son.prototype.sayAge = function () {
console.log(this.age);
};
let son1 = new Son("Nicholas", 29);
son1.colors.push("black");
console.log(son1.colors); // "red,blue,green,black"
son1.sayName(); // "Nicholas";
son1.sayAge(); // 29
// 构造函数指向被修改了,指向 Father
console.log(son1.constructor) // [Function: Son]
let son2 = new Son("Greg", 27);
console.log(son2.colors); // "red,blue,green"
son2.sayName(); // "Greg";
son2.sayAge(); // 27
我们也可以使用 Object.create()
来代理 inheritPrototype
方法
Son.prototype = Object.create(Father.prototype);
Son.prototype.constructor = Son;
写在最后
我是 AndyHu,目前暂时是一枚前端搬砖工程师。
文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注呀