JavaScript 原型链:一场关于"遗传"的编程奇遇 🧬
各位码农朋友们,今天我们来聊聊 JavaScript 中最有趣也最容易让人迷糊的概念——原型链。如果说 JavaScript 是一个大家族,那么原型链就是这个家族的"族谱",记录着每个对象的"血缘关系"!
开场白:什么是原型?🤔
想象一下,你是一个汽车制造商,每次造车都要重新设计轮子、发动机、座椅...这得累死!聪明的做法是什么?做一个标准模板!
这就是 JavaScript 原型的核心思想:
// 传统做法:每个实例都重复定义相同的方法
function Car(color) {
this.color = color
this.start = function() { console.log('启动!') } // 每个实例都有一份
this.stop = function() { console.log('停车!') } // 浪费内存
}
// 原型做法:共享方法,节省内存
Car.prototype.name = 'su7'
Car.prototype.height = 1.4
Car.prototype.weight = 1.5
Car.prototype.long = 4800
function Car(color) {
this.color = color // 每个实例独有的属性
}
const car1 = new Car('orange')
const car2 = new Car('red')
console.log(car1.name) // 'su7' - 来自原型
console.log(car2.height) // 1.4 - 来自原型
显式原型 vs 隐式原型:双胞胎兄弟 👯♂️
显式原型(prototype):函数的"基因库"
每个函数天生就有一个 prototype 属性,就像每个人都有 DNA 一样:
// 函数天生拥有一个属性 prototype,是一个对象
function Car() {}
console.log(Car.prototype) // 一个对象,包含 constructor 等属性
意义:
- 将构造函数中的固定属性和方法挂载到原型上
- 创建实例时不需要重复执行这些属性和方法
- 所有实例共享原型上的属性和方法
隐式原型(proto):对象的"族谱"
每个对象都有一个 __proto__ 属性,指向它的"祖先":
function Person() {
// new 的魔法过程:
// 1. var obj = {} // 创建空对象
// 2. Person.call(obj) // 让 this 指向 obj
// 3. this.name = 'zz' // 执行构造函数
// 4. obj.__proto__ = Person.prototype // 建立原型关系
// 5. return obj // 返回对象
}
const p = new Person()
console.log(p.__proto__ === Person.prototype) // true - 关键关系!
核心关系:
- 实例对象的隐式原型 === 构造函数的显式原型
- 这就是为什么实例能访问到构造函数原型上的属性和方法!
new 操作符:造物主的五步神功 ⚡
让我们揭秘 new 操作符背后的魔法:
Person.prototype.age = 18
function Person() {
// new 的实现原理:
// 1. var obj = {} // 创建空对象
// 2. Person.call(obj) // 让 this 指向这个对象
// 3. this.name = 'zz' // 执行构造函数中的代码
// 4. obj.__proto__ = Person.prototype // 建立原型链关系
// 5. return obj // 返回这个对象
}
const p = new Person()
console.log(p.age) // 18 - 来自原型链查找
这五步神功确保了:
- 每个实例都是独立的对象
- 每个实例都能继承构造函数原型上的属性和方法
原型链:JavaScript 的"族谱查找" 🔍
当 V8 引擎访问对象属性时,它会进行一场"寻根问祖"的旅程:
function GrandParent() {
this.name = 'grandparent'
this.card = 'visa' // 爷爷的信用卡
}
Parent.prototype = new GrandParent() // 父亲继承爷爷
function Parent() {
this.lastName = 'z' // 父亲的姓氏
}
Child.prototype = new Parent() // 孩子继承父亲
function Child() {
this.name = 'zz' // 孩子的名字
this.age = 18 // 孩子的年龄
}
const c = new Child()
console.log(c.card) // 'visa' - 从爷爷那里继承来的
console.log(c.lastName) // 'z' - 从父亲那里继承来的
查找过程:
- 先在
c对象自身查找card属性 ❌ - 去
c.__proto__(Child.prototype)查找 ❌ - 去
c.__proto__.__proto__(Parent.prototype)查找 ❌ - 去
c.__proto__.__proto__.__proto__(GrandParent.prototype)查找 ✅
这就像家族寻宝:找不到就问爸爸,爸爸找不到就问爷爷,直到找到或者到达 Object.prototype(族谱的顶端)!
constructor:身份证明 🆔
每个原型对象都有一个 constructor 属性,指向创建它的构造函数:
function Bus() {}
Car.prototype = {
constructor: Bus // 故意搞错身份
}
function Car() {
this.name = 'su7'
}
const car = new Car()
console.log(car.constructor) // Bus - 身份被篡改了!
constructor 的存在是为了让所有实例对象都知道自己是从哪个构造函数创建的,就像身份证一样!
实战技巧:给数组添加超能力 💪
const arr = [] // 等同于 const arr = new Array()
// 给所有数组添加一个自定义方法
Array.prototype.myMethod = function() {
return '我是自定义方法!'
}
console.log(arr.myMethod()) // '我是自定义方法!'
这就是为什么我们能给内置对象添加方法的原因!
特殊情况:没有原型的对象 👻
// 创建一个没有原型的对象
const obj = Object.create(null)
console.log(obj.__proto__) // undefined
// 创建一个指定原型的对象
const parent = { name: 'parent' }
const child = Object.create(parent)
console.log(child.name) // 'parent' - 来自指定的原型
原型链的意义:代码复用的艺术 🎨
原型链的设计让 JavaScript 实现了:
- 内存优化:共享方法,避免重复创建
- 代码复用:一次定义,处处使用
- 动态扩展:可以随时给原型添加新方法
- 继承机制:实现面向对象编程
总结:原型链就是 JavaScript 的"家族树" 🌳
- 显式原型(prototype):函数的"基因库",存储共享的属性和方法
- 隐式原型(proto):对象的"族谱",指向它的原型对象
- 原型链:V8 引擎的"寻根问祖"机制,层层向上查找属性
- constructor:对象的"身份证",标识创建者
记住这个口诀:"函数有 prototype,对象有 proto,new 连接两者,形成原型链!" 🎯