JavaScript 原型链:一场关于"遗传"的编程奇遇 🧬

84 阅读4分钟

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 等属性

意义

  1. 将构造函数中的固定属性和方法挂载到原型上
  2. 创建实例时不需要重复执行这些属性和方法
  3. 所有实例共享原型上的属性和方法

隐式原型(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 - 来自原型链查找

这五步神功确保了:

  1. 每个实例都是独立的对象
  2. 每个实例都能继承构造函数原型上的属性和方法

原型链: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' - 从父亲那里继承来的

查找过程

  1. 先在 c 对象自身查找 card 属性 ❌
  2. c.__proto__(Child.prototype)查找 ❌
  3. c.__proto__.__proto__(Parent.prototype)查找 ❌
  4. 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 实现了:

  1. 内存优化:共享方法,避免重复创建
  2. 代码复用:一次定义,处处使用
  3. 动态扩展:可以随时给原型添加新方法
  4. 继承机制:实现面向对象编程

总结:原型链就是 JavaScript 的"家族树" 🌳

  • 显式原型(prototype):函数的"基因库",存储共享的属性和方法
  • 隐式原型(proto:对象的"族谱",指向它的原型对象
  • 原型链:V8 引擎的"寻根问祖"机制,层层向上查找属性
  • constructor:对象的"身份证",标识创建者

记住这个口诀:"函数有 prototype,对象有 proto,new 连接两者,形成原型链!" 🎯