__proto__ vs prototype:90% 的人分不清的 JavaScript 核心

__proto__ vs prototype:90% 的人分不清的 JavaScript 核心机制

"每个对象都有一个秘密通道,通向另一个对象。这个通道叫 __proto__,而这条通道连成的高速公路,就叫原型链。"


一、先搞清楚两个容易混淆的东西

很多初学者看到 __proto__prototype 就头大,觉得它们是同一个东西。不是的。 它们的关系就像"钥匙"和"锁"------有关联,但用途完全不同。

__proto__ prototype
谁拥有它 所有对象都有 只有函数才有
它指向什么 指向"创建我的那个构造函数的 prototype" 指向这个函数作为构造函数时,实例共享的那个对象
一句话概括 "我是谁造的?" "我造出来的东西都能用啥?"

二、从一个故事开始

假设你开了一家奶茶店:

javascript 复制代码
// 你设计了一个"奶茶配方"(构造函数)
function MilkTea(flavor) {
  this.flavor = flavor;
}

// 所有奶茶共享的能力,写在 prototype 上
MilkTea.prototype.drink = function() {
  console.log(`喝了一口${this.flavor}奶茶,真好喝!`);
};

MilkTea.prototype.addTopping = function(topping) {
  console.log(`给${this.flavor}奶茶加了${topping},口感升级!`);
};

// 做出两杯奶茶(实例)
const pearlTea = new MilkTea('珍珠');
const taroTea = new MilkTea('芋泥');

现在来验证一下关系:

javascript 复制代码
console.log(pearlTea.__proto__ === MilkTea.prototype);  // true ✅
console.log(taroTea.__proto__ === MilkTea.prototype);   // true ✅
console.log(MilkTea.prototype.constructor === MilkTea);  // true ✅

看明白了吗?

  • pearlTea.__proto__ → 指向 MilkTea.prototype("我是 MilkTea 造的")
  • MilkTea.prototype → 是一个对象,上面挂着 drinkaddTopping("我造的实例都能用这些方法")

三、图解:对象之间的秘密通道

javascript 复制代码
  pearlTea                          taroTea
  { flavor: '珍珠' }                { flavor: '芋泥' }
       │                                │
       │ __proto__                      │ __proto__
       ▼                                ▼
  ┌─────────────────────────────────────────┐
  │         MilkTea.prototype               │
  │  {                                      │
  │    constructor: MilkTea,                │
  │    drink: function() { ... },           │
  │    addTopping: function() { ... }       │
  │  }                                      │
  └─────────────────────┬───────────────────┘
                        │
                        │ __proto__
                        ▼
  ┌─────────────────────────────────────────┐
  │         Object.prototype                │
  │  {                                      │
  │    toString: function() { ... },        │
  │    hasOwnProperty: function() { ... },  │
  │    valueOf: function() { ... }          │
  │  }                                      │
  └─────────────────────┬───────────────────┘
                        │
                        │ __proto__
                        ▼
                      null          ← 终点站,原型链的尽头

这就是原型链! 当你访问 pearlTea.drink() 时,JavaScript 引擎的查找过程是:

  1. pearlTea 自己身上有 drink 吗?→ 没有
  2. pearlTea.__proto__(即 MilkTea.prototype)上找 → 找到了!执行它
  3. 如果还没找到,继续沿 __proto__ 往上找 Object.prototype
  4. 如果还没找到,继续找 null没有就报 undefined

就像你找东西:先翻自己的口袋,再翻家里的柜子,再翻储藏室,都没有那就是没有。


四、__proto__ 的"正经写法"

__proto__ 其实是一个 getter/setter ,它在浏览器中被实现为 Object.prototype 上的属性。正经开发中,推荐用这两个方法代替:

javascript 复制代码
// 设置原型
Object.setPrototypeOf(pearlTea, someOtherProto);

// 获取原型
Object.getPrototypeOf(pearlTea);  // 等价于 pearlTea.__proto__

创建对象时直接指定原型,更推荐用 Object.create

javascript 复制代码
const baseTea = {
  stir() {
    console.log('搅拌均匀');
  }
};

const greenTea = Object.create(baseTea);
greenTea.flavor = '抹茶';

greenTea.stir();  // "搅拌均匀" ------ 沿原型链找到的
console.log(Object.getPrototypeOf(greenTea) === baseTea);  // true

五、函数也是对象!所以函数也有 __proto__

这是最容易让人脑壳疼的部分。记住一个铁律:

在 JavaScript 中,函数也是对象。

javascript 复制代码
function Foo() {}

// 函数 Foo 也是一个对象,所以它有 __proto__
console.log(Foo.__proto__ === Function.prototype);  // true

// Function.prototype 也是一个对象
console.log(Function.prototype.__proto__ === Object.prototype);  // true

// Object.prototype 是终点
console.log(Object.prototype.__proto__ === null);  // true

来,画一张函数的原型链全景图:

javascript 复制代码
        Foo(函数)
          │
          │ __proto__
          ▼
  Function.prototype       ← 所有函数共享的方法(call, apply, bind...)
          │
          │ __proto__
          ▼
  Object.prototype         ← 所有对象共享的方法(toString, hasOwnProperty...)
          │
          │ __proto__
          ▼
        null

Foo.prototype 是另一条线:

javascript 复制代码
        Foo(函数)
          │
          │ prototype(只有函数有这个属性)
          ▼
  Foo.prototype            ← Foo 的实例的 __proto__ 指向这里
          │
          │ __proto__
          ▼
  Object.prototype
          │
          │ __proto__
          ▼
        null

两条不同的链! Foo.__proto__Foo.prototype 走的是完全不同的路。


六、new 关键字到底做了什么?

理解 new 的过程,原型链就彻底通了:

javascript 复制代码
function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  console.log(`Hi, I'm ${this.name}`);
};

const alice = new Person('Alice');

new Person('Alice') 背后发生了 4 件事

javascript 复制代码
// 等价于以下过程:
function fakeNew(Constructor, ...args) {
  // 1. 创建一个空对象
  const obj = {};

  // 2. 把这个对象的 __proto__ 指向构造函数的 prototype
  Object.setPrototypeOf(obj, Constructor.prototype);
  // 等价于 obj.__proto__ = Constructor.prototype;

  // 3. 用这个对象作为 this,执行构造函数
  const result = Constructor.apply(obj, args);

  // 4. 如果构造函数返回了一个对象,就用那个对象;否则返回 obj
  return result instanceof Object ? result : obj;
}

验证一下:

javascript 复制代码
const bob = fakeNew(Person, 'Bob');
bob.sayHi();  // "Hi, I'm Bob" ✅
console.log(Object.getPrototypeOf(bob) === Person.prototype);  // true ✅

七、ES6 的 class ------ 语法糖而已

ES6 的 class 写法看起来像 Java/C++,但本质上还是原型链:

javascript 复制代码
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks`);
  }
}

const rex = new Dog('Rex');
rex.speak();  // "Rex barks"

上面的代码,底层等价于:

javascript 复制代码
function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function() {
  console.log(this.name + ' makes a sound');
};

function Dog(name) {
  Animal.call(this, name);  // 调用父类构造函数
}

// 建立原型链:Dog.prototype → Animal.prototype → Object.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.speak = function() {
  console.log(this.name + ' barks');
};

const rex = new Dog('Rex');
rex.speak();  // "Rex barks"

原型链图:

javascript 复制代码
  rex(Dog 的实例)
    │
    │ __proto__
    ▼
  Dog.prototype
    │
    │ __proto__  (通过 Object.create(Animal.prototype) 建立)
    ▼
  Animal.prototype
    │
    │ __proto__
    ▼
  Object.prototype
    │
    │ __proto__
    ▼
  null

当调用 rex.speak() 时:

  1. rex 自己没有 speak → 去 Dog.prototype
  2. Dog.prototypespeak找到了,执行

如果调用 rex.hasOwnProperty('name')

  1. rex 没有 → Dog.prototype 没有 → Animal.prototype 没有
  2. Object.prototype 有 → 找到了!

八、几个经典面试题,来检验一下

题目 1:instanceof 的本质

javascript 复制代码
console.log(rex instanceof Dog);    // true
console.log(rex instanceof Animal); // true

instanceof 的原理:沿着 rex.__proto__ 这条链,看 Dog.prototypeAnimal.prototype 是否在链上。

javascript 复制代码
// 模拟 instanceof
function myInstanceof(obj, Constructor) {
  let proto = Object.getPrototypeOf(obj);
  while (proto !== null) {
    if (proto === Constructor.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

题目 2:属性遮蔽(Shadowing)

javascript 复制代码
const obj = { a: 1 };
const child = Object.create(obj);

console.log(child.a);      // 1(从原型链上找到的)
console.log('a' in child); // false(child 自己没有 a)

child.a = 2;               // 在 child 自己身上创建了 a
console.log(child.a);      // 2(自己的属性优先)
console.log(obj.a);        // 1(原型上的没被修改)

delete child.a;            // 删掉自己的 a
console.log(child.a);      // 1(又从原型链上找到了)

原型链上的属性不会被实例修改 ------你写 child.a = 2 不会改原型,而是在 child 自己身上新建一个同名属性"遮住"原型上的。

题目 3:Function.__proto__ === Function.prototype

javascript 复制代码
console.log(Function.__proto__ === Function.prototype);  // true 🤯

函数 Function 自己就是由自己创建的。 这是 JavaScript 的一个自举(bootstrap)设计------Function 是整个类型系统的起点,它打破了"函数由别人创建"的规则,自己指向自己。


九、总结:一张图记住一切

javascript 复制代码
                    null
                     ▲
                     │ __proto__
             Object.prototype
              ▲             ▲
     __proto__│             │ __proto__
             │              │
    Function.prototype    Foo.prototype
              ▲             ▲
     __proto__│             │ __proto__
             │              │
           Foo ─────────────┘
           (Foo.prototype 指向右边)

   实例:
   foo.__proto__ → Foo.prototype → Object.prototype → null

三条核心规则:

  1. 所有对象都有 __proto__ ,它指向创建该对象的构造函数的 prototype
  2. 只有函数有 prototype,它是一个对象,用于存放实例共享的属性和方法
  3. 查找属性时沿 __proto__ 链向上搜索 ,直到找到或到达 null 为止

理解了这三点,原型链就彻底通了。


十、最后的灵魂拷问

javascript 复制代码
console.log(Object.__proto__ === Function.prototype);           // true
console.log(Function.__proto__ === Function.prototype);         // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null);               // true

试着画出 ObjectFunction 之间互相纠缠的原型链图吧------如果你能画对,说明你真的懂了。


记住:JavaScript 没有类,只有对象。原型链就是对象之间的"关系网",__proto__ 是网线,prototype 是网线另一头的插座。

相关推荐
国科安芯1 小时前
国科安芯推出商业航天级抗辐照半双工 RS485 收发器 ASC485S2Y
前端·单片机·嵌入式硬件·架构·安全性测试
丑过三八线1 小时前
Umi 运行时配置 app.tsx 详解
前端
提子拌饭1331 小时前
个人月事记录表应用 - 鸿蒙PC Electron框架完整实现指南
前端·javascript·华为·electron·前端框架·开源·鸿蒙系统
神奇小汤圆1 小时前
SEATA:Server 到 Golang Client 全链路走读
面试
超人气王1 小时前
新手学前端JS浅拷贝和深拷贝:对象复制竟然是个“替身文学”?
javascript·面试
YHL1 小时前
📚 JS执行机制(执行上下文 + 调用栈 + 编译流程)
前端·javascript
不简说1 小时前
这次真香!sv-print 可视化打印设计器更新:插件脚手架、Excel 导出、弹窗 API 三连发
前端·javascript·前端框架
无聊的老谢1 小时前
Web GIS 最佳实践:Vue 集成 Leaflet/OpenLayers 实现基站海量点位渲染
前端·javascript·vue.js
yingyima1 小时前
GCP Cloud Scheduler 核心语法与实战示例速查手册
前端