JS 中的原型和继承到底复杂在哪?这篇能不能一次讲清楚?

JavaScript原型与继承:一篇文章彻底讲透核心难点

为什么开发者总是对原型链感到困惑?

当Java/Python开发者初次接触JavaScript时,常会陷入原型继承的迷雾中。尽管ES6引入了class语法糖,但浏览器底层依然基于原型机制运行。这种表面类写法与底层原型逻辑的割裂,加上原型链的动态特性,构成了75%开发者在面试和实践中频频踩坑的根本原因。

解剖JavaScript的原型DNA

核心三要素的三角关系

每个JavaScript对象都存在三个关键要素:
1. 构造函数(Constructor):负责创建实例的模板
2. prototype属性:构造函数的原型对象
3. __proto__属性:实例指向原型的链接

```javascript
function Phone(brand) {
this.brand = brand // 构造函数属性
}
Phone.prototype.getPrice = function() { // 原型方法
return this.price
}

const iphone = new Phone('Apple')
console.log(iphone.__proto__ === Phone.prototype) // true
```

原型链的运作真相

当访问对象属性时,引擎会执行三级跳查询
1. 检查实例自身属性
2. 沿__proto__查找构造函数原型
3. 直到Object.prototype终止

六种继承方式优劣全解析

1. 原型链继承:最基础的实现

```javascript
function BasicPhone() {
this.coreFeatures = ['通话', '短信']
}
function SmartPhone() {
this.advancedFeatures = ['上网']
}
SmartPhone.prototype = new BasicPhone()

const device = new SmartPhone()
console.log(device.coreFeatures) // ['通话', '短信']
```
缺陷:所有实例共享引用类型属性,修改会导致污染

2. 构造函数继承:解决属性隔离

```javascript
function SmartPhone(brand) {
BasicPhone.call(this)
this.brand = brand
}
```
进步点:实现实例属性独立
新问题:无法复用原型方法

3. 组合继承(经典方案)

```javascript
function SmartPhone(brand) {
BasicPhone.call(this) // 第二次调用父构造
this.brand = brand
}
SmartPhone.prototype = new BasicPhone() // 第一次调用父构造
```
优势:综合前两种方案优点
代价:父构造函数被重复调用

4. 原型式继承:Object.create的哲学

```javascript
const basic = { core: ['通话'] }
const phone = Object.create(basic, {
brand: { value: 'Xiaomi' }
})
```
适用场景:简单对象继承
注意事项:仍需警惕引用类型共享

5. 寄生式继承:工厂模式增强

```javascript
function createDevice(original) {
let clone = Object.create(original)
clone.loadOS = function() { /.../ }
return clone
}
```

6. 寄生组合继承(终极方案)

```javascript
function inherit(subType, superType) {
let prototype = Object.create(superType.prototype)
prototype.constructor = subType
subType.prototype = prototype
}

function SmartPhone() {
BasicPhone.call(this) // 只执行一次构造
}
inherit(SmartPhone, BasicPhone)
```
此方案被Douglas Crockford推崇为最理想的继承实现,既避免重复调用,又保持原型链完整。

ES6 class的本质揭密

虽然现代语法看似类似传统类继承,但需特别注意:
```javascript
class Student {
constructor(name) {
this.name = name
}
// 方法实际存储在原型上
study() { console.log('学习中') }
}

class CollegeStudent extends Student {
constructor(name, major) {
super(name) // 必须首先调用
this.major = major
}
}

console.log(typeof Student) // 'function'
```
关键认知:
1. class本质仍是构造函数
2. extends建立了原型链关联
3. 方法声明自动绑定到prototype
4. super实现了[[HomeObject]]机制

高频踩坑点深度预警

1. 原型污染危机

```javascript
Array.prototype.sum = function() { // 危险操作!
return this.reduce((a,b) => a+b)
}
```
后果:所有数组实例突然拥有sum方法,可能导致命名冲突

2. constructor的迷思

```javascript
function Phone() {}
Phone.prototype = { // 直接覆盖原型
ring: function() {}
}

const p = new Phone()
console.log(p.constructor === Object) // true
```
修正方案:手动维护constructor指向

3. 原型链动态性陷阱

```javascript
function Animal() {}
const dog = new Animal()

Animal.prototype.run = function() {}
dog.run() // 正常工作

Animal.prototype = { jump: function() {} }
const cat = new Animal()
cat.jump() // 正常
dog.jump() // TypeError!
```
原理:已创建实例保持对旧原型的引用

性能优化关键策略

1. 避免过长的原型链(建议不超过5层)
2. 使用Object.create(null)创建纯净字典
3. 在构造函数内定义方法(V8引擎优化场景)
4. 使用hasOwnProperty严格检测属性来源

```javascript
const dict = Object.create(null) // 无原型干扰
dict.key = 'value'

for(let key in obj) {
if(obj.hasOwnProperty(key)) { // 过滤原型属性
// 处理自有属性
}
}
```

从原型到现代框架的演进

现代框架通过包装原型机制实现更优雅的API:
1. React Class Component:基于原型实现生命周期方法继承
2. Vue Options API:利用mixin扩展原型功能
3. ES6 Proxy:实现更灵活的原型控制

理解原型机制,不仅有助于阅读框架源码,更能帮助开发者在需要时突破框架限制,实现定制化解决方案。

终极建议:在掌握原型原理的基础上,建议项目中使用ES6 class保持代码可读性,但必须清楚其底层运作机制。当需要实现特殊继承逻辑时,灵活运用寄生组合等模式,同时做好团队技术方案的文档沉淀。