揭开 JavaScript 原型链的神秘面纱

16 阅读5分钟

❓ 原型链是什么?

每个JavaScript对象都有一个原型对象,通过__proto__属性指向它的原型,而原型本身也是一个对象,它也可能有自己的原型,这样就形成了一个链式结构,直到某个对象的原型为null为止。比如:Object.prototype.__proto__为null


🔍相关定义

  1. prototype

    • 定义只有函数拥有的属性,指向一个对象。该对象是构造函数创建的实例的原型(即实例继承属性和方法的来源)。
    • 🌰示例Dog.prototype 定义了所有由 new Dog() 创建的实例的共享属性和方法。
  2. [[Prototype]]

    • 定义:是所有对象(包括函数、实例等)的内部隐藏属性,指向构造函数的 prototype (即继承的来源)。
    • 🌰示例myDog.[[Prototype]] 指向 Dog.prototype(实际通过__proto__或者Object.getPrototypeOf())访问。
  3. __proto__(实际为[[Prototype]]属性的访问器)

    • 定义:所有对象(包括实例和函数)的内部属性 [[Prototype]] 的非标准访问方式,指向该对象的原型(即构造函数的 prototype)。
    • 🌰示例myDog.__proto__ === Dog.prototype

🔍 prototype vs [[Prototype]]

特性prototype[[Prototype]]
所属对象仅函数对象(构造函数)拥有所有对象(包括函数、实例等)拥有
用途定义实例的共享属性和方法实现继承时查找属性的原型链指针
访问方式显式属性(如 Dog.prototype内部属性,需通过 __proto__ 或 Object.getPrototypeOf() 访问
是否可修改可直接修改(如 Dog.prototype = {}可通过 Object.setPrototypeOf() 修改,但性能不推荐

🌟 单链表与原型链

原型链本质上是一种单向链表,而链表的终点需要一个明确的终止符。在计算机科学中,链表通常以 null(或空指针)结尾,以防止无限遍历。原型链的设计完全遵循这一原则:

数据结构特性原型链实现
节点(Node)对象(如 objFoo.prototype
指针(Pointer)[[Prototype]](即 __proto__
终止符(Sentinel)null

🎯 构造函数、实例、原型的关系

function Dog() {} // 构造函数 
const myDog = new Dog(); // 实例 
myDog.__proto__ === Dog.prototype; // true

🌰 举个例子:

1. 核心角色设定

  • 构造函数(掌门) :负责收徒传功,自带「门派秘籍」(prototype 属性)。
  • 实例对象(徒弟) :通过 new 拜师,获得「师父联系方式」(__proto__)。
  • 原型对象(秘籍) :存储门派共享功法(prototype 指向的对象)。
// 开宗立派:创建一个构造函数(掌门)
function 武当派(弟子名) {
    this.道号 = 弟子名;
}

// 编写门派秘籍(prototype)
武当派.prototype.内功 = "太极心法";
武当派.prototype.招式 = function() { 
    console.log(this.道号 + "施展了太极拳!"); 
};

// 收徒(实例化)
const 张三 = new 武当派("张翠山");

  • 秘籍传递规则

    • 徒弟的 __proto__ = 掌门的 prototype
    • 掌门的 prototype 本身也是个对象,它的 __proto__ 指向更古老的秘籍(Object.prototype
    • 终极秘籍的 __proto__ 是 null(武学源头不可考)

3. 功法调用流程(比试现场)

console.log(张三.道号);      // "张翠山"(直接获取)
console.log(张三.内功);      // "太极心法"(查秘籍)
张三.招式();                // "张翠山施展了太极拳!"(查秘籍)

// 掌门偷偷更新秘籍,所有徒弟自动升级!
武当派.prototype.轻功 = "梯云纵";
console.log(张三.轻功);      // "梯云纵"(动态继承)

4. 叛出师门(修改原型链)

// 张三偷学明教功法(修改 __proto__)
const 明教秘籍 = { 内功: "乾坤大挪移", 招式: function() { console.log("圣火令武功!"); } };
张三.__proto__ = 明教秘籍;

console.log(张三.内功);      // "乾坤大挪移"(已叛变)
张三.招式();                // "圣火令武功!"
console.log(张三.轻功);      // undefined(原门派的升级失效)

5. 江湖辈分验证(instanceof 原理)

console.log(张三 instanceof 武当派); // false(已修改 __proto__)
console.log(明教秘籍.__proto__ === Object.prototype); // true 
console.log(Object.prototype.__proto__); // null(武学尽头)

6.关键知识点总结

概念比喻代码表现核心作用
prototype掌门编写的门派秘籍构造函数.prototype定义实例共享的属性和方法
__proto__徒弟掌握的师父联系方式实例.__proto__实现原型链查找的指针
new 操作拜师仪式const 徒弟 = new 掌门()绑定 proto 到 prototype
原型链终点武学源头失传Object.prototype.__proto__ = null终止查找,防止死循环

7.一句话理解 prototype 和 __proto__

  • prototype 是师父的教案(只有函数有):定义徒弟能学什么
  • __proto__ 是徒弟的笔记(所有对象有):记录实际跟谁学的
  • new 操作是拜师仪式:把徒弟的笔记(__proto__)指向师父的教案(prototype

8.冷知识:为什么不能「我师父的师父是我」?

如果试图制造循环链:

武当派.prototype.__proto__ = 张三; // 试图让秘籍指向徒弟

JS 引擎会直接拒绝并报错:TypeError: Cyclic __proto__ value
—— 江湖规矩:辈分不能乱!

💡 Function 与 Object 的“鸡蛋理论”问题

在 JavaScript 中,Function 和 Object 的原型链确实存在一个经典的“鸡生蛋还是蛋生鸡”的循环依赖问题。这个问题源于它们的原型链相互指向对方,形成了一个闭环,但 JavaScript 引擎通过内部机制解决了这一矛盾。

1. Function 与 Object 的相互依赖

  • Object 是 Function 的实例
    Object 构造函数本身是一个函数,因此它是 Function 的实例:

    console.log(Object instanceof Function); // true
    console.log(Object.__proto__ === Function.prototype); // true
    
  • Function 是 Object 的实例
    Function 构造函数本身也是一个对象,因此它继承自 Object

    console.log(Function instanceof Object); // true
    console.log(Function.__proto__.__proto__ === Object.prototype); // true
    

2. Function.prototype 与 Object.prototype

  • Function.prototype 是一个对象
    Function.prototype 本身是一个空函数,但它继承自 Object.prototype

    console.log(Function.prototype.__proto__ === Object.prototype); // true
    
  • Object.prototype 是原型链的终点
    Object.prototype 的原型是 null,终结了原型链:

    console.log(Object.prototype.__proto__); // null
    

3. 循环依赖的关键路径

  • Object → Function.prototype → Object.prototype

    • Object 是 Function 的实例,因此 Object.__proto__ === Function.prototype
    • Function.prototype 是对象,因此 Function.prototype.__proto__ === Object.prototype
  • Function → Function.prototype → Object.prototype

    • Function 是自身的实例(Function.__proto__ === Function.prototype),但最终也继承自 Object.prototype

4. 为什么没有矛盾?

JavaScript 引擎在底层通过初始化顺序特殊处理解决了这一问题:

  1. 初始化顺序

    • 引擎首先创建 Object.prototype(原型链终点)。
    • 然后创建 Function.prototype,并让它继承 Object.prototype
    • 最后创建 Function 和 Object 构造函数,并让它们继承 Function.prototype
  2. 特殊处理

    • Function.prototype 被设计为一个空函数对象,既是函数又是对象,作为原型链的桥梁。
    • Function 和 Object 的 __proto__ 被硬编码指向 Function.prototype,形成闭环。