参透 JavaScript —— 彻底理解原型与原型链

前言

本篇文章主要讲解 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 系列》,全文地址:我的 GitHub 博客 | 掘金专栏

交流讨论

对文章内容有任何疑问、建议,或发现有错误,欢迎交流和指正

相关推荐
再学一点就睡2 小时前
手写 Promise 静态方法:从原理到实现
前端·javascript·面试
再学一点就睡3 小时前
前端必会:Promise 全解析,从原理到实战
前端·javascript·面试
前端工作日常3 小时前
我理解的eslint配置
前端·eslint
前端工作日常4 小时前
项目价值判断的核心标准
前端·程序员
90后的晨仔4 小时前
理解 Vue 的列表渲染:从传统 DOM 到响应式世界的演进
前端·vue.js
OEC小胖胖5 小时前
性能优化(一):时间分片(Time Slicing):让你的应用在高负载下“永不卡顿”的秘密
前端·javascript·性能优化·web
烛阴5 小时前
ABS - Rhomb
前端·webgl
植物系青年5 小时前
10+核心功能点!低代码平台实现不完全指南 🧭(下)
前端·低代码
植物系青年5 小时前
10+核心功能点!低代码平台实现不完全指南 🧭(上)
前端·低代码
小小李程序员5 小时前
JSON.parse解析大整数踩坑
开发语言·javascript·json