# 原型的理解

面向对象编程,就要理解对象的创建,原型属性、原型链,继承等。

# 构造器函数

对象可以使用构造器函数来创建,函数默认返回undefined,但构造器函数隐式返回this。构造器函数创建对象后,该对象有一个特殊属性-构造器属性,该属性指向创建该对象的构造器函数。其实这个属性是继承自原型对象的构造器属性,原型对象这个属性指向它的构造函数上。

如下通过构造函数来创建一个对象:

function Person(age) {
  this.age = age;
  this.say = function() {
    console.log(this.age)
  }
}
var person = new Person(18);
person.say(); // 18

构造器属性:

当构造器函数创建对象后,这个对象就有了一个构造器属性 constructor,该属性指向创建该对象的构造器函数。

function Fn() {
  this.name = 'xxx'
}
var fn = new Fn();
fn.constructor === Fn // true

var obj = {}
obj.constructor
ƒ Object() { [native code] }

instanceof操作符:

instanceof操作符可以判断一个对象是否是由某个指定的构造器函数所创建。像JS中内置的Array、Object、Date这构造器,在创建好对象后都可以通过该属性进行判断。

因为数组也属于引用类型,所以instanceof无法直接判断一个数据类型是不是对象。

function Fn() {
  this.name = 'xxx'
}
var fn = new Fn();
fn instanceof Fn  // true
var obj = {}
obj instanceof Object  // true
obj instanceof Array  // false

构造函数的原理就相当于内部有个this变量,属性和方法都挂在这个this上,等到结束时被返回。其实也可以通过一个工厂函数来创建,直接返回一个对象。

new一个对象的过程:

  • 创建一个新对象;
  • 将构造函数的作用域赋值给新对象(因此this就指向了这个新对象);
  • 执行构造函数中的代码(为这个新对象添加属性和方法);
  • 返回这个新对象。

# 原型属性

通过构造函数来创建对象时,如果每创建一个对象就需要new一下,我们可以使用原型对象来节省空间,把其他属性和方法都挂在原型对象上,这样就不需要每次new来多次创建了。

函数也属于对象,每个函数都有一个prototype原型属性,但只有当做构造函数时才会起作用。

因为原型对象是引用类型,所以,每次创建的对象都是对原型的引用,没有自己的原型副本,所以修改原型对象的属性会同时改变掉。

function Person(name) {
  this.name = name;
}
Person.prototype.name = 'chang';
Person.prototype.talk = function() {
  console.log(this.name);
};

var person = new Person('zhen');
person.name; // zhen
person.talk(); // 'zhen'

可以看出实例属性会覆盖原型上的属性,如果要判断一个属性是自身的还是原型属性,可以使用 hasOwnProperty()方法

person.hasOwnProperty('name');  // true

有时会直接把一个对象赋值给原型,我们可以通过 isPrototypeOf() 方法来判断当前对象是否另一个对象的原型。

function Person(name) {
  this.name = name;
}
var obj = {
  age: 18
}
Person.prototype = obj;

var person = new Person('zhen');
// obj是否是person的原型
obj.isPrototypeOf(person); // true

# 原型陷阱

看下面代码,对原型对象重新赋值后的变化

function Fn() {
    this.name = 'xxx'
}
Fn.prototype.age = 18

var fn = new Fn();
fn.age // 18
Fn.prototype = {
  class: 1
}

var fn1 = new Fn();
fn.class // undefined 
fn1.age // undefined

fn.constructor === Fn // true
fn1.constructor === Fn // false

原型对象是实例之间共享的,但是,如果我们使用一个新的对象重新覆盖之前的原型对象时,就会发现之前的实例对象虽然还能访问覆盖前的原型,但是新的原型无法访问了,新创建的实例对象也无法访问之前的原型了,这是因为实例对象与原型对象之间的连接更换了。但是为什么新的实例对象的constructor属性却无法指向构造函数了呢?

这就要事先了解一点知识了,在我们创建构造函数时,该函数就会创建一个prototype属性,这属性指向函数的原型对象。默认情况下,原型对象会自动获得一个constructor(构造函数)属性,这个属性又指向了之前的构造函数了。

function Person() {}

var person = new Person();

Person.prototype.constructor === Person  // true

person.constructor === Person // true

person.constructor === Person.prototype.constructor // true  继承自原型对象上

在new一个实例对象后,该实例对象通过一个神秘链接 __prote__ 属性继承了原型对象上的属性,所以该实例对象上的constructor也是继承自原型对象上的。所以原型对象的替换重写了prototype对象,constructor属性变成了新的constructor属性(指向Object构造函数)了,也就导致了新创建的实例对象的构造器属性无法指向了构造函数。

解决办法:

重新把原型对象上的constructor属性设置到构造函数上来解决这个问题,这也是之后原型实现继承要做的。

Person.prototype.constructor = Person

# 原型链

实例对象为何能获取构造函数上原型对象的属性,因为实例对象有一个秘密链接 __proto__ 指向其原型对象,通过此链接获取到原型对象上的属性。

function SuperType() {
  this.property = true;
}
SuperType.prototype.getSuperValue = function() {
  return this.property;
}

function SubType() {
  this.subproperty = false;
}

// 继承了SuperType的实例对象
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;  // 解决原型对象更新后无法指向构造函数的问题

SubType.prototype.getSubValue = function() {
  return this.subproperty;
}

var sub = new SubType();
console.log(sub.getSuperValue());  // true  获取了SuperType的原型对象了

从上面例子看出,原型链基本概念就是,一个实例有一个__proto__链接指向构造函数的原型对象上去,而这个原型对象也可以被赋值一个实例对象,所以也有一个__proto__链接指向构造函数的原型对象上去,依次就形成了一个原型链

通过原型链也实现了基本的基础,sub继承了SubType的实例对象和原型对象,可以获取到SubType所有的属性。

给原型添加方法一定要放在替换原型对象之后。

# 继承

如上面原型链就实现了最基本的继承,但是这种继承有某些不足。一是父类的实例属性和方法被共享,子类每个实例访问的都是同一个属性和方法。二是在不能给父类传参。

function SuperType() {
  this.colors = ['red', 'blue', 'green'];
}
function SubType() {}

SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType; 

var sub1 = new SubType();
sub1.colors.push('black'); // ["red", "blue", "green", "black"]
var sub2 = new SubType(); 
sub1.colors;  // ["red", "blue", "green", "black"]

sub1通过原型链继承了SuperType的实例,获取到colors数组,但是修改数组后再创建其他对象,获取的的colors数组却也是被修改的。

借用构造函数:

上面使用原型链实现继承的话,父类中的属性和方法是共享的,我们可以借助借用构造函数法来在我们需要使用的时候调用父类构造函数,既通过call() 和 apply() 借用父类构造函数,因为父类构造函数本质是一个函数,这就相当于在子类中调用父类构造函数得到了自己的属性。

并且在借用的时候还可以向其中传递参数。

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green'];
}
function SubType() {
  // 借用父类,继承并传递了参数
  SuperType.call(this, 'chang');
  this.age = 18
}

var sub1 = new SubType();
sub1.colors.push('black'); // ["red", "blue", "green", "black"]
sub1.name // 'chang'
sub1.age // 18
var sub2 = new SubType(); 
sub1.colors;  // ["red", "blue", "green"]

虽然每个子类实例对象都复制了一份父类的实例属性和方法,并且也能传递参数了,但是导致了问题——方法都在构造函数中定义,无法在原型上定义,因此函数复用就无从谈起了。

组合继承:

组合继承就是将原型链和借用构造函数方法相结合,从而达到又能复用,又能传递参数的模式。其思想是使用原型链实现对原型属性和方法的继承,使用借用构造函数来实现对实例属性的继承。这样,即通过在原型上定义方法实习了函数复用,又能保证每个实例都有他自己的属性。

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
}

function SubType(name, age) {
  // 继承实例属性
  SuperType.call(this, name);
  this.age = age
}

// 继承原型属性
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function() {
  console.log(this.age);
};

var sub1 = new SubType('chang', 18);
sub1.colors.push('black');
sub1.colors  // ["red", "blue", "green", "black"]
sub1.sayName(); // chang
sub1.sayAge(); // 18
var sub2 = new SubType('zhen', 19); 
sub2.colors;  // ["red", "blue", "green"]
sub2.sayName(); // zhen
sub2.sayAge(); // 19

组合继承是最常用的一种继承模式,并且 instanceof 和 isPrototypeOf() 识别组合继承创建的对象。

其他的继承方法还有 寄生式继承、多重继承等多种方法,不再深入研究了。