原型与继承

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__ → … → nullundefined
  • 并发模型:不适用(单线程)

原型链查找的完整图解

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() 时的查找过程:

  1. 查找 dog 自身 → 无 toString
  2. 查找 Dog.prototype → 无 toString
  3. 查找 Animal.prototype → 无 toString
  4. 查找 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

参数说明:

参数类型说明
protoObject / null新对象的原型,必须为对象或 null
propertiesObjectObject(可选)属性描述符对象,同 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 上的任何属性和方法
  • 没有 toStringhasOwnPropertyvalueOf 等方法
  • 不会受到 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 构造函数的关键差异:

维度classES5 构造函数
是否提升不提升(TDZ)声明提升
不用 new 调用抛出 TypeError作为普通函数执行
方法可枚举性不可枚举(原型方法)手动添加的可枚举
严格模式默认严格模式需显式 "use strict"
内部 [[IsClassConstructor]]truefalse

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 的关系

toStringvalueOf 都定义在 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.hasInstanceObject.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__constructorprototype
  • 私有状态使用 #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 上的任何方法(toStringhasOwnProperty 等),也不会被 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 支持 superstatic#private 等语法级特性。但本质上 class 仍然是基于原型的,class Foo {} 中的方法仍然定义在 Foo.prototype 上。

Q7: 什么是原型污染攻击?如何防范?

原型污染是通过修改 Object.prototype(通常通过 __proto__ 键),向所有对象注入恶意属性的攻击。常见场景:不安全的深度合并/克隆函数将用户输入中的 __proto__ 作为普通键处理,导致 Object.prototype 被篡改。防范措施:1) 深度合并时过滤 __proto__constructorprototype 键;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 说明是继承的或不存在。


相关链接: