原型和原型链

前言

原型和原型链是 JavaScript 继承的基石,也是面试中除了闭包之外问得最多的基础知识。本文理清 prototype、__proto__、constructor 三者的关系,并结合实际应用和 Vue3 源码加深理解。

一、三个核心概念

先把最容易混淆的三个属性说清楚:

属性 归属 说明
prototype 函数独有 函数的原型对象,普通对象没有
__proto__ 所有对象(除 null) 指向构造函数的 prototype
constructor prototype 上的属性 指回构造函数本身
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name) {
this.name = name
}

const p = new Person('张三')

// 三者关系
console.log(Person.prototype) // Person 的原型对象
console.log(p.__proto__) // 指向 Person.prototype
console.log(p.__proto__ === Person.prototype) // true
console.log(Person.prototype.constructor === Person) // true

// 验证
console.log(p.constructor === Person) // true(从原型链上找到的)

一句话总结:__proto__ 和 constructor 是对象和函数之间的桥梁,prototype 是函数的独有属性,三者构成了一组闭环关系。

二、原型链

1
2
3
4
5
6
7
function Person() {}
const p = new Person()

// 完整的原型链查找路径
console.log(p.__proto__) // → Person.prototype
console.log(p.__proto__.__proto__) // → Object.prototype
console.log(p.__proto__.__proto__.__proto__) // → null(原型链终点)

原型链图示:

1
2
3
4
p  ──__proto__──►  Person.prototype  ──__proto__──►  Object.prototype  ──__proto__──►  null
│
├── constructor: Person
└── sayHi: function() {}

属性查找规则

1
2
3
4
5
6
7
8
function Person() { this.name = '实例属性' }
Person.prototype.name = '原型属性'

const p = new Person()
console.log(p.name) // '实例属性' — 先找自身

delete p.name
console.log(p.name) // '原型属性' — 找不到再往原型链上找

读取时从下往上查找,直到 null;hasOwnProperty 可以判断属性是自身的还是原型链上的。

三、instanceof 原理

instanceof 的本质是沿着 __proto__ 链向上查找,匹配 prototype:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function myInstanceof(instance, constructor) {
let proto = Object.getPrototypeOf(instance) // instance.__proto__

while (proto) {
if (proto === constructor.prototype) return true
proto = Object.getPrototypeOf(proto) // 继续往上
}

return false
}

// 验证
console.log(myInstanceof([], Array)) // true
console.log(myInstanceof([], Object)) // true(Array.prototype.__proto__ === Object.prototype)
console.log(myInstanceof({}, Array)) // false

四、继承的 6 种方式

1. 原型链继承

1
2
3
4
function Parent() { this.colors = ['red', 'blue'] }
function Child() {}
Child.prototype = new Parent()
// ❌ 问题:引用类型被所有实例共享

2. 构造函数继承

1
2
function Child() { Parent.call(this) }
// ❌ 问题:无法继承原型上的方法

3. 组合继承(最常用)

1
2
3
4
function Child() { Parent.call(this) }
Child.prototype = new Parent()
Child.prototype.constructor = Child
// ✅ 一次搞定,但调用了两次 Parent

4. 原型式继承

1
2
3
4
5
6
function create(o) {
function F() {}
F.prototype = o
return new F()
}
// 即 Object.create() 的原理

5. 寄生组合继承(最优方案)

1
2
3
4
5
6
7
8
9
10
11
12
function inherit(Child, Parent) {
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
}

function Parent(name) { this.name = name }
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
inherit(Child, Parent)
// ✅ ES6 class extends 的 Babel 编译结果就是它

6. ES6 class(语法糖)

1
2
3
4
5
6
7
8
9
10
11
class Parent {
constructor(name) { this.name = name }
say() { console.log(this.name) }
}

class Child extends Parent {
constructor(name, age) {
super(name) // 等价 Parent.call(this, name)
this.age = age
}
}

五、字符串 / 数字为什么能调方法

1
2
3
4
5
6
7
const str = 'hello'
console.log(str.toUpperCase()) // 'HELLO' — 原始值怎么能调方法?

// 等价于
const temp = new String('hello') // 自动装箱,创建临时包装对象
temp.toUpperCase() // 调用 String.prototype 上的方法
// temp 被销毁
1
2
3
4
5
6
7
8
9
10
// 完整原型链
const s = 'text'

s.__proto__ // → String.prototype
String.prototype.__proto__ // → Object.prototype
Object.prototype.__proto__ // → null

// 同样适用于 Number 和 Boolean
;(1).__proto__ // → Number.prototype
true.__proto__ // → Boolean.prototype

六、实际开发应用场景

1. 数组方法统一扩展(非侵入式)

1
2
3
4
5
6
// 给所有数组加方法(不推荐直接改 Array.prototype,但理解其原理很重要)
Array.prototype.sum = function () {
return this.reduce((a, b) => a + b, 0)
}

console.log([1, 2, 3, 4].sum()) // 10

2. 判断数据类型

1
2
3
4
5
6
7
8
9
10
11
// typeof 无法区分 Array 和 Object
console.log(typeof []) // 'object'

// Object.prototype.toString 精准判断
function getType(val) {
return Object.prototype.toString.call(val).slice(8, -1)
}

console.log(getType([])) // 'Array'
console.log(getType(null)) // 'Null'
console.log(getType(new Date())) // 'Date'

3. 类数组转数组

1
2
3
4
5
6
7
8
function foo() {
// arguments 是类数组,没有 forEach/map 等方法
const args = Array.prototype.slice.call(arguments)
// 或现代写法
const args2 = Array.from(arguments)
console.log(args) // [1, 2, 3]
}
foo(1, 2, 3)

4. 方法借用

1
2
3
4
5
6
7
8
9
const obj = { 0: 'a', 1: 'b', length: 2 }

// 借用 Array 的方法操作类数组对象
Array.prototype.push.call(obj, 'c')
console.log(obj) // { 0: 'a', 1: 'b', 2: 'c', length: 3 }

// 借用 filter
const filtered = Array.prototype.filter.call(obj, v => v !== 'b')
console.log(filtered) // ['a', 'c']

七、Vue3 中原型和原型链的应用

1. Vue 实例的原型方法挂载

Vue3 的 app.config.globalProperties 本质上就是把方法挂到组件实例的原型链上:

1
2
3
4
5
6
7
8
9
// main.js
const app = createApp(App)

// 全局挂载 → 所有组件实例都可以通过 this.$http 访问
app.config.globalProperties.$http = axios

// 内部实现简化
app.config.globalProperties = Object.create(null)
// 组件实例的 __proto__ 指向 globalProperties,原型链查找机制
1
2
3
4
5
6
7
8
9
10
<script>
export default {
mounted() {
// $http 通过原型链查找获取
this.$http.get('/api/data').then(res => {
console.log(res)
})
}
}
</script>

2. Vue3 响应式的去原型技巧

Vue3 对某些特殊对象(如纯对象)会劫持 __proto__ 避免原型链上的修改触发更新:

1
2
3
4
5
6
7
8
9
10
11
// Vue3 中标记原始对象(用 Object.create(null) 去掉原型链)
const raw = Object.create(null)
raw.key = 'value'

// reactive 处理时会跳过 __proto__
function toReactive(value) {
// 如果 value 已经是 proxy,直接返回
if (isProxy(value)) return value
// 如果配置了 __v_skip,跳过代理
return reactive(value)
}

3. 组件实例原型链

1
2
3
4
5
6
7
// Vue3 组件实例的原型链关系
const instance = getCurrentInstance()

// 组件实例 → 组件原型 → globalProperties → Object.prototype
instance.__proto__ // 组件配置的 methods/computed
instance.__proto__.__proto__ // globalProperties(有 $http、$router 等)
instance.__proto__.__proto__.__proto__ // Object.prototype

4. 自定义 Hook 中的原型应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 用原型封装通用请求类
class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL
}

async get(path) {
const res = await fetch(`${this.baseURL}${path}`)
return res.json()
}

async post(path, data) {
const res = await fetch(`${this.baseURL}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
return res.json()
}
}

// 基于原型继承扩展
class AuthClient extends ApiClient {
constructor(baseURL, token) {
super(baseURL)
this.token = token
}

// 重写父类方法,加上认证头
async get(path) {
const res = await fetch(`${this.baseURL}${path}`, {
headers: { Authorization: `Bearer ${this.token}` }
})
return res.json()
}
}

// 在 Vue3 中使用
import { ref } from 'vue'

const client = new AuthClient('/api', localStorage.getItem('token'))

export function useUser() {
const user = ref(null)

async function fetchUser(id) {
user.value = await client.get(`/users/${id}`)
}

return { user, fetchUser }
}

5. Vue3 的 __v_skip 标记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Vue3 内部用 __v_skip 标记对象跳过代理
// 本质是给对象打私有属性,避免不必要的 Proxy 包裹

import { markRaw, reactive } from 'vue'

const heavyObj = markRaw({
// 大体积数据,标记后不会被 reactive 代理
mapData: new Map(),
hugeList: new Array(100000)
})

const state = reactive({
normal: 'data',
heavyObj // 不会触发 Proxy,跳过响应式
})

// markRaw 内部实现简化:
// function markRaw(obj) { obj.__v_skip = true; return obj }

总结

  • prototype 是函数独有、__proto__ 是所有对象都有、constructor 指回构造函数
  • 原型链查找从下往上,终点是 null
  • instanceof 就是沿着 __proto__ 逐级匹配 prototype
  • 寄生组合继承是 ES6 class extends 的底层实现
  • 原始值调方法靠自动装箱,临时创建包装对象
  • Vue3 的 globalProperties、组件实例原型链、markRaw 都在用原型机制
  • Object.create(null) 创建无原型对象,避免属性查找污染

内容由 AI 生成,仅供参考。本文发布于 码上学习,转载请注明出处。