1.28 - 原型链、继承的多种实现方式
JavaScript 原型链与继承深度解析
目录
核心概念
1. 三个关键对象属性
__proto__ (隐式原型)
- 每个对象都有的内部属性
- 指向创建该对象的构造函数的
prototype - 实现原型链查找的关键
prototype (显式原型)
- 只有函数才有的属性
- 是一个对象,包含可被实例共享的属性和方法
constructor属性指回函数本身
constructor (构造器)
prototype对象上的属性- 指向创建该原型对象的构造函数
function Person(name) {
this.name = name;
}
const person = new Person('张三');
console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
console.log(person.constructor === Person); // true (通过原型链查找)
2. 关系图解
person 实例
↓ __proto__
Person.prototype
↓ __proto__
Object.prototype
↓ __proto__
null
原型链机制
1. 属性查找规则
当访问对象的属性时,JavaScript 引擎按以下顺序查找:
- 对象自身是否有该属性
- 对象的
__proto__(即构造函数的prototype) __proto__的__proto__(继续向上查找)- 直到
Object.prototype - 最后是
null,返回undefined
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} 正在吃东西`);
};
const dog = new Animal('旺财');
// 属性查找过程
console.log(dog.name); // 自身属性: "旺财"
console.log(dog.eat); // 原型属性: function
console.log(dog.toString); // Object.prototype 上的方法
console.log(dog.notExist); // undefined (查找到 null)
2. 原型链的本质
原型链实际上是通过 __proto__ 连接起来的一条查找链:
function Parent() {
this.parentProp = 'parent';
}
Parent.prototype.getParent = function() {
return this.parentProp;
};
function Child() {
this.childProp = 'child';
}
// 建立原型链
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child = new Child();
// 原型链结构
console.log(child.__proto__ === Child.prototype); // true
console.log(child.__proto__.__proto__ === Parent.prototype); // true
console.log(child.__proto__.__proto__.__proto__ === Object.prototype); // true
console.log(child.__proto__.__proto__.__proto__.__proto__); // null
3. instanceof 原理
instanceof 通过原型链判断对象是否是某个构造函数的实例:
function myInstanceof(obj, constructor) {
let proto = obj.__proto__;
const prototype = constructor.prototype;
while (proto) {
if (proto === prototype) return true;
proto = proto.__proto__;
}
return false;
}
console.log(myInstanceof(child, Child)); // true
console.log(myInstanceof(child, Parent)); // true
console.log(myInstanceof(child, Object)); // true
继承的多种实现方式
1. 原型链继承
实现原理:将子类的原型指向父类的实例
function Parent() {
this.name = 'parent';
this.hobbies = ['reading', 'coding'];
}
Parent.prototype.getName = function() {
return this.name;
};
function Child() {
this.age = 18;
}
// 继承
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child1 = new Child();
const child2 = new Child();
console.log(child1.getName()); // "parent"
child1.hobbies.push('gaming');
console.log(child2.hobbies); // ["reading", "coding", "gaming"]
优点:
- 简单易实现
- 可以访问父类原型上的方法
缺点:
- 引用类型属性被所有实例共享
- 无法向父类构造函数传参
- 不能实现多继承
2. 构造函数继承(经典继承)
实现原理:在子类构造函数中调用父类构造函数
function Parent(name) {
this.name = name;
this.hobbies = ['reading', 'coding'];
}
Parent.prototype.getName = function() {
return this.name;
};
function Child(name, age) {
// 继承父类属性
Parent.call(this, name);
this.age = age;
}
const child1 = new Child('小明', 18);
const child2 = new Child('小红', 20);
child1.hobbies.push('gaming');
console.log(child1.hobbies); // ["reading", "coding", "gaming"]
console.log(child2.hobbies); // ["reading", "coding"]
console.log(child1.getName); // undefined (无法继承原型方法)
优点:
- 解决了引用类型共享问题
- 可以向父类传参
- 可以实现多继承(多次 call)
缺点:
- 无法继承父类原型上的方法
- 每次创建实例都会创建一遍方法,内存浪费
3. 组合继承(原型链 + 构造函数)
实现原理:结合原型链继承和构造函数继承
function Parent(name) {
this.name = name;
this.hobbies = ['reading', 'coding'];
}
Parent.prototype.getName = function() {
return this.name;
};
function Child(name, age) {
// 第二次调用 Parent
Parent.call(this, name); // 继承属性
this.age = age;
}
// 第一次调用 Parent
Child.prototype = new Parent(); // 继承方法
Child.prototype.constructor = Child;
const child1 = new Child('小明', 18);
const child2 = new Child('小红', 20);
child1.hobbies.push('gaming');
console.log(child1.hobbies); // ["reading", "coding", "gaming"]
console.log(child2.hobbies); // ["reading", "coding"]
console.log(child1.getName()); // "小明"
优点:
- 结合了两种继承的优点
- 可以继承原型方法
- 引用类型不共享
- 可以传参
缺点:
- 调用了两次父类构造函数,浪费性能
- 子类原型上会有一份多余的父类实例属性
4. 原型式继承
实现原理:基于已有对象创建新对象(Object.create 的原理)
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
const parent = {
name: 'parent',
hobbies: ['reading', 'coding'],
getName: function() {
return this.name;
}
};
const child1 = object(parent);
const child2 = object(parent);
child1.name = '小明';
child1.hobbies.push('gaming');
console.log(child1.name); // "小明"
console.log(child2.name); // "parent"
console.log(child2.hobbies); // ["reading", "coding", "gaming"]
// ES5 原生方法
const child3 = Object.create(parent);
console.log(child3.getName()); // "parent"
优点:
- 不需要构造函数,可以快速创建对象
缺点:
- 引用类型属性被所有实例共享
- 无法传参
5. 寄生式继承
实现原理:在原型式继承基础上,增强对象
function createChild(original) {
const clone = Object.create(original);
// 增强对象
clone.sayHello = function() {
console.log('Hello!');
};
return clone;
}
const parent = {
name: 'parent',
hobbies: ['reading']
};
const child = createChild(parent);
child.sayHello(); // "Hello!"
优点:
- 可以在创建时增强对象
缺点:
- 方法无法复用,每次创建都会创建一遍方法
- 引用类型共享问题依然存在
6. 寄生组合式继承(最优方案)
实现原理:通过寄生方式继承父类原型,避免调用两次父类构造函数
function inheritPrototype(child, parent) {
// 创建父类原型的副本
const prototype = Object.create(parent.prototype);
// 修正 constructor
prototype.constructor = child;
// 设置子类原型
child.prototype = prototype;
}
function Parent(name) {
this.name = name;
this.hobbies = ['reading', 'coding'];
}
Parent.prototype.getName = function() {
return this.name;
};
function Child(name, age) {
Parent.call(this, name); // 只调用一次
this.age = age;
}
// 继承原型
inheritPrototype(Child, Parent);
Child.prototype.getAge = function() {
return this.age;
};
const child1 = new Child('小明', 18);
console.log(child1.getName()); // "小明"
console.log(child1.getAge()); // 18
为什么最优?
- 只调用一次父类构造函数
- 原型链保持不变
- 能够正常使用
instanceof和isPrototypeOf() - 避免在子类原型上创建多余属性
7. ES6 Class 继承
实现原理:语法糖,底层依然基于原型链
class Parent {
constructor(name) {
this.name = name;
this.hobbies = ['reading', 'coding'];
}
getName() {
return this.name;
}
static staticMethod() {
return 'static method';
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 必须调用 super
this.age = age;
}
getAge() {
return this.age;
}
// 重写父类方法
getName() {
return `Child: ${super.getName()}`;
}
}
const child = new Child('小明', 18);
console.log(child.getName()); // "Child: 小明"
console.log(Child.staticMethod()); // "static method"
console.log(child instanceof Parent); // true
Class 继承的特点:
super关键字调用父类构造函数- 子类必须在
constructor中调用super,否则无法使用this - 静态方法也会被继承
- 内部方法不可枚举
与寄生组合继承的对比:
// ES6 Class 本质
class Child extends Parent {}
// 等价于
function Child() {
Parent.call(this);
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// 额外:静态属性继承
Object.setPrototypeOf(Child, Parent);
实战应用与面试要点
1. 手写 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) {
this.name = name;
}
const person = myNew(Person, '张三');
console.log(person.name); // "张三"
2. 手写 Object.create
function myCreate(proto) {
function F() {}
F.prototype = proto;
return new F();
}
const parent = { name: 'parent' };
const child = myCreate(parent);
console.log(child.__proto__ === parent); // true
3. 判断对象属性来源
function Person(name) {
this.name = name;
}
Person.prototype.age = 18;
const person = new Person('张三');
// 判断自有属性
console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('age')); // false
// 判断属性是否在对象上(包括原型链)
console.log('name' in person); // true
console.log('age' in person); // true
// 判断属性是否在原型上
function hasPrototypeProperty(obj, name) {
return !obj.hasOwnProperty(name) && (name in obj);
}
console.log(hasPrototypeProperty(person, 'age')); // true
4. 完整的继承实现(寄生组合式)
// 工具函数
function extend(Child, Parent) {
const F = function() {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.super = Parent.prototype; // 保存父类引用
}
// 父类
function Animal(name) {
this.name = name;
this.sleep = function() {
console.log(`${this.name} 正在睡觉`);
};
}
Animal.prototype.eat = function(food) {
console.log(`${this.name} 正在吃 ${food}`);
};
// 子类
function Dog(name, color) {
Animal.call(this, name);
this.color = color;
}
// 继承
extend(Dog, Animal);
Dog.prototype.bark = function() {
console.log('汪汪汪!');
};
const dog = new Dog('旺财', '黑色');
dog.eat('骨头'); // "旺财 正在吃 骨头"
dog.bark(); // "汪汪汪!"
dog.sleep(); // "旺财 正在睡觉"
5. 面试高频问题
Q1: prototype 和 __proto__ 的区别?
prototype是函数的属性,指向一个对象,用于存放共享属性和方法__proto__是对象的属性,指向创建该对象的构造函数的prototype- 所有函数都有
prototype,所有对象都有__proto__
Q2: 原型链的终点是什么?
Object.prototype.__proto__ === null // true
Q3: 如何获取对象的原型?
// 方法一:ES6 标准方法
Object.getPrototypeOf(obj)
// 方法二:非标准但广泛支持
obj.__proto__
// 方法三:通过 constructor
obj.constructor.prototype
Q4: 为什么要修正 constructor?
function Parent() {}
function Child() {}
Child.prototype = new Parent();
// 不修正的话
console.log(new Child().constructor === Parent); // true (错误!)
// 修正后
Child.prototype.constructor = Child;
console.log(new Child().constructor === Child); // true (正确)
Q5: Class 继承与寄生组合继承的区别?
- Class 必须使用
new调用 - Class 内部方法不可枚举
- Class 有暂时性死区,不存在变量提升
- Class 支持静态方法继承
// Class 不能直接调用
class Parent {}
Parent(); // TypeError
// 函数可以
function Parent() {}
Parent(); // 正常执行
6. 性能优化建议
// ❌ 不好的做法:每次创建实例都创建方法
function Person(name) {
this.name = name;
this.sayName = function() {
console.log(this.name);
};
}
// ✅ 好的做法:方法放在原型上共享
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(this.name);
};
7. 实战场景:插件系统
// 基础插件类
class Plugin {
constructor(options = {}) {
this.options = options;
}
init() {
throw new Error('子类必须实现 init 方法');
}
destroy() {
console.log('插件已销毁');
}
}
// 具体插件
class TooltipPlugin extends Plugin {
init() {
console.log('Tooltip 插件初始化', this.options);
}
show() {
console.log('显示提示');
}
}
const tooltip = new TooltipPlugin({ position: 'top' });
tooltip.init(); // "Tooltip 插件初始化 {position: 'top'}"
tooltip.show(); // "显示提示"
tooltip.destroy(); // "插件已销毁"
总结
继承方式对比表
| 继承方式 | 优点 | 缺点 | 使用场景 |
|---|---|---|---|
| 原型链继承 | 简单 | 引用类型共享、无法传参 | 不推荐 |
| 构造函数继承 | 解决引用共享、可传参 | 无法继承原型方法 | 不推荐 |
| 组合继承 | 功能完整 | 调用两次父类构造函数 | 可用但不最优 |
| 原型式继承 | 简单快速 | 引用类型共享 | 简单对象复制 |
| 寄生式继承 | 可增强对象 | 方法无法复用 | 特殊场景 |
| 寄生组合式 | 完美 | 实现相对复杂 | 推荐 |
| ES6 Class | 语法简洁、功能强大 | 需要转译支持旧浏览器 | 强烈推荐 |
核心要点
- 原型链是 JavaScript 继承的基础
__proto__连接起整条原型链- 寄生组合式继承是 ES5 最优方案
- ES6 Class 是现代开发首选
- 理解原理比记忆语法更重要
学习建议
- 手写实现各种继承方式
- 理解每种方式的适用场景
- 在实际项目中灵活运用
- 关注性能和可维护性
- 深入理解原型链查找机制