原型与原型链

478 阅读4分钟

在掌握原型之前,请读者务必先掌握JS中的数据类型。

构造函数

一些读者可能对于面向类的语言有一定了解,导致混淆一些概念,但是JS中并没有类的固有概念,只是有类似的语法,如newinstanceof,以及在后来的es6中新增了类一些元素,如class关键字。所以暂时专注于了解JS中的对象机制。先来看看如何创建出一个对象吧!

function Person(){
...
}
var person = new Person()

首先要说明一点,Person 本身不是构造函数,它就是一个普通的函数,但是当使用new调用它,这个函数调用就变成了一个构造函数调用,构造出一个对象。只是不同于像Array,Object这样的原生构造函数,Person是我们自定义的构造函数。

原型的由来

使用 new 调用了 Person 函数,构造出一个对象 person。但是使用构造函数来创建对象有一个弊端:每个方法都会在每个实例上重新创建一遍。对于这样的创建方式,不同实例上的同名函数是不相等的,getInfo在这里是两个Function实例。

function Person(name, age){
    this.name = name
    this.age = age
    this.getInfo = function(){
        alert(this.name + this.age)
    }
}

var person1 = new Person('Alice',23)
var person2 = new Person('Bob',25)

console.log(person1.getInfo === person2.getInfo)  //false

虽说要想解决这个问题,只要把函数定义挂载到全局作用域中就可以,但是如果对象需要多个方法呢?因此原型模式应运而来。

prototype

在JS中,创建的每个函数(注意:不是所有数据类型都拥有原型属性,只有函数对象拥有)都有一个 prototype 属性。它是通过调用构造函数而创建的那个对象实例的原型对象,目的是让所有对象实例共享它包含的属性和方法。有点绕是不是?没事,先记住它的作用是什么,如你所见,我们给 Person.prototype 添加了同样的属性和方法,它确实解决了构造函数模式创建对象的问题:person1 和person2 访问的都是同一组属性和同一个getInfo函数。

function Person(){}
Person.prototype.name = 'Bob'
Person.prototype.age = 24
Person.prototype.getInfo=function(){
    alert(this.name + this.age)
}

var person1 = new Person()
var person2 = new Person()
alert(person1.getInfo === person2.getInfo) //true

注意:有的人可能觉得 prototype 还是有点抽象,其实它就是一个对象,不过有普通对象和函数对象之分。

 console.log(typeof Person.prototype) //    Object
 console.log(typeof Function.prototype) //   Function

原型链

person 是如何关联 Person.prototype 的呢?原来当调用一个构造函数创建一个新实例后,这个实例的内部包含一个指针指向构造函数的原型对象。

person.__proto__ == Person.prototype //对象实例person指向构造函数Person的原型对象

可以用isPrototypeof()来确定是否存在这样的关系。

Person.prototype.isPrototypeof(person) //true

和prototype属性一样,只有函数对象拥有__proto__属性吗?没道理吧,以原生构造函数Object为例,

var obj = {}
obj.__proto__=== Object.prototype // true

所以一般的普通对象也有。并且表面上看基本类型也有这个属性。

5F96E6E5-1057-45DA-B3C0-D3225A51A3CE.png

这是因为当我们访问基本类型的属性的时候,会临时创建一个基本包装类型对象,可以像对象一样操作基本类型,执行完成之后立即销毁。正是因为Number, String, Boolean具有这种特殊的特性,所以它们又被称为基本包装类型。或许这也是大家认为”js中万物皆是对象“的原因吧,虽然从数据类型上来看并不是这样。

言归正传,proto 可以实现:如果在对象上没有找到需要的属性或者方法引用,会继续在原型链上进行查找。这个链接存在与实例与构造函数的原型对象之间,因此是__proto__实现了原型链机制。原型链的尽头是Object.prototype

Object.prototype.__proto__ === null

constructor

原型对象会默认拥有一个 constructor 属性,并且指向这个构造函数。

Person.prototype.constructor == Person //true

7816790E-8405-41B5-8669-3CE0E5C1E3F7.png 但是 person 好像也有一个constructor属性。

person.constructor == Person  //true

字面意思好像是 person 由 Person 构造,但实际上 person.constructor 只是通过默认的__proto__属性与 Person 关联了起来,试着改变一下 Person.prototype,看看发生了什么吧。

function Person() { /* .. */ }
Person.prototype = { /* .. */ }; // 创建一个新原型对象
var person = new Person();
person.constructor === Person; //false
person.construtor === Object //true

其实 person 并没有 constructor 这个属性,但是它会寻找原型链上的 Person.prototype 是否有这个属性,它也没有,因为被修改了,于是找到了原型链的顶端,Object.prototype,这个对象有 .construtor 属性,指向内置的 Object() 函数。

总结

  1. JS中的构造函数就是所有带 new 的函数调用;

  2. 原型对象本质上也是通过构造函数创建出的实例,只是有点特殊,因为其他的实例会继承 prototype 的所有属性和方法,实例通过设置自己的__proto__指向构造函数的prototype来实现这种继承;

  3. 原型对象本身其实就是普通对象(但 Function.prototype 除外,它是函数对象);

  4. 原型和原型链是JS实现继承的一种模型。原型链的形成是靠__proto__ 而非prototype;

  5. 至于原型的原型是什么?只有浏览器环境下可访问__proto__属性,代码中如何实现等问题,这里不再赘述,多点尝试和研究吧。