说来话长的 Javascript 原型链

2,317 阅读6分钟

JS的原型链总是被端上前端面试桌上的一盘经典菜,不同的人从不同的角度去品鉴。今天我想从构造函数模式到原型模式再到原型链来阐述我的理解。

构造函数模式

以前的我只是知道构造函数就是定义一个函数,函数名大写,函数里面给隐式返回的this对象添加属性和方法,这个函数就是构造函数。

至于为什么要用构造函数呢?就有些迷茫了,所以再次翻开了红包书那些曾经令人头疼的面向对象章节(不得不说JS高级程序设计真是一本好书)

为什么要用构造函数?

首先因为面向对象的程序设计需要很多对象,很多相似又不同的对象,有对象才好办事嘛。

相似的地方抽离出来就成了抽象对象,比如你要找个身高多少、体重多少、长相如何......的异性。那每个人的标准就不一样,这些标准就成了对象的特殊性,而相同的就是都是某个人的异性朋友

我们试着来实现一下

function sexFriend(height, weight,appearance){
    var o = new Object();
    o.height=height;
    o.weight=weight;
    o.appearance=appearance;
    o.saySth=function(){
        alert('我愿意')
    }
    return o
}
var friend1 = new sexFriend(169, 90, '沉鱼落雁,闭月羞花');
var firend2 = new sexFriend(182, 150, '清秀俊朗,风度翩翩');
    

你看我们能通过给这个函数传入我们期望的参数就能找到对的那个人吧。

这就是设计模式之——工厂模式,因为JS语言没有其他语言中的类的概念, 所以用函数来封装以特定接口创建对象的细节。

但是这种工厂模式仅仅解决了创建多个相似对象的问题,但却没有解决对象识别的问题,即没有办法仅仅通过这些对象知道他们属于什么类型的对象,便是这些对象和构建他们的函数缺少了关联,就比如说有个人说上面创建的对象是猪的时候,你拿什么证据去反驳他呢?

随着JS的发展,构造函数模式应运而生 构造函数模式就能够解决工厂模式所带来的问题,因为构造函数创建的对象与创建的函数之间建立了直接的联系。通过constructor

function SexFriend(height, weight, appearance){
    this.height=height;
    this.weight=weight;
    this.appearance=appearance;
    this.saySth=function(){
        alert('我愿意')
    }
}
var friend1 = new SexFriend(168, 90, '倾国倾城');
var friend2 = new SexFriend(182, 150, '风流倜傥');

仔细比较上面的构造函数与工厂函数,他们有很多代码都是相似的,以下是他们的不同:

  1. 没有显式地创建对象
  2. 直接将属性和方法赋给了 this 对象
  3. 没有 return 语句
  4. 按照惯例构造函数名字首字母大写(虽然小写并不会有不同,为了与普通函数做区分)
  5. 构造函数创建的对象friend1、friend2都有一个constructor的属性
    friend1.constructor == SexFriend; // true
    friend2.constructor == SexFriend; // true
    

这就使得创建的对象通过constructor属性找到了标识他的对象类型

构造函数的不足之处

仅仅通过构造函数创建的对象实例是不够的,每创建一个实例对象,属性和方法都会隔离存在,无法共用,从而导致资源的浪费。我们来看一种解决方案

function SexFriend(height, weight, appearance){
    this.height=height;
    this.weight=weight;
    this.appearance=appearance;
    this.saySth=saySth;
}
function saySth=function(){
    alert('我愿意')
}
var friend1 = new SexFriend(168, 90, '倾国倾城');
var friend2 = new SexFriend(182, 150, '风流倜傥');

上面的栗子中,把saySth方法放到全局作用域就能实现共用,如果对象需要定义许多方法,那么就要定义很多的全局函数,使得这些自定义的引用类型丝毫没有封装可言。

原型模式就是解决上面的问题的

原型模式

我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针指向一个对象(了解这个很重要

我们在浏览器打印上面的构造函数SexFriend的prototype出来看 它是这样的

console.log(SexFriend.prototype);
/*
{
    constructor: ƒ SexFirend(height, weight, appearance),
    __proto__: Object
}
*/

这个对象的用途就是由其构造函数创建的所有对象实例都可以共用这个原型对象的属性。 这个对象的constructor 属性 就是指向的构造函数本身

SexFriend.prototype.constructor === SexFriend // true

想想前面我们说构造函数模式的时候,创建的每个实例都有一个 constructor 属性,事实上并不是实例本身的属性,而是共用了原型对象上的 constructor 属性。

在chrome 浏览器去打印前面构造函数创建的实例对象frend1和friend2就会知道,这两个对象除了拥有在构造函数内添加的属性和方法之外,还有一个属性 proto 这个属性也是一个指针,就是指向原型对象的,因此这两个实例对象都能访问到共同的 constructor 属性。

所以当我们需要共用某些方法和属性的时候就可以利用原型模式将方法属性绑定到原型对象上

来看具体实现:

function SexFriend(height, weight, appearance){
    this.height=height;
    this.weight=weight;
    this.appearance=appearance;
    // 将共用的方法绑定到原型对象里
    SexFriend.prototype.saySth=function(){
    alert('我愿意')
    }
}
var friend1 = new SexFriend(168, 90, '倾国倾城');
var friend2 = new SexFriend(182, 150, '风流倜傥');
总结上面所说,要隔离的属性方法就在构造函数内创建,要完全共用的方法属性就通过原型对象。

原型链

我们在前面说到的原型对象本质上就是一个普通的对象,只不过这个对象与构造函数之间通过 constructor 属性创建了联系。

假如我们将构造函数的 prototype 的指针指向另外一个构造函数的实例对象,如下:

function A(){
    this.a = 1;
}
function B(){
    this.b=2;
}
var b = new B();
A.prototype = b

var a = new A()
a.constructor === A // false

a.__proto__ === b // true
a.constructor === b.constructor // true
b.constructor=== b.__proto__.constructor; // true
b.__proto__.constructor=== B.prototype.constructor; // true
B.prototype.constructor=== B; // true

我们把构造函数 A 的 prototype 属性的指针指向了构造函数 B 的实例对象 b; 那么再通过 A 创建的 a 的__proto__属性指向的就是 b 了,即原型对象的指针指向了 b ,这时 a 与 A 之间的连接就断了,但是 A 原本的Prototype(原型)对象仍然存在,只是指针不再指向它了,

并且 a.constructor 已经指向了B

如果再将 B prototype 属性指针指向构造函数 C 的实例 c , 那么就有 a.constuctor === C了,由__proto__构成连接的常常的链子就是原型链了,使得 a.constuctor 找到最后的 C 。也使得实例对象 a 能沿着原型链继承到所有链上的方法和属性,从最近的原型到最远的原型直到找到相应的属性方法为止。而最顶级的原型对象就是Object

这就是我所理解的原型链,希望对你有所帮助。

感恩红宝书。