# 原型与原型链
与大部分面向对象语言不同,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
属性是:函数被当作构造函数调用时,函数关联的原型(即构造器原型)。
# 各类方法与属性的统称
- 构造函数中定义的方法/属性,统称为『静态方法/属性』。
- 原型中定义的方法/属性,统称为『原型方法/属性』。
- 实例中定义的方法/属性,统称为『实例方法/属性』。
当然,方法也属于属性。只是我们通常会把定义在对象中的函数,称为方法。
# 原型
只有对象类型才有原型概念。
普通对象(即使用对象字面量或者Object构造器创建的对象)的原型为
__proto__
属性,此属性其实是一个访问器属性,并不是真实存在的属性,或者可以使用ES6的Reflect
方法获取对象的原型。// 其关系为 Reflect.getPrototypeOf({}) === Object.getPrototypeOf({}) === {}.__proto__
普通函数同时有这两个属性:
__proto__
(与普通对象类似),prototype
(函数专有)。因为函数具有双重身份,既可以是实例也可以是构造器。所以比较特殊。
不是所有对象都有原型
例如对象原型
Object.prototype
的原型Object.prototype.__proto__
就指向null。字典对象的原型也为null(把对象的
__proto__
设置为null,或者使用Object.create(null)创建一个没有原型的字典对象,但是这个对象还是属于对象类型
),所以原始对象原型(Object.prototype)就是最原始的原型,其他对象类型都要继承自它。箭头函数虽然属于函数,由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即原始对象原型,也就是继承对象原型,这么一来实例也继承了对象的原型,则实例也属于对象类型
← ES6与未来 作用域、执行上下文与闭包 →