原型与继承
问题
JavaScript 的原型和原型链是什么?如何实现继承?
答案
JavaScript 是基于原型的语言,每个对象都有一个内部链接指向另一个对象(称为原型)。原型对象也有自己的原型,形成原型链,直到某个对象的原型为 null。
核心概念
prototype 与 __proto__
// 每个函数都有 prototype 属性
function Person(name: string) {
this.name = name;
}
// prototype 是函数的属性,指向原型对象
console.log(Person.prototype); // { constructor: Person }
// 实例的 __proto__ 指向构造函数的 prototype
const person = new Person('Alice');
console.log(person.__proto__ === Person.prototype); // true
// 原型对象的 constructor 指回构造函数
console.log(Person.prototype.constructor === Person); // true
原型链图解
属性查找机制
function Animal(name: string) {
this.name = name;
}
Animal.prototype.speak = function(): void {
console.log(`${this.name} makes a sound`);
};
const dog = new Animal('Dog');
// 属性查找顺序:
// 1. dog 自身属性 → 找到 name
console.log(dog.name); // 'Dog'
// 2. dog 自身没有 speak → 查找 dog.__proto__ (Animal.prototype) → 找到
dog.speak(); // 'Dog makes a sound'
// 3. dog 和原型链都没有 → 返回 undefined
console.log((dog as any).age); // undefined
// hasOwnProperty 检查自身属性
console.log(dog.hasOwnProperty('name')); // true
console.log(dog.hasOwnProperty('speak')); // false
继承方式
1. 原型链继承
function Parent(this: any) {
this.name = 'Parent';
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function(): void {
console.log(this.name);
};
function Child(this: any) {
// 没有调用父构造函数
}
// 子类原型指向父类实例
Child.prototype = new (Parent as any)();
const child1 = new (Child as any)();
const child2 = new (Child as any)();
child1.colors.push('green');
console.log(child2.colors); // ['red', 'blue', 'green'] ❌ 引用类型共享
// 缺点:
// 1. 引用类型属性被所有实例共享
// 2. 无法向父构造函数传参
2. 构造函数继承(经典继承)
function Parent(this: any, name: string) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function(): void {
console.log(this.name);
};
function Child(this: any, name: string) {
// 调用父构造函数
Parent.call(this, name);
}
const child1 = new (Child as any)('Alice');
const child2 = new (Child as any)('Bob');
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
console.log(child2.colors); // ['red', 'blue'] ✅ 不共享
// child1.sayName(); // ❌ 报错,无法继承原型方法
// 缺点:
// 1. 无法继承原型上的方法
// 2. 方法都在构造函数中定义,无法复用
3. 组合继承(最常用)
function Parent(this: any, name: string) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function(): void {
console.log(this.name);
};
function Child(this: any, name: string, age: number) {
Parent.call(this, name); // 第二次调用父构造函数
this.age = age;
}
Child.prototype = new (Parent as any)(); // 第一次调用父构造函数
Child.prototype.constructor = Child;
const child = new (Child as any)('Alice', 18);
child.sayName(); // 'Alice' ✅
console.log(child.colors); // ['red', 'blue'] ✅
// 缺点:父构造函数被调用两次
4. 原型式继承
// Object.create 的原理
function createObject<T extends object>(proto: T): T {
function F() {}
F.prototype = proto;
return new (F as any)();
}
const parent = {
name: 'Parent',
colors: ['red', 'blue'],
sayName() {
console.log(this.name);
},
};
const child = Object.create(parent);
child.name = 'Child';
// 缺点:引用类型属性仍然共享
5. 寄生式继承
function createChild(original: any): any {
const clone = Object.create(original);
clone.sayHi = function(): void {
console.log('Hi');
};
return clone;
}
const parent = { name: 'Parent' };
const child = createChild(parent);
// 缺点:方法无法复用
6. 寄生组合式继承(最佳)
function inheritPrototype(Child: Function, Parent: Function): void {
// 创建父类原型的副本
const prototype = Object.create(Parent.prototype);
// 修复 constructor
prototype.constructor = Child;
// 赋值给子类
Child.prototype = prototype;
}
function Parent(this: any, name: string) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function(): void {
console.log(this.name);
};
function Child(this: any, name: string, age: number) {
Parent.call(this, name); // 只调用一次父构造函数
this.age = age;
}
inheritPrototype(Child, Parent);
Child.prototype.sayAge = function(): void {
console.log(this.age);
};
const child = new (Child as any)('Alice', 18);
child.sayName(); // 'Alice'
child.sayAge(); // 18
// ✅ 最佳实践:
// 1. 只调用一次父构造函数
// 2. 原型链保持完整
// 3. 能够正常使用 instanceof
7. ES6 class 继承
class Parent {
name: string;
colors: string[];
constructor(name: string) {
this.name = name;
this.colors = ['red', 'blue'];
}
sayName(): void {
console.log(this.name);
}
}
class Child extends Parent {
age: number;
constructor(name: string, age: number) {
super(name); // 必须先调用 super
this.age = age;
}
sayAge(): void {
console.log(this.age);
}
}
const child = new Child('Alice', 18);
child.sayName(); // 'Alice'
child.sayAge(); // 18
// class 继承是寄生组合式继承的语法糖
console.log(child instanceof Child); // true
console.log(child instanceof Parent); // true
继承方式对比
| 继承方式 | 优点 | 缺点 |
|---|---|---|
| 原型链继承 | 简单 | 引用类型共享、无法传参 |
| 构造函数继承 | 避免共享、可传参 | 无法继承原型方法 |
| 组合继承 | 综合优点 | 父构造函数调用两次 |
| 原型式继承 | 简单 | 引用类型共享 |
| 寄生式继承 | 增强对象 | 方法无法复用 |
| 寄生组合式 | 最佳方案 | 实现稍复杂 |
| ES6 class | 语法简洁、标准化 | 需要转译(旧环境) |
原型方法
const obj = { a: 1 };
// 获取原型
Object.getPrototypeOf(obj); // 推荐
obj.__proto__; // 不推荐,非标准
// 设置原型
Object.setPrototypeOf(obj, newProto); // 性能差,避免使用
Object.create(proto); // 创建时指定原型
// 检查原型关系
obj instanceof Object; // true
Object.prototype.isPrototypeOf(obj); // true
obj.hasOwnProperty('a'); // true(自身属性)
Object.hasOwn(obj, 'a'); // true(ES2022)
'a' in obj; // true(包括原型链)
常见面试问题
Q1: prototype 和 __proto__ 的区别?
答案:
| 属性 | 所属 | 作用 |
|---|---|---|
prototype | 函数 | 指向原型对象,实例会继承这个对象 |
__proto__ | 对象 | 指向创建该对象的构造函数的 prototype |
function Foo() {}
const foo = new Foo();
// prototype 是函数的属性
console.log(Foo.prototype); // { constructor: Foo }
// __proto__ 是实例的属性,指向 prototype
console.log(foo.__proto__ === Foo.prototype); // true
// 函数也是对象,也有 __proto__
console.log(Foo.__proto__ === Function.prototype); // true
Q2: 如何判断属性是自身的还是继承的?
答案:
const obj = { a: 1 };
const child = Object.create(obj);
child.b = 2;
// hasOwnProperty - 只检查自身
console.log(child.hasOwnProperty('a')); // false
console.log(child.hasOwnProperty('b')); // true
// in - 包括原型链
console.log('a' in child); // true
console.log('b' in child); // true
// Object.hasOwn (ES2022) - 推荐
console.log(Object.hasOwn(child, 'a')); // false
console.log(Object.hasOwn(child, 'b')); // true
Q3: new 操作符做了什么?
答案:
function myNew<T>(
Constructor: new (...args: any[]) => T,
...args: any[]
): T {
// 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(this: any, name: string) {
this.name = name;
}
const p = myNew(Person as any, 'Alice');
console.log(p.name); // 'Alice'
Q4: ES6 class 和 ES5 构造函数的区别?
答案:
| 特性 | ES5 构造函数 | ES6 class |
|---|---|---|
| 调用方式 | 可以普通调用 | 必须用 new |
| 变量提升 | 函数提升 | 不提升 |
| 严格模式 | 默认非严格 | 默认严格 |
| 原型方法 | 可枚举 | 不可枚举 |
| 继承原生类 | 困难 | 可以 |
// ES5 可以普通调用
function Foo() {}
Foo(); // 不报错
// ES6 必须 new
class Bar {}
// Bar(); // TypeError: Class constructor Bar cannot be invoked without 'new'
Q5: 如何实现多继承?
答案:
JavaScript 不支持多继承,但可以用 Mixin 模式:
// Mixin 函数
type Constructor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
timestamp = Date.now();
};
}
function Activatable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
isActive = false;
activate() {
this.isActive = true;
}
};
}
// 组合多个 Mixin
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
const TimestampedActivatableUser = Timestamped(Activatable(User));
const user = new TimestampedActivatableUser('Alice');
console.log(user.name); // 'Alice'
console.log(user.timestamp); // 时间戳
user.activate();
console.log(user.isActive); // true
Q6: instanceof 的实现原理(手写 instanceof)
答案:
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。其核心原理是沿着对象的 __proto__ 链逐级向上查找,看是否能找到与构造函数 prototype 相同的引用。
function myInstanceof(obj: any, Constructor: Function): boolean {
// 基本类型直接返回 false
if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
return false;
}
// 获取构造函数的 prototype
const prototype = Constructor.prototype;
// 获取对象的原型
let proto = Object.getPrototypeOf(obj);
// 沿原型链向上查找
while (proto !== null) {
if (proto === prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
// 测试
class Animal {}
class Dog extends Animal {}
const dog = new Dog();
console.log(myInstanceof(dog, Dog)); // true
console.log(myInstanceof(dog, Animal)); // true
console.log(myInstanceof(dog, Object)); // true
console.log(myInstanceof(dog, Array)); // false
// 基本类型
console.log(myInstanceof(1, Number)); // false
console.log(myInstanceof('str', String)); // false
// 包装对象
console.log(myInstanceof(new Number(1), Number)); // true
instanceof 检查的是原型链,而不是构造函数本身。因此如果手动修改了 prototype,结果可能不符合预期:
function Foo() {}
const foo = new (Foo as any)();
console.log(foo instanceof Foo); // true
// 修改 prototype 后
Foo.prototype = {};
console.log(foo instanceof Foo); // false(原型链断了)
此外,ES6 提供了 Symbol.hasInstance 来自定义 instanceof 行为:
class MyArray {
static [Symbol.hasInstance](instance: any): boolean {
return Array.isArray(instance);
}
}
console.log([] instanceof MyArray); // true
console.log({} instanceof MyArray); // false
Q7: Object.create() 的原理和应用
答案:
Object.create(proto, propertiesObject?) 创建一个新对象,使用指定的对象作为新对象的原型。它是实现原型式继承和寄生组合式继承的关键方法。
手写实现:
function objectCreate(
proto: object | null,
propertiesObject?: PropertyDescriptorMap
): any {
if (typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object or null');
}
// 创建空的构造函数
function F() {}
// 将构造函数的 prototype 指向传入的原型对象
F.prototype = proto;
// 通过 new 创建一个以 proto 为原型的新对象
const obj = new (F as any)();
// 处理 proto 为 null 的情况(创建纯净对象)
if (proto === null) {
Object.setPrototypeOf(obj, null);
}
// 如果传入了属性描述符,使用 defineProperties 定义属性
if (propertiesObject !== undefined) {
Object.defineProperties(obj, propertiesObject);
}
return obj;
}
核心应用场景:
// 1. 寄生组合式继承(最重要的应用)
function Parent(this: any, name: string) {
this.name = name;
}
Parent.prototype.sayName = function (): void {
console.log(this.name);
};
function Child(this: any, name: string, age: number) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// 2. 创建纯净字典对象(无原型链干扰)
const dict = Object.create(null);
// dict 没有 toString、hasOwnProperty 等方法
// 常用于做 Map、缓存等场景,避免键名和原型属性冲突
dict['__proto__'] = 'safe'; // 只是普通属性,不会污染原型
console.log(Object.getPrototypeOf(dict)); // null
// 3. 使用属性描述符精细控制属性
const config = Object.create(Object.prototype, {
apiUrl: {
value: 'https://api.example.com',
writable: false, // 不可修改
enumerable: true, // 可枚举
configurable: false, // 不可删除/重新配置
},
});
// config.apiUrl = 'xxx'; // 严格模式下报错
Object.create(null) 创建的对象没有任何原型方法(如 toString、hasOwnProperty),非常适合用作纯粹的键值对存储。而 {} 等价于 Object.create(Object.prototype),会继承 Object.prototype 上的所有方法。Vue 2 源码中大量使用 Object.create(null) 来创建纯净对象。
Q8: ES6 class 的 super 关键字原理
答案:
super 关键字在 ES6 class 中有两种用法:作为函数调用和作为对象使用,它们的行为完全不同。
1. super() 作为函数 —— 只能在 constructor 中使用:
class Parent {
name: string;
constructor(name: string) {
this.name = name;
console.log('Parent this:', new.target.name);
}
}
class Child extends Parent {
age: number;
constructor(name: string, age: number) {
super(name); // 相当于 Parent.prototype.constructor.call(this, name)
// 但实际更复杂:super() 会创建 this 并绑定到子类
this.age = age;
}
}
const child = new Child('Alice', 18);
// 输出: Parent this: Child (new.target 指向 Child)
在 ES6 继承中,子类自身没有 this 对象。this 是通过 super() 调用父类构造函数创建的,然后子类再对其进行修改。如果不调用 super(),直接使用 this 会抛出 ReferenceError。
这与 ES5 继承不同 —— ES5 是先创建子类实例(this),再通过 Parent.call(this) 增强。
2. super 作为对象 —— 在普通方法中指向父类原型,在静态方法中指向父类本身:
class Parent {
static staticMethod(): string {
return 'Parent static';
}
greet(): string {
return 'Hello from Parent';
}
}
class Child extends Parent {
static staticMethod(): string {
// 静态方法中,super 指向父类(Parent)
console.log(super.staticMethod()); // 'Parent static'
return 'Child static';
}
greet(): string {
// 普通方法中,super 指向父类原型(Parent.prototype)
console.log(super.greet()); // 'Hello from Parent'
return 'Hello from Child';
}
}
3. super 的特殊行为 —— 通过 super 调用方法时 this 指向当前实例:
class Parent {
value: number = 1;
getValue(): number {
return this.value;
}
}
class Child extends Parent {
value: number = 2;
getParentValue(): number {
// super.getValue() 中的 this 指向 child 实例,而非 parent
return super.getValue(); // 返回 2,不是 1
}
}
const child = new Child();
console.log(child.getParentValue()); // 2
4. super 赋值行为:
class Parent {
x: number = 0;
}
class Child extends Parent {
x: number = 0;
setX(): void {
// 通过 super 赋值,实际上是赋给 this
super.x = 100;
console.log(super.x); // undefined(读取的是 Parent.prototype.x)
console.log(this.x); // 100(赋值赋给了 this)
}
}
| 场景 | super 指向 | this 指向 |
|---|---|---|
constructor 中 super() | 父类构造函数 | 新创建的实例 |
普通方法中 super.xxx | Parent.prototype | 当前实例 |
静态方法中 super.xxx | Parent | 当前子类 |
super.xxx = val 赋值 | 写入到 this | 当前实例 |
Q9: 为什么不推荐修改原型链?有什么替代方案?
答案:
修改原型链(特别是使用 Object.setPrototypeOf() 或给 __proto__ 赋值)在几乎所有 JavaScript 引擎中都是一个非常慢的操作,并且会带来一系列问题。
不推荐修改原型链的原因:
const obj = { name: 'Alice' };
const proto = { greet(): string { return `Hello, ${this.name}`; } };
// ❌ 不推荐:运行时修改原型链
Object.setPrototypeOf(obj, proto);
- V8 隐藏类(Hidden Classes)失效:V8 引擎会为对象创建隐藏类来优化属性访问。修改原型链会导致所有依赖该对象隐藏类的优化代码失效,触发去优化(deoptimization)
- 内联缓存(Inline Caches)失效:引擎对属性访问路径做的缓存全部作废,后续访问会退化为慢速路径查找
- 影响范围大:不仅影响被修改的对象,还会影响所有原型链上游相关对象的访问性能
- MDN 官方警告:MDN 明确指出 "修改对象的
[[Prototype]]是一个非常慢的操作"
替代方案:
// ✅ 方案一:Object.create() —— 在创建时指定原型
const proto = {
greet(): string {
return `Hello, ${this.name}`;
},
};
const obj = Object.create(proto);
obj.name = 'Alice';
console.log(obj.greet()); // 'Hello, Alice'
// ✅ 方案二:class 继承 —— 标准的继承方式
class Greeter {
name: string;
constructor(name: string) {
this.name = name;
}
greet(): string {
return `Hello, ${this.name}`;
}
}
class SpecialGreeter extends Greeter {
greet(): string {
return `Special: ${super.greet()}`;
}
}
// ✅ 方案三:组合(Composition)—— 优于继承
interface CanGreet {
greet(): string;
}
function withGreeting(name: string): CanGreet {
return {
greet(): string {
return `Hello, ${name}`;
},
};
}
function withLogging(greeter: CanGreet): CanGreet & { log(): void } {
return {
...greeter,
log(): void {
console.log(greeter.greet());
},
};
}
const user = withLogging(withGreeting('Alice'));
user.log(); // 'Hello, Alice'
// ✅ 方案四:Mixin 模式 —— 在类定义时组合功能
type Constructor<T = {}> = new (...args: any[]) => T;
function Serializable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
serialize(): string {
return JSON.stringify(this);
}
};
}
class BaseModel {
id: number;
constructor(id: number) {
this.id = id;
}
}
// 在定义时组合,而不是运行时修改原型链
const SerializableModel = Serializable(BaseModel);
const model = new SerializableModel(1);
console.log(model.serialize()); // '{"id":1}'
在对象创建时确定原型关系,而不是在运行时动态修改。如果需要复用行为,优先考虑组合(Composition)而非继承,遵循"组合优于继承"的设计原则。
Q10: 如何实现一个不可被 new 调用的函数?如何实现一个只能被 new 调用的函数?
答案:
这道题考察对 new 操作符、new.target、ES6 class 特性以及 Symbol.hasInstance 的综合理解。
一、不可被 new 调用的函数:
// 方法一:使用 new.target 检测(推荐)
function notNewable(this: any): string {
if (new.target) {
throw new TypeError('notNewable 不能作为构造函数使用');
}
return 'normal call';
}
notNewable(); // 'normal call' ✅
// new (notNewable as any)(); // TypeError ❌
// 方法二:使用箭头函数(箭头函数天然不能被 new)
const arrowFn = (): string => {
return 'arrow function';
};
arrowFn(); // 'arrow function' ✅
// new (arrowFn as any)(); // TypeError: arrowFn is not a constructor ❌
// 方法三:使用 ES2022 Object.hasOwn 概念 —— 删除 prototype
function noCtor(): string {
return 'no constructor';
}
Object.defineProperty(noCtor, 'prototype', { value: undefined });
// new (noCtor as any)();
// 注意:此方式在某些引擎中不一定可靠,推荐使用 new.target 方式
箭头函数没有 prototype 属性,也没有内部的 [[Construct]] 方法。new 操作符需要调用 [[Construct]],所以箭头函数天然不能作为构造函数。
二、只能被 new 调用的函数:
// 方法一:使用 new.target 检测(推荐)
function OnlyNewable(this: any, name: string): void {
if (!new.target) {
throw new TypeError('OnlyNewable 必须使用 new 调用');
}
this.name = name;
}
// OnlyNewable('Alice'); // TypeError ❌
new (OnlyNewable as any)('Alice'); // ✅
// 方法二:使用 ES6 class(class 本身就强制 new 调用)
class OnlyNew {
name: string;
constructor(name: string) {
this.name = name;
}
}
new OnlyNew('Alice'); // ✅
// OnlyNew('Alice'); // TypeError: Class constructor OnlyNew cannot be invoked without 'new' ❌
// 方法三:使用 instanceof 检测(兼容 ES5,但不够安全)
function LegacyNewable(this: any, name: string): void {
if (!(this instanceof LegacyNewable)) {
throw new TypeError('必须使用 new 调用');
}
this.name = name;
}
new (LegacyNewable as any)('Alice'); // ✅
// LegacyNewable('Alice'); // TypeError ❌
// 但可以被欺骗:
// LegacyNewable.call(Object.create(LegacyNewable.prototype), 'Alice'); // 不会报错
三、综合实践 —— 实现类似 class 的安全构造函数:
function SafeClass(this: any, name: string): void | never {
// 使用 new.target 是最可靠的方式
if (!new.target) {
throw new TypeError(
`Class constructor SafeClass cannot be invoked without 'new'`
);
}
// 防止被子类 new 调用时绕过
if (new.target !== SafeClass) {
throw new TypeError('SafeClass 不允许被继承');
}
this.name = name;
}
new (SafeClass as any)('Alice'); // ✅
各方案对比:
| 方案 | 不可 new | 只能 new | 可靠性 | 兼容性 |
|---|---|---|---|---|
new.target 检测 | ✅ | ✅ | 高 | ES6+ |
| 箭头函数 | ✅ | - | 高 | ES6+ |
| ES6 class | - | ✅ | 高 | ES6+ |
instanceof 检测 | - | ✅ | 中(可被欺骗) | ES5+ |
new.target 是一个元属性(meta property),在普通函数调用时为 undefined,通过 new 调用时指向被 new 调用的构造函数。在继承场景中,new.target 指向最外层被 new 的那个类(即 new Child() 时,Parent 构造函数中的 new.target 是 Child),这也是 ES6 class 能正确继承内置类(如 Array、Error)的关键机制。