Skip to content

深入理解原型链与继承

__proto__class,彻底理清 JavaScript 对象系统的来龙去脉

什么是原型链?

定义:原型链(Prototype Chain)是 JavaScript 实现对象继承的核心机制。每个对象都有一个内部链接 [[Prototype]] 指向另一个对象(称为"原型"),原型自身也有原型,如此层层链接形成一条"链",直到 null 为止。当访问对象的属性时,引擎会沿着这条链逐级查找。

涉及场景

  • 方法共享:多个实例共享构造函数 prototype 上的方法,节省内存
  • 类继承class extends 底层就是原型链的语法糖
  • 类型判断instanceof 运算符沿原型链检查构造函数的 prototype
  • 框架源码:React 类组件、Vue2 选项式 API 都基于原型继承
  • Polyfill:在 Array.prototype 等原生原型上扩展方法

作用

  1. 代码复用:子类自动获得父类的所有方法和属性
  2. 内存高效:方法定义在原型上,所有实例共享同一份,不重复创建
  3. 动态扩展:运行时可以修改原型,所有实例立即生效
  4. 理解 JS 本质:原型链是 JavaScript 面向对象编程的基石,面试必考

为什么 JavaScript 选择原型继承?

JavaScript 之父 Brendan Eich 在1995年设计JS时,受到 Self 语言(基于原型的面向对象语言)的影响,选择了原型继承而非类继承

  • 类继承(Java/C++):先定义类(蓝图),再创建实例
  • 原型继承(JavaScript):直接从一个已有对象"克隆"出新对象,新对象通过原型链委托访问共享方法

本质区别:类继承是"复制",原型继承是"委托链接"。

三个核心概念

1. prototype(函数的属性)

每个函数都有一个 prototype 属性,指向一个对象(原型对象)。当函数作为构造函数(new)使用时,创建的实例会链接到这个原型对象。

2. __proto__(对象的属性)

每个对象都有一个 __proto__(即 [[Prototype]]),指向创建它的构造函数的 prototype

3. constructor(原型对象的属性)

原型对象有一个 constructor 属性,指回构造函数本身。

完整关系图

                    ┌─────────────────────────────────────────┐
                    │           Function.prototype             │
                    │  (所有函数的原型,本身是一个函数对象)        │
                    └─────────────┬───────────────────────────┘
                                  │ __proto__

                    ┌─────────────────────────────────────────┐
                    │           Object.prototype               │
                    │  toString(), hasOwnProperty(), ...       │
                    │  __proto__ → null (原型链终点)             │
                    └─────────────────────────────────────────┘

                                  │ __proto__
┌──────────────┐    ┌─────────────────────────────────────────┐
│  Person       │───→│        Person.prototype                  │
│  (构造函数)    │    │  constructor → Person                    │
│              │    │  sayHello()                              │
│  prototype ──┼───→│  __proto__ → Object.prototype            │
└──────────────┘    └─────────────────────────────────────────┘

                                  │ __proto__
                    ┌─────────────────────────────────────────┐
                    │        person1 (实例)                     │
                    │  name: '张三'                             │
                    │  __proto__ → Person.prototype             │
                    └─────────────────────────────────────────┘
javascript
function Person(name) {
  this.name = name;
}
Person.prototype.sayHello = function() {
  return `Hi, I'm ${this.name}`;
};

const person1 = new Person('张三');

// 验证关系
console.log(person1.__proto__ === Person.prototype);       // true
console.log(Person.prototype.constructor === Person);       // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null);           // true(原型链终点)

// Function 本身的原型链
console.log(Person.__proto__ === Function.prototype);       // true
console.log(Function.__proto__ === Function.prototype);     // true(自引用!)
console.log(Function.prototype.__proto__ === Object.prototype); // true

// Object 也是函数
console.log(Object.__proto__ === Function.prototype);       // true
console.log(typeof Object); // 'function'

原型链查找机制

当访问对象的属性时,引擎按以下顺序查找:

javascript
// 查找过程
person1.sayHello();

// 1. person1 自身有 sayHello 吗? → 没有
// 2. person1.__proto__(Person.prototype)有吗? → ✅ 找到了!
// 3. 如果还没找到,继续往上:Person.prototype.__proto__(Object.prototype)
// 4. 如果还没找到:Object.prototype.__proto__ → null → 返回 undefined

// 演示查找链
const obj = { a: 1 };
const child = Object.create(obj);
child.b = 2;
const grandchild = Object.create(child);
grandchild.c = 3;

console.log(grandchild.c); // 3(自身属性)
console.log(grandchild.b); // 2(来自 child)
console.log(grandchild.a); // 1(来自 obj)
console.log(grandchild.toString); // [Function](来自 Object.prototype)
console.log(grandchild.xxx); // undefined(整条链都没找到)

属性遮蔽(Property Shadowing)

javascript
function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function() {
  return `${this.name} makes a sound`;
};

const dog = new Animal('Buddy');

// 在实例上设置同名属性 → 遮蔽原型上的方法
dog.speak = function() {
  return `${this.name} barks!`;
};

console.log(dog.speak()); // 'Buddy barks!'(使用自身的)
delete dog.speak;
console.log(dog.speak()); // 'Buddy makes a sound'(回退到原型的)

// hasOwnProperty 判断是否是自身属性
console.log(dog.hasOwnProperty('name'));  // true
console.log(dog.hasOwnProperty('speak')); // false(已删除)

new 操作符的完整过程

javascript
function myNew(Constructor, ...args) {
  // 1. 创建一个新对象,其 __proto__ 指向构造函数的 prototype
  const obj = Object.create(Constructor.prototype);

  // 2. 将构造函数的 this 绑定到新对象,执行构造函数
  const result = Constructor.apply(obj, args);

  // 3. 如果构造函数返回一个对象,则使用该对象;否则使用新创建的对象
  return (result !== null && typeof result === 'object') ? result : obj;
}

// 测试
function Car(brand) {
  this.brand = brand;
}
Car.prototype.drive = function() {
  return `Driving ${this.brand}`;
};

const car = myNew(Car, 'Tesla');
console.log(car.brand);    // 'Tesla'
console.log(car.drive());  // 'Driving Tesla'
console.log(car instanceof Car); // true

// 特殊情况:构造函数返回对象
function Strange() {
  this.a = 1;
  return { b: 2 }; // 返回了一个对象
}
const s = new Strange();
console.log(s.a); // undefined(被返回的对象替代了)
console.log(s.b); // 2

instanceof 的原理

javascript
// instanceof 沿着原型链查找
function myInstanceOf(obj, Constructor) {
  let proto = Object.getPrototypeOf(obj);
  const target = Constructor.prototype;

  while (proto !== null) {
    if (proto === target) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

// 验证
console.log(myInstanceOf(person1, Person)); // true
console.log(myInstanceOf(person1, Object)); // true(原型链上有 Object.prototype)
console.log(myInstanceOf([], Array));       // true
console.log(myInstanceOf([], Object));      // true

// ⚠️ instanceof 的局限
// 1. 不能判断基本类型
console.log(1 instanceof Number);     // false
console.log('str' instanceof String); // false

// 2. 可以被 Symbol.hasInstance 自定义
class Even {
  static [Symbol.hasInstance](num) {
    return typeof num === 'number' && num % 2 === 0;
  }
}
console.log(2 instanceof Even);  // true
console.log(3 instanceof Even);  // false

六种继承方式

1. 原型链继承

javascript
function Parent() {
  this.colors = ['red', 'green'];
}
Parent.prototype.getColors = function() {
  return this.colors;
};

function Child() {}
Child.prototype = new Parent(); // 子类原型 = 父类实例

const c1 = new Child();
const c2 = new Child();
c1.colors.push('blue');
console.log(c2.colors); // ['red', 'green', 'blue'] ❌ 引用类型共享!

// 缺点:
// 1. 引用类型属性被所有实例共享
// 2. 创建子类时不能向父类传参

2. 构造函数继承(借用构造函数)

javascript
function Parent(name) {
  this.name = name;
  this.colors = ['red', 'green'];
}
Parent.prototype.getName = function() {
  return this.name;
};

function Child(name, age) {
  Parent.call(this, name); // 借用父类构造函数
  this.age = age;
}

const c1 = new Child('张三', 20);
const c2 = new Child('李四', 25);
c1.colors.push('blue');
console.log(c2.colors); // ['red', 'green'] ✅ 不再共享

// c1.getName(); // ❌ TypeError: c1.getName is not a function
// 缺点:不能继承原型上的方法

3. 组合继承(最常用的经典方案)

javascript
function Parent(name) {
  this.name = name;
  this.colors = ['red', 'green'];
}
Parent.prototype.getName = function() {
  return this.name;
};

function Child(name, age) {
  Parent.call(this, name); // 第二次调用 Parent
  this.age = age;
}
Child.prototype = new Parent(); // 第一次调用 Parent
Child.prototype.constructor = Child; // 修复 constructor 指向

const c1 = new Child('张三', 20);
console.log(c1.getName()); // '张三' ✅
c1.colors.push('blue');
const c2 = new Child('李四', 25);
console.log(c2.colors); // ['red', 'green'] ✅

// 缺点:Parent 构造函数被调用了两次
// Child.prototype 上有多余的 name 和 colors 属性

4. 原型式继承

javascript
// Object.create 的原理
function objectCreate(proto) {
  function F() {}
  F.prototype = proto;
  return new F();
}

const parent = {
  name: 'parent',
  colors: ['red', 'green'],
  getName() { return this.name; }
};

const child = Object.create(parent);
child.name = 'child';

console.log(child.getName()); // 'child'
console.log(child.__proto__ === parent); // true

// 缺点:与原型链继承一样,引用类型属性共享

5. 寄生式继承

javascript
function createChild(parent) {
  const child = Object.create(parent);
  // 增强对象
  child.sayHi = function() {
    return `Hi, I'm ${this.name}`;
  };
  return child;
}

const parent = { name: 'parent' };
const child = createChild(parent);
child.name = 'child';
console.log(child.sayHi()); // "Hi, I'm child"

// 缺点:方法无法复用(每次都创建新函数)

6. 寄生组合继承(最优方案)

javascript
function inheritPrototype(Child, Parent) {
  // 创建父类原型的副本
  const prototype = Object.create(Parent.prototype);
  prototype.constructor = Child;
  Child.prototype = prototype;
}

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'green'];
}
Parent.prototype.getName = function() {
  return this.name;
};

function Child(name, age) {
  Parent.call(this, name); // 只调用一次 Parent
  this.age = age;
}

inheritPrototype(Child, Parent);

Child.prototype.getAge = function() {
  return this.age;
};

const c1 = new Child('张三', 20);
console.log(c1.getName()); // '张三' ✅
console.log(c1.getAge());  // 20 ✅
c1.colors.push('blue');
const c2 = new Child('李四', 25);
console.log(c2.colors); // ['red', 'green'] ✅
console.log(c1 instanceof Parent); // true ✅
console.log(c1 instanceof Child);  // true ✅

继承方式对比

方式优点缺点
原型链继承简单,能继承原型方法引用类型共享,不能传参
构造函数继承独立引用类型,可传参不能继承原型方法
组合继承综合以上两者优点父类构造函数调用两次
原型式继承无需构造函数引用类型共享
寄生式继承可增强对象方法无法复用
寄生组合继承最优方案代码略复杂

ES6 class 的本质

class 是寄生组合继承的语法糖

javascript
class Animal {
  // 构造函数
  constructor(name) {
    this.name = name;
  }

  // 原型方法(定义在 Animal.prototype 上)
  speak() {
    return `${this.name} makes a sound`;
  }

  // 静态方法(定义在 Animal 自身上)
  static create(name) {
    return new Animal(name);
  }

  // Getter/Setter
  get info() {
    return `Animal: ${this.name}`;
  }
}

// 等价的 ES5 代码:
function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function() {
  return `${this.name} makes a sound`;
};
Animal.create = function(name) {
  return new Animal(name);
};
Object.defineProperty(Animal.prototype, 'info', {
  get() { return `Animal: ${this.name}`; }
});

// 验证 class 的本质
console.log(typeof Animal); // 'function'
console.log(Animal.prototype.speak); // [Function: speak]

class 继承(extends + super)

javascript
class Dog extends Animal {
  #breed; // 私有字段(ES2022)

  constructor(name, breed) {
    super(name); // 必须在使用 this 之前调用 super
    this.#breed = breed;
  }

  speak() {
    return `${this.name} barks!`; // 覆盖父类方法
  }

  parentSpeak() {
    return super.speak(); // 调用父类方法
  }

  get breedInfo() {
    return this.#breed;
  }
}

const dog = new Dog('Buddy', 'Labrador');
console.log(dog.speak());       // 'Buddy barks!'
console.log(dog.parentSpeak()); // 'Buddy makes a sound'
console.log(dog.breedInfo);     // 'Labrador'
console.log(dog instanceof Dog);    // true
console.log(dog instanceof Animal); // true

// 底层实现相当于:
// Dog.prototype = Object.create(Animal.prototype)
// Dog.prototype.constructor = Dog
// + Object.setPrototypeOf(Dog, Animal)(继承静态方法)

class 与 function 的重要区别

javascript
// 1. class 不会提升(有暂时性死区)
const a = new A(); // ❌ ReferenceError
class A {}

const b = new B(); // ✅ 函数声明会提升
function B() {}

// 2. class 必须用 new 调用
class C {}
C(); // ❌ TypeError: Class constructor C cannot be invoked without 'new'

function D() {}
D(); // ✅ 可以直接调用(虽然不推荐)

// 3. class 内部默认严格模式
class E {
  method() {
    // 'use strict' 自动启用
    console.log(this); // 不会是 window/global
  }
}

// 4. class 的方法不可枚举
class F {
  method() {}
}
console.log(Object.keys(F.prototype)); // [](method 不可枚举)

function G() {}
G.prototype.method = function() {};
console.log(Object.keys(G.prototype)); // ['method'](可枚举)

私有字段与方法(ES2022)

javascript
class BankAccount {
  // 私有字段(# 开头)
  #balance = 0;
  #owner;

  // 私有静态字段
  static #bankName = 'MyBank';

  constructor(owner, initialBalance) {
    this.#owner = owner;
    this.#balance = initialBalance;
  }

  // 私有方法
  #validate(amount) {
    if (amount <= 0) throw new Error('金额必须大于0');
    if (amount > this.#balance) throw new Error('余额不足');
  }

  withdraw(amount) {
    this.#validate(amount);
    this.#balance -= amount;
    return this.#balance;
  }

  deposit(amount) {
    if (amount <= 0) throw new Error('金额必须大于0');
    this.#balance += amount;
    return this.#balance;
  }

  get balance() {
    return this.#balance;
  }

  // 静态方法访问私有静态字段
  static getBankName() {
    return BankAccount.#bankName;
  }

  // 私有字段存在性检查(ES2022)
  static isBankAccount(obj) {
    return #balance in obj;
  }
}

const account = new BankAccount('张三', 1000);
console.log(account.balance); // 1000
account.withdraw(200);
console.log(account.balance); // 800

// account.#balance; // ❌ SyntaxError: Private field
console.log(BankAccount.isBankAccount(account)); // true
console.log(BankAccount.isBankAccount({}));      // false

Object.create 深入

javascript
// Object.create(proto, propertiesObject)
// 创建一个新对象,以 proto 为原型

// 创建纯净对象(无原型)
const dict = Object.create(null);
console.log(dict.toString); // undefined(没有原型链)
// 适合做纯字典/映射,避免原型污染

// 带属性描述符
const obj = Object.create(Object.prototype, {
  name: {
    value: '张三',
    writable: true,
    enumerable: true,
    configurable: true
  },
  age: {
    get() { return this._age; },
    set(val) { this._age = val > 0 ? val : 0; },
    enumerable: true,
    configurable: true
  }
});

// 多级原型链
const base = { type: 'base' };
const mid = Object.create(base);
mid.level = 'mid';
const top = Object.create(mid);
top.name = 'top';

console.log(top.name);  // 'top'(自身)
console.log(top.level); // 'mid'(来自 mid)
console.log(top.type);  // 'base'(来自 base)

原型相关的所有 API

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

// 获取原型
Object.getPrototypeOf(child);     // obj(推荐用法)
child.__proto__;                   // obj(不推荐,但广泛支持)
Reflect.getPrototypeOf(child);    // obj

// 设置原型(⚠️ 性能极差,不推荐在运行时使用)
Object.setPrototypeOf(child, null);
Reflect.setPrototypeOf(child, obj);

// 检查原型关系
obj.isPrototypeOf(child);          // true
child instanceof Object;           // true(沿原型链查找)

// 检查自身属性
child.hasOwnProperty('a');         // false
Object.hasOwn(child, 'a');         // false(ES2022,推荐)

// 获取自身属性
Object.keys(child);                // [](可枚举)
Object.getOwnPropertyNames(child); // [](包括不可枚举)
Object.getOwnPropertySymbols(child); // [](Symbol属性)

// for...in 遍历自身 + 原型链上所有可枚举属性
for (const key in child) {
  console.log(key, Object.hasOwn(child, key));
}
// 'a' false(来自原型)

// 属性描述符
Object.getOwnPropertyDescriptor(obj, 'a');
// { value: 1, writable: true, enumerable: true, configurable: true }

Object.defineProperty(obj, 'b', {
  value: 2,
  writable: false,    // 不可修改
  enumerable: false,  // 不可枚举
  configurable: false // 不可删除/重新定义
});

// 冻结与密封
Object.freeze(obj);     // 不可增删改
Object.seal(obj);       // 不可增删,可改
Object.preventExtensions(obj); // 不可增,可删可改

Object.isFrozen(obj);
Object.isSealed(obj);
Object.isExtensible(obj);

原型污染攻击

javascript
// 原型污染:修改 Object.prototype 影响所有对象
// ⚠️ 这是一种安全漏洞,常出现在深合并(deepMerge)中

// 不安全的深合并
function unsafeMerge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object' && source[key] !== null) {
      if (!target[key]) target[key] = {};
      unsafeMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

// 攻击:通过 __proto__ 注入
const malicious = JSON.parse('{"__proto__":{"isAdmin":true}}');
unsafeMerge({}, malicious);
console.log({}.isAdmin); // true ❌ 所有对象都被污染了!

// 防御方案
function safeMerge(target, source) {
  for (const key of Object.keys(source)) { // 只遍历自身属性
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue; // 跳过危险属性
    }
    if (typeof source[key] === 'object' && source[key] !== null) {
      target[key] = target[key] || {};
      safeMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

// 或使用 Object.create(null) 创建无原型对象
const safeDict = Object.create(null);

Mixin 模式(多继承替代方案)

JavaScript 不支持多继承,但可以用 Mixin 实现:

javascript
// Mixin 函数
const Serializable = (Base) => class extends Base {
  serialize() {
    return JSON.stringify(this);
  }

  static deserialize(json) {
    return Object.assign(new this(), JSON.parse(json));
  }
};

const Validatable = (Base) => class extends Base {
  validate() {
    for (const [key, rules] of Object.entries(this.constructor.rules || {})) {
      for (const rule of rules) {
        if (!rule.check(this[key])) {
          throw new Error(`${key}: ${rule.message}`);
        }
      }
    }
    return true;
  }
};

const EventEmitting = (Base) => class extends Base {
  #listeners = {};

  on(event, fn) {
    (this.#listeners[event] ??= []).push(fn);
    return this;
  }

  emit(event, ...args) {
    (this.#listeners[event] || []).forEach(fn => fn(...args));
    return this;
  }
};

// 组合使用多个 Mixin
class User extends EventEmitting(Validatable(Serializable(class {}))) {
  static rules = {
    name: [{ check: v => v?.length > 0, message: '名字不能为空' }],
    age: [{ check: v => v > 0 && v < 150, message: '年龄不合法' }]
  };

  constructor(name, age) {
    super();
    this.name = name;
    this.age = age;
  }
}

const user = new User('张三', 25);
user.validate();                        // ✅
console.log(user.serialize());          // '{"name":"张三","age":25}'
user.on('login', () => console.log('已登录'));
user.emit('login');                      // '已登录'

面试高频问题

1. Object.create(null) vs {} 的区别?

javascript
const obj1 = {};
const obj2 = Object.create(null);

console.log(obj1.__proto__);  // Object.prototype
console.log(obj2.__proto__);  // undefined

console.log('toString' in obj1); // true
console.log('toString' in obj2); // false

// Object.create(null) 适用于纯数据字典
// 避免原型属性干扰(如 toString, hasOwnProperty 等)

2. 如何判断属性来自自身还是原型链?

javascript
const parent = { inherited: true };
const child = Object.create(parent);
child.own = true;

// 方法1: hasOwnProperty(可能被覆盖)
child.hasOwnProperty('own');       // true
child.hasOwnProperty('inherited'); // false

// 方法2: Object.hasOwn(ES2022,推荐)
Object.hasOwn(child, 'own');       // true
Object.hasOwn(child, 'inherited'); // false

// 方法3: in 操作符(包括原型链)
'own' in child;       // true
'inherited' in child; // true

3. __proto__ vs prototype 一句话总结?

javascript
// __proto__ 是每个对象都有的,指向它的原型(创建它的构造函数的 prototype)
// prototype 是函数才有的,指向该函数作为构造函数时实例的原型

// 关系:obj.__proto__ === Constructor.prototype

总结

原型链核心知识点:
┌──────────────────────────────────────────────────────────┐
│ 1. 每个对象都有 __proto__,指向其构造函数的 prototype       │
│ 2. 原型链终点:Object.prototype.__proto__ === null        │
│ 3. 属性查找沿原型链向上,找到即停止(遮蔽效应)              │
│ 4. new 做了什么:创建对象 → 链接原型 → 绑定this → 返回     │
│ 5. 寄生组合继承 = ES5 最优方案 = class extends 的底层实现   │
│ 6. class 是语法糖,typeof class === 'function'           │
│ 7. 私有字段 # 是真正的封装(不在原型链上)                   │
│ 8. Object.create(null) 创建无原型的纯净对象               │
│ 9. 原型污染是安全漏洞,深合并时要防御                       │
└──────────────────────────────────────────────────────────┘