# 原型与原型链

与大部分面向对象语言不同,JavaScript中并没有引入类class的概念。

但JavaScript大量地使用了对象,为了保证对象之间的联系,JavaScript引入了原型与原型链的概念。

# 相关知识点

ES2015中,将Object原型上操作对象的一系列方法,整合在Reflect (opens new window)内置对象中。

为了降低阅读门槛,本文仍使用Object原型方法操作对象。

Reflect相关知识,点击阅读。

# 对象的__proto__属性

__proto__ (opens new window)不属于ECMAScript的语言规范,它只是大多数现代浏览器厂商自己实现的功能。

由于JS早期无法获取对象的原型,即对象内部 (opens new window)[[prototype]]属性。所以各大浏览器厂商对Object.prototype通过访问描述符实现__proto__的getter和setter来达到访问调用对象的[[prototype]]属性。

[[prototype]]属性属于对象内部属性,无法直接访问,此属性指向对象原型。

__proto__大致实现:

Object.defineProperty(Object.prototype, '__proto__', {
	get: function(){
    return Object.getPrototypeOf(this);  // 获取引用对象的[[Prototype]]
  },
  set: function(o){
    Object.setPrototypeOf(this, o); // 设置引用对象[[Prototype]]属性关联的原型为o
    return o;
  }
})

本质上是通过访问器属性来获取与设置对象关联的原型,可以理解为通过__proto__属性能获取或设置原型的引用。

这里先把普通对象的__proto__属性就称呼为对象原型,以便接下来讲解。

# 函数的prototype属性

所有函数都有prototype属性(函数独有的属性)。并且在JS中,函数也属于对象的子类型,因此函数也具备对象的__proto__属性。

当函数使用new关键字修饰时,我们可以理解此函数被当做构造函数使用,也就是构造器。当函数被当作构造函数调用时,其prototype发挥了作用,使得由构造器new出来对象的__proto__属性指向了构造函数的prototype。

以下代码演示了函数的prototype属性在实例化时的作用:

function Foo() {} // 定义构造函数
console.log(Foo.prototype); // 定义Foo构造函数时,自动创建的“干净的实例原型”

const obj = new Foo(); // 创建一个实例对象

console.dir(obj.__proto__ === Foo.prototype) // true
// 因此,实例关联的原型即为构造函数的prototype指向的原型对象

为了便于理解,这里把函数的prototype属性称呼为构造器原型,以便接下来讲解。

现在需要理解的是:

函数的__proto__属性是:函数被当做一个对象时,对象关联的原型(即对象原型)。

函数的prototype属性是:函数被当作构造函数调用时,函数关联的原型(即构造器原型)。

# 各类方法与属性的统称

  • 构造函数中定义的方法/属性,统称为『静态方法/属性』。
  • 原型中定义的方法/属性,统称为『原型方法/属性』。
  • 实例中定义的方法/属性,统称为『实例方法/属性』。

当然,方法也属于属性。只是我们通常会把定义在对象中的函数,称为方法。

# 原型

  1. 只有对象类型才有原型概念。

  2. 普通对象(即使用对象字面量或者Object构造器创建的对象)的原型为__proto__属性,此属性其实是一个访问器属性,并不是真实存在的属性,或者可以使用ES6的Reflect方法获取对象的原型。

    // 其关系为
    Reflect.getPrototypeOf({}) === Object.getPrototypeOf({}) === {}.__proto__
    
  3. 普通函数同时有这两个属性:__proto__(与普通对象类似),prototype(函数专有)。

    因为函数具有双重身份,既可以是实例也可以是构造器。所以比较特殊。

  4. 不是所有对象都有原型

    例如对象原型Object.prototype的原型Object.prototype.__proto__就指向null。

    字典对象的原型也为null(把对象的__proto__设置为null,或者使用Object.create(null)创建一个没有原型的字典对象,但是这个对象还是属于对象类型),所以原始对象原型(Object.prototype)就是最原始的原型,其他对象类型都要继承自它。

  5. 箭头函数虽然属于函数,由Function产生,但是没有prototype属性没有构造器特性,所以也就没有所谓的constructor,就不能作为构造器使用。

# 原型链

下面详细了解原型、原型链、实例、构造器的关系。

由上图关系可以验证

Function.prototype.__proto__.constructor.__proto__.constructor === Function; // true
Object.prototype.constructor === Object; // true
Function.prototype === Function.__proto__; // true
  • 所有函数都是由Function函数构造器实例化生成

  • 所以实例的原型都指向构造它的构造器的prototype

  • 每个构造器自身特有的方法就是静态方法,原型上的方法可供所有继承它或间接继承它的实例使用

  • 构造器也是函数,也是被Function实例化出来的,所以构造器的__proto__就是Function,但是构造器的prototype属性指向的原型,是此构造器实例化出来的实例所指向的原型。也就是:构造器的prototype就是作为它的实例的原型

# 函数原型链

  • JS中函数具有多重身份,作为类就是构造器使用,定义静态方法;作为普通函数调用
  • 只有由原始函数构造器(Function)实例化的函数才拥有直接使用函数原型(Function.prototype)的内置方法,创造函数只能通过原始函数构造器生成
  • 普通函数作为构造器使用(new)时相当于类(class)使用,类的prototype就是实例的原型,可以给原型添加属性,给类添加属性时就相当于给构造器添加静态属性
  • 普通函数创建实例的时候,会生成一个实例的原型,此原型指向Object.prototype即原始对象原型,也就是继承对象原型,这么一来实例也继承了对象的原型,则实例也属于对象类型
最近更新时间: 2020/5/26 19:03:58