🚀 从 "共享仓库" 到 "寻宝链":揭秘 JS 原型的隐藏法则(面试必看)

40 阅读5分钟

🚀 从 "共享仓库" 到 "寻宝链":揭秘 JS 原型的隐藏法则(面试必看)

你是否也曾在调试代码时,对着控制台里的__proto__一脸茫然?是否疑惑过为什么两个实例能共享同一个方法,却又能拥有自己的独特属性?今天,我们就来扒开 JavaScript 中最 "低调却核心" 的机制 ——原型(prototype)  的神秘面纱。

🌰 一个让新手崩溃的场景

先看一段代码:

javascript 运行

// 定义一个"人"的构造函数
function Person(name) {
  this.name = name;
}

// 给原型加一个"物种"属性
Person.prototype.species = "人类";

// 创建两个实例
const zhangsan = new Person("张三");
const lisi = new Person("李四");

console.log(zhangsan.species); // 输出"人类"
console.log(lisi.species);    // 输出"人类"
console.log(zhangsan.species === lisi.species); // 输出true

明明没给张三李四直接赋值species,它们却能 "共享" 这个属性?这就像两个陌生人共用一个储物柜 —— 而这个 "储物柜",就是原型

📦 什么是原型?本质是 "共享仓库"

在 JavaScript 中,每个构造函数(首字母大写的函数,比如PersonCar)都自带一个prototype属性,它是一个对象。你可以把它理解为:给这个构造函数创建的所有实例准备的 "共享仓库"

  • 存在于prototype上的属性 / 方法,会被所有实例 "共享"(不用重复创建,节省内存)
  • 实例虽然没直接持有这些属性 / 方法,但能 "访问" 到它们(就像共享仓库的钥匙)

用代码举例:

javascript 运行

// 构造函数:汽车
function Car(color) {
  this.color = color; // 每个实例独有的颜色(自己的东西)
}

// 原型上的共享方法(大家共用的工具)
Car.prototype.drive = function() {
  console.log(`开着${this.color}的车下赛道~`);
};

const car1 = new Car("霞光紫");
const car2 = new Car("海湾蓝");

car1.drive(); // 开着霞光紫的车下赛道~
car2.drive(); // 开着海湾蓝的车下赛道~

这里的drive方法只在Car.prototype上存了一份,却能被car1car2同时使用 —— 这就是原型的核心价值:减少重复,实现共享

🔗 prototype vs __proto__:别再搞混了!

很多人刚学原型时,会被这两个长得像的东西绕晕。记住一句话:构造函数用prototype,实例用__proto__

  • 构造函数.prototype:共享仓库本身(存放共享内容)
  • 实例.__proto__:实例指向共享仓库的 "指针"(钥匙)

用代码验证:

javascript 运行

function Person(name) {
  this.name = name;
}
Person.prototype.speci = "人类"; // 共享仓库

const su = new Person("舒老板");

// 实例的__proto__ 指向 构造函数的prototype
console.log(su.__proto__ === Person.prototype); // 输出true ✅

就像你(实例)用钥匙(__proto__)打开了仓库(prototype),才能拿到里面的东西(共享属性 / 方法)。

注意:__proto__是浏览器提供的私有属性(非标准但常用),标准方法是用Object.getPrototypeOf(实例)获取原型。

🧬 原型链:对象的 "寻宝之路"

如果实例自己没有某个属性,它会顺着__proto__去原型里找;如果原型里也没有,会继续顺着原型的__proto__找 —— 这就是原型链,一条从实例通向 "根源" 的寻宝之路。

看一个完整的链条:

javascript 运行

const su = new Person("舒老板");

// 第一层:实例的原型是 Person.prototype
console.log(su.__proto__ === Person.prototype); // true

// 第二层:Person.prototype的原型是 Object.prototype(所有对象的默认原型)
console.log(su.__proto__.__proto__ === Object.prototype); // true

// 第三层:Object.prototype的原型是null(终点)
console.log(su.__proto__.__proto__.__proto__ === null); // true

当你调用su.toString()时,su自己没有toString,就会顺着链条找到Object.prototype.toString—— 这就是为什么任何对象都能调用toString

💡 实战技巧:原型的正确用法

  1. 存放公共方法,节省内存避免在构造函数里定义方法(会重复创建),把方法放在prototype上:

    javascript 运行

    // 不好的写法
    function Person(name) {
      this.name = name;
      this.sayHi = function() {}; // 每个实例都有一个sayHi,浪费内存
    }
    
    // 推荐的写法
    function Person(name) {
      this.name = name;
    }
    Person.prototype.sayHi = function() {}; // 所有实例共享一个sayHi
    
  2. 实例属性会 "覆盖" 原型属性如果实例自己有某个属性,就不会去原型里找:

    javascript

    运行

    Person.prototype.species = "人类";
    const su = new Person("舒老板");
    su.species = "LOL达人"; // 实例自己的属性
    
    console.log(su.species); // 输出"LOL达人"(用自己的)
    console.log(su.__proto__.species); // 输出"人类"(原型上的还在)
    
  3. 原型链继承(简单实现) 让一个构造函数的原型指向另一个构造函数的实例,实现继承:

    javascript 运行

    function Animal() {}
    Animal.prototype.species = "动物";
    
    function Person() {}
    Person.prototype = new Animal(); // Person的原型指向Animal实例
    
    const su = new Person();
    console.log(su.species); // 输出"动物"(顺着链条找到Animal的原型)
    

🚨 避坑指南:这些错误不要犯!

  1. 直接修改__proto__ __proto__是一个访问器属性,直接修改它的性能很差,并且会破坏代码的可预测性。如果你想改变一个对象的原型,应该使用Object.setPrototypeOf(obj, newProto)

    javascript 运行

    const obj = {};
    const newProto = { x: 10 };
    
    // ❌ 不推荐
    obj.__proto__ = newProto;
    
    // ✅ 推荐
    Object.setPrototypeOf(obj, newProto);
    
  2. 混淆构造函数和普通函数只有通过new关键字调用的函数才是构造函数,它的prototype才会发挥作用。直接调用构造函数会把this绑定到全局对象(非严格模式下),通常会导致意外的结果。

    javascript 运行

    function Person(name) {
      this.name = name;
    }
    
    // ❌ 这不是创建实例!这会给全局对象添加name属性。
    const wrongPerson = Person("错误的用法"); 
    
  3. **忘记原型链的终点是null**当在原型链上查找一个不存在的属性时,最终会查到Object.prototype.__proto__,而它的值是null。所以,访问一个不存在的属性会返回undefined,而不是报错。

    javascript 运行

    const obj = {};
    console.log(obj.nonExistentProperty); // 输出: undefined
    // console.log(obj.nonExistentProperty.method()); // 报错: Cannot read properties of undefined
    

📝 面试高频考点(划重点)

  • 🔥 prototype__proto__的区别?(上文已讲清)
  • 🔥 原型链的终点是什么?(null
  • 🔥 如何判断一个属性是实例自身的还是原型上的?(obj.hasOwnProperty('属性名')
  • 🔥 为什么说 JS 的面向对象是 "原型式" 而非 "类式"?(类式是血缘关系,原型式是共享机制)

总结:原型是 JS 的 "隐形骨架"

原型就像 JS 对象的 "共享基因",默默支撑着属性和方法的复用,也是理解继承、原型链的基础。看似抽象,实则藏在每一个对象的__proto__里。

下次再看到prototype,不妨把它想成一个贴满便利贴的共享冰箱 —— 每个实例都能打开,拿里面的东西,也能贴自己的便利贴(但不会覆盖冰箱原有的)。

你在学习原型时踩过哪些坑?欢迎在评论区分享你的经历~ 👇

(如果觉得有用,别忘了点赞 + 收藏,关注我解锁更多 JS 底层知识~)