likes
comments
collection
share

原型、原型链不完全指南

作者站长头像
站长
· 阅读数 19

一、序言: 原型、原型链的意义

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]] 关系说明

  1. __proto__ 并不是 ECMAScript 语法规范的标准, 它只是大部分浏览器厂商实现或说是支持的一个属性, 通过该属性方便我们访问、修改原型对象
  2. 遵循 ECMAScript 标准, [[Prototype]] 才是正统, [[Prototype]] 无法被直接修改、引用
  3. ECMAScript 6 开始, 可通过 Object.getPrototypeOf()Object.setPrototypeOf() 来访问、修改 原型对象
  4. 简单理解: __proto__[[Prototype]] 是同一个东西, __proto__ 是非标准的, [[Prototype]] 才是标准的, 但是它们都是指向 原型对象
  5. 那么问题来了, 我们访问的 __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 运算符做了哪些事情

  1. 创建一个新的空对象 A
  2. 挂载 原型对象: 对象 A 创建 __proto__ 属性, 并将 构造函数prototype 属性赋值给 __proto__
  3. 改变 构造函数 this 指向, 指向对象 A
  4. 得到一个新对象
  • 一般是返回第一步创建的对象 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 几个例子

  1. 直接创建一个对象
const obj = { age: 18 }

从对象 obj 视角来看:

  1. obj 本质上是通过 Object 构建出来的, 那么 obj.__proto__ 等于 Object.prototype
  2. Object.prototype原型对象null, 原型链 到此结束

如此得到 原型链(红色) 如下:

原型、原型链不完全指南

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

原型、原型链不完全指南

  1. 数字、字符串、数组等类型数据, 下面以数字为例, 其他类型大同小异
const num = 1

num 视角来看

  1. num 本质上是通过 Number 构建出来的, 那么 num.__proto__ 等于 Number.prototype
  2. Number.prototype 本质上是个对象, 是通过 Object 构建出来了, 那么 Number.prototype.__proto__ 等于 Object.prototype
  3. Object.prototype原型对象null, 原型链 到此结束

如此得到 原型链(红色) 如下:

原型、原型链不完全指南

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

原型、原型链不完全指南

  1. 下面我们来看一个复杂的例子

有代码如下:

function Person(age) {
  this.age = age       
}
var person = new Person(100)

从对象 person 视角来看:

  1. person 是通过 Person 构建出来的, 那么 person.__proto__ 等于 Person.prototype
  2. Person.prototype 是个对象, 是通过 Object 构建出来了, 那么 Person.prototype.__proto__ 等于 Object.prototype
  3. Object.prototype原型对象null, 原型链 到此结束

如此得到 原型链(红色) 如下:

原型、原型链不完全指南

下面我们换一个角度来思考, 站在 构造函数 Person 视角来看:

  1. Person 本质上是个函数, 是通过 Function 构建出来的, 那么 Person.__proto__ 等于 Function.prototype
  2. Function.prototype 本质上是个对象, 是通过 Object 构建出来了, 那么 Function.prototype.__proto__ 等于 Object.prototype
  3. Object.prototype原型对象null, 原型链 到此结束

补充上 Person 相关 原型链(红色) 有:

原型、原型链不完全指南

同时, 构造函数 Object 又是 Function 构建出来的, 那么如果从构造函数 Object 视角来看:

  1. Object 本质上也是个函数, 是通过 Function 构建出来的, 那么 Object.__proto__ 等于 Function.prototype
  2. Function.prototype 本质上是个对象, 是通过 Object 构建出来了, 那么 Function.prototype.__proto__ 等于 Object.prototype
  3. Object.prototype原型对象null, 原型链 到此结束

补充上 Object 相关 原型链(红色) 有:

原型、原型链不完全指南

再有 构造函数 Function 是个函数, 它自己构建了自己, 那么从构造函数 Function 的视角来看:

  1. Function 是个函数, 是通过自己构造出来的, 那么 Function.__proto__ 等于 Function.prototype
  2. Function.prototype 本质上是个对象, 是通过 Object 构建出来了, 那么 Function.prototype.__proto__ 等于 Object.prototype
  3. Object.prototype原型对象null, 原型链 到此结束

补充上 Function 相关 原型链(红色) 有:

原型、原型链不完全指南

小小总结

  • 所有 原型链 最后都会到 Object.prototype, 因为 原型对象, 本质上就是个对象, 由 Object 进行创建, 其 __proto__ 指向 Object.prototype
  • Object.prototype.__proto__ 等于 null, 所以 原型链 的终点必然是: Object.prototype => null

3.2 原型链的作用

  1. 查找属性: 当我们试图访问 对象属性 时, 它会先在 当前对象 上进行搜寻, 搜寻没有结果时会继续搜寻该对象的 原型对象, 以及该对象的 原型对象原型对象, 依次层层向上搜索, 直到找到一个名字匹配的属性或到达原型链的末尾

有代码如下:

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 属性时, 是按照下图链路一层层往下搜寻

原型、原型链不完全指南

  1. 属性屏蔽: 原型对象中的属性, 如果在实例对象中重新声明, 根据属性查找规则, 在查找该属性时会直接返回实例中声明的值; 这时原型对象中的属性可以简单理解为被 屏蔽 了, 在很多文章中称该现象为 属性覆盖 但个人认为说 覆盖 是不准确的, 因为 原型对象 中属性并没有被 覆盖, 用 屏蔽 或许更为准确

如下代码, 在实例对象 p2 中, 屏蔽了原型对象 Person.prototypename 属性

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, 取自实例对象
  1. 原型对象 中的函数被调用时, 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
  • 原型、原型链的优点: 为同类型对象提供 共享属性、将通用属性抽离大大 节约内存

六、参考资料

  1. 继承与原型链
  2. 轻松理解JS 原型原型链
  3. JavaScript原型及原型链
  4. JS 的 new 做了哪些事情呢?
  5. JavaScript深入之从原型到原型链
  6. 彻底搞懂Object和Function的关系
  7. 面试官:JavaScript原型, 原型链 ? 有什么特点?
  8. 最详尽的 JS 原型与原型链终极详解, 没有「可能是」