JavaScript中常见的继承方式总结
作者:beckyyyy
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; }
适合主要关注对象,而不在乎类型和构造函数的场景
存在的问题: 必须在构造函数中定义方法(属于实例非原型对象的方法),函数不能重用
到此这篇关于JavaScript中常见的继承方式总结的文章就介绍到这了,更多相关JavaScript继承方式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!