JS面向对象:从"猫"的视角看JavaScript的OOP进化史
一只猫说:我本是一只普通的猫,却要被迫卷入JavaScript的OOP革命。------《猫的自白》
1. JS:不是真正的OOP,但"猫"式对象语言
"JS是一种基于对象(Object-based)的语言,你遇到的几乎所有东西都是有对象(连简单数据类型都有包装类)。他又不是一种真正的面向对象(OOP)..."
现实版: 就像你买了一台"智能猫",结果发现它只会喵喵叫,不会给你倒水。JS的"面向对象"就像这只"智能猫",包装得很高级,但底层还是"喵"的哲学。
javascript
// 简单数据类型也有包装类
let str = "Hello";
console.log(typeof str); // "string"
console.log(str.toUpperCase()); // "HELLO"
真相: JS没有真正的class,连ES6的class也只是语法糖,底层还是原型式OOP。就像你买了一台"智能猫",结果发现它只是个披着羊皮的猫。
2.对象字面量的"猫"式生活
"对象字面量创建实例太冗长,重复,需要的实例多就应接不暇且相互之间没有联系"
javascript
var Cat = { name: '', color: '' };
var cat1 = {};
cat1.name = '加菲猫';
cat1.color = '橘色';
var cat2 = {};
cat2.name = '黑猫警长';
cat2.color = '黑色';
console.log(cat1.name);
console.log(cat2.name);
console.log(cat1 instanceof Cat);
结果展示:
text
加菲猫
黑猫警长
TypeError: Right-hand side of 'instanceof' is not callable
为什么的console.log(cat1 instanceof Cat)结果会抛出错误呢?因为var Cat = { name: '', color: '' }; 是一个普通对象 (对象字面量),不是构造函数 !而 instanceof 操作符只认构造函数 (比如 function Cat() {})
猫的吐槽: "我是一只猫,不是流水线工人!为什么每次都要我重复'名字'和'颜色'?我只想优雅地喵一声!"
3.构造函数的"猫"式进化
"猫:我不要一次次重复------使用函数来封装对象"
javascript
function Cat(name, color) {
this.name = name;
this.color = color;
}
const cat1 = new Cat('加菲猫', '橘色');
const cat2 = new Cat('黑猫警长', '黑色');
console.log(cat1.name);
console.log(cat2.color);
console.log(cat1 instanceof Cat);
结果展示:
text
加菲猫
黑色
true
用 var Cat = { ... }(普通对象)当构造函数 → instanceof 直接崩溃,而function Cat(name, color) { ... } 是真正的构造函数 → cat1 是它创建的实例 → instanceof 瞬间认证成功!加菲猫和黑猫警长也终于建立起了联系!
猫的自白: "终于不用重复写了!我终于能用new来召唤自己了!但为什么this指向这么神秘?"
真相:
new操作符创建了一个空对象,this指向这个空对象。这就是JS的"猫式"实例化魔法!
4. 构造函数的痛点
"通过构造函数来写对象共有的属性和方法十分浪费,且容易被外界修改"
javascript
function Cat(name, color) {
this.name = name;
this.color = color;
this.say = function() {
console.log('喵~');
};
}
const cat1 = new Cat('加菲猫', '橘色');
const cat2 = new Cat('黑猫警长', '黑色');
console.log(cat1.say === cat2.say); // false
结果展示:
text
false
为什么结果是false?每次调用new Cat(),都会在实例上重新创建一个新的say函数。就像你每次做蛋糕都要重新买一次面粉一样,虽然做出来的蛋糕味道一样,但面粉是新的、独立的。
所以cat1.say和cat2.say是两个完全不同的函数对象,尽管它们的代码一模一样,但它们在内存中是两个不同的位置,就像两个不同的蛋糕。 想象一下,如果你有1000个猫对象,每个对象都有一个独立的say方法,那会浪费很多内存。
这就是为什么cat1.say === cat2.say会返回false。
猫的悲鸣: "我只想喵一声,为什么每次都要创建一个新的say方法?我的内存不够了!"
5.prototype的"猫"式革命
"下面这串代码就是很好的例子,使用prototype解决浪费和共享"
javascript
function Cat(name, color) {
this.name = name;
this.color = color;
}
Cat.prototype.say = function() {
console.log('喵~');
};
const cat1 = new Cat('加菲猫', '橘色');
const cat2 = new Cat('黑猫警长', '黑色');
console.log(cat1.say === cat2.say); // true
结果展示:
text
true
原型(prototype)就像猫的祖先,有种"不管外国喵还是本土喵,统统都为我后"的先祖之态,所有Cat实例(cat1、cat2)都从同一个猫的后代,所以它们指向的函数是同一个对象。
猫的欢呼: "终于!我再也不用重复创建say方法了!我的内存终于能喘口气了!"
真相:
prototype让所有实例共享同一个方法,就像猫群共享同一个喵声。
6.ES6 class的"猫"式语法糖
猫的真相: "哦,ES6给我加了个'猫式class',看起来很高级,但其实只是给原型链穿了件小西装!让我扒开看看------"
🔍 底层真相:class是如何被JS引擎"翻译"的
当JS引擎看到class Cat,它会自动执行以下编译步骤(伪代码级解释):
javascript
// 1. 创建构造函数(相当于 constructor 方法)
function Cat(name, color) {
this.name = name;
this.color = color;
}
// 2. 将类方法添加到 prototype 上(关键!)
Cat.prototype.say = function() {
console.log('喵~');
};
// 3. 设置 constructor 属性(ES6 会自动添加)
Cat.prototype.constructor = Cat;
这就是class的全部! 没有魔法,只有原型链的优雅包装。
🧪 用代码直面真相:揭开class的"西装"
javascript
// 模拟ES6引擎的编译过程
function Cat(name, color) {
this.name = name;
this.color = color;
}
Cat.prototype.say = function() { console.log('喵~'); };
Cat.prototype.constructor = Cat;
// 创建实例
const cat1 = new Cat('加菲猫', '橘色');
const cat2 = new Cat('黑猫警长', '黑色');
// 验证核心真相
console.log("1. class 编译后是函数?", typeof Cat === "function"); // true
console.log("2. 实例的 __proto__ 指向 Cat.prototype?",
cat1.__proto__ === Cat.prototype); // true
console.log("3. say 方法是否共享?",
cat1.say === cat2.say); // true
// 深度验证:直接查看原型链
console.log("4. Cat.prototype 的结构:",
Object.getOwnPropertyNames(Cat.prototype));
// 输出: ["say", "constructor"]
console.log("5. 实例的 say 属性是否存在?",
'say' in cat1); // false(不在实例上,而在原型上)
结果展示:
javascript
1. class 编译后是函数? true
2. 实例的 __proto__ 指向 Cat.prototype? true
3. say 方法是否共享? true
4. Cat.prototype 的结构: ["say", "constructor"]
5. 实例的 say 属性是否存在? false
🐱 猫的终极真相:为什么说class只是语法糖?
✅ 证据1:instanceof的底层逻辑
javascript
console.log(cat1 instanceof Cat); // true
console.log(cat1 instanceof Object); // true
// 实际上,instanceof 在JS引擎中这样工作:
function instanceof(obj, constructor) {
const proto = obj.__proto__;
return proto === constructor.prototype ||
(proto && instanceof(proto, constructor));
}
猫的吐槽: "我是一只猫,但JS说我是Cat的实例,也是Object的实例------原来我同时是猫和万物之猫!"
✅ 证据2:constructor属性的陷阱
javascript
console.log("constructor 属性指向:", Cat.prototype.constructor);
// 输出: ƒ Cat(name, color) { ... }
// 但如果我们覆盖了 prototype:
Cat.prototype = { say: function() { console.log('喵喵!'); } };
console.log("覆盖后 constructor:", Cat.prototype.constructor);
// 输出: ƒ Object() { [native code] } (不再是Cat!)
猫的警告: "别乱改prototype!否则你的constructor会变成'万物之祖'------就像给猫穿了件西装,结果西装变成了猫的爸爸!"
✅ 证据3:class和手动原型的完全等价
javascript
// 手动实现(ES5写法)
function CatManual(name, color) {
this.name = name;
this.color = color;
}
CatManual.prototype.say = function() { console.log('喵~'); };
// class实现
class CatClass {
constructor(name, color) { this.name = name; this.color = color; }
say() { console.log('喵~'); }
}
// 验证等价性
console.log("手动 vs class 是否等价:",
CatManual.prototype.say === CatClass.prototype.say); // true
💡 为什么理解底层如此重要?(猫的血泪教训)
| 场景 | 错误写法 | 底层真相 | 猫的悲鸣 |
|---|---|---|---|
| 继承 | class Cat extends Animal |
实际是Cat.prototype = Object.create(Animal.prototype) |
"我继承了爸爸的毛色,但为什么constructor指向了妈妈?" |
| 方法覆盖 | Cat.prototype.say = function() { ... } |
覆盖了Cat.prototype,导致constructor丢失 |
"我改了喵声,结果猫的身份证没了!" |
| 静态方法 | static eat() { ... } |
实际是Cat.eat = function() { ... } |
"我只想喵,为什么还要吃?" |
猫的血泪总结: "ES6的class让你用OOP思维写JS,但当你遇到
constructor丢失、继承链断裂时,必须知道原型链的真相------否则你就是那只在迷宫里乱跑的猫!"
7.apply继承的"猫"式尴尬
".apply只能继承父类的属性而不能继承方法"
javascript
function Animal(name, color) {
this.name = name;
this.color = color;
}
function Cat(name, color) {
Animal.apply(this, [name, color]);
}
const cat = new Cat('加菲猫', '橘色');
console.log(cat.name); // "加菲猫"
console.log(cat.color); // "橘色"
console.log(cat.say); // undefined
结果展示:
text
加菲猫
橘色
undefined
🔍 底层真相:为什么.apply只能继承属性,不能继承方法?
当JS引擎执行以下代码时,发生了什么?
javascript
function Animal(name, color) {
this.name = name;
this.color = color;
}
Animal.prototype.say = function() {
console.log('喵~');
};
function Cat(name, color) {
Animal.apply(this, [name, color]); // 重点在这里
}
const cat = new Cat('加菲猫', '橘色');
🧪 用Chrome DevTools扒开JS引擎的"内裤"
在控制台执行console.dir(Cat),你会看到:
javascript
function Cat(name, color) {
Animal.apply(this, [name, color]);
}
执行console.dir(cat),结果:
javascript
{
name: "加菲猫",
color: "橘色",
__proto__: {
constructor: ƒ Cat(name, color)
}
}
关键发现: cat.__proto__ 指向 Cat.prototype,而 Cat.prototype 是默认的空对象 {}(因为没手动设置)。
而 Animal.prototype.say 仍然在 Animal.prototype 上,和 cat 的原型链完全无关。
猫的吐槽: "我继承了爸爸的'名字'和'颜色',但为什么没有'喵'声?这不就是个没有声音的猫吗?"
8.的"猫"式完美继承
javascript
function Animal(name, color) {
this.name = name;
this.color = color;
}
Animal.prototype.say = function() {
console.log('喵~');
};
function Cat(name, color) {
Animal.call(this, name, color);
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
const cat = new Cat('加菲猫', '橘色');
console.log(cat.name); // "加菲猫"
console.log(cat.say); // "喵~"
console.log(cat instanceof Cat); // true
console.log(cat instanceof Animal); // true
结果展示:
arduino
text
加菲猫
喵~
true
true
让我们用**Chrome DevTools的"透视眼"**扒开JS引擎的底层逻辑(伪代码级解析):
ini
javascript
// 1. 创建Animal实例(关键!)
const animalInstance = new Animal(); // 执行Animal构造函数
animalInstance.__proto__ = Animal.prototype; // 原型链建立
// 2. 将Cat的原型指向这个实例
Cat.prototype = animalInstance;
// 于是Cat.prototype的__proto__ = Animal.prototype
// 3. 创建Cat实例(new Cat())
const cat = new Cat('加菲猫', '橘色');
// 1) 创建空对象:cat = {}
// 2) 设置原型链:cat.__proto__ = Cat.prototype (即animalInstance)
// 3) 调用构造函数:Animal.call(cat, '加菲猫', '橘色') → 给cat添加name/color
猫的终极真相:
Cat.prototype = new Animal() 实际上是在原型链上"嫁接"了Animal的DNA,而不是简单复制属性!
🧪 深度验证:用代码"解剖"猫的血脉
javascript
javascript
// 1. 验证原型链结构
console.log("Cat.prototype的__proto__:",
Cat.prototype.__proto__);
// 输出: Animal.prototype (这才是关键!)
// 2. 验证实例的原型链
console.log("cat.__proto__:", cat.__proto__);
// 输出: Cat.prototype (即animalInstance)
console.log("cat.__proto__.__proto__:",
cat.__proto__.__proto__);
// 输出: Animal.prototype (终于找到say方法!)
// 3. 验证方法继承
console.log("cat.say exists?", 'say' in cat); // true
console.log("cat.say:", cat.say); // ƒ say() { console.log('喵~'); }
// 4. 验证实例身份
console.log("cat instanceof Cat:", cat instanceof Cat); // true
console.log("cat instanceof Animal:", cat instanceof Animal); // true
结果展示:
yaml
text
Cat.prototype的__proto__: {say: ƒ, constructor: ƒ}
cat.__proto__: {name: "加菲猫", color: "橘色", __proto__: ...}
cat.__proto__.__proto__: {say: ƒ, constructor: ƒ}
cat.say exists?: true
cat.say: ƒ say() { console.log('喵~'); }
cat instanceof Cat: true
cat instanceof Animal: true
🐱 猫的血泪对比:为什么这个方案"完美"?
| 方案 | 原型链 | 属性继承 | 方法继承 | 猫的评价 |
|---|---|---|---|---|
.apply |
Cat.prototype → Object.prototype |
✅ | ❌ | "我只会叫,不会说话!" |
Cat.prototype = Animal.prototype |
Cat.prototype → Animal.prototype |
✅ | ✅ | "我继承了爸爸的领带,但领带是爸爸的!" |
Cat.prototype = new Animal() |
Cat.prototype → Animal.prototype |
✅ | ✅ | "我继承了爸爸的血脉,但领带是新的!" |
猫的真相:
Cat.prototype = new Animal()创建了独立的原型链 ,而Cat.prototype = Animal.prototype是共享同一个原型对象 。用猫的视角:
new Animal()→ 给猫生了个"新爸爸"(原型链独立)Animal.prototype→ 直接用"老爸爸"(共享原型,容易被修改)
💡 为什么需要Cat.prototype.constructor = Cat?(猫的血泪教训)
javascript
javascript
// 未修正前
console.log("Cat.prototype.constructor:", Cat.prototype.constructor);
// 输出: ƒ Animal(name, color) { ... } (错误!指向Animal)
// 修正后
Cat.prototype.constructor = Cat;
console.log("修正后 constructor:", Cat.prototype.constructor);
// 输出: ƒ Cat(name, color) { ... } (正确!)
猫的悲鸣:
"我本是Cat,但JS引擎说我是个Animal!这就像我叫加菲猫,但身份证写成'黑猫警长'------太尴尬了!"
底层真相:
new Animal()会将Animal.prototype.constructor赋值给新实例的constructor,导致Cat.prototype.constructor指向Animal。
必须手动修正 ,否则instanceof和toString()会出错。
🌟 猫的终极验证:为什么说这是"完美"继承?
javascript
javascript
// 创建两个Cat实例
const cat1 = new Cat('加菲猫', '橘色');
const cat2 = new Cat('黑猫警长', '黑色');
// 验证方法共享(关键!)
console.log("cat1.say === cat2.say:", cat1.say === cat2.say); // true
// 验证属性独立(不共享)
cat1.name = '新名字';
console.log("cat1.name:", cat1.name); // "新名字"
console.log("cat2.name:", cat2.name); // "黑猫警长" (未受影响)
// 验证原型链安全
Animal.prototype.say = function() { console.log('喵喵!'); };
console.log("cat1.say:", cat1.say); // "喵喵!" (方法被修改,但Cat实例不受影响)
结果展示:
javascript
text
cat1.say === cat2.say: true
cat1.name: 新名字
cat2.name: 黑猫警长
cat1.say: ƒ say() { console.log('喵喵!'); }
猫的顿悟:
"我继承了爸爸的'喵'声,但不会被爸爸的'喵'声修改影响;
我有自己的名字,爸爸的名字不会跑到我身上------
这才是真正的猫式继承! "
结语:JS面向对象的进化史
从刚开始的"猫式冗长",到后来的的"构造函数魔法",再到"猫"的"prototype革命",到最后的的"完美继承",JS的OOP进化史就是一只猫从"喵喵叫"到"会说话"的历程。
真相总结:
- JS不是真正的OOP,而是"基于对象"的语言
- ES6的
class只是语法糖,底层还是原型式prototype是JS面向对象的核心new操作符创建实例,this指向新对象- 继承的最佳实践:
Cat.prototype = new Animal()
最后,一只猫的感悟:
"我是一只猫,不是对象。但JS的OOP让我学会了优雅地喵~"
附:猫式总结表
| 方式 | 优点 | 缺点 | 猫的评价 |
|---|---|---|---|
| 对象字面量 | 简单 | 冗长、无联系 | "我只想喵,不想写这么多代码!" |
| 构造函数 | 封装实例化 | 方法重复 | "我的内存快不够了!" |
| prototype | 共享方法 | 需要手动设置constructor | "终于不用重复创建方法了!" |
| ES6 class | 语法优雅 | 底层还是原型 | "穿了西装,还是那只猫" |
| .apply继承 | 继承属性 | 不能继承方法 | "我只会叫,不会说话" |
| Cat.prototype = new Animal() | 完美继承 | 需要设置constructor | "我终于能优雅地喵了!" |
最后的喵: JS的OOP不是终点,而是起点。理解了原型,你就理解了JS的"喵"式哲学。现在,让我们一起优雅地喵~ 🐱