JTangming / blog

My repository on GitHub.
Other
53 stars 0 forks source link

原型、原型链以及几种继承方式 #22

Open JTangming opened 5 years ago

JTangming commented 5 years ago

原型

关于创建对象,最普通的办法就是工厂模式,工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

构造函数模式

一个简单的构造函数如下:

function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {alert(this.name); };
}

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍,如上每个实例都有一个名为 sayName() 的方法,但那两个方法不是同一个 Function 的实例。解决办法是构造函数中的方法指向某个外部函数,这样做问题是多个方法没有封装性。

原型模式

原型模式不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,如下面的例子所示:

function Person(){}
Person.prototype.name = "Nicholas”; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer”; 
Person.prototype.sayName = function(){ alert(this.name); };

创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向原型对象, 该对象是所有实例共享的属性和方法,简单说就是 prototype 是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法,与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。

可以通过下图来理解上面的表述: oop_1

tips: 使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在于原型中;通过 isPrototypeOf() 方法来确定是否是对象实例。

更简单的原型语法是使用对象字面量来重写这个原型对象,但通过 constructor 已经无法确定对象的类型了,也可以使用如下方式操作。

function Person(){} 
Person.prototype = {
  constructor : Person, 
  // ...
};

原型模式的问题是,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值,原型模式的最大问题是由其共享的本性所导致的。如在原型对象里边有一引用型属性,那么对象实例的修改,就会对其他对象实例造成影响。

解决办法是组合使用构造函数和原型模式,下面的代码解决了前面提到的问题。

function Person(name){ 
  this.name = name; 
  this.friends = ["Shelby", "Court"]; 
} 
Person.prototype = {
  constructor : Person,     
  sayName : function(){alert(this.name);}
}

在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor 和方法 sayName() 则是在原型中定义的。而修改了某一个对象实例的 friends并不会影响到其他对象实例的 friends,因为它们分别引用了不同的数组。

用以下关系图来总结上面的内容: prototype-2

原型链与继承

ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。实现继承是让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,如此层层递进,就构成了实例与原型的链条,这就是所谓原型链的基本概念。关系图如下: prototype-3

原型链的问题有:

那么除了直接使用原型实现继承,还有其他的方式吗?

借用构造函数继承

这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数,示例代码如下:

function Person(name, age) {
    this.name = name,
    this.age = age,
    this.setName = function () {}
  }
  Person.prototype.setAge = function () {}
  function Student(name, age) {
    Person.call(this, name, age);
    // ...
  }

这种方式只是实现部分的继承,如果父类的原型还有方法和属性,子类是拿不到这些方法和属性的,若将其写在构造函数中复用就无从谈起了。

组合继承(原型链+借用构造函数)

其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

function SuperType (name) {
  this.name = name,
  this.color = [‘red’, ‘blue'];
}
SuperType.prototype.setAge = function () {
  // ...
}
function SubType (name, age) {
  SuperType.call(this, name);
  this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.setAge = function () {
  // ...
}

组合继承的优点是可以继承实例属性,也可以继承原型方法,解决了引用属性的共享问题,可传参,缺点是调用了两次父类构造函数,生成了两份实例。

那要优化生成两份实例的问题,可以使用 SubType.prototype = new prototype; 的方式,缺点是没办法辨别实例是子类还是父类创造的。

寄生组合继承方式

直接上高级程序设计上的代码示例吧:

function object(original){ 
  function F() {}
  F.prototype = original;
  return new F();
}
function inheritPrototype(subType, superType) {  
  var prototype = object(superType.prototype);   
  prototype.constructor = subType;   
  subType.prototype = prototype; 
}
function SuperType(name) {
  this.name = name;   
  this.colors = ["red", "blue", "green"]; 
} 
SuperType.prototype.sayName = function() {alert(this.name); };
function SubType(name, age) {   
  SuperType.call(this, name);   
  this.age = age; 
}
inheritPrototype(SubType, SuperType); 
SubType.prototype.sayAge = function(){alert(this.age);}

这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType. prototype 上面创建不必要的、多余的属性,且原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf()。

ES6 class 继承

ES6 的继承机制与之前的方式完全不同,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this,其本质还是原型链的继承。