小白的JS学习之路(五)------ 原型
学习笔记整理,从 prototype 到 proto 再到原型链,一次性搞懂 JavaScript 的原型机制。
前言
学完了闭包,接下来是 JavaScript 的另一个"大BOSS"------原型。
原型这个东西,很多人刚接触时一头雾水:prototype、__proto__、constructor、原型链......这些概念之间到底是什么关系?
别急,我们一个一个来,从最基础的开始。
一、prototype(显式原型)
1.1 什么是 prototype?
prototype 是函数天生就拥有的一项属性。
注意我说的是函数 ,不是普通对象。只有函数才有 prototype 属性。
javascript
function Car() {}
console.log(Car.prototype) // { constructor: f Car() }
console.log(typeof Car.prototype) // "object"
const obj = {}
console.log(obj.prototype) // undefined ------ 普通对象没有 prototype
1.2 prototype 有什么用?
我们可以把公共的属性和方法 挂载到 prototype 上。之后通过这个函数 new 出来的所有实例,都能访问到这些属性和方法。
javascript
function Car(color) {
this.color = color // 每个实例独有的属性 → 放在构造函数里
}
// 公共属性和方法 → 放在原型上
Car.prototype.name = 'SU7'
Car.prototype.run = function() {
console.log(`${this.name} 正在跑,颜色是 ${this.color}`)
}
const car1 = new Car('蓝色')
const car2 = new Car('白色')
car1.run() // SU7 正在跑,颜色是 蓝色
car2.run() // SU7 正在跑,颜色是 白色
为什么要这样做?
如果把
run方法写在构造函数里,每new一个实例,就会在内存中创建一个新的函数对象。100 个实例 = 100 个run方法,浪费内存。放到
prototype上,所有实例共享同一个run方法,只占一份内存。这就是原型最大的意义------减少重复,节省性能开销。
1.3 实例能修改原型上的属性吗?
不能直接修改,但可以"遮蔽"(shadow)。
javascript
function Car(color) {
this.color = color
}
Car.prototype.name = 'SU7'
const car = new Car('蓝色')
console.log(car.name) // "SU7" ------ 从原型上找到的
// 尝试"修改"
car.name = '蔚来'
console.log(car.name) // "蔚来" ------ 看起来改了?
// 但原型上的值没变!
console.log(Car.prototype.name) // "SU7" ------ 没变
当 car.name = '蔚来' 时,并没有修改原型上的 name,而是给 car 自己添加了一个新的 name 属性,遮蔽了原型上的同名属性。
总结:实例对象无法修改原型上的属性值,它只能给自身添加同名属性来"遮蔽"原型属性。
二、proto(隐式原型)
2.1 什么是 proto?
每一个对象 都拥有一个 __proto__ 属性(注意:是对象,不是函数独有的)。
javascript
function Car() {}
const car = new Car()
console.log(car.__proto__) // { constructor: f Car(), ... }
console.log(typeof car.__proto__) // "object"
2.2 实例的隐式原型 === 构造函数的显式原型
这是一条铁律:
javascript
console.log(car.__proto__ === Car.prototype) // true
new Car() 创建实例时,V8 会自动把实例的 __proto__ 指向构造函数的 prototype。
2.3 属性查找规则
当 V8 访问对象的某个属性时:
- 先找对象自身的属性(显式拥有的属性)
- 如果没有,再去
__proto__上找(隐式拥有的属性)
javascript
function Car(color) {
this.color = color // 显式属性
}
Car.prototype.name = 'SU7' // 隐式属性(来自原型)
const car = new Car('蓝色')
car.hasOwnProperty('color') // true ------ car 自己的属性
car.hasOwnProperty('name') // false ------ 不是 car 自己的
console.log(car.name) // "SU7" ------ 从原型上找到的
- 显式属性 :来自构造函数中通过
this.xxx = ...定义的属性,实例自己拥有- 隐式属性 :来自构造函数的
prototype上定义的属性,实例通过__proto__访问
2.4 constructor(构造器)
每个原型对象上都有一个 constructor 属性,它指向创建该实例的构造函数:
javascript
console.log(Car.prototype.constructor === Car) // true
console.log(car.__proto__.constructor === Car) // true
console.log(car.constructor === Car) // true
constructor 就像一个"名片",记录了这个实例是谁创建的。
三、new 操作到底做了什么?
理解原型,必须搞清楚 new 的执行过程:
javascript
const car = new Car('蓝色')
V8 在后台做了三件事:
javascript
// 第一步:创建一个空对象
const this = new Object()
// 第二步:执行构造函数,绑定 this
this.constructor = Car.prototype
this.color = '蓝色' // 构造函数中 this.color = color 执行
// 第三步:将实例的隐式原型指向构造函数的显式原型
this.__proto__ = Car.prototype
用一句话概括:
new创建一个新对象 → 执行构造函数赋值 → 把新对象的__proto__指向构造函数的prototype。
这就是为什么实例能访问原型上的属性------因为在第三步,V8 已经帮你把"通道"打通了。
四、原型链
4.1 什么是原型链?
当 V8 访问对象的某个属性时,查找顺序是:
- 对象自身
- 对象的
__proto__(构造函数的prototype) __proto__.__proto__(上一层的prototype)- 一路往上找,直到找到
null为止
这条从实例出发,顺着
__proto__逐级向上查找的链路,就叫原型链。
4.2 原型链的终点
javascript
function Car() {}
const car = new Car()
car.__proto__ // Car.prototype
car.__proto__.__proto__ // Object.prototype
car.__proto__.__proto__.__proto__ // null ------ 到头了!
原型链的最终指向是 Object.prototype,再往上就是 null。
用一个例子来验证:
javascript
function Car(color) {
this.color = color
}
Car.prototype.name = 'SU7'
const car = new Car('蓝色')
// car 自身有 color
console.log(car.color) // "蓝色" ------ 在自身找到
// car 自身没有 name,去 Car.prototype 上找
console.log(car.name) // "SU7" ------ 在第一层原型找到
// car 自身没有 toString,Car.prototype 上也没有
// 继续去 Object.prototype 上找 → 找到了!
console.log(car.toString()) // "[object Object]" ------ 在第二层原型找到
// Object.prototype 上没有 foo,再往上 → null,到头了
console.log(car.foo) // undefined ------ 整条原型链都没找到
4.3 原型链示意图
javascript
car(实例对象)
│
│ __proto__
▼
Car.prototype(构造函数的显式原型)
│ ├── name: "SU7"
│ ├── run: function()
│ └── constructor: Car
│
│ __proto__
▼
Object.prototype(所有原型链的顶层)
│ ├── toString: function()
│ ├── hasOwnProperty: function()
│ ├── constructor: Object
│ └── ...
│
│ __proto__
▼
null(原型链的终点)
五、一个容易踩的坑
5.1 两个对象一定不相等
javascript
const a = { name: '小明' }
const b = { name: '小明' }
console.log(a === b) // false
虽然 a 和 b 的结构完全一样,但它们是两个不同的内存地址。
- 基本类型 判断相等:只比值 (
1 === 1→true)- 引用类型 判断相等:比值 + 内存地址 (两个对象永远
!==,因为地址不同)
5.2 修改原型的正确方式
javascript
function Car() {}
// ❌ 错误:实例没有 prototype 属性
// car.prototype = ... // 这是给实例加了一个普通属性,不是修改原型
// ✅ 正确:通过构造函数的显式原型来修改
Car.prototype.name = 'SU7'
记住:只有函数才有
prototype(显式原型),普通对象只有__proto__(隐式原型)。要修改原型,必须通过构造函数的prototype来操作。
六、总结
用一个表格把核心知识点整理清楚:
| 概念 | 拥有者 | 说明 |
|---|---|---|
| prototype(显式原型) | 只有函数 | 存放公共属性和方法,供所有实例共享 |
| proto(隐式原型) | 所有对象 | 指向创建该对象的构造函数的 prototype |
| constructor | 原型对象 | 指向创建该实例的构造函数 |
| 原型链 | 对象的查找路径 | 自身 → __proto__ → __proto__.__proto__ → ... → null |
| new 操作 | 创建实例 | 创建对象 → 执行构造函数 → 绑定 __proto__ |
核心等式:
ini
实例.__proto__ === 构造函数.prototype
原型链终点:Object.prototype.__proto__ === null
一句话总结原型:
原型就是"共享属性和方法"的机制。实例通过
__proto__指向构造函数的prototype,找不到属性时顺着原型链往上找,直到null。
写在最后
原型是 JavaScript 面向对象编程的基石,也是面试中的高频考点。理解原型的关键在于搞清 prototype、__proto__、constructor 三者的关系 ,以及属性查找的原型链机制。
如果这篇文章对你有帮助,欢迎点赞收藏,我们下期见!👋
🔗 系列文章: