JavaScript 面向对象编程从零到精通:原型链、继承与现代 class 的前世今生
JavaScript 是一门「看起来像面向对象,其实是基于原型」的语言。
很多人学到 class 就以为自己已经掌握了 OOP,其实那只是语法糖,真正的精髓藏在 prototype、__proto__ 和原型链里。
今天我们从最原始的对象字面量开始,一步一步带你捋清 JavaScript 面向对象的完整演进路径
1. 最原始的写法:对象字面量(根本算不上 OOP)
var cat1 = {
name: '大橘',
color: '橘色',
eat: function() { console.log('吃 Jerry') }
}
var cat2 = {
name: '加菲',
color: '橘色',
eat: function() { console.log('吃 Jerry') }
}
问题显而易见:
- 每只猫都要重复写一遍
eat方法 → 内存浪费 - 猫与猫之间没有任何关系 → 无法体现「同类」的概念
这只是「对象」,不是「面向对象」。
2. 工厂函数:第一次封装(已经有了雏形)
function createCat(name, color) {
return {
name: name,
color: color,
eat: function() { console.log('吃 Jerry') }
}
}
const cat1 = createCat('大橘', '橘色')
const cat2 = createCat('加菲', '橘色')
优点:不再手写重复代码
缺点:
- 每次创建对象,
eat方法都会重新创建一份,仍然浪费内存 cat1.constructor === Object,看不出它们是「猫」
3. 构造函数模式:真正的「类」诞生了
function Cat(name, color) {
this.name = name
this.color = color
// 注意:这里千万别写方法!会重复创建!
}
const cat1 = new Cat('大橘', '橘色')
const cat2 = new Cat('加菲', '橘色')
console.log(cat1.constructor === Cat) // true
console.log(cat1 instanceof Cat) // true
new 到底干了什么?(面试必问!)
- 创建一个空对象
{} - 将
this绑定到这个新对象 - 执行构造函数代码,给新对象添加属性
- 自动返回这个新对象(除非手动 return 其他对象)
这才是真正意义上的「实例化」!
4. 原型 prototype:解决方法重复创建的终极方案
如果把每个实例独有的属性(name、color)放 this 上,把所有实例共享的属性和方法放 prototype 上,就能完美解决内存浪费问题。
function Cat(name, color) {
this.name = name
this.color = color
}
Cat.prototype.type = '猫科动物'
Cat.prototype.eat = function() {
console.log('喜欢吃 Jerry')
}
const cat1 = new Cat('大橘', '橘色')
const cat2 = new Cat('加菲', '橘色')
cat1.eat() // 正常调用
console.log(cat1.type) // 猫科动物
为什么这么香?
eat方法只在内存中存在一份,所有实例共享- 修改原型会实时影响所有实例
易错点提醒:
cat1.type = '铲屎官的主人' // 只改了 cat1 实例自身
console.log(cat2.type) // 仍然是 '猫科动物'
Cat.prototype.type = '猫科' // 改原型,所有实例立刻更新
判断属性来源的三个常用方法(建议熟记):
cat1.hasOwnProperty('name') // true → 实例自身
cat1.hasOwnProperty('type') // false → 来自原型
'type' in cat1 // true → 原型或自身都有算
判断实例:谁是不是谁的实例
console.log(cat1 instanceof Cat)//true
这就是传说中的「原型链查找机制」:
当你访问一个对象的属性时,先查自身 → 没有就沿着
__proto__往上找 → 直到找到或到 null 为止
这也是为什么 cat1.toString() 能用,因为 Object.prototype 上有!
6. 继承:最让人头疼的知识点(我们用最清晰的方式讲)
目标:让 Cat 继承 Animal 的属性和方法。
有瑕疵做法(少用!)
function Animal() {
this.species = '动物'
}
function Cat(name, color) {
this.name = name
this.color = color
}
Cat.prototype = new Animal() // 改变了consreuctor属性!
正确做法:构造函数继承 + 原型链继承
// 父类
function Animal() {
this.species = '动物'
this.eat = function() {
console.log('吃东西')
}
}
Animal.prototype.sayHi = function() {
console.log('你好呀~')
}
// 子类
function Cat(name, color) {
Animal.call(this) // ① 继承实例属性(关键!)
this.name = name
this.color = color
}
// ② 继承原型方法(关键!)
Cat.prototype = new Animal() // 或者更现代的 Object.create
// 修复 constructor 指向
Cat.prototype.constructor = Cat
const cat1 = new Cat('加菲', '橘色')
console.log(cat1.species) // 动物
cat1.eat() // 吃东西
cat1.sayHi() // 你好呀~
两步缺一不可:
| 步骤 | 目的 | 方法 |
|---|---|---|
| ① 继承实例属性 | 让子类拥有父类的 this.xxx | Animal.call(this) 或 apply |
| ② 继承原型方法 | 让子类能访问父类原型上的方法 | Cat.prototype = new Animal() |
现代推荐写法(ES5+):
Cat.prototype = Object.create(Animal.prototype)
Cat.prototype.constructor = Cat
更简洁,且不执行 Animal 构造函数。
7. ES6 class:只是语法糖,但写起来真香
小知识:什么是语法糖呢?
语法糖是指编程语言中那些让代码更易读、更简洁的语法特性,它不会增加语言的功能,只是提供了更便捷的表达方式。(底层实现相同)
class Animal {
constructor() {
this.species = '动物'
}
sayHi() {
console.log('你好')
}
}
class Cat extends Animal {
constructor(name, color) {
super() // 必须先调用 super!
this.name = name
this.color = color
}
eat() {
console.log('吃 Jerry')
}
}
const cat = new Cat('大橘', '橘色')
cat.sayHi() // 你好(来自父类原型)
注意:class 内部默认是严格模式!
注意:子类 constructor 必须先 super() 才能使用 this!
底层实现?还是原型!
console.log(cat.__proto__ === Cat.prototype) // true
console.log(Cat.prototype.__proto__ === Animal.prototype) // true
所以:class 只是让你用更像 Java/C++ 的方式写原型继承,本质没变!
总结:JavaScript OOP 全景图
| 阶段 | 写法 | 内存是否浪费 | 是否有继承关系 | 推荐度 |
|---|---|---|---|---|
| 对象字面量 | {} | 是 | 否 | ★☆☆☆☆ |
| 工厂函数 | createCat() | 是 | 否 | ★★☆☆☆ |
| 构造函数 + prototype | new Cat() + Cat.prototype | 否 | 是 | ★★★★☆ |
| ES5 继承 | call + prototype = new Parent() | 否 | 是 | ★★★★☆ |
| ES6 class | class Cat extends Animal | 否 | 是 | ★★★★★ |
原型链口诀
实例.__proto__ === 构造函数.prototype
构造函数.prototype.__proto__ === 父构造函数.prototype
Object.prototype.__proto__ === null
属性查找:先自身 → __proto__ → 再__proto__ → 直到 null
方法只在原型上放一份,所有实例共享
继承两步走:call 继承实例 + prototype 继承方法
class 是语法糖,底层还是原型链
灵魂追问环节(99% 的人死在这里)
Q1:为什么 Animal.call(this) 只能继承属性,不能继承原型上的方法?
因为 call/apply 只能“偷”父类构造函数里通过 this.xxx 添加的实例属性/方法,而真正的共享方法都应该放在 prototype 上,根本偷不到!
Q2:那 Cat.prototype = new Animal() 为什么说少用有瑕疵
因为它有两大致命副作用:
- 多执行一次 Animal 构造函数(可能有副作用)
- 彻底破坏了 constructor 指向(这就是罪魁祸首!)
那么这两个副作用又到底是什么意思呢 我们用最直观、最硬核的方式来告诉你:constructor 到底是个啥?被破坏后会发生什么?为什么必须手动修复?
1. constructor 本来是干什么的?
在 JavaScript 里,每个函数(包括构造函数)都会自动拥有一个 prototype 属性,而每个 prototype 对象上又天生自带一个 constructor 属性,它指向创建这个原型的函数本身。
JavaScript
function Animal() {}
console.log(Animal.prototype.constructor === Animal) // true
function Cat() {}
console.log(Cat.prototype.constructor === Cat) // true
这就像身份证一样,告诉全世界:“我这个原型对象是由哪个构造函数创造的”。
2. 当你写 Cat.prototype = new Animal() 的时候,发生了什么?
JavaScript
function Cat() {}
console.log(Cat.prototype.constructor) // 原来是 Cat
Cat.prototype = new Animal() // ← 一行代码,天崩地裂!
console.log(Cat.prototype.constructor) // 现在变成了 Animal!!!
Cat 的原型对象被彻底换掉了! 换成的是 new Animal() 创建出来的那个实例对象,而这个实例的 constructor 自然是 Animal!
text
原来的结构(正常):
Cat.prototype ──constructor ──→ Cat
现在的结构(被破坏了):
Cat.prototype(其实是 Animal 的实例) ──constructor ──→ Animal
3. 被破坏后到底会出什么问题?
问题①:instanceof 虽然还能用,但逻辑变得诡异
JavaScript
const cat = new Cat('大橘')
console.log(cat instanceof Cat) // 仍然是 true(因为原型链还在)
console.log(cat instanceof Animal) // 也是 true(正常)
但:
console.log(cat.constructor) // Animal!!!而不是 Cat!
console.log(cat.constructor.name) // "Animal" 而不是 "Cat"
这就很尴尬了:明明是一只猫,cat.constructor 却告诉你它是由 Animal 构造出来的!
问题②:很多框架/库/工具会偷偷依赖 constructor 判断类型
举几个真实场景(你以后一定会遇到):
JavaScript
// 场景1:JSON.stringify 自定义 toJSON 时可能判断 constructor
// 场景2:某些 ORM 框架(如 TypeORM)会用 constructor 做映射
// 场景3:React、Vue 某些旧插件会用 constructor.name 做调试或判断
// 场景4:你自己写工具函数时经常会用:
function getType(obj) {
return obj.constructor.name // 被破坏后就全乱了!
}
getType(cat) // 返回 "Animal" 而不是 "Cat" → 彻底懵逼
问题③:new Cat() 时,某些老浏览器或特殊代码会依赖 constructor
极少数情况下(老 IE、某些 polyfill),new 操作会偷偷读取 prototype.constructor 来做一些判断。虽然现代浏览器不这么干,但历史上确实出过血案。
4. 正确修复方式(两行代码,永绝后患)
JavaScript
function Cat(name, color) {
Animal.call(this) // 继承实例属性
this.name = name
this.color = color
}
Cat.prototype = Object.create(Animal.prototype) // 推荐写法
// Cat.prototype = new Animal() // 传统写法也行,但要修复
// 关键修复!必须加这一行!
Cat.prototype.constructor = Cat
修复后:
JavaScript
const cat = new Cat('大橘')
console.log(cat.constructor === Cat) // true
console.log(cat.constructor.name) // "Cat"
console.log(cat instanceof Cat) // true
console.log(cat instanceof Animal) // true
完美!所有问题全部解决!
搞懂原型链和继承,你就真正掌握了 JavaScript 的灵魂。从此不再被“call 还是 apply”“new Animal() 行不行”“constructor 到底要不要修”这些问题缠身。