💡 JavaScript 是一门基于对象(Object-based)的语言。
它虽不像 Java、C++ 那样是“纯正”的面向对象语言,但通过 原型链(Prototype Chain) 机制,依然实现了 OOP 的三大核心思想:封装、继承、多态。
本文将带你从零开始,深入理解 JS 中的 OOP 实现方式。
🔍 一、为什么说 JS 是“基于对象”而非“真正面向对象”?
- 📦 万物皆对象:字符串、数字等基本类型也有对应的包装对象(如
new String())。 - ⏳ 历史原因:早期 JS 没有
class关键字,直到 ES6 才引入——但它只是 语法糖 🍬。 - 🔄 底层机制:JS 的 OOP 基于 原型(Prototypal) ,而非传统“类(Class-based)”。
✅ 核心概念速览:
- 🗃️ 对象字面量 → 封装数据
- 🏗️ 构造函数 +
new→ 创建实例- 🔗
prototype→ 共享方法与属性- 🧬 原型链 → 实现继承
🧱 二、原始模式:对象字面量
最简单的封装方式:
var Cat = {
name: "",
color: ""
};
var cat1 = { name: '加菲猫', color: '橘色' };
var cat2 = { name: '黑猫警长', color: '黑色' };
📌 注:
Cat首字母大写是开发约定,表示“类模板”。
❌ 问题:
- 🔄 代码重复:每个实例都要手写属性
- 🤝 无关联性:无法判断
cat1和cat2是否同属一类 - ♻️ 无法复用方法:比如
eat()要写两遍
🏗️ 三、构造函数模式:封装实例化过程
使用函数作为“构造器”,配合 new:
function Cat(name, color) {
this.name = name;
this.color = color;
}
const cat1 = new Cat('Tom', '黑色');
const cat2 = new Cat('哆啦A梦', '蓝色');
console.log(cat1.constructor === Cat); // ✅ true
console.log(cat1 instanceof Cat); // ✅ true
🔧 new 的执行过程(关键!):
new创建一个空Cat对象{};- 🎯 将
this指向这个新对象; - 🛠️ 执行构造函数代码(为
this添加属性); - 📤 返回这个新对象。
⚠️ 注意:若直接调用
Cat()(不加new),this会指向全局对象(如window),造成污染!
🔗 四、原型模式:解决方法重复问题
❌ 问题代码(浪费内存):
function Cat(name, color) {
this.eat = function() { console.log('吃Jerry'); }; // 每个实例都新建函数!
}
✅ 解决方案:使用 prototype 共享方法
function Cat(name, color) {
this.name = name;
this.color = color;
}
Cat.prototype.type = '猫科动物';
Cat.prototype.eat = function() {
console.log('eat Jerry');
};
🧬 原型链关系图解:
cat1.__proto__ → Cat.prototype → Object.prototype → null
↑ ↑
实例对象 构造函数的原型对象
🔎 四个关键判断(彻底搞懂属性来源)
1️⃣ cat1.constructor === Cat → ✅ true
🧾 含义:这个对象是由哪个构造函数创建的?
🧠 原理:cat1自身没有constructor,但通过原型链继承自Cat.prototype.constructor。(超关键)
2️⃣ Cat.prototype.isPrototypeOf(cat1) → ✅ true
🧬 含义:
Cat.prototype是不是cat1的原型?
🔍 用途:检测“血缘关系”,确认原型链连接。
3️⃣ cat1.hasOwnProperty('name') → ✅ true
🏠 含义:
name是cat1自己的属性吗?
✅ 只查自身,不看原型链。
4️⃣ 'type' in cat1 → ✅ true
🌐 含义:
cat1能不能用到type?
✅ 查自身 + 整个原型链。
📊 对比总结:
| 方法 / 操作符 | 查找范围 | 用途说明 |
|---|---|---|
obj.hasOwnProperty('prop') | 🏠 仅自身属性 | 判断是不是“自己的” |
'prop' in obj | 🌐 自身 + 原型链 | 判断能不能“用到”这个属性 |
💬 一句话记住:
hasOwnProperty看“产权证”,in看“使用权”。
🧬 五、继承:让子类拥有父类的能力
方法1️⃣:借用构造函数(仅继承实例属性)
function Animal() {
this.species = '动物';
}
function Cat(name, color) {
Animal.apply(this); // 👈 关键!
this.name = name;
this.color = color;
}
🔧
Animal.apply(this):调用Animal,并让其内部的this指向当前Cat实例,从而添加species属性。
❌ 缺点:
无法继承
Animal.prototype上的方法!
因为apply只执行函数体,没有修改原型链,所以sayHi()等方法无法访问。
方法2️⃣:原型链继承(完整继承)→ 组合继承
function Animal() {
this.species = '动物';
}
Animal.prototype.sayHi = function() {
console.log('啦啦啦啦啦');
};
function Cat(name, color) {
Animal.apply(this); // 继承实例属性
this.name = name;
this.color = color;
}
// 🔑 关键:让 Cat 的原型 = Animal 的实例
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat; // 修复 constructor
🧠 原理:
cat.__proto__→new Animal()→Animal.prototype- 于是
cat.sayHi()可以顺着原型链找到方法!
✅ 这就是经典的 组合继承:构造函数(属性)+ 原型链(方法)
🍬 六、ES6 class:语法糖,更清晰的 OOP 写法
什么是“语法糖”(Syntactic Sugar)?
语法糖(Syntactic Sugar)是指编程语言中添加的某种语法,这种语法对语言的功能没有影响,但更方便程序员阅读和编写代码。
简单说:
语法糖 = 更甜、更好写的写法,底层其实没变。
它就像给苦咖啡加了一勺糖——味道更好了,但咖啡还是那杯咖啡 ☕。
class Animal {
constructor() {
this.species = '动物';
}
sayHi() {
console.log('啦啦啦啦啦');
}
}
class Cat extends Animal {
constructor(name, color) {
super(); // 👈 调用父类构造函数
this.name = name;
this.color = color;
}
}
🧩 class 与原型的关系:
cat.__proto__ === Cat.prototype; // ✅ true
Cat.prototype.constructor === Cat; // ✅ true
Cat.prototype.__proto__ === Animal.prototype; // ✅ true
💡
extends本质就是:
Cat.prototype.__proto__ = Animal.prototype
📈 七、JS OOP 的演进路径
| 阶段 | 方式 | 特点 |
|---|---|---|
| 🟢 初级 | 对象字面量 | 简单但无法复用 |
| 🔵 进阶 | 构造函数 | 封装实例化,但方法重复 |
| 🟡 成熟 | 原型模式 | 方法共享,节省内存 |
| 🔴 完整 | 组合继承 | 构造函数 + 原型链,完整继承 |
| 🟣 现代 | ES6 class | 语法糖,语义清晰,贴近传统 OOP |
✅ 八、最佳实践建议
-
🎯 优先使用
class(ES6+) :语义清晰,不易出错; -
🚫 避免在构造函数中定义方法:应放在
prototype或class方法中; -
🔁 继承时记得调用
super(); -
🧠 理解两个关键属性的区别:
__proto__:实例的原型(指向构造函数的prototype)prototype:构造函数的原型对象(用于共享方法)
🌟 结语
JavaScript 的面向对象虽不同于传统语言,但其 灵活、动态、基于原型 的特性,反而赋予了它强大的表达能力。
🧠 记住:
“在 JS 中,万物皆对象,而对象皆可继承。”
掌握原型链,是成为高级 JS 开发者的必经之路!
✅ 现在,你已经打通了 JS OOP 的任督二脉! 💪