深入理解原型链与继承
从
__proto__到class,彻底理清 JavaScript 对象系统的来龙去脉
什么是原型链?
定义:原型链(Prototype Chain)是 JavaScript 实现对象继承的核心机制。每个对象都有一个内部链接 [[Prototype]] 指向另一个对象(称为"原型"),原型自身也有原型,如此层层链接形成一条"链",直到 null 为止。当访问对象的属性时,引擎会沿着这条链逐级查找。
涉及场景:
- 方法共享:多个实例共享构造函数
prototype上的方法,节省内存 - 类继承:
class extends底层就是原型链的语法糖 - 类型判断:
instanceof运算符沿原型链检查构造函数的prototype - 框架源码:React 类组件、Vue2 选项式 API 都基于原型继承
- Polyfill:在
Array.prototype等原生原型上扩展方法
作用:
- 代码复用:子类自动获得父类的所有方法和属性
- 内存高效:方法定义在原型上,所有实例共享同一份,不重复创建
- 动态扩展:运行时可以修改原型,所有实例立即生效
- 理解 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); // 2instanceof 的原理
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({})); // falseObject.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; // true3. __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. 原型污染是安全漏洞,深合并时要防御 │
└──────────────────────────────────────────────────────────┘