原型、原型链不完全指南
一、序言: 原型、原型链的意义
JS 中基于原型、原型链, 可以让 JS 对象拥有封装、继承和多态等众多特性; 对原型、原型链的了解可以帮助我们更深入学习 JS, 可以让我们更好的理解 JS 继承、 new 关键字原理, 帮助我们更好的封装组件、优化代码……
二、原型
2.1「函数」中「prototype」指向「原型对象」
当我们创建一个函数时, 函数都会有一个默认属性 prototype, 该属性指向一个对象, 该对象就被称之为 原型对象
function fun(){}
fun.prototype // 原型对象

2.2「对象」中「__proto__」指向「原型对象」
- 当函数作为
普通函数进行调用时, 该属性不会有任何作用 - 当函数被作为
构造函数进行调用 (使用new运算符调用) 时, 构建出来的实例对象会有一个属性__proto__指向原型对象
function fun(name){
this.name = name
}
fun.prototype // 原型对象
const obj = new fun('moyuanjun') // 函数被作为构造函数进行调用
obj.__proto__ === fun.prototype // true, 实例对象.__proto__ 指向 构造函数.prototype

2.3「原型对象」中「constructor」指向「构造函数」
原型对象 默认会有一个特殊的属性 constructor, 该属性又指向了函数本身
function fun(name){
this.name = name
}
fun.prototype // 原型对象
const obj = new fun('moyuanjun') // 函数被作为构造函数进行调用
obj.__proto__ === fun.prototype // true
fun.prototype.constructor === fun // true

2.4「__proto__」与「[[Prototype]]」
认真的你如果将 实例对象 打印出来, 会发现对象中并不具有 __proto__ 属性, 恰恰相反有个特殊的 [[Prototype]] 属性, 那么这又是怎么回事呢?

__proto__和[[Prototype]]关系说明
__proto__并不是ECMAScript语法规范的标准, 它只是大部分浏览器厂商实现或说是支持的一个属性, 通过该属性方便我们访问、修改原型对象- 遵循
ECMAScript标准,[[Prototype]]才是正统,[[Prototype]]无法被直接修改、引用 - 从
ECMAScript 6开始, 可通过Object.getPrototypeOf()和Object.setPrototypeOf()来访问、修改原型对象 - 简单理解:
__proto__和[[Prototype]]是同一个东西,__proto__是非标准的,[[Prototype]]才是标准的, 但是它们都是指向原型对象 - 那么问题来了, 我们访问的
__proto__在哪里呢? 实际上它是被添加在Object.prototype上, 然后通过原型链(后面会详细展开说明) 我们就能够访问到该属性
补充: 这里只是对 __proto__ 与 [[Prototype]] 属性做了简单说明, 虽然 __proto__ 是非标准的, 但是下文依然会继续使用 __proto__ 来进行演示、说明
2.5 所有非空类型数据, 都具有「原型对象」
任何 非空数据 , 本质上都是通过对应 构造函数 构建出来的, 所有它们都具有 __proto__ 属性, 指向 构造函数 的原型对象
所以要判断某个值其 原型对象, 只需要确认该值是通过哪个 构造函数 构建的即可, 只要确认了 构造函数 那么该值的 __proto__ 必然指向该 构造函数 的 prototype
// 数字
const num = 1
// 数字是通过 Number 构建的, 那么其原型对象等于 Number.prototype
num.__proto__ === Number.prototype // true
// 字符串
const str = 'str'
// 字符串是通过 String 构建的, 那么其原型对象等于 String.prototype
str.__proto__ === String.prototype // true
// 布尔类型
const bool = false
// 布尔值是通过 Boolean 构建的, 那么其原型对象等于 Boolean.prototype
bool.__proto__ === Boolean.prototype // true
// Symbol
const sym = Symbol('symbol')
// sym 是通过 Symbol 构建的, 那么其原型对象等于 Symbol.prototype
sym.__proto__ === Symbol.prototype // true
// BigInt
const big = BigInt(1)
// big 是通过 BigInt 构建的, 那么其原型对象等于 BigInt.prototype
big.__proto__ === BigInt.prototype // true
// 对象
const obj = { age: 18 }
// 对象是通过 Object 构建的, 那么其原型对象等于 Object.prototype
obj.__proto__ === Object.prototype // true
// 函数
const fun = () => {}
// 函数是通过 Function 构建的, 那么其原型对象等于 Function.prototype
fun.__proto__ === Function.prototype // true
// 数组
const arr = [1, 2, 3]
// 数组是通过 Array 构建的, 那么其原型对象等于 Array.prototype
arr.__proto__ === Array.prototype // true
2.6 补充: new 运算符做了哪些事情
- 创建一个新的空对象
A - 挂载
原型对象: 对象A创建__proto__属性, 并将构造函数的prototype属性赋值给__proto__ - 改变
构造函数this指向, 指向对象A - 得到一个新对象
- 一般是返回第一步创建的对象
A - 但是如果
构造函数也返回了一个对象B则返回对象B否则返回对象A
因此当我们执行
var o = new Foo();
实际上执行的是:
// 1. 创建一个新的空对象 A
let A = {};
// 2. 挂载原型对象: obj.__proto__ = Con.prototype;
Object.setPrototypeOf(A, Con.prototype);
// 3. 改变构造函数 this 指向, 指向对象 A
let B = Con.apply(obj, args);
// 4. 对构造函数返回值做判断, 然后返回对应的值
const newObj = B instanceof Object ? res : A;
三、原型链
根据上文, 所有非空数据, 都可以通过 __proto__ 指向 原型对象, 故而如果 原型对象 非空, 那么必然会有 __proto__ 指向它自己的 原型对象, 如此一层层往上追溯, 以此类推, 就形成了一整条链路, 一直到某个 原型对象 为 null, 才到达最后一个链路的最后环节, 而 原型对象 之间这种 链路关系 被称之为 原型链 (prototype chain)
3.1 几个例子
- 直接创建一个对象
const obj = { age: 18 }
从对象
obj视角来看:
obj本质上是通过Object构建出来的, 那么obj.__proto__等于Object.prototypeObject.prototype的原型对象为null,原型链到此结束
如此得到 原型链(红色) 如下:

从数据上来看(看 [[Prototype]]):

- 数字、字符串、数组等类型数据, 下面以数字为例, 其他类型大同小异
const num = 1
从
num视角来看
num本质上是通过Number构建出来的, 那么num.__proto__等于Number.prototypeNumber.prototype本质上是个对象, 是通过Object构建出来了, 那么Number.prototype.__proto__等于Object.prototypeObject.prototype的原型对象为null,原型链到此结束
如此得到 原型链(红色) 如下:

从数据上来看(看 [[Prototype]]):

- 下面我们来看一个复杂的例子
有代码如下:
function Person(age) {
this.age = age
}
var person = new Person(100)
从对象
person视角来看:
person是通过Person构建出来的, 那么person.__proto__等于Person.prototypePerson.prototype是个对象, 是通过Object构建出来了, 那么Person.prototype.__proto__等于Object.prototypeObject.prototype的原型对象为null,原型链到此结束
如此得到 原型链(红色) 如下:

下面我们换一个角度来思考, 站在
构造函数Person视角来看:
Person本质上是个函数, 是通过Function构建出来的, 那么Person.__proto__等于Function.prototypeFunction.prototype本质上是个对象, 是通过Object构建出来了, 那么Function.prototype.__proto__等于Object.prototypeObject.prototype的原型对象为null,原型链到此结束
补充上 Person 相关 原型链(红色) 有:

同时,
构造函数Object又是Function构建出来的, 那么如果从构造函数Object视角来看:
Object本质上也是个函数, 是通过Function构建出来的, 那么Object.__proto__等于Function.prototypeFunction.prototype本质上是个对象, 是通过Object构建出来了, 那么Function.prototype.__proto__等于Object.prototypeObject.prototype的原型对象为null,原型链到此结束
补充上 Object 相关 原型链(红色) 有:

再有
构造函数Function是个函数, 它自己构建了自己, 那么从构造函数Function的视角来看:
Function是个函数, 是通过自己构造出来的, 那么Function.__proto__等于Function.prototypeFunction.prototype本质上是个对象, 是通过Object构建出来了, 那么Function.prototype.__proto__等于Object.prototypeObject.prototype的原型对象为null,原型链到此结束
补充上 Function 相关 原型链(红色) 有:

小小总结
- 所有
原型链最后都会到Object.prototype, 因为原型对象, 本质上就是个对象, 由Object进行创建, 其__proto__指向Object.prototype Object.prototype.__proto__等于null, 所以原型链的终点必然是:Object.prototype=>null
3.2 原型链的作用
- 查找属性: 当我们试图访问
对象属性时, 它会先在当前对象上进行搜寻, 搜寻没有结果时会继续搜寻该对象的原型对象, 以及该对象的原型对象的原型对象, 依次层层向上搜索, 直到找到一个名字匹配的属性或到达原型链的末尾
有代码如下:
function Person(age) {
this.age = age
}
Person.prototype.name = 'klx'
Person.prototype.age = 18
const person = new Person(28)
person // 当前对象: { age: 28 }
person.name // klx, 取自原型对象 Person.prototype
person.age // 28, 取自当前对象
person.toString() // [object Object], 取自原型对象 Object.prototype
person.address // undefined, 沿着原型链找不到 address
根据代码, 得到如下简化的
原型链示意图, 在访问person属性时, 是按照下图链路一层层往下搜寻

- 属性屏蔽: 原型对象中的属性, 如果在实例对象中重新声明, 根据属性查找规则, 在查找该属性时会直接返回实例中声明的值; 这时原型对象中的属性可以简单理解为被
屏蔽了, 在很多文章中称该现象为属性覆盖但个人认为说覆盖是不准确的, 因为原型对象中属性并没有被覆盖, 用屏蔽或许更为准确
如下代码, 在实例对象
p2中, 屏蔽了原型对象Person.prototype中name属性
function Person() {}
Person.prototype.name = 'klx'
Person.prototype.age = 18
const p1 = new Person()
const p2 = new Person()
p2.name = 'myj' // p2 声明 name 属性, 屏蔽原型对象 Person.prototype 中 name 属性
p1.name // klx, 取自原型对象 Person.prototype
p2.name // myj, 取自实例对象
原型对象中的函数被调用时,this指向是当前对象, 而不是函数所在的原型对象
// 1. 调用「普通对象」中的方法
const obj = {
a: 10,
name: {
a: 1,
printA: function(){
console.log(this.a + 1)
}
}
}
obj.name.printA() // 2, printA 函数 this 指向函数所在的对象
// 2. 调用「原型对象」中的方法
function Person() {
this.a = 10
}
Person.prototype.a = 1
Person.prototype.printA = function(){
console.log(this.a + 1)
}
const person = new Person()
person.printA() // 11, printA 函数 this 指向的是原型对象 Person.prototype
五、总结
- 所有函数都有一个属性
prototype指向原型对象(所有函数都有原型对象) - 所有
原型对象都有一个constructor属性, 指向原型对象所属的函数 - 所有非空数据都有
__proto__指向其原型对象 - 要判断一个数据的
原型对象, 只需要确认该数据是通过哪个构造函数构建出来的 那么这个数据的原型对象等于构造函数的原型对象 - 所以原型链的终点都是
Object.prototype=>null - 原型、原型链的优点: 为同类型对象提供
共享属性、将通用属性抽离大大节约内存
六、参考资料
转载自:https://juejin.cn/post/7198388410381271096