前言
本篇文章主要讲解 JavaScript 中的原型与原型链
函数、构造函数
构造函数其实就是一个普通函数,只是使用 new
操作符调用的函数,我们就称为构造函数
还有一点,构造函数约定规范是首字母大写,用于区分普通函数
构造函数本质上是实例对象的模板 ,new
操作符会改变构造函数内部 this 指向,使创建的对象会拥有其定义的所有属性和方法
比如,一个人的基本信息:姓名、年龄、性别等属性:
js
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
const p1 = new Person('张三', 18, '男');
const p2 = new Person('李四', 20, '女');
console.log(p1); // {name: '张三', age: 18, gender: '男'}
console.log(p2); // {name: '李四', age: 20, gender: '女'}
原型对象与prototype
JavaScript 是基于原型实现的面向对象(OOP)编程,这个原型也很好理解
在 《Javascript 高级程序设计》第四版介绍原型时,第一句话就是:每个函数都会创建一个 prototype 属性,这个属性指向的就是原型
那原型其实也是一个对象,我们说每个 JavaScript 对象内部都会有一个 [[prototype]]
属性,它指向该对象的原型,所以原型对象也有原型
并且,在原型对象上定义的属性和方法是所有实例共享的
js
function Person(name) {
this.name = name;
}
Person.prototype.getName = function (){
console.log(`Hello, I'm ${this.name}`);
}
const p1 = new Person('张三')
const p2 = new Person('李四')
p1.getName() // Hello, I'm 张三
p2.getName() // Hello, I'm 李四
// 在原型对象上定义的属性和方法是所有实例共享的
console.log(p1.getName === p2.getName) // true
原型的作用体现在什么地方呢?
《JavaScript 高级程序设计》第四版有一篇内容介绍了《原始值包装类型》
原始值(string、number、boolean)本身不是对象,没有属性和方法,当你用到某个原始值的方法或属性时,后台会创建相应原始包装类型对象
js
const n = 123;
console.log(n.toFixed(2)); // 123.00
// 等价于:
console.log((new Number(n)).toFixed(2));
这里,我们就可以说 n
的原型是 Number.prototype
,并且通过原型链机制访问包装类型原型上的方法,在这个例子里是 toFixed
方法
原型的作用表现在:
- 让所有实例对象共享属性和方法
- 实现继承(通过原型链查找属性和方法)
constructor
js
function Person(name) {
this.name = name;
}
Person.prototype.getName = function (){
console.log(`Hello, I'm ${this.name}`);
}
console.log(Person.prototype);

以上面例子为例,打印了 Person.prototype
对象,这个原型对象上除了自定义的内容外(在本例子是 getName
),还存在一个 constructor
属性,指回与之关联的函数
事实上每个原型对象默认都有一个
constructor
属性,指回关联的构造函数
在这个例子里,constructor
指向 Person
js
//...
console.log(Person.prototype.constructor === Person) // true
构造函数与原型对象的关系是循环引用的:

实例与__proto__
实例是通过 new
操作符创建的对象,并且拥有构造函数定义的所有属性和方法
实例对象上有一个 [[Prototype]]
属性,上面也讲过,这个属性指向原型对象,也称为隐式原型
要访问这个属性,各大浏览器实现了一个 __proto__
属性,比如
或者使用
Object
对象的getPrototypeOf
静态方法,访问指定对象的原型对象
js
function Person(name) {
this.name = name;
}
const p1 = new Person('张三')
// 实例对象上的 __proto__ 属性和构造函数上的 prototype 都指向原型对象
console.log(p1.__proto__ === Person.prototype) // true
console.log(Object.getPrototypeOf(p1) === Person.prototype) // true
构造函数、原型对象与实例对象的关系
实例与构造函数之间没有直接的"父子"关系,实例与构造函数原型之间有直接的联系

原型链
理解原型链,其实是理解原型链的行为,原型链是一个不断上溯寻找的过程,也就是说:
当访问一个对象的属性时,先在对象本身查找,如果没有找到,就会沿着 [[Prototype]]
指针向上查找,直到找到该属性为止。如果最终到达最顶层(Object.prototype
),依然没有找到该属性,则返回 undefined
再回顾之前讲的《原始值包装类型》的例子,就能明白 n
调用的 toFixed
方法就是沿着指针找到原型 Number.protytype
对象上定义的方法
比如一个基本的例子:
js
function Person(name) {
this.name = name;
}
Person.prototype.age = 18;
const p1 = new Person('张三')
// 在原型链上找到 age 属性,输出 18
console.log(p1.age) // 18
// 沿着原型链找到最顶层依然没有找到
console.log(p1.gender) // undefined
要注意一种情况,如果对象本身和原型对象上都存在某个属性,会优先使用本身的属性,这种情况有个专业术语,叫做属性遮蔽
属性是否存在对象本身,可以使用 Object.prototype.hasOwnProperty
方法判断,返回一个布尔值,参考 MDN - hasOwnProperty
js
//...
// 实例对象上有 name 属性,返回 true
console.log(p1.hasOwnProperty("name")) // true
// 实例对象上没有 age 属性,返回 false
console.log(p1.hasOwnProperty("age")) // false
在之前的关系图基础加上原型链,可以是这样的:

总结
本篇文章讲了构造函数、原型对象、实例对象,无非就是在介绍三者的关系,
- 构造函数通过
prototype
指向原型对象 - 原型对象通过
constructor
指回构造函数 - 实例对象通过
__proto__
指向原型对象
原型链要理解,就是在对象本身上找不到属性时,会沿着指针向上寻找,直到找到属性,找到最顶层依然没有,就返回 undefined
参考资料
- 《JavaScript 高级程序设计》第四版
- JavaScript 深入之从原型到原型链
- Object.prototype.hasOwnProperty()
- Object.getPrototypeOf()
参透JavaScript系列
本文已收录至《参透 JavaScript 系列》,全文地址:我的 GitHub 博客 | 掘金专栏
交流讨论
对文章内容有任何疑问、建议,或发现有错误,欢迎交流和指正