JavaScript每日一学:熟悉下JavaScript中常见的8种继承方式。
JS和Java中虽然都有对象的概念,但这两种对象却大有不同。Java的对象是基于类创建的,JS的对象却是基于一个特殊的对象------原型对象------创建的,之前看到一个盖房子的比喻,在Java中盖房子是先画好图纸再盖房子,JS中盖房子却是先盖一个样板房再盖其他房子,觉得也挺贴切。
所以JS中的继承和Java中的继承就大有不同了,是基于原型对象的,如果两个对象形成继承关系,那必然是其中一个对象的原型链上存在一个指针指向另一个对象。即使JS中的两个类声明了继承关系,也是表现在原型对象上。比如:
class A {
say() {
console.log('say: hello!');
}
}
class B extends A {
constructor() {
super();
}
}
console.log(A.prototype); // {constructor: ƒ, say: ƒ}
console.log(B.__proto__); // class A {}
console.log(B.prototype); // A {constructor: ƒ}
首先,类是JS中函数的语法糖,并且在JS中函数本身也是对象,也就是说A和B是两个对象,所以extends操作使得B自身的原型属性__proto__
指向了A,相当于const B = Object.create(A);
。
其次,类的继承关系也影响其生成的实例,众所周知,函数本身存在一个特殊的对象属性:prototype,函数经过构造调用产生的实例的原型属性__proto__
是指向这个对象的,而extends操作修改了B的prototype对象,所以B实例上的原型属性__proto__
也就被修改了,通过B实例的原型属性__proto__
能找到A的prototype,即在B实例的原型链上能找到A的prototype。
const b = new B();
console.log(b.__proto__); // A {constructor: ƒ} 即B.prototype
console.log(b.__proto__.__proto__); // {constructor: ƒ, say: ƒ} 即A.prototype
在JS中使用字面量定义的对象时,其默认的原型属性__proto__
指向Object的prototype对象,相当于默认继承自Object,所以字面量对象可以调用Object
的实例方法。
可以使用isPrototypeOf
来判断一个对象是否在另一个对象的原型链上。
由上述可知,JS中的继承关系与原型对象密切相关,为了达到继承的关联关系(共享某些属性和方法),就要从原型对象着手:
1.使用Object.create的方式创建对象,使两个对象直接产生继承关系
const o1 = {
name: 'o1',
age: 18,
walk() {
console.log('walking...')
}
};
const o2 = Object.create(o1);
console.log(o2.__proto__); // {name: 'o1', age: 18}
console.log(o2.walk()); // walking...
console.log(o1.isPrototypeOf(o2)); // true
2.使用new操作创建对象,使产生的实例和类(或函数)的原型对象产生继承关系
const b = new B();
console.log(B.prototype); // A {constructor: ƒ}
console.log(b.__proto__); // A {constructor: ƒ} 即B.prototype
console.log(B.prototype.isPrototypeOf(b)); // true
3.使用extends关键字使类形成继承关系,扩展类实例的原型链
class A {
say() {
console.log('say: hello!');
}
}
class B extends A {
constructor() {
super();
}
}
console.log(A.prototype); // {constructor: ƒ, say: ƒ}
const b = new B();
console.log(b.__proto__.__proto__); // {constructor: ƒ, say: ƒ} 即A.prototype
console.log(A.isPrototypeOf(B)); // true
console.log(A.isPrototypeOf(b)); // false
console.log(A.prototype.isPrototypeOf(b)); // true
4.修改函数的prototype属性使函数形成继承关系,扩展函数实例的原型链
function C() {
this.name = 'c';
this.operation = function() { return 'printing...'};
}
function D() {}
D.prototype = new C();
const d = new D();
console.log(d.__proto__.__proto__ === C.prototype); // true
console.log(C.prototype.isPrototypeOf(d)); // true
console.log(D.prototype.isPrototypeOf(d)); // true
这里存在一个问题,就是子类实例化时无法向父类的构造函数传参
5.盗用父类构造函数
在函数内部通过call或apply调用父类函数(非构造调用),可继承父类实例自身(非原型对象)的属性和方法(相当于把子类实例(即this)传递进父类函数,对这个this做了一遍操作),虽然可在初始化时传递参数给父类,但无法形成原型链
function E() {
C.call(this);
this.do = function () { return 'do homework'; }
}
const e = new E();
console.log(E.prototype.isPrototypeOf(e)); // true
console.log(C.prototype.isPrototypeOf(e)); // false
console.log(e); // E {name: 'c', operation: ƒ, do: ƒ}
console.log(e.do()); // do homework
子类产生的实例无法对父类及其原型对象应用instanceof和isPrototypeOf方法。
此时如果父类想共享方法给子类,必须把方法直接在定义在函数内部,绑定到实例上,而无法通过父类的prototype对象共享。
6.结合4和5,使得子类实例可继承父类原型对象的属性和方法,且能形成原型链
function E() {
C.call(this);
this.do = function () { return 'do homework'; }
}
E.prototype = new C();
const e = new E();
console.log(E.prototype.isPrototypeOf(e)); // true
console.log(C.prototype.isPrototypeOf(e)); // true
console.log(e); // E {name: 'c', operation: ƒ, do: ƒ}
console.log(e.do()); // do homework
7.用Object.create()替换new父类实例来重写子类的原型对象
function inheritatePrototype(subT, superT) {
let proto = Object.create(superT.prototype);
proto.constructor = subT;
subT.prototype = proto;
}
inheritatePrototype(E, C);
可以舍去new中不需要的操作
8.通过工厂方式共享属性和方式
类似工厂函数,但不是用裸的Object,以某种方式取得对象(如new等返回新对象的函数),对此对象加属性或方法以增强功能,并返回对象。
function createAnother(original) {
let clone = Object.create(original);
clone.xx = xxx;
return clone;
}
适合主要关注对象,而不在乎类型和构造函数的场景。
存在的问题: 必须在构造函数中定义方法(属于实例非原型对象的方法),函数不能重用。