Project-Ideas:前端原型链与继承实现
你还在为这些问题烦恼吗?
- 为什么
[] instanceof Array返回 true 而[] instanceof Object也返回 true? - 为什么构造函数里的 this 能指向实例对象?
- 原型链继承、class 继承、组合继承到底有什么区别?
- 为什么说
Object.prototype.__proto__是原型链的尽头?
读完你能得到:
- 原型链(Prototype Chain)的底层工作原理
- 6种继承方式的实现代码与优缺点对比
- 原型污染的成因与防御措施
- 继承在React/Vue组件开发中的最佳实践
- 10道大厂面试题解析与避坑指南
一、JavaScript对象模型核心:原型链详解
1.1 什么是原型(Prototype)
在JavaScript中,每个对象都有一个特殊的内部属性[[Prototype]](可通过Object.getPrototypeOf()访问),这个属性指向另一个对象,这就是原型。当访问对象的属性时,如果对象本身没有该属性,JavaScript引擎会沿着原型链向上查找,直到找到该属性或到达原型链的终点。
// 创建一个普通对象
const obj = { name: '原型示例' };
// 获取对象的原型
const proto = Object.getPrototypeOf(obj);
// 原型链终点
console.log(Object.getPrototypeOf(Object.prototype)); // null
1.2 原型链的工作流程图
1.3 构造函数与原型的关系
每个构造函数都有一个prototype属性,指向实例对象的原型。当使用new关键字创建实例时,实例的[[Prototype]]会指向构造函数的prototype属性。
// 构造函数
function Person(name) {
this.name = name;
}
// 构造函数的prototype属性
Person.prototype.sayHello = function() {
console.log(`Hello, ${this.name}`);
};
// 创建实例
const person = new Person('张三');
// 实例的原型就是构造函数的prototype
console.log(Object.getPrototypeOf(person) === Person.prototype); // true
// 原型的constructor属性指回构造函数
console.log(Person.prototype.constructor === Person); // true
1.4 原型链查找规则
- 当访问对象属性时,先检查对象本身是否存在该属性
- 如果不存在,就查找对象的原型
- 以此类推,直到找到属性或到达原型链终点(null)
- 如果整个原型链都找不到该属性,则返回undefined
const obj = { a: 1 };
// 给原型添加属性
Object.prototype.b = 2;
// 查找a:obj自身有a属性
console.log(obj.a); // 1
// 查找b:obj自身没有,在原型上找到
console.log(obj.b); // 2
// 查找c:整个原型链都没有
console.log(obj.c); // undefined
二、JavaScript继承的6种实现方式
2.1 原型链继承
实现原理:让子类的原型对象指向父类的实例,从而继承父类的属性和方法。
// 父类
function Animal(name) {
this.name = name;
this.colors = ['black', 'white'];
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
// 子类
function Dog(name, age) {
this.age = age;
}
// 关键:让Dog的原型指向Animal的实例
Dog.prototype = new Animal();
// 修复constructor指向
Dog.prototype.constructor = Dog;
// 添加子类特有方法
Dog.prototype.bark = function() {
console.log('Woof! Woof!');
};
// 测试
const dog1 = new Dog('小黑', 2);
const dog2 = new Dog('小白', 3);
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white', 'brown'] (引用类型共享问题)
优缺点: | 优点 | 缺点 | |------|------| | 实现简单,易于理解 | 父类的引用类型属性会被所有子类实例共享 | | 可以继承父类原型上的方法 | 创建子类实例时,无法向父类构造函数传递参数 |
2.2 构造函数继承
实现原理:在子类构造函数中调用父类构造函数,通过call/apply改变this指向,实现属性继承。
// 父类
function Animal(name) {
this.name = name;
this.colors = ['black', 'white'];
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
// 子类
function Dog(name, age) {
// 关键:调用父类构造函数
Animal.call(this, name);
this.age = age;
}
// 测试
const dog1 = new Dog('小黑', 2);
const dog2 = new Dog('小白', 3);
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white'] (解决引用类型共享问题)
// 无法继承原型上的方法
console.log(dog1.eat); // undefined
优缺点: | 优点 | 缺点 | |------|------| | 解决了引用类型属性共享问题 | 无法继承父类原型上的方法 | | 可以向父类构造函数传递参数 | 每个实例都会拥有父类方法的副本,浪费内存 |
2.3 组合继承
实现原理:结合原型链继承和构造函数继承的优点,使用原型链继承原型上的方法,使用构造函数继承实例属性。
// 父类
function Animal(name) {
this.name = name;
this.colors = ['black', 'white'];
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
// 子类
function Dog(name, age) {
// 构造函数继承:继承实例属性
Animal.call(this, name);
this.age = age;
}
// 原型链继承:继承原型方法
Dog.prototype = new Animal();
// 修复constructor指向
Dog.prototype.constructor = Dog;
// 添加子类特有方法
Dog.prototype.bark = function() {
console.log('Woof! Woof!');
};
// 测试
const dog = new Dog('小黑', 2);
dog.eat(); // 小黑 is eating
dog.bark(); // Woof! Woof!
console.log(dog.name); // 小黑
优缺点: | 优点 | 缺点 | |------|------| | 既可以继承实例属性,又可以继承原型方法 | 父类构造函数被调用两次(一次创建子类原型,一次在子类构造函数中) | | 避免了引用类型属性共享问题 | 子类原型上会有父类的实例属性,造成内存浪费 |
2.4 原型式继承
实现原理:创建一个临时构造函数,将传入的对象作为这个构造函数的原型,然后返回这个构造函数的实例。
// 实现原型式继承的函数
function objectCreate(proto) {
function F() {};
F.prototype = proto;
return new F();
}
// 等同于ES5的Object.create()
// 使用示例
const animal = {
name: '动物',
colors: ['black', 'white'],
eat: function() {
console.log(`${this.name} is eating`);
}
};
// 创建继承自animal的对象
const dog = objectCreate(animal);
dog.name = '狗';
dog.bark = function() {
console.log('Woof! Woof!');
};
console.log(dog.name); // 狗
dog.eat(); // 狗 is eating
dog.bark(); // Woof! Woof!
优缺点: | 优点 | 缺点 | |------|------| | 简单易用,适合创建对象的副本 | 引用类型属性仍然会被共享 | | 不需要定义构造函数 | 无法传递参数 |
2.5 寄生式继承
实现原理:在原型式继承的基础上,增强对象,返回增强后的对象。
// 原型式继承函数
function objectCreate(proto) {
function F() {};
F.prototype = proto;
return new F();
}
// 寄生式继承函数
function createDog(original) {
// 创建对象
const clone = objectCreate(original);
// 增强对象
clone.bark = function() {
console.log('Woof! Woof!');
};
// 返回对象
return clone;
}
// 使用示例
const animal = {
name: '动物',
eat: function() {
console.log(`${this.name} is eating`);
}
};
const dog = createDog(animal);
dog.name = '狗';
console.log(dog.name); // 狗
dog.eat(); // 狗 is eating
dog.bark(); // Woof! Woof!
优缺点: | 优点 | 缺点 | |------|------| | 可以增强对象,添加新的方法 | 与原型式继承一样,引用类型属性会被共享 | | 不需要定义构造函数 | 每次创建对象都会创建新的方法,造成内存浪费 |
2.6 寄生组合式继承
实现原理:结合寄生式继承和组合继承的优点,通过寄生式继承父类的原型,然后将结果指定给子类的原型。
// 寄生组合式继承的核心函数
function inheritPrototype(subType, superType) {
// 创建父类原型的副本
const prototype = Object.create(superType.prototype);
// 修复constructor指向
prototype.constructor = subType;
// 将子类原型指向这个副本
subType.prototype = prototype;
}
// 父类
function Animal(name) {
this.name = name;
this.colors = ['black', 'white'];
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
// 子类
function Dog(name, age) {
Animal.call(this, name);
this.age = age;
}
// 关键:继承父类原型
inheritPrototype(Dog, Animal);
// 添加子类特有方法
Dog.prototype.bark = function() {
console.log('Woof! Woof!');
};
// 测试
const dog = new Dog('小黑', 2);
console.log(dog.name); // 小黑
dog.eat(); // 小黑 is eating
dog.bark(); // Woof! Woof!
console.log(Dog.prototype.constructor === Dog); // true
优缺点: | 优点 | 缺点 | |------|------| | 父类构造函数只调用一次 | 实现较为复杂 | | 避免了原型链上的多余属性 | | | 是目前最理想的继承方式 | |
三、ES6 Class继承
ES6引入了class关键字,提供了更简洁的语法来实现继承。虽然class本质上是原型继承的语法糖,但它使代码更加清晰易懂。
3.1 Class基本语法
// 父类
class Animal {
// 构造函数
constructor(name) {
this.name = name;
this.colors = ['black', 'white'];
}
// 原型方法
eat() {
console.log(`${this.name} is eating`);
}
// 静态方法(不会被实例继承,只能通过类调用)
static isAnimal(obj) {
return obj instanceof Animal;
}
}
// 子类继承父类
class Dog extends Animal {
// 构造函数
constructor(name, age) {
// 调用父类构造函数
super(name);
this.age = age;
}
// 子类特有方法
bark() {
console.log('Woof! Woof!');
}
// 重写父类方法
eat() {
console.log(`${this.name} is eating quickly`);
}
}
// 使用示例
const dog = new Dog('小黑', 2);
console.log(dog.name); // 小黑
dog.eat(); // 小黑 is eating quickly
dog.bark(); // Woof! Woof!
console.log(Animal.isAnimal(dog)); // true
3.2 extends关键字的工作原理
extends关键字不仅可以继承类,还可以继承原生构造函数(如Array、Object等)。
// 继承Array
class MyArray extends Array {
// 自定义方法
first() {
return this[0];
}
last() {
return this[this.length - 1];
}
}
const arr = new MyArray(1, 2, 3, 4);
console.log(arr.first()); // 1
console.log(arr.last()); // 4
console.log(arr instanceof Array); // true
3.3 super关键字
super关键字有两种用法:
- 作为函数调用:
super(...args),代表父类的构造函数 - 作为对象调用:
super.method(),代表父类的原型对象
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating`);
}
}
class Dog extends Animal {
constructor(name, age) {
// 调用父类构造函数
super(name);
this.age = age;
}
eat() {
// 调用父类的eat方法
super.eat();
console.log('after eating');
}
}
const dog = new Dog('小黑', 2);
dog.eat();
// 输出:
// 小黑 is eating
// after eating
3.4 Class继承与ES5继承的区别
| ES5继承 | ES6 Class继承 |
|---|---|
| 通过原型链和构造函数实现 | 通过extends和super关键字实现 |
| 子类构造函数需要手动调用父类构造函数 | 子类构造函数必须调用super(),否则会报错 |
| 无法继承原生构造函数 | 可以继承原生构造函数 |
| 原型方法需要手动添加到原型对象上 | 方法直接定义在class内部,自动成为原型方法 |
| 静态方法需要手动添加到构造函数上 | 使用static关键字定义静态方法 |
四、原型链与继承的应用场景
4.1 实现对象的复用
通过原型继承可以实现对象方法的复用,避免每个实例都创建相同的方法。
// 公共方法定义在原型上
const utils = {
formatDate(date) {
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
},
formatPrice(price) {
return `¥${price.toFixed(2)}`;
}
};
// 其他对象继承utils
const orderUtils = Object.create(utils);
orderUtils.calculateTotal = function(items) {
return items.reduce((total, item) => total + item.price, 0);
};
console.log(orderUtils.formatPrice(100)); // ¥100.00
4.2 React组件继承
在React中,可以通过继承React.Component来创建组件。
import React from 'react';
// 父组件
class BaseComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
error: null
};
}
// 公共方法
showLoading() {
this.setState({ loading: true });
}
hideLoading() {
this.setState({ loading: false });
}
}
// 子组件继承父组件
class UserComponent extends BaseComponent {
constructor(props) {
super(props);
this.state = {
...this.state,
users: []
};
}
componentDidMount() {
this.showLoading();
// 加载数据
fetch('/api/users')
.then(res => res.json())
.then(users => {
this.setState({ users });
this.hideLoading();
})
.catch(error => {
this.setState({ error });
this.hideLoading();
});
}
render() {
if (this.state.loading) return <div>Loading...</div>;
if (this.state.error) return <div>Error: {this.state.error.message}</div>;
return (
<ul>
{this.state.users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
}
4.3 Vue组件继承
在Vue中,可以通过extends选项来继承组件。
// 父组件
const BaseComponent = {
data() {
return {
loading: false,
error: null
};
},
methods: {
showLoading() {
this.loading = true;
},
hideLoading() {
this.loading = false;
}
}
};
// 子组件继承父组件
export default {
extends: BaseComponent,
data() {
return {
users: []
};
},
mounted() {
this.showLoading();
// 加载数据
fetch('/api/users')
.then(res => res.json())
.then(users => {
this.users = users;
this.hideLoading();
})
.catch(error => {
this.error = error;
this.hideLoading();
});
}
};
五、原型链常见问题与解决方案
5.1 原型污染
原型污染(Prototype Pollution) 是指修改了Object.prototype等内置对象的原型,可能导致意想不到的后果。
// 原型污染示例
Object.prototype.log = function() {
console.log(this);
};
const obj = {};
obj.log(); // 会调用原型上的log方法
// 更危险的情况
const user = { name: '张三' };
// 如果恶意代码修改了原型
Object.prototype.toString = function() {
return '被污染了';
};
console.log(user.toString()); // 被污染了
防御措施:
- 避免直接修改内置对象的原型
- 使用
Object.create(null)创建没有原型的对象 - 使用
hasOwnProperty方法检查属性是否是对象自身的
// 创建没有原型的对象
const safeObj = Object.create(null);
// 这样就不会继承Object.prototype上的属性
console.log(safeObj.toString); // undefined
// 使用hasOwnProperty检查属性
const obj = { a: 1 };
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key); // a
}
}
5.2 原型链过长
过长的原型链会影响性能,查找属性时需要遍历更多的对象。
解决方案:
- 避免过深的继承层次
- 优先使用组合而非继承
- 使用
Object.hasOwn()(ES2022)快速检查属性
// 组合优于继承
const canEat = {
eat() {
console.log(`${this.name} is eating`);
}
};
const canBark = {
bark() {
console.log('Woof! Woof!');
}
};
// 通过Object.assign组合功能
const dog = Object.assign({
name: '狗'
}, canEat, canBark);
dog.eat(); // 狗 is eating
dog.bark(); // Woof! Woof!
5.3 忘记调用super()
在ES6 Class继承中,如果子类定义了constructor,必须调用super(),否则会报错。
class Animal {
constructor(name) {
this.name = name;
}
}
class Dog extends Animal {
constructor(name, age) {
// 忘记调用super()
this.age = age; // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
}
}
解决方案:确保在子类构造函数中首先调用super()。
class Dog extends Animal {
constructor(name, age) {
super(name); // 必须先调用super()
this.age = age;
}
}
六、继承方式对比与选择指南
6.1 各种继承方式对比表
| 继承方式 | 实例属性继承 | 原型方法继承 | 引用类型共享 | 父类构造函数调用次数 | 复杂度 | 推荐指数 |
|---|---|---|---|---|---|---|
| 原型链继承 | 是 | 是 | 是 | 1 | 简单 | ★★☆☆☆ |
| 构造函数继承 | 是 | 否 | 否 | 1 | 简单 | ★★☆☆☆ |
| 组合继承 | 是 | 是 | 否 | 2 | 中等 | ★★★☆☆ |
| 原型式继承 | 是 | 是 | 是 | 0 | 简单 | ★★☆☆☆ |
| 寄生式继承 | 是 | 是 | 是 | 0 | 中等 | ★★☆☆☆ |
| 寄生组合式继承 | 是 | 是 | 否 | 1 | 复杂 | ★★★★☆ |
| ES6 Class继承 | 是 | 是 | 否 | 1 | 简单 | ★★★★★ |
6.2 继承方式选择建议
- ES6环境:优先使用
class+extends,语法清晰,易于维护 - ES5环境:使用寄生组合式继承,这是ES5中最理想的继承方式
- 简单对象复用:使用
Object.create()实现原型式继承 - 避免深层次继承:超过3层的继承层次会导致代码难以理解和维护
- 优先考虑组合:在很多情况下,组合(将功能封装到对象中,通过Object.assign组合)比继承更灵活
七、面试题解析
7.1 题目1:以下代码输出什么?为什么?
function Foo() {
getName = function() { console.log(1); };
return this;
}
Foo.getName = function() { console.log(2); };
Foo.prototype.getName = function() { console.log(3); };
var getName = function() { console.log(4); };
function getName() { console.log(5); }
// 输出以下结果
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
答案与解析:
Foo.getName():输出2,调用Foo的静态方法getNamegetName():输出4,变量声明提升优先级高于函数声明Foo().getName():输出1,Foo函数修改了全局的getNamegetName():输出1,全局的getName已经被修改new Foo.getName():输出2,相当于new (Foo.getName())new Foo().getName():输出3,先创建Foo实例,再调用实例的getName方法(原型上的方法)new new Foo().getName():输出3,相当于new (new Foo().getName())
7.2 题目2:如何实现一个完整的继承?
答案:使用寄生组合式继承或ES6 Class继承。
// 寄生组合式继承实现
function inheritPrototype(subType, superType) {
const prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
};
7.3 题目3:什么是原型链?原型链的终点是什么?
答案:
- 原型链是JavaScript中实现继承的机制,每个对象都有一个原型,原型对象也可能有自己的原型,这样就形成了一条链式结构,称为原型链。
- 当访问对象的属性时,JavaScript引擎会沿着原型链向上查找,直到找到该属性或到达原型链的终点。
- 原型链的终点是
null,Object.prototype.__proto__的值就是null。
7.4 题目4:instanceof运算符的工作原理是什么?
答案: instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。
实现原理:
function myInstanceof(left, right) {
// 获取对象的原型
let proto = Object.getPrototypeOf(left);
// 获取构造函数的prototype
const prototype = right.prototype;
// 遍历原型链
while (true) {
if (proto === null) return false;
if (proto === prototype) return true;
proto = Object.getPrototypeOf(proto);
}
}
7.5 题目5:Object.prototype.toString.call()为什么能判断数据类型?
答案: 因为每个对象的toString方法都继承自Object.prototype,但很多内置对象(如Array、Date等)重写了toString方法。Object.prototype.toString方法返回一个表示对象类型的字符串,格式为[object Type],其中Type是对象的类型。
console.log(Object.prototype.toString.call(123)); // [object Number]
console.log(Object.prototype.toString.call('abc')); // [object String]
console.log(Object.prototype.toString.call(true)); // [object Boolean]
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call({})); // [object Object]
console.log(Object.prototype.toString.call(null)); // [object Null]
console.log(Object.prototype.toString.call(undefined)); // [object Undefined]
八、总结与展望
8.1 核心知识点回顾
- 原型链是JavaScript实现继承的基础,每个对象都有一个原型,形成链式结构
- 继承的本质是复用代码,避免重复劳动
- ES5中有多种继承方式,各有优缺点,寄生组合式继承是ES5中最理想的继承方式
- ES6的
class和extends提供了更简洁的继承语法 - 继承虽然强大,但过度使用会导致代码耦合度高,应优先考虑组合
- 原型污染是一个需要注意的安全问题,避免修改内置对象的原型
8.2 未来发展趋势
- 随着ES6及以上版本的普及,
class语法会成为主流 - 函数式编程思想的兴起,组合(composition)会越来越多地替代继承
- TypeScript等静态类型语言的普及,会提供更严格的类型检查,减少继承相关的错误
- Web Components标准中的自定义元素也使用了类继承的思想
九、互动与反馈
如果本文对你有帮助,请点赞、收藏、关注三连支持!如果你有任何问题或建议,欢迎在评论区留言讨论。下一篇文章我们将深入探讨JavaScript的异步编程模型,敬请期待!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



