阅读 327

javascript继承机制详解

基础知识

javascript 依靠原型链进行属性的查找,当调用对象的属性时js会一步一步链式往上查找直到找到对应属性或到顶层对象(null),如(A是一个构造函数,a是通过构造函数生成的一个实例):

function A(){}
const a = new A()
复制代码
  • js对象有一个constructor函数和一个原型对象(可以通过__proto__属性或Object.getPrototypeOf方法获取)。
  • 函数本身在js中也是一个对象。
  • 函数具有prototype属性,prototype属性本身是一个js对象,prototype对象的constructor指向函数自身。(当通过new 关键词调用函数时,js首先创建一个空对象({}),并将该空对象的原型对象指向该函数的prototype属性,之后将函数体的this指向新建的空对象,执行函数体代码后返回对象)。
  • 实例对象的原型对象指向生成该实例对象的构造函数的prototype属性。

为了便于观看拆成了3个图,可以按照最后的节点映射到其他图上,实际是一张图上的关系(粗横线表示相等关系,箭头代表前面的对象拥有后面的属性)

graph LR
a --> a.constructor===A
a --> a.__proto__===A.prototype
A --> A.prototype
A.prototype --> A.prototype.__proto__===Object.prototype
A.prototype --> A.prototype.constructor===A
A --> A.constructor===Funciton
A --> A.__proto__===Function.prototype
graph LR
Funciton --> Function.prototype
Function.prototype -->Function.prototype.constructor===Funciton
Function.prototype -->Function.prototype.__proto__===Object.prototype
Funciton === Function.constructor
Funciton --> Function.__proto__===Function.prototype
graph LR
Object --> Object.prototype
Object --> Object.constructor===Funciton
Object.prototype --> Object.prototype.__proto__===null
Object --> Object.__proto__
Object.__proto__ --> Object.__proto__.constructor===Function
Object.__proto__ --> Object.__proto__.__proto__
Object.__proto__.__proto__ --> Object.__proto__.__proto__.__proto__===null
Object.__proto__.__proto__ --> Object.__proto__.__proto__.constructor===Object
Object.prototype --> Object.prototype.constructor===Object

a做为实例对象是由A函数生成的,拥有constructor和原型对象,a.constructor是A,原型对象指向A.prototype;

console.log(a.constructor === A) // true
console.log(a.__proto__===A.prototype) // true
复制代码

A是一个函数且本身也是一个实例对象;

console.log(A instanceof Object) // true
复制代码

A做为实例对象是由Function函数生成的,拥有constructor和原型对象,A.constructor是Function,原型对象指向Function.prototype;

console.log(A.constructor === Function) // true
console.log(Function.__proto__===Function.prototype) // true
复制代码

A是函数,所以有prototype属性,A.prototype属性是一个对象,其构造函数指向A自身(按照其他对象的规律推测应该指向Object,从原型对象可以看出,但这正是函数特殊的地方),原型对象指向Object.prototype;

console.log(A.prototype.constructor === A) // true
console.log(A.prototype.__proto__===Object.prototype) // true
复制代码

javascript继承

原型链继承

通过上面可以看出构造函数中的prototype对象会体现在实例对象的原型对象__proto__(尽量使用Object.getPrototypeOf获取)上,而js获取属性的时候是沿着原型链逐级往上找,直到拿到属性或者到顶层的null为止,所以可以修改构造函数的prototype对象实现属性的扩展

function Parent(name) {
  this.name = name;
}
Parent.prototype.getName = function () {
  return this.name;
}

function Child(desc) {
  this.desc = desc;
}
const parent = new Parent('parentName');
Child.prototype = parent;
const child = new Child("这是一个子类实例");
console.log(child.getName(), child.desc); // parentName 这是一个子类实例
复制代码

原型链继承的弊端在于如果子类实例修改了所继承的实例的引用类属性(如对象,数组等),会导致全部的子类实例的属性都被修改:

function Parent(name) {
  this.name = name;
  this.type = "initType";
  this.obj = { key: 'key', value: 'value' }; // 引用类型
  this.getName = function () {
    return this.name;
  }
}
Parent.prototype.getName = function () {
  return this.name;
}

function Child(desc) {
  this.desc = desc;
}

const parent = new Parent('parentName');
Child.prototype = parent;
const child1 = new Child('desc1');
const child2 = new Child('desc2');

child1.type = 'child1';
child1.obj.customProperty = 'child1';

console.log(child1.getName(), child1.desc, child1.type, child1.obj); // parentName desc1 child1 { key: 'key', value: 'value', customProperty: 'child1' }
console.log(child2.getName(), child2.desc, child2.type, child2.obj); // parentName desc2 initType { key: 'key', value: 'value', customProperty: 'child1' }
复制代码

借用构造函数继承

上述原型链继承可以看到没有办法在实例初始化的时候修改其继承的属性(子类给父类构造函数传参),只能在实例中访问和修改,如果修改的是引用类型的值,会导致所有子类实例都被修改的bug,因为是共用一个父类实例作为子类原型对象。可以通过借用父类的构造函数来解决这些问题

function Parent(name) {
  this.name = name;
  this.type = "initType";
  this.obj = { key: 'key', value: 'value' }; // 引用类型
  this._getName = function () {
    return this.name;
  }
}
Parent.prototype.getName = function () {
  return this.name;
}

function Child(name, desc) {
  Parent.call(this, name);
  this.desc = desc;
}

const child1 = new Child('name1', 'desc1');
const child2 = new Child('name2', 'desc2');

child1.type = 'child1';
child1.obj.customProperty = 'child1';

// 注意使用的是_getName(),因为getName不存在
console.log(child1._getName(), child1.desc, child1.type, child1.obj); // name1 desc1 child1 { key: 'key', value: 'value', customProperty: 'child1' }
console.log(child2._getName(), child2.desc, child2.type, child2.obj); // name2 desc2 initType { key: 'key', value: 'value' }
// console.log(child1.getName()); // TypeError: child1.getName is not a function
// console.log(child2.getName()); // TypeError: child1.getName is not a function
复制代码

借用构造函数的弊端在于无法复用方法和不能访问父类prototype中的属性,因为采用构造函数的方式所有方法都要在构造函数中指定,根本没有扩展一说(所以上面例子中没法使用getName而是在构造函数中新建了_getName)。借用构造函数的本质是在子类中执行父类的构造方法,然后将父类构造方法中的this指向当前子类,所以父类的原型对象和子类没有任何关系,导致子类实例无法访问父类的原型对象。

组合继承

结合上面两种方法的利弊几乎可以完成我们需要达到的继承效果,也是javascript中最常用的继承模式,组合继承就是值将原型链和借用构造函数两种方法组合到一起,从而发挥两者之长的一直继承模式。注意:引用类型的值只能在构造函数中,如果写在父类的原型上,依然会出现一个子类实例修改后其他所有子类实例变更的问题(共用一个父类原型,正好复用方法)。

function Parent(name) {
  this.name = name;
  this.type = "initType";
  this.obj = { key: 'key', value: 'value' }; // 引用类型
}
Parent.prototype.getName = function () {
  return this.name;
}

function Child(name, desc) {
  Parent.call(this, name);
  this.desc = desc;
}
Child.prototype = new Parent();

const child1 = new Child('name1', 'desc1');
const child2 = new Child('name2', 'desc2');

child1.type = 'child1';
child1.obj.customProperty = 'child1';

console.log(child1.getName(), child1.desc, child1.type, child1.obj); // name1 desc1 child1 { key: 'key', value: 'value', customProperty: 'child1' }
console.log(child2.getName(), child2.desc, child2.type, child2.obj); // name2 desc2 initType { key: 'key', value: 'value' }
复制代码

原型式继承

借助原型基于已有对象创建新对象,和Object.create方法仅传入第一个参数的时候的行为一致:

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}
复制代码

通过原型链的形式加上一个临时的构造函数,最后返回一个由临时构造函数生成的新对象,对于属性的读取可以到原型链,但对于属性的修改因为返回是新对象所以相当于直接在新对象中加入一个属性值,但对于修改原型中引用类型的属性中的一个值时,原型链继承的弊端也会体现出来,修改之后其他的实例对象也会全部被修改:

function object(o) {
  function F() { }
  F.prototype = o;
  return new F();
}

const parent = {
  type: "parentType",
  color: ["red", "green", "blue"],
}

const o1 = object(parent);
const o2 = object(parent);
console.log(o1, o2); // {} {}
console.log(Object.getPrototypeOf(o1) === Object.getPrototypeOf(o2), Object.getPrototypeOf(o1)); // true { type: 'sourceType', color: [ 'red', 'green', 'blue' ] }
o1.type = 'o1Type';
console.log(o1, o2); // { type: 'o1Type' } {}
o1.color[0] = 'o1 hohoho';
console.log(o1, o2); // { type: 'o1Type' } {}
console.log(Object.getPrototypeOf(o1) === Object.getPrototypeOf(o2), Object.getPrototypeOf(o1)); // true { type: 'sourceType', color: [ 'o1 hohoho', 'green', 'blue' ] }
console.log(o2.type, o2.color); // parentType [ 'o1 hohoho', 'green', 'blue' ]
复制代码

寄生式继承

在原型式继承的基础上叠加属性或方法后返回新的对象(工厂模式)

function object(o) {
  function F() { }
  F.prototype = o;
  return new F();
}

const parent = {
  type: "sourceType",
  color: ["red", "green", "blue"],
}

function proxyFactory(o) {
  const clone = object(o);
  clone.getPrototypeObj = function () {
    return Object.getPrototypeOf(clone);
  }
  return clone;
}

const child = proxyFactory(parent);
console.log(child.getPrototypeObj()); // { type: 'sourceType', color: [ 'red', 'green', 'blue' ] }
复制代码

寄生组合式继承

组合继承几乎完美实现了js的继承,但这种方法必定需要调用两次父类的构造函数(生成父类实例时调用一次,借用构造函数时调用一次)。生成父类实例这一步调用构造函数生成的属性,其实被后面借用构造函数调用生成的属性全部覆盖,而我们只是使用父类实例的原型对象(父类构造函数的prototype),并不需要他生成属性,所以可以去掉这一步构造函数的调用:

function Parent(name) {
  this.name = name;
  this.type = "initType";
  this.obj = { key: 'key', value: 'value' }; // 引用类型
}
Parent.prototype.getName = function () {
  return this.name;
}

function Child(name, desc) {
  Parent.call(this, name);
  this.desc = desc;
}
Child.prototype = Parent.prototype; // 将原来的 new Parent()改成Parent.prototype

const child1 = new Child('name1', 'desc1');
const child2 = new Child('name2', 'desc2');

child1.type = 'child1';
child1.obj.customProperty = 'child1';

console.log(child1.getName(), child1.desc, child1.type, child1.obj); // name1 desc1 child1 { key: 'key', value: 'value', customProperty: 'child1' }
console.log(child2.getName(), child2.desc, child2.type, child2.obj); // name2 desc2 initType { key: 'key', value: 'value' }
复制代码

这样将原来的 new Parent()改成Parent.prototype的方法看似达到了想要的想过,但是如果这个时候我们在子类自己的原型上加上一些方法,却发现改动了父类的实例,这样也导致其他同样继承自这个父类的子类出现不该有的属性:

function Parent(name) {
  this.name = name;
  this.type = "initType";
  this.obj = { key: 'key', value: 'value' }; // 引用类型
}
Parent.prototype.getName = function () {
  return this.name;
}

function Child(name, desc) {
  Parent.call(this, name);
  this.desc = desc;
}
Child.prototype = Parent.prototype; // 将原来的 new Parent()改成Parent.prototype
Child.prototype.getDesc = function () {
  return this.desc;
}
const child1 = new Child('name1', 'desc1');
const child2 = new Child('name2', 'desc2');

child1.type = 'child1';
child1.obj.customProperty = 'child1';

console.log(child1.getName(), child1.getDesc(), child1.type, child1.obj); // name1 desc1 child1 { key: 'key', value: 'value', customProperty: 'child1' }
console.log(child2.getName(), child2.getDesc(), child2.type, child2.obj); // name2 desc2 initType { key: 'key', value: 'value' }

const parentInstance = new Parent('parent');
// 父类实例的原型对象也被迫加入该getDesc方法
console.log(Object.getPrototypeOf(parentInstance)); // { getDesc: [Function (anonymous)], getName: [Function (anonymous)] }
复制代码

所以只需复制Parent.prototype对象给Child.prototype当成自己的对象就行了,这正是原型式继承干的事情,而例子中又在原型上加了getDesc方法,这正好是寄生式继承干的事情,寄生式继承和组合继承合起来就出现寄生组合式继承。

function object(o) {
  function F() { }
  F.prototype = o;
  return new F();
}
function proxyFactory(superFun, subFun, propertySettionCallback) {
  const clonePrototype = object(superFun.prototype);
  // 之前原型链基础时有提到构造函数的prototype的constructor应该等于自身
  clonePrototype.constructor = subFun;
  subFun.prototype = clonePrototype;
  propertySettionCallback(subFun.prototype);
  return subFun;
}

function Parent(name) {
  this.name = name;
  this.type = "initType";
  this.obj = { key: 'key', value: 'value' }; // 引用类型
}
Parent.prototype.getName = function () {
  return this.name;
}

function Child(name, desc) {
  Parent.call(this, name);
  this.desc = desc;
}


Child = proxyFactory(Parent, Child, function (prototype) {
  prototype.getDesc = function () {
    return this.desc;
  }
})

const child1 = new Child('name1', 'desc1');
const child2 = new Child('name2', 'desc2');

child1.type = 'child1';
child1.obj.customProperty = 'child1';

console.log(child1.getName(), child1.getDesc(), child1.type, child1.obj); // name1 desc1 child1 { key: 'key', value: 'value', customProperty: 'child1' }
console.log(child2.getName(), child2.getDesc(), child2.type, child2.obj); // name2 desc2 initType { key: 'key', value: 'value' }

const parentInstance = new Parent('parent');
console.log(Object.getPrototypeOf(parentInstance)); // { getName: [Function (anonymous)] }
复制代码

父类的原型没有了子类在原型上加的属性或方法,父类被多个子类继承的问题也解决了,寄生组合式继承普遍被认为是最理想的继承模式。

文章分类
前端
文章标签