javaScript(ES5)面向对象——原型模式实现原理

前言

在javaScript(ES6)以前,并没有引入类,而是靠用函数结合原型对象模拟的类;尽管ES6开始引入类,但仍然是依靠原型对象实现的。因此理解原型对象与原型链就显得特别重要,将决定能否学JS中的类,所以下面我们来对原型对象原型链进行剖析。

如何使用原型对象共享属性和方法?

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

Person.prototype.class = '5班'     // 原型对象共享属性
Person.prototype.say = function(){      // 原型对象共享方法
    console.log(this.name);
    console.log(this.class);
}

var xdl = new Person('xdl', 19); 
var xdl2 = new Person('xdl2', 20);
xdl.say();    // xdl   5班
xdl2.say();   // xdl2   5班
console.log(xdl.say == xdl2.say);   // true     

我们可以看到,两个不同的对象实例,使用的是同一个对象方法(say)以及同样的属性(class),从而实现了多个对象实例之间共享属性和方法。我们程序猿可是很喜欢打破砂锅问到底的高级生物,既然js中没有类,只能靠原型对象模拟类,那内部是怎么实现这种类似于继承类属性和方法的?
我们先回忆一下,js中创建一个对象实例的过程有哪些?

创建对象四步曲

  1. 创建一个空对象,作为将要返回的对象实例
  2. 为这个空对象创建__proto__属性并指向构造函数的原型对象(prototype)
  3. 执行构造函数中的代码,为这个空对象创建属性
  4. 返回这个对象实例

原型对象

什么是原型对象?

原型对象(prototype)说简单一点,实际上就是一个对象,只是这个对象是专门用来共享属性和方法,其目的很简单——防止内存空间被大量使用。(至于为什么会出现这个问题,可以访问我之前一篇文章:javaScript(ES5)中面向对象创建类的各种方法详解
是骡子是马,溜出来看看,我们打印一个对象实例:

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

var xdl = new Person('xdl', 19);
console.log(xdl);

在这里插入图片描述
我创建了一个Person|类,创建了一个对象实例"xdl"。我们可以看到(嗯,年龄好像暴露了),除了构造函数中的属性之外,还有一个__proto__属性,该属性指向的就是Person类的原型对象(prototype)。
我们点开这个对象看看,里面有什么宝贝:
在这里插入图片描述
可以看到,Person原型对象中包含两个属性,一个是constructor,指向着它的构造函数,也就是Person;另一个是**proto**,为什么原型对象也有这个属性?原型对象也是一个对象,既然是对象,那在javaScript的规定中,肯定就有__proto__属性。

到这里,我们就可以揭示谜底了,因为每一个对象实例都有一个__proto__属性指向着类的原型对象,所以自然而然就可以通过类的原型对象共享属性和方法了。当实例对象本身没有某个属性或方法时,便会向类的原型对象进行寻找,那万一类的原型对象里面也没有这个属性或方法呢?那我们就不得不说到一个很重要的概念——原型链

原型链

在原型对象中,我们对Person的原型对象进行了解剖,但还没有完全解剖完,下面我们继续挖掘宝藏。
我们知道,既然Person的原型对象也是一个对象,那它也应该有__proto__属性,但它的__proto__属性又是指向何方?
我们使用console.log(xdl.__proto__.__proto__);语句打印出来看看:
在这里插入图片描述
原来Person原型对象的__proto__属性是指向Object类的原型对象。那这样一来,我们创建的任何一个用户类,不都是继承了Object类吗?那当我们创建的对象实例在类的原型对象中也没有找到某个需要的属性和方法时,便会再向上一级寻找,那便是向Object类的原型对象寻找相应的属性和方法。那万一Object类的原型对象也没有对应的属性和方法呢?

百说不如一敲,我们使用console.log(Object.__proto__);语句打印Object的原型对象出来瞧瞧是什么结构:
在这里插入图片描述
遗憾的是,Object的原型对象的__proto__属性并没有指向任何的原型对象,我们可以认为是一个NULL

我们一直在利用__proto__属性向上走,那我们可以向两边走吗?当然可以。不要忘记了,一个对象实例除了__proto__属性之外还有constructor属性,这个属性指向着创造该对象实例的构造函数。

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

var xdl = new Person('xdl', 19); 
var xdl2 = new Person('xdl2', 20);
console.log(xdl.constructor);
console.log(xdl2.constructor);

在这里插入图片描述
可能有读者会注意到,我们的实例对象并没有constructor属性,那它是怎么使用这个属性的?结合前面讲的向上寻找机制,我们就可以想到,Person类的原型对象是有constructor属性,而Person类的对象实例只是通过原型对象得到了这个属性而已。

到这里我们就可以画出这样一个图(构造函数、对象实例与原型对象之间的关系):
在这里插入图片描述
既然Person类的原型对象有constructor属性,那Object类的原型对象肯定也有constructor属性,因此我们继续画图:
在这里插入图片描述
是不是感觉像一条“链”,的确是的,但并没有画完。我们知道,ES6之前的类,是通过函数结合原型对象模拟的类,既然是使用了函数,那其本质不就是一个函数吗?

我们使用console.log(Person.constructor);语句验证一下:
在这里插入图片描述
看到了吗?如果我们把Person类当做一个对象实例,那它的构造函数是Function,也就验证了我们上面的假设。
那我们继续画图(快成美术大师了):
在这里插入图片描述
各位读者注意一下,我为什么把Function.prototype的__proto__属性指向Object类的原型对象?我们验证一下console.log(Function.prototype.__proto__);
在这里插入图片描述
事实证明,就是这样的。

原型链的图,我们已经画完了,这就是JS中用函数和原型对象模拟类的原理,是不是感觉有点晕?不要紧,多敲几遍,多打印出来瞧瞧,很快便能明白。


版权声明:本文为qq_35149975原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。