原型与继承
What — 是什么
JavaScript 使用原型链(Prototype Chain)实现继承,每个对象都有一个
[[Prototype]]内部链接指向其原型对象,属性查找沿原型链向上直到null。
核心概念:
- prototype:函数的属性,指向原型对象(用于实例继承)
__proto__:对象的属性,指向其构造函数的prototype(非标准,推荐Object.getPrototypeOf())- 原型链:
obj.__proto__ → Constructor.prototype.__proto__ → Object.prototype.__proto__ → null class:ES6 语法糖,本质仍是原型继承
关键特性:
- 属性查找沿原型链向上,找到即停
- 原型对象被所有实例共享
instanceof检查原型链上是否存在指定构造函数的prototype
运行机制:
- 内存模型:原型对象在堆上,多个实例共享同一个原型
- 执行模型:属性查找 → 自身 →
__proto__→ … →null→undefined - 并发模型:不适用(单线程)
原型链查找的完整图解
以 class Dog extends Animal 为例,完整的原型链链路如下:
实例 dog
│
├── dog 自身属性(如 name)
│
├── dog.__proto__ === Dog.prototype
│ ├── Dog.prototype 上的方法(如 bark())
│ ├── Dog.prototype.constructor === Dog
│ │
│ ├── Dog.prototype.__proto__ === Animal.prototype
│ │ ├── Animal.prototype 上的方法(如 speak())
│ │ ├── Animal.prototype.constructor === Animal
│ │ │
│ │ ├── Animal.prototype.__proto__ === Object.prototype
│ │ │ ├── Object.prototype 上的方法(toString / valueOf / hasOwnProperty ...)
│ │ │ ├── Object.prototype.constructor === Object
│ │ │ │
│ │ │ └── Object.prototype.__proto__ === null ← 链的终点
当访问 dog.toString() 时的查找过程:
- 查找
dog自身 → 无toString - 查找
Dog.prototype→ 无toString - 查找
Animal.prototype→ 无toString - 查找
Object.prototype→ 找到toString,调用
构造函数本身的原型链:
Dog.__proto__ === Animal // 函数也是对象,Dog 继承自 Animal
Animal.__proto__ === Function.prototype
Function.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
Object.create() 详解
Object.create(proto, propertiesObject) 创建一个新对象,并将其 [[Prototype]] 指向 proto。
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
proto | Object / null | 新对象的原型,必须为对象或 null |
propertiesObject | Object(可选) | 属性描述符对象,同 Object.defineProperties 第二个参数 |
基本原理:
// Object.create 的简化实现
function myCreate(proto, propertiesObject) {
if (proto !== null && typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object or null');
}
function F() {}
F.prototype = proto;
const obj = new F();
if (propertiesObject !== undefined) {
Object.defineProperties(obj, propertiesObject);
}
return obj;
}
常见用法:
// 1. 创建以指定对象为原型的对象
const parent = { name: 'parent', greet() { return `I am ${this.name}`; } };
const child = Object.create(parent);
child.name = 'child';
child.greet(); // "I am child"(this 指向 child)
// 2. 创建纯净对象(无任何原型)
const pure = Object.create(null);
pure.name = 'clean';
pure.hasOwnProperty; // undefined(没有继承任何方法)
// 3. 使用属性描述符
const obj = Object.create(Object.prototype, {
name: {
value: 'Alice',
writable: true,
enumerable: true,
configurable: true,
},
age: {
value: 25,
writable: false,
enumerable: false,
},
});
Object.create(null) 创建纯净对象
Object.create(null) 创建的对象没有 [[Prototype]],即 __proto__ 为 null,因此:
- 不继承
Object.prototype上的任何属性和方法 - 没有
toString、hasOwnProperty、valueOf等方法 - 不会受到
Object.prototype扩展的影响 - 适合用作干净的字典/映射对象
const dict = Object.create(null);
dict['key'] = 'value';
// 不会受到原型污染
Object.prototype.evil = 'hacked';
dict.evil; // undefined(纯净对象不受影响)
// 普通对象会受影响
const normalObj = {};
normalObj.evil; // "hacked"
// 典型用途:Vue 源码中的数据字典
// 用于存储内部状态,避免与 Object.prototype 上的属性冲突
ES6 class 的底层机制
ES6 的 class 是原型继承的语法糖,但做了更多规范化处理。
constructor:
class Person {
constructor(name) {
// 等价于 ES5 构造函数体
this.name = name;
}
// 等价于 Person.prototype.sayHi = function() {...}
sayHi() {
return `Hi, I'm ${this.name}`;
}
}
// 底层等价关系
typeof Person; // "function"
Person === Person.prototype.constructor; // true
Person.prototype.sayHi; // [Function: sayHi]
class 与 ES5 构造函数的关键差异:
| 维度 | class | ES5 构造函数 |
|---|---|---|
| 是否提升 | 不提升(TDZ) | 声明提升 |
| 不用 new 调用 | 抛出 TypeError | 作为普通函数执行 |
| 方法可枚举性 | 不可枚举(原型方法) | 手动添加的可枚举 |
| 严格模式 | 默认严格模式 | 需显式 "use strict" |
内部 [[IsClassConstructor]] | true | false |
super 的两种用法:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a noise`;
}
static create(name) {
return new this(name);
}
}
class Dog extends Animal {
constructor(name, breed) {
// 用法一:super 作为函数调用(必须在 constructor 中先调用 super)
// 等价于 Animal.call(this, name)
super(name);
this.breed = breed;
}
speak() {
// 用法二:super 作为对象调用父类方法
// super.speak 指向 Animal.prototype.speak,但 this 指向当前实例
return super.speak() + `, breed: ${this.breed}`;
}
static create(name, breed) {
// super 也可在静态方法中使用,指向父类
const dog = super.create(name);
dog.breed = breed;
return dog;
}
}
const dog = new Dog('Rex', 'Shepherd');
dog.speak(); // "Rex makes a noise, breed: Shepherd"
Dog.create('Buddy', 'Lab'); // Dog { name: 'Buddy', breed: 'Lab' }
static 静态方法和属性:
class MathUtils {
// 静态方法:在类上调用,不在实例上调用
static add(a, b) { return a + b; }
static multiply(a, b) { return a * b; }
// ES2022 静态公共字段
static PI = 3.14159;
// 静态私有字段
static #secretKey = 'abc123';
static getKey() {
return MathUtils.#secretKey;
}
}
MathUtils.add(1, 2); // 3
MathUtils.PI; // 3.14159
MathUtils.getKey(); // "abc123"
const mu = new MathUtils();
mu.add; // undefined(实例上没有静态方法)
// 静态方法也通过原型链继承
class AdvancedMath extends MathUtils {}
AdvancedMath.add(1, 2); // 3(继承了静态方法)
// 静态方法的原型链:AdvancedMath.__proto__ === MathUtils
私有字段 #field 和私有方法:
class BankAccount {
// 私有字段:只能在类内部访问
#balance = 0;
#owner;
constructor(owner, initialBalance) {
this.#owner = owner;
this.#balance = initialBalance;
}
// 私有方法
#validate(amount) {
if (amount <= 0) throw new Error('Amount must be positive');
return true;
}
// 公共方法访问私有成员
deposit(amount) {
this.#validate(amount);
this.#balance += amount;
return this;
}
withdraw(amount) {
this.#validate(amount);
if (amount > this.#balance) throw new Error('Insufficient funds');
this.#balance -= amount;
return this;
}
get balance() {
return this.#balance;
}
// 静态私有字段
static #bankCode = 'CN001';
static getBankCode() {
return BankAccount.#bankCode;
}
}
const account = new BankAccount('Alice', 1000);
account.deposit(500);
account.balance; // 1500
account.#balance; // SyntaxError: Private field '#balance' must be declared
account.#validate(100); // SyntaxError: Private method '#validate' must be declared
// 私有字段 vs 闭包模拟私有:
// - #private 是语言级支持,更强安全
// - #private 子类无法访问
// - #private 不能通过 Object.keys / for...in 枚举
new.target 元属性
new.target 用于检测函数是否通过 new 调用,在普通函数调用时为 undefined,在 new 调用时指向被调用的构造函数。
function Person(name) {
// 防止不使用 new 直接调用
if (!new.target) {
throw new Error('Person() must be called with new');
}
this.name = name;
}
Person('Alice'); // Error: Person() must be called with new
new Person('Alice'); // 正常
// 在 class 的 constructor 中
class Shape {
constructor() {
// new.target 在继承中指向最终被 new 的类
console.log(new.target.name);
}
}
class Circle extends Shape {}
new Shape(); // "Shape"
new Circle(); // "Circle"(不是 "Shape")
// 实际应用:抽象类
class AbstractWidget {
constructor() {
if (new.target === AbstractWidget) {
throw new Error('AbstractWidget cannot be instantiated directly');
}
}
render() {
throw new Error('render() must be implemented');
}
}
class Button extends AbstractWidget {
render() { return '<button>'; }
}
new AbstractWidget(); // Error: cannot be instantiated directly
new Button(); // OK
原型链与 toString / valueOf 的关系
toString 和 valueOf 都定义在 Object.prototype 上,所有对象默认继承它们。不同内置类型重写了这些方法:
// Object.prototype 上的默认实现
const obj = {};
obj.toString(); // "[object Object]"
obj.valueOf(); // {} (返回对象自身)
// 各内置类型的重写
const num = 42;
num.toString(); // "42" → Number.prototype.toString
num.valueOf(); // 42 → Number.prototype.valueOf
const str = 'hello';
str.toString(); // "hello" → String.prototype.toString
str.valueOf(); // "hello" → String.prototype.valueOf
const arr = [1, 2, 3];
arr.toString(); // "1,2,3" → Array.prototype.toString
arr.valueOf(); // [1, 2, 3] → Array.prototype.valueOf(返回自身)
const fn = function() {};
fn.toString(); // "function() {}" → Function.prototype.toString
// 类型检测的经典方法
Object.prototype.toString.call(42); // "[object Number]"
Object.prototype.toString.call('hi'); // "[object String]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call([1,2]); // "[object Array]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call(/re/); // "[object RegExp]"
// 隐式转换调用顺序:
// 1. 优先调用 valueOf(),若返回原始值则使用
// 2. 否则调用 toString()
// 3. 若均不返回原始值,抛出 TypeError
const custom = {
valueOf() { return 100; },
toString() { return 'custom'; },
};
custom + 1; // 101(valueOf 优先)
String(custom); // "custom"(字符串转换优先 toString)
Why — 为什么
适用场景:
- 方法共享(避免每个实例复制一份方法)
- 实现类式继承
- 内置对象的扩展(如
Array.prototype.myMethod)
对比其他语言:
| 维度 | JS 原型继承 | Java 类继承 | Go 组合 |
|---|---|---|---|
| 性能 | 共享原型,内存高效 | 虚方法表查找 | 编译时确定 |
| 生态 | 语言核心机制 | 语言核心机制 | 语言核心机制 |
| 上手难度 | 高(概念抽象) | 中 | 低 |
| 灵活性 | 极高(动态修改原型) | 低(编译时固定) | 中 |
优缺点:
- ✅ 优点:
- 方法共享,节省内存
- 动态性强,运行时修改原型
- 灵活的多重继承(mixin)
- ❌ 缺点:
- 概念不直观,学习曲线陡
- 原型链过长影响性能
- 修改原型影响所有实例(包括已创建的)
5 种继承方式完整对比
| 继承方式 | 核心思路 | 能否继承原型方法 | 能否继承实例属性 | 引用类型共享 | 调用父构造函数次数 |
|---|---|---|---|---|---|
| 原型链继承 | Sub.prototype = new Parent() | ✅ | ❌ | ⚠️ 共享 | 1次 |
| 构造函数继承 | Parent.call(this) | ❌ | ✅ | ✅ 独立 | 1次 |
| 组合继承 | 原型链 + 构造函数 | ✅ | ✅ | ✅ 独立 | 2次 |
| 原型式继承 | Object.create(parent) | ✅ | ⚠️ 浅拷贝 | ⚠️ 共享 | 0次 |
| 寄生组合继承 | 原型式 + 构造函数 | ✅ | ✅ | ✅ 独立 | 1次 |
1. 原型链继承
原理: 将子类原型指向父类实例
function Animal(name) {
this.name = name;
this.colors = ['white', 'black']; // 引用类型属性
}
Animal.prototype.speak = function() {
return `${this.name} makes a noise`;
};
function Dog(name) {
this.name = name;
}
Dog.prototype = new Animal(); // 原型链继承
Dog.prototype.constructor = Dog;
const dog1 = new Dog('Rex');
const dog2 = new Dog('Buddy');
dog1.colors.push('brown');
dog2.colors; // ['white', 'black', 'brown'] — 共享了引用类型!
- ✅ 优点:子类能访问父类原型上的方法
- ❌ 缺点:引用类型属性被所有实例共享;创建子类实例时无法向父构造函数传参
2. 构造函数继承(借用构造函数)
原理: 在子构造函数中调用父构造函数
function Animal(name) {
this.name = name;
this.colors = ['white', 'black'];
}
Animal.prototype.speak = function() {
return `${this.name} makes a noise`;
};
function Dog(name, breed) {
Animal.call(this, name); // 借用构造函数
this.breed = breed;
}
const dog1 = new Dog('Rex', 'Shepherd');
const dog2 = new Dog('Buddy', 'Lab');
dog1.colors.push('brown');
dog2.colors; // ['white', 'black'] — 独立的!
dog1.speak(); // TypeError: dog1.speak is not a function
- ✅ 优点:引用类型独立;可以向父构造函数传参
- ❌ 缺点:无法继承父类原型上的方法;方法在每个实例中重复创建
3. 组合继承
原理: 原型链继承方法 + 构造函数继承属性
function Animal(name) {
this.name = name;
this.colors = ['white', 'black'];
console.log('Animal called'); // 会被调用两次
}
Animal.prototype.speak = function() {
return `${this.name} makes a noise`;
};
function Dog(name, breed) {
Animal.call(this, name); // 第二次调用 Animal
this.breed = breed;
}
Dog.prototype = new Animal(); // 第一次调用 Animal
Dog.prototype.constructor = Dog;
const dog = new Dog('Rex', 'Shepherd');
dog.speak(); // "Rex makes a noise"
dog.colors; // ['white', 'black']
- ✅ 优点:既能继承原型方法,引用类型也独立
- ❌ 缺点:父构造函数被调用了两次,子类原型上有冗余属性
4. 原型式继承
原理: 基于已有对象创建新对象(Object.create 的本质)
function createObject(proto) {
function F() {}
F.prototype = proto;
return new F();
}
const parent = {
name: 'parent',
colors: ['white', 'black'],
greet() { return `I am ${this.name}`; },
};
const child1 = createObject(parent);
child1.name = 'child1';
child1.colors.push('brown');
const child2 = createObject(parent);
child2.colors; // ['white', 'black', 'brown'] — 共享引用类型!
- ✅ 优点:简单,不需要自定义构造函数
- ❌ 缺点:引用类型共享;无法给父对象传参
5. 寄生组合继承(最优方案)
原理: 构造函数继承属性 + 原型式继承方法(只复制父原型,不调用父构造函数)
function inheritPrototype(SubType, SuperType) {
// 以父类原型为原型创建新对象(不调用父构造函数)
const prototype = Object.create(SuperType.prototype);
prototype.constructor = SubType;
SubType.prototype = prototype;
}
function Animal(name) {
this.name = name;
this.colors = ['white', 'black'];
}
Animal.prototype.speak = function() {
return `${this.name} makes a noise`;
};
function Dog(name, breed) {
Animal.call(this, name); // 只调用一次父构造函数
this.breed = breed;
}
inheritPrototype(Dog, Animal);
Dog.prototype.bark = function() {
return `${this.name} barks`;
};
const dog = new Dog('Rex', 'Shepherd');
dog.speak(); // "Rex makes a noise"
dog.bark(); // "Rex barks"
dog.colors; // ['white', 'black']
- ✅ 优点:只调用一次父构造函数;原型链保持完整;没有冗余属性
- ❌ 缺点:实现稍复杂
ES6 的
class extends底层就是寄生组合继承,是官方推荐的最优方案。
How — 怎么用
快速上手
// ES6 class 语法(底层仍是原型)
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a noise`;
}
}
class Dog extends Animal {
speak() {
return `${this.name} barks`;
}
}
const dog = new Dog("Rex");
dog.speak(); // "Rex barks"
// 原型链验证
dog instanceof Dog; // true
dog instanceof Animal; // true
Object.getPrototypeOf(dog) === Dog.prototype; // true
代码示例
示例 1:5 种继承方式的完整代码实现
// ========== 1. 原型链继承 ==========
function Animal1(name) {
this.name = name || 'animal';
this.colors = ['white'];
}
Animal1.prototype.speak = function() {
return `${this.name} speaks`;
};
function Dog1(name) { this.name = name || 'dog'; }
Dog1.prototype = new Animal1();
Dog1.prototype.constructor = Dog1;
// ========== 2. 构造函数继承 ==========
function Animal2(name) {
this.name = name;
this.colors = ['white'];
}
Animal2.prototype.speak = function() {
return `${this.name} speaks`;
};
function Dog2(name) {
Animal2.call(this, name);
}
// Dog2 实例无法访问 Animal2.prototype 上的方法
// ========== 3. 组合继承 ==========
function Animal3(name) {
this.name = name;
this.colors = ['white'];
}
Animal3.prototype.speak = function() {
return `${this.name} speaks`;
};
function Dog3(name, breed) {
Animal3.call(this, name); // 第二次调用
this.breed = breed;
}
Dog3.prototype = new Animal3(); // 第一次调用
Dog3.prototype.constructor = Dog3;
// ========== 4. 原型式继承 ==========
const animal4 = {
name: 'animal',
colors: ['white'],
speak() { return `${this.name} speaks`; },
};
const dog4 = Object.create(animal4);
dog4.name = 'dog';
// ========== 5. 寄生组合继承 ==========
function Animal5(name) {
this.name = name;
this.colors = ['white'];
}
Animal5.prototype.speak = function() {
return `${this.name} speaks`;
};
function Dog5(name, breed) {
Animal5.call(this, name); // 只调用一次
this.breed = breed;
}
// 寄生组合继承的核心
Dog5.prototype = Object.create(Animal5.prototype);
Dog5.prototype.constructor = Dog5;
Dog5.prototype.bark = function() {
return `${this.name} barks`;
};
示例 2:Object.create() 多种用法
// 用法 1:实现纯原型继承
const base = {
type: 'base',
init(options) {
Object.assign(this, options);
return this;
},
greet() {
return `I am a ${this.type}`;
},
};
const child = Object.create(base).init({ type: 'child', name: 'Alice' });
child.greet(); // "I am a child"
// 用法 2:创建纯净字典对象
const dict = Object.create(null);
dict.foo = 'bar';
dict.toString; // undefined
'foo' in dict; // true
Object.keys(dict); // ['foo']
// 用法 3:使用属性描述符精细控制
const config = Object.create(null, {
host: {
value: 'localhost',
enumerable: true,
writable: true,
},
port: {
value: 3000,
enumerable: true,
writable: false,
},
_secret: {
value: 'key123',
enumerable: false,
},
});
// 用法 4:实现对象间的委托
const validator = {
validate() {
return this.rules.every(rule => rule(this.value));
},
};
const emailValidator = Object.create(validator);
emailValidator.value = 'test@example.com';
emailValidator.rules = [
v => typeof v === 'string',
v => v.includes('@'),
];
emailValidator.validate(); // true
示例 3:class extends 完整示例(含 super、static、#private)
class EventEmitter {
#listeners = new Map(); // 私有字段
on(event, handler) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, []);
}
this.#listeners.get(event).push(handler);
return this;
}
emit(event, ...args) {
const handlers = this.#listeners.get(event);
if (handlers) {
handlers.forEach(h => h(...args));
}
return this;
}
// 静态方法:合并多个 EventEmitter
static merge(...emitters) {
const merged = new EventEmitter();
emitters.forEach(e => {
e.#listeners.forEach((handlers, event) => {
handlers.forEach(h => merged.on(event, h));
});
});
return merged;
}
}
class Model extends EventEmitter {
#data = {}; // 私有字段
#dirty = false; // 私有字段
static #validators = {}; // 静态私有字段
// 注册验证器
static registerValidator(field, fn) {
Model.#validators[field] = fn;
}
constructor(initialData = {}) {
super(); // 必须在访问 this 之前调用 super()
Object.assign(this.#data, initialData);
}
get(key) {
return this.#data[key];
}
set(key, value) {
const validator = Model.#validators[key];
if (validator && !validator(value)) {
throw new Error(`Invalid value for ${key}: ${value}`);
}
const oldValue = this.#data[key];
this.#data[key] = value;
this.#dirty = true;
this.emit('change', { key, oldValue, newValue: value });
return this;
}
get isDirty() {
return this.#dirty;
}
// 覆写 toString
toString() {
return JSON.stringify(this.#data);
}
}
// 使用
Model.registerValidator('age', v => v >= 0 && v <= 150);
const user = new Model({ name: 'Alice', age: 25 });
user.on('change', ({ key, newValue }) => {
console.log(`${key} changed to ${newValue}`);
});
user.set('age', 30); // "age changed to 30"
user.set('age', 200); // Error: Invalid value for age: 200
user.isDirty; // true
示例 4:instanceof 手写实现
function myInstanceof(instance, Constructor) {
// 基本类型直接返回 false
if (instance === null || (typeof instance !== 'object' && typeof instance !== 'function')) {
return false;
}
// 获取构造函数的 prototype
const prototype = Constructor.prototype;
// 获取实例的原型
let proto = Object.getPrototypeOf(instance);
// 沿原型链向上查找
while (proto !== null) {
if (proto === prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
// 测试
myInstanceof([], Array); // true
myInstanceof([], Object); // true
myInstanceof('hello', String); // false(基本类型)
myInstanceof(new String('hello'), String); // true
myInstanceof(null, Object); // false
// Symbol.hasInstance 自定义 instanceof 行为
class Even {
static [Symbol.hasInstance](num) {
return typeof num === 'number' && num % 2 === 0;
}
}
42 instanceof Even; // true
43 instanceof Even; // false
示例 5:new 操作符手写实现
function myNew(Constructor, ...args) {
// 步骤 1:创建空对象,原型指向构造函数的 prototype
const obj = Object.create(Constructor.prototype);
// 步骤 2:执行构造函数,绑定 this
const result = Constructor.apply(obj, args);
// 步骤 3:如果构造函数返回对象,则使用返回值;否则返回新对象
return result instanceof Object ? result : obj;
}
// 测试
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
return `Hi, I'm ${this.name}`;
};
const p = myNew(Person, 'Alice', 25);
p.name; // "Alice"
p.age; // 25
p.greet(); // "Hi, I'm Alice"
p instanceof Person; // true
// 构造函数返回对象的情况
function Factory() {
return { custom: true };
}
const f = myNew(Factory);
f.custom; // true(返回了构造函数的对象)
// 构造函数返回原始值的情况
function PrimitiveReturn() {
this.value = 1;
return 'ignored'; // 原始值被忽略
}
const pr = myNew(PrimitiveReturn);
pr.value; // 1(返回了新创建的对象)
示例 6:原型污染攻击与防护
// ===== 原型污染攻击原理 =====
// 恶意代码修改 Object.prototype
Object.prototype.isAdmin = true;
const user = { name: 'Alice' };
user.isAdmin; // true — 所有对象都被污染了!
// ===== 常见攻击场景:不安全的深度合并 =====
function vulnerableMerge(target, source) {
for (const key in source) {
// ❌ 危险:没有检查 key 是否为 __proto__
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) target[key] = {};
vulnerableMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// 攻击 payload
const payload = JSON.parse('{"__proto__":{"isAdmin":true}}');
vulnerableMerge({}, payload);
({}).isAdmin; // true — 原型被污染!
// ===== 安全的深度合并 =====
function safeMerge(target, source) {
// 防护:拒绝危险 key
const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
for (const key of Object.keys(source)) {
if (dangerousKeys.includes(key)) continue; // ✅ 跳过危险 key
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
if (!target[key]) target[key] = {};
safeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// ===== 其他防护措施 =====
// 1. 使用 Object.create(null) 创建纯净对象
const safeDict = Object.create(null);
safeDict.__proto__; // undefined(没有原型链,无法被污染)
// 2. 使用 Map 替代普通对象
const map = new Map();
map.set('__proto__', 'safe'); // Map 的 key 不会污染原型
// 3. 使用 Object.freeze(Object.prototype)
Object.freeze(Object.prototype);
Object.prototype.evil = 'hacked'; // TypeError(静默失败或抛错)
// 4. JSON.parse 后验证
function safeParse(json) {
const obj = JSON.parse(json);
function sanitize(o) {
for (const key in o) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
delete o[key];
} else if (typeof o[key] === 'object' && o[key] !== null) {
sanitize(o[key]);
}
}
}
sanitize(obj);
return obj;
}
示例 7:Object.getOwnPropertyNames vs Object.keys vs for…in
const parent = { inheritedProp: 'from parent' };
const child = Object.create(parent);
child.ownProp = 'own value';
child.enumerableProp = 'enumerable';
// 添加不可枚举属性
Object.defineProperty(child, 'hiddenProp', {
value: 'hidden',
enumerable: false,
});
// ========== for...in ==========
// 遍历自身 + 继承的【可枚举】属性
const forInKeys = [];
for (const key in child) {
forInKeys.push(key);
}
forInKeys; // ['ownProp', 'enumerableProp', 'inheritedProp']
// ========== Object.keys() ==========
// 只返回自身【可枚举】属性
Object.keys(child); // ['ownProp', 'enumerableProp']
// ========== Object.getOwnPropertyNames() ==========
// 返回自身【所有】属性(含不可枚举),不含 Symbol
Object.getOwnPropertyNames(child); // ['ownProp', 'enumerableProp', 'hiddenProp']
// ========== Object.getOwnPropertySymbols() ==========
const sym = Symbol('sym');
child[sym] = 'symbol value';
Object.getOwnPropertySymbols(child); // [Symbol(sym)]
// ========== Reflect.ownKeys() ==========
// 返回自身所有属性(可枚举 + 不可枚举 + Symbol)
Reflect.ownKeys(child); // ['ownProp', 'enumerableProp', 'hiddenProp', Symbol(sym)]
// ========== 完整对比表 ==========
// | 方法 | 自身 | 继承 | 可枚举 | 不可枚举 | Symbol |
// |------|:----:|:----:|:------:|:--------:|:------:|
// | for...in | ✅ | ✅ | ✅ | ❌ | ❌ |
// | Object.keys() | ✅ | ❌ | ✅ | ❌ | ❌ |
// | Object.getOwnPropertyNames() | ✅ | ❌ | ✅ | ✅ | ❌ |
// | Object.getOwnPropertySymbols()| ✅ | ❌ | — | — | ✅ |
// | Reflect.ownKeys() | ✅ | ❌ | ✅ | ✅ | ✅ |
// ========== 判断属性来源 ==========
child.hasOwnProperty('ownProp'); // true(自身属性)
child.hasOwnProperty('inheritedProp'); // false(继承属性)
// 更安全的方式(防止对象覆盖 hasOwnProperty)
Object.prototype.hasOwnProperty.call(child, 'ownProp'); // true
// 使用 Object.hasOwn()(ES2022)
Object.hasOwn(child, 'ownProp'); // true
Object.hasOwn(child, 'inheritedProp'); // false
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
constructor 丢失 | 重写 prototype 后 constructor 指向错误 | 手动修复 SubClass.prototype.constructor = SubClass |
| 原型上的引用类型共享 | 多个实例共享同一个数组/对象 | 引用类型放在构造函数中,不放在原型上 |
__proto__ 修改 | 直接修改 __proto__ 严重影响性能 | 使用 Object.create() 或 class extends |
instanceof 误判 | 跨 iframe/Realm 时原型链断裂 | 用 Symbol.hasInstance 或 Object.prototype.toString |
| class 中未调用 super | 子类 constructor 必须先调用 super | 在访问 this 之前调用 super() |
| 箭头函数没有 prototype | 箭头函数不能作为构造函数 | 使用普通函数或 class |
| Object.create(null) 没有 toString | 纯净对象无继承方法 | 手动添加或使用普通对象 |
| 深度合并导致原型污染 | __proto__ 键被赋值到原型 | 过滤危险 key,使用 Map 或 Object.create(null) |
最佳实践
- 优先使用
class语法,语义清晰 - 共享方法放原型,实例数据放构造函数
- 用
Object.getPrototypeOf()替代__proto__ - 避免
Object.prototype扩展(污染全局) - 使用
Object.create(null)创建字典对象,避免原型链干扰 - 深度合并时过滤
__proto__、constructor、prototype键 - 私有状态使用
#field语法而非命名约定(_field) - 使用
Object.hasOwn()(ES2022)替代hasOwnProperty
面试题
Q1: 描述原型链的查找过程,obj.a 是如何查找到值的?
先查找
obj自身属性(hasOwnProperty),若没有则沿__proto__向上查找obj.__proto__(即构造函数的prototype),再查找Object.prototype,直到null。找到即返回,到null仍未找到则返回undefined。
Q2: __proto__ 和 prototype 有什么区别?
prototype是函数的属性,指向该函数的原型对象,用于实例继承方法和属性。__proto__是对象的属性,指向其构造函数的prototype,是原型链查找的实际链接。每个对象都有__proto__,只有函数才有prototype(推荐用Object.getPrototypeOf()替代__proto__)。
Q3: JavaScript 有哪些继承实现方式?各有什么优缺点?
主要方式:原型链继承(共享引用类型属性)、构造函数继承(无法继承原型方法)、组合继承(调用两次父构造函数)、原型式继承/寄生继承、寄生组合继承(最优方案,只调用一次父构造函数)。ES6 的
class extends是语法糖,底层采用寄生组合继承。
Q4: new 操作符做了什么?手写其实现。
四步:1) 创建空对象;2) 将对象的
__proto__指向构造函数的prototype;3) 将构造函数的this绑定到新对象并执行;4) 如果构造函数返回对象则返回该对象,否则返回新对象。核心实现:const obj = Object.create(Constructor.prototype); const result = Constructor.apply(obj, args); return result instanceof Object ? result : obj;
Q5: Object.create(null) 和 {} 有什么区别?
Object.create(null)创建的对象没有[[Prototype]],不继承Object.prototype上的任何方法(toString、hasOwnProperty等),也不会被for...in遍历到继承属性。{}等价于Object.create(Object.prototype),继承了所有Object.prototype方法。Object.create(null)适合用作纯净字典/映射,避免键名与原型属性冲突(如__proto__、toString),也天然免疫原型污染攻击。
Q6: ES6 class 和 ES5 构造函数有什么本质区别?
核心区别:1) class 不存在变量提升(有 TDZ),构造函数会提升;2) class 必须用
new调用,否则抛 TypeError,构造函数可直接调用;3) class 方法默认不可枚举(enumerable: false),ES5 手动添加到 prototype 的方法可枚举;4) class 默认严格模式;5) class 内部有[[IsClassConstructor]]标记,某些场景行为不同;6) class 支持super、static、#private等语法级特性。但本质上 class 仍然是基于原型的,class Foo {}中的方法仍然定义在Foo.prototype上。
Q7: 什么是原型污染攻击?如何防范?
原型污染是通过修改
Object.prototype(通常通过__proto__键),向所有对象注入恶意属性的攻击。常见场景:不安全的深度合并/克隆函数将用户输入中的__proto__作为普通键处理,导致Object.prototype被篡改。防范措施:1) 深度合并时过滤__proto__、constructor、prototype键;2) 使用Object.create(null)创建纯净对象;3) 使用Map替代普通对象存储键值对;4)Object.freeze(Object.prototype)冻结原型;5) JSON.parse 后验证并清洗输入。
Q8: 如何判断属性是自身的还是继承的?
使用
Object.prototype.hasOwnProperty.call(obj, key)或 ES2022 的Object.hasOwn(obj, key)判断是否为自身属性。hasOwnProperty本身也可能被覆盖,所以推荐Object.hasOwn()或Object.prototype.hasOwnProperty.call()形式。结合for...in+hasOwnProperty可只遍历自身可枚举属性,或直接使用Object.keys()仅获取自身可枚举属性。Object.getOwnPropertyDescriptor(obj, key)返回描述符时说明是自身属性,返回undefined说明是继承的或不存在。
相关链接:
- 作用域与闭包
- this指向与绑定
- [[ES6+核心特性]]
- 手写实现与polyfill
- Proxy与Reflect