什么是原型(原型对象)
先来看这样一个例子。
function Student (name, sex, age) {
this.name = name;
this.sex = sex;
this.age = age;
}
const xiaoming = new Student('小明', '男', '成年');
const xiaowang = new Student('小王', '男', '成年');
console.log(xiaoming);
console.log(xiaowang);
很明显,我们通过构造函数创建了两个对象,xiaowang和xiaoming。他们有各自的姓名、性别、年龄属性。
在js中,通过new关键字创建的对象都独立占用一份内存空间。
这个时候我们回头看xiaowang和xiaoming两个对象,他们除了名字不同,年龄和性别都是完全相同的,并且随着成年男性的同学越来越多,内存中会多存储很多份相同的{sex: '男',age: '成年'}
这样的数据。
于是我们思考,是否可以只创建一个{sex: '男',age: '成年'}
的根对象放在内存中,然后再让不同name的对象去继承这个根对象,这样就可以大大节省重复的内存。
如图所示,我们已经做完了上述的优化,接下来我们想通过某种方式来得到这个根对象{sex: '男',age: '成年'}
。
那么首先我们想到,所有Student对象都是通过Student这个函数new出来的,于是我们在Student这个函数中创建一个属性prototype
,这样就能轻松找到我们的根对象了,这个根对象也就是所谓的原型。
由于js中对于普通函数和构造函数没有严格区分,只要使用new关键字,js就试图创建一个对象,因此所有的函数都会拥有prototype
属性。
我们再考虑到,所有通过new关键字创造出来的对象(下文将这种对象称为实例对象)都指向这个原型,因此我们对于所有的对象都提供一个[[Prototype]]
属性,用于指向这个原型。
在最初的设计中,实例对象的原型应该是一个隐藏起来的指针,但各个浏览器为了使用这个[[Prototype]]
属性,给开发者提供了__proto__
属性,通过这个属性可以访问对象的原型。而在ES 6中,官方也对这种情况做出了妥协,定义了Object.getPrototypeof()
和Object.setPrototypeof()
两个方法来获取这个属性。
知识点:
-
prototype
:所有的函数都拥有prototype
属性,这个属性是一个指针(内存地址),指向这个函数的原型。 -
原型:原型也是一个对象,因此也被叫做原型对象。所有对象都可以从原型对象中继承(也就是获取)属性和函数。
-
__proto__
或者[[Prototype]]
:所有的对象都拥有[[Prototype]]
属性,这个属性是一个指针(内存地址),指向这个对象的原型。在大多数浏览器中,可以使用__proto__
属性来获取[[Prototype]]
属性。- 需要注意的是
__proto__
并不是一个官方的属性,而是浏览器自动添加的。
- 需要注意的是
什么是原型链
刚刚我们讲到了原型对象、构造函数、实例对象的三角关系,我们可以通过任何一个来获取另外两个。
// 构造函数
function Student (name, sex, age) {
this.name = name;
this.sex = sex;
this.age = age;
}
// 构造函数 -> 实例对象
const xiaoming = new Student('小明', '男', '成年');
// 构造函数 -> 原型对象
const constr = Student.prototype
// 原型对象 -> 构造函数
console.log(constr.constructor === Student)
// 实例对象 -> 构造函数
console.log(xiaoming.constructor === Student)
// 实例对象 -> 原型对象
console.log(xiaoming.__proto__ === constr)
// 原型对象 -> 实例对象 x 不存在
如下图
可以看到,原型对象也被叫做对象,所以当然也有[[prototype]]属性,如果构造函数没有显式继承某一个构造函数(extends),那么原型的[[prototype]]属性指向的就是Object.prototype。
如果足够敏锐可以发现,Object.prototype是Object类的原型对象,那么他自然也有[[prototype]]属性,Object.prototype.__proto__ === null
知识点
-
如果一个子构造函数通过
extends
关键字,继承(extends)了另一个父构造函数,那么这个子构造函数的原型的[[prototype]]属性(原型),就是父构造函数的原型。(prototype)。 -
这种通过原型不断继承下来的单向链表,就是所谓的原型链。
举一个例子。请尝试分析以下的三行打印结果分别是什么,尝试运行并得到答案。
class Animal { }
class Dog extends Animal { } // 显式继承
// Dog.prototype 的 [[Prototype]] 指向 Animal.prototype
console.log(Dog.prototype.__proto__ === Animal.prototype);
// Animal.prototype 的 [[Prototype]] 指向 Object.prototype
console.log(Animal.prototype.__proto__ === Object.prototype);
// Object.prototype → null
console.log(Object.prototype.__proto__ === null);