原型和原型链

196 阅读5分钟

原型

JavaScript 只有对象,而没有类,但是又需要支持面向对象编程,那么该怎么办呢?我们可以使用构造函数来模拟类。

function Human(name) {
    this.name = name;
    this.speakName = function(){
  	    console.log(this.name);
    }
}
var humanA = new Human('A');
var humanB = new Human('B');

其实 new 指令相当于执行了以下操作

function createNewObject(constructor, agruments){
	var obj = {};
    constructor.apply(obj, agruments);
    return obj;
}

也就是执行了以下三个步骤:

  • 先创建一个空对象
  • 将构造函数的作用域赋给该空对象,并且执行构造函数
  • 返回该对象

但是构造函数有一个问题,那就是每一次创建的对象的属性都是私有的。

function Human(name) {
	this.name = name;
    this.speakName = function(){
  	    console.log(this.name);
    }
}
var humanA = new Human('A');
var humanB = new Human('B');
humanA.speakName === humanB.speakName // false

这样会导致内存的浪费,所以我们可以通过设置一个公有对象的方式,将所有 Human 对象的共有成员放进这个共有对象中,而这个共有对象,就是原型。

  • 每个函数对象都有一个 prototype 引用指向该对象。
  • 而每个 prototype 对象会有一个 constructor 引用指回其的构造函数对象
  • 每个对象都有 _proto_ 属性指向 它们的构造函数中的 prototype 对象
function Human(name) {
	this.name = name;
}
Human.prototype.speakName = function () {
	console.log(this.name);
}

var humanA = new Human('A');
var humanB = new Human('B');
humanA.__proto__ === Human.prototype // true
humanB.__proto__ === Human.prototype // true
humanA.speakName(); // A
humanB.speakName(); // B
humanA.speakName === humanB.speakName // true

当执行 humanA.speakName() 时,JS会先从 humanA  自己的属性里去寻找 speakName , 找不到就去它的原型对象,也就是 humanA.__proto__ 里寻找。另外, Object.keys 是无法获取到原型里的属性的。

Obeject.keys(humanA) // ["name"]

经过测试,在chrome中,用 for in 还是能够遍历出原型中的属性,所以可以加个条件判断

var keys = [];
for (var key in humanA) {
	if (humanA.hasOwnProperty(key)) {
  	    keys.push(key);
    }
}
keys // ['name']

当然,你可以通过 Object.defineProperty 来设置原型中的属性禁止被枚举

function Human(name) {
	this.name = name;
}
Object.defineProperty(Human.prototype, 'speakName', {
	value:function () {
		console.log(this.name);
	},
    enumerable: false
});

var humanA = new Human('A');
humanA.speakName(); //  A

const keys = [];

for (var key in humanA) {
	keys.push(key);
}
keys // ['name']

解决了共有属性的问题,在面向对象的语言中,我们还需要考虑继承的问题。这种时候,原型链就派上用场了。

原型链

如何实现一个继承呢?其实很急单,那就是让子类的原型等于父类的一个实例, 同时再加上子类原型所需要的一些特性就可以了。

function Animal() {
	this.size = ['1','2','3'];
}

Animal.prototype.eat = function (){
	console.log('eat');
}

function Human(name) {
	this.name = name;
}

Human.prototype = new Animal();

Human.prototype.speakName = function () {
	console.log(this.name);
}

var humanA = new Human('A');

humanA.eat(); // eat
humanA.eat === humanA.__proto__.__proto__.eat // true

正像上面的代码所显示的,当执行 humanA.eat() 时,JS在 humanA 自己的属性中没找到 eat 函数,那么就会在它的原型中找,如果还没找到,那么就会往上,去继承的原型链里找。

但是这样有一个问题,那就是父类的非共有属性也包含再子类的原型中了。

var humanA = new Human('A');
var humanB = new Human('B');
humanA.size === humanB.size // true
humanA.__proto__.size === humanB.__proto__.size // true
humanA.size.push('4');
humanB.size // ['1','2','3','4'];

其实也很简单,我们只需要在子类的构造函数中,调用一下父类的构造函数(将作用域指向子类代表的作用域)。

...
function Human(name) {
  Animal.call(this);
	this.name = name;
}
Human.prototype = new Animal();
...

var humanA = new Human('A');
var humanB = new Human('B');

humanA.size === humanB.size // false

但是这样还有一个问题,Human 生成的实例,不仅在它的属性里有 size 属性,而且在它的原型中还有一个 size属性(因为其的原型 是 Animal 的一个实例),只不过先在它的属性里找到了size,所以就不会再去它的原型里找了。也就是说,父类的构造函数被调用了两次,一次在子类的构造函数中,一次在生成子类的原型时。

humanA.__proto__.size === humanB.__proto__.size // true
humanA.__proto__.size.push('4')
humanB.__proto__.size // ['1','2','3','4']

所以我们要找到一个方法,让子类的原型的原型可以指向父类的原型,但不包括父类的私有属性。

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

function inhertPrototype(father, child){
	var prototype = object(father.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}

function Animal() {
	this.size = ['1','2','3'];
}

Animal.prototype.eat = function (){
	console.log('eat');
}

function Human(name) {
	this.name = name;
}

inhertPrototype(Animal, Human);

Human.prototype.speakName = function () {
	console.log(this.name);
}

其实我们在这里做的事也非常简单。

首先, object 函数创建了一个临时的构造函数,并且让该构造函数的原型等于传入的构造函数的原型,并且返回该临时的构造函数的一个实例。

inhertPrototype 函数中,我们先生成一个临时构造函数的实例,让子类的原型等于该临时对象就可以了。同时我们还修改了该原型指向的 contructor 指向子类构造函数,否则其的 contructor 就会指向 object 中生成的临时构造函数了。经过这么一通操作,我们就不会在生成子类的原型的时候,再去调用父类的构造函数了,也就不会在子类的构造函数中多出了父类构造函数生成的属性。

而以上这种方式被叫做寄生组合式继承,至于如何去解释这个名词,这里就不展开了,这种继承模式也是现在最普遍的继承模式。

但,为什么我们不直接让子类原型的 __proto__ 引用指向 父类的原型对象呢?

像这样:

function Animal() {
	this.size = ['1','2','3'];
}

Animal.prototype.eat = function (){
	console.log('eat');
}

function Human(name) {
	this.name = name;
}

Human.prototype.__proto__ = Animal.prototype;

Human.prototype.speakName = function () {
	console.log(this.name);
}

我在 chrome 这样尝试是可以的, 而且这种做法的好处是还不需要去设置子类的原型的 contructor ,因为它没有变化过。

但有一个问题是,Firefox、Safari和Chrome等浏览器在每个对象上都显示 __proto__ ;而在其他实现中,这个属性对脚本则是完全不可见的。所以在其它地方就完全不能用这种方法。

如果还有其它原因的话,希望某位大神可以打在下面的评论中。

instanceof

此外,我们在这里讲一下 instanceof 的原理。 例如,

humanA instanceof Animal // true

instanceof 的原理很简单,一句话就能讲明白,即:如果在 humanA  的原型链中找到了 Animal  的原型,就返回 true ,否则就是 false 。