JS面向对象:从"猫"的视角看JavaScript的OOP进化史

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.saycat2.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.prototypeObject.prototype "我只会叫,不会说话!"
Cat.prototype = Animal.prototype Cat.prototypeAnimal.prototype "我继承了爸爸的领带,但领带是爸爸的!"
Cat.prototype = new Animal() Cat.prototypeAnimal.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
必须手动修正 ,否则instanceoftoString()会出错。


🌟 猫的终极验证:为什么说这是"完美"继承?

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的"喵"式哲学。现在,让我们一起优雅地喵~ 🐱

相关推荐
小p1 小时前
react学习12:状态管理redux
前端·react.js
AAA阿giao1 小时前
深入理解 JavaScript 中的 Symbol:独一无二的“魔法钥匙”
前端·javascript·ecmascript 6
Gomiko1 小时前
JavaScript基础(七):数组
开发语言·javascript·ecmascript
lichong9511 小时前
Android 弹出进度条对话框 避免用户点击界面交互
java·前端·javascript
ycgg1 小时前
别再只用 --xxx!CSS @property 解锁自定义属性的「高级玩法」
前端·css
Amy_yang1 小时前
UniApp Vue3 词云组件开发实战:从原理到应用
javascript·vue.js·uni-app
JHC0000001 小时前
47. 全排列 II
开发语言·python·面试
灵犀坠1 小时前
前端知识体系全景:从跨域到性能优化的核心要点解析
前端·javascript·vue.js·性能优化·uni-app·vue
超哥的一天1 小时前
【前端】每天一个知识点-NPM
前端·node.js