继承
大多面向对象的语言,都是采用class实现对象继承,而在 JS 中,继承是通过原型链实现的。
构造函数与原型对象
➡️构造函数的缺点:
之前提到,JS 通过构造函数生成新对象,构造函数是对象的模板,但是只靠构造函数的话,同一个构造函数生产的多个实例之前无法共享一些公共属性,每个实例都是独立的完整的对象,没有复用,造成了资源的浪费。
➡️原型对象:
为了解决这个痛点,所以引入了原型对象 prototype。 原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,节省了内存,实例对象可视为原型对象的派生子对象。
➡️原型链:
每个对象都有自己的原型对象,可以在__proto__属性或Object.getPrototypeOf()方法来拿到。
万物皆对象,所有对象的原型都能一层层向上追溯到Object.prototype,其值为 null.
这也解释了 JS 如何实现的boxing机制,就是因为万物皆对象,且Object.prototype上挂载了 valueOf与toString方法,各个派生对象或派生对象的原型或有或无地对这俩方法进行了重写,对于任何对象都依赖同一接口进行类型转换,由此支撑boxing
➡️constructor属性:
原型对象上有一个constructor属性,指向原型对象所在的构造函数
constructor属性可被继承
function P() { }
const p = new P()
console.log(p.__proto__.constructor === P) // true
console.log(p.constructor === P) // true因此,我们可以从一个实例对象,新建另一个实例对象
const p2 = new p.constructor()
console.log(p2 instanceof P) // true⚠ 注意:
修改了原型对象的话,通常也会显式修改constructor属性。
当定义一个 JS 函数时,这个函数的prototype属性会自动获得一个constructor属性,这个属性就指向该函数。
为什么还要显式指定一下呢,这不是必要的,但是是推荐的, 可以避免直接 Person.prototype = {xxx}导致的constructor丢失
function Person(name) {
this.name = name;
}
Person.prototype.hi = function(){console.log('hello')}
Person.prototype.constructor = Person // 显示指定
const p = new Person('wang')
p.hi()
console.log(p)原型链
顺便回顾一下原型链。
注意:函数也是对象,下面的例子中,F 只是 Function 的一个派生对象而已, Function 也是一个对象, 只是 Object 的派生对象而已
function F() { this.n = '' }
var f = new F()
console.log(f.__proto__ === Object.getPrototypeOf(f))
// 从 f 对象出发
console.log(f.__proto__ === F.prototype)
console.log(f.__proto__.__proto__ === Object.prototype)
console.log(f.__proto__.__proto__.__proto__ === null)
console.log('===============================')
// 从 F 构造函数出发, 构造函数其实就是Function的派生实例
console.log(F.__proto__ === Function.prototype)
console.log(F.__proto__.__proto__ === Object.prototype)
console.log(F.__proto__.__proto__.__proto__ === null)instanceof
instanceof 判断对象是否为某个构造函数的实例, 它会查找对象的整条原型链。原始类型会直接false,这里不会做装箱。
// 手撕 instanceof
function myInstanceOf(obj, constructor){
// 原始类型直接返回false
if(typeof obj !== 'object' || obj === null) return false
let targetProto = constructor.prototype
let objProto = obj.__proto__
// 循环沿原型链向上找
while(true){
if(objProto === null) return false
if(objProto === targetProto) return true
objProto = objProto.__proto__
}
}继承的实现
在ES6之前,JS 没有 class 关键字,实现继承的方式经过了几代演变,这里主要说几个有用的。
组合继承
- 原理:将原型链继承和借用构造函数继承结合起来,既可以继承父类原型上的方法,又可以避免属性共享的问题。
- 优点:每个子类实例有自己的父类属性,互不影响 | 父类原型上的方法可以复用,是比较通用的实现
- 缺点:父类原型上的方法被调用两次,一次在原型链继承时,一次在借用构造函数继承时。
function Parent() {
this.name = 'parent';
}
Parent.prototype.sayName = function() {
console.log(this.name);
}
function Child() {
Parent.call(this);
this.type = 'child';
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var child = new Child();
child.sayName(); // 'parent'
console.log(child.name); // 'parent'寄生组合式继承
这是最理想的继承方式,使用Object.create()方法来创建父类原型的副本,避免了调用父类构造函数时创建不必要的实例。
- 原理:创建父类原型的一个副本,并将其赋值给子类原型,避免了调用两次父类构造函数。
- 优点:解决了所有组合继承的缺点,引用类型共享和父类构造函数被调用两次的问题。
function inherit(child, parent) {
var prototype = Object.create(parent.prototype); // 创建父类原型的一个副本 ! 隔离
prototype.constructor = child;
child.prototype = prototype; // 把副本作为 child 的原型
}
function Parent() {
this.name = 'parent';
}
Parent.prototype.sayName = function() {
console.log(this.name);
}
function Child() {
Parent.call(this);// 只调用1次父类构造函数
this.type = 'child';
}
inherit(Child, Parent);
var child = new Child();
child.sayName(); // 'parent'
console.log(child.name); // 'parent'ES6 Class 继承
ES6 引入了 class 关键字,但它本质上仍然是基于寄生组合式继承实现的。它并没有引入新的继承模型,只是让 JavaScript 的继承写法更接近传统面向对象语言。
extends 关键字和 super 关键字封装了寄生组合式继承的底层逻辑。
下面的例子,体现了,子类实例对象的原型是父类原型的副本,可以看到子类重写afunc时,父类的 afunc 并未改变。 先有个初印象,在学习 ES6 时,再充分学习。
class A {
afunc() {
console.log('afunc')
}
constructor() {
this.a = 'a'
// console.log('A constructor')
}
}
class B extends A {
b = 'b'
afunc() {
console.log('hello')
}
bfunc() {
console.log('bfunc')
}
constructor(name) {
// console.log('B constructor 1')
super()
this.name = name
// console.log('B constructor 2')
}
}
const b1 = new B('b1')
const b2 = new B('b2')
console.log(b1.afunc) // afunc() { console.log('hello') }
console.log(b1.__proto__.afunc, b1.afunc === b1.__proto__.afunc) // true
console.log(b1.__proto__.__proto__.afunc) // afunc() { console.log('afunc') }
b1.afunc()
console.log(b1) // B {a: 'a', b: 'b', name: 'b1'}
console.log(b1.__proto__) // A {afunc: ƒ, bfunc: ƒ}
console.log(b1.__proto__.__proto__) // {afunc: ƒ}