原型、原型链不完全指南
一、序言: 原型、原型链的意义
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.prototype
Object.prototype
的原型对象
为null
,原型链
到此结束
如此得到 原型链(红色)
如下:
从数据上来看(看 [[Prototype]]
):
- 数字、字符串、数组等类型数据, 下面以数字为例, 其他类型大同小异
const num = 1
从
num
视角来看
num
本质上是通过Number
构建出来的, 那么num.__proto__
等于Number.prototype
Number.prototype
本质上是个对象, 是通过Object
构建出来了, 那么Number.prototype.__proto__
等于Object.prototype
Object.prototype
的原型对象
为null
,原型链
到此结束
如此得到 原型链
(红色) 如下:
从数据上来看(看 [[Prototype]]
):
- 下面我们来看一个复杂的例子
有代码如下:
function Person(age) {
this.age = age
}
var person = new Person(100)
从对象
person
视角来看:
person
是通过Person
构建出来的, 那么person.__proto__
等于Person.prototype
Person.prototype
是个对象, 是通过Object
构建出来了, 那么Person.prototype.__proto__
等于Object.prototype
Object.prototype
的原型对象
为null
,原型链
到此结束
如此得到 原型链
(红色) 如下:
下面我们换一个角度来思考, 站在
构造函数
Person
视角来看:
Person
本质上是个函数, 是通过Function
构建出来的, 那么Person.__proto__
等于Function.prototype
Function.prototype
本质上是个对象, 是通过Object
构建出来了, 那么Function.prototype.__proto__
等于Object.prototype
Object.prototype
的原型对象
为null
,原型链
到此结束
补充上 Person
相关 原型链
(红色) 有:
同时,
构造函数
Object
又是Function
构建出来的, 那么如果从构造函数
Object
视角来看:
Object
本质上也是个函数, 是通过Function
构建出来的, 那么Object.__proto__
等于Function.prototype
Function.prototype
本质上是个对象, 是通过Object
构建出来了, 那么Function.prototype.__proto__
等于Object.prototype
Object.prototype
的原型对象
为null
,原型链
到此结束
补充上 Object
相关 原型链
(红色) 有:
再有
构造函数
Function
是个函数, 它自己构建了自己, 那么从构造函数Function
的视角来看:
Function
是个函数, 是通过自己构造出来的, 那么Function.__proto__
等于Function.prototype
Function.prototype
本质上是个对象, 是通过Object
构建出来了, 那么Function.prototype.__proto__
等于Object.prototype
Object.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