🐱 从“猫厂”倒闭到“鸭子”横行:一篇让你笑出腹肌的 JS 面向对象指南

🐱 从"猫厂"倒闭到"鸭子"横行:一篇让你笑出腹肌的 JS 面向对象指南

摘要 :还在为 this 指向头晕?还在纠结 prototype__proto__ 的爱恨情仇?别慌!今天咱们不整那些晦涩的术语,把 JavaScript 的面向对象(OOP)想象成一家名为"JS 动物园"的创业公司。从手写"猫厂"的辛酸史,到 ES6 的"精装房",再到"鸭子类型"的野生智慧,带你笑着把 OOP 拿捏得死死的。

关键词:JavaScript, OOP, 原型链, 继承, 多态, 鸭子类型, 稀土掘金风格


🎬 序幕:JS 是个"假"面向对象?

听说你想学面向对象(OOP)?朋友,欢迎来到 JavaScript 的世界。这里有点特殊。

在 Java 或 C++ 的世界里,一切皆类(Class),严丝合缝,像是一个纪律严明的军队。但在 JS 早期(ES5 之前),这地方简直就是个** anarchic(无政府主义)** 的集市。

JS 官方自己都承认:"我是基于对象(Object-based)的,但我不是那种传统的面向对象(OOP)。"

为啥?因为早年连个 class 关键字都没有!你想搞个"猫"?行,你自己捏个泥人(对象字面量)吧。

javascript 复制代码
// 😭 原始社会的痛苦:手工打造每一只猫
var cat1 = { name: 'Tom', age: 3, color: 'gray' };
var cat2 = { name: 'Kitty', age: 2, color: 'white' };
// ...如果要造一万个猫,你的手会断,内存会爆(每个方法都复制一份)

这哪叫编程?这叫"捏泥人"!于是,聪明的程序员发明了构造函数


🏗️ 第一章:封装------"猫厂"流水线诞生

为了不再手捏泥人,我们决定开个"猫厂"。构造函数就是那条流水线。

1. 神奇的 new 关键字

当你写下 new Cat() 时,JS 引擎其实像个贴心的管家,在幕后帮你干了四件事(这才是面试考点!):

  1. 凭空变出一个空对象 {}
  2. 绑定 this :把这个空对象塞给构造函数里的 this
  3. 执行代码:往这个空对象里塞属性(name, age...)。
  4. 返回对象:除非你非要返回别的,否则就把这个填好的对象交给你。
javascript 复制代码
function Cat(name, age, color) {
    // 此时 this 就是那个刚出生的空对象
    this.name = name;
    this.age = age;
    this.color = color;
    
    // ❌ 错误示范:把方法写在里面
    // 这意味着每生一只猫,就要重新打印一份"吃饭说明书",浪费纸张(内存)!
    this.eat = function() { console.log(this.name + '正在吃饭'); }
}

const tom = new Cat('Tom', 3, 'gray');
const kitty = new Cat('Kitty', 2, 'white');

// ✅ 验证身份
console.log(tom instanceof Cat); // true:Tom 确实是猫厂出品的
console.log(tom.constructor === kitty.constructor); // true:都是同一个厂长造的

2. 原型(Prototype):公用的"食堂"和"图书馆"

既然每只猫都复印一份"吃饭说明书"太浪费,那我们能不能把说明书贴在工厂墙上?谁需要谁去看?

这就是 prototype 的本质!

  • 实例属性 (如名字、年龄):每只猫都不一样,必须自己带着(写在 this 上)。
  • 共有方法 (如 eat, sleep):所有猫都一样,挂在工厂的墙上(写在 Cat.prototype 上)。
javascript 复制代码
function Cat(name, age, color) {
    this.name = name;
    this.age = age;
    this.color = color;
}

// ✅ 正确姿势:方法挂到原型上,大家共享
Cat.prototype.eat = function() {
    console.log(this.name + '正在吃饭');
};
Cat.prototype.type = '猫科动物';

const tom = new Cat('Tom', 3, 'gray');
const kitty = new Cat('Kitty', 2, 'white');

tom.eat(); // Tom 正在吃饭
kitty.eat(); // Kitty 正在吃饭
// 内存里只有一份 eat 函数,省下的钱可以买小鱼干啦!
🔍 灵魂三问:属性到底在哪?

这时候你可能会晕:tom.type 到底是谁的?

  1. hasOwnProperty :查户口。只查自己身上 有没有,不管祖传的。
    • tom.hasOwnProperty('name')true (自己的)
    • tom.hasOwnProperty('type')false (祖传的,在原型上)
  2. in 操作符 :查家族谱。只要自己或祖先 有,就算有。
    • "type" in tomtrue
  3. 属性遮蔽(Shadowing) :如果你非要给自己改个类型 tom.type = '怪兽',那你身上就多了一个自有属性,把祖传的挡住了。但隔壁 Kitty 还是"猫科动物"。

🧬 第二章:继承------"拼爹"的艺术

猫厂做大了,想拓展业务做"老虎"、"狮子"。它们都有"猫"的特征,但又有点不一样。这就需要继承

在 JS 里,继承就是一场**"拼爹" + "偷师"**的组合拳。

1. 借用构造函数(拼爹,只传家产)

想在子类里拥有父类的属性(比如 species),直接用 apply 把父类的 this 绑过来。

javascript 复制代码
function Animal() {
    this.species = '动物'; // 家产
}

function Cat(name, color) {
    // 👇 关键:把 Animal 的 this 绑定到 Cat 的实例上
    Animal.apply(this); 
    this.name = name;
    this.color = color;
}
// 缺点:只能继承家产(属性),学不到手艺(原型上的方法)

2. 原型链继承(偷师,只学手艺)

想让子类学会父类的方法(比如 sayHi),直接把子类的原型指向父类的实例。

javascript 复制代码
Cat.prototype = new Animal(); 
// 现在 Cat 的实例顺着 __proto__ 就能找到 Animal 的方法了
  • 缺点:没法给父类传参,而且所有猫共享一份家产(引用类型会打架)。

3. 终极方案:组合继承(既要家产,又要手艺)

这也是 ES6 class extends 底层的逻辑:

  1. apply 继承属性。
  2. Object.createnew 继承方法。
  3. 别忘了修好 constructor 指针,不然猫会以为自己是动物造的。

🎩 第三章:ES6 Class------给"毛坯房"装上了"精装门脸"

ES6 来了,老板说:"JS 太土了,我们要跟 Java 看齐!" 于是 class 关键字横空出世。

⚠️ 真相预警class 只是语法糖!它底层还是原型那一套,只是让你写起来像那么回事。

javascript 复制代码
class Cat {
    constructor(name, age, color) {
        this.name = name;
        this.age = age;
        this.color = color;
    }
    
    // 注意:这里的方法自动挂到 prototype 上,不用手动写了!
    eat() {
        console.log(`${this.name} 正在吃鱼`);
    }
    
    // ⚠️ 陷阱:这种写法是实例属性,每只猫都有一份!
    type = '猫科动物'; 
}

const tom = new Cat('Tom', 3, 'gray');
tom.eat();

// 看看它的内心(原型链)
console.log(tom.__proto__ === Cat.prototype); // true
console.log(Cat.prototype.__proto__ === Object.prototype); // true

一句话总结 :写 class 爽歪歪,心里要懂 prototype


🦆 第四章:多态------"管它是猫是狗,会叫就是好兽"

多态,听起来高大上,其实就是:"一种接口,多种实现"。 在 JS 里,多态有两种玩法:

玩法一:正统派(基于继承)

就像动物园点名。

javascript 复制代码
class Animal { speak() { return '声音'; } }
class Dog extends Animal { speak() { return '汪汪'; } }
class Cat extends Animal { speak() { return '喵喵'; } }

// 统一调用,各自表演
[new Dog(), new Cat()].forEach(animal => console.log(animal.speak()));
  • 优点:结构清晰,适合大型系统。
  • 缺点:得先建个庞大的家族族谱。

玩法二:野路派(鸭子类型 Duck Typing)🌟 JS 的精髓

JS 最牛的地方在于:我不关心你是什么类,我只关心你会不会这个方法。

"如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。"

javascript 复制代码
// 一个机器人,没继承任何类
const robot = { speak() { return '滴滴滴'; } };

// 一个路人甲,也没继承
const human = { speak() { return '你好'; } };

// 甚至是一个字面量
const dog = { speak: () => '汪汪' };

// 通用函数:完全不关心出身,只看能力
function makeItSpeak(entity) {
    if (typeof entity.speak === 'function') {
        console.log(entity.speak());
    }
}

makeItSpeak(robot); // 滴滴滴
makeItSpeak(human); // 你好
makeItSpeak(dog);   // 汪汪

为什么这很酷? 因为在 JS 的实际开发中(尤其是回调、Promise、事件处理),我们很少去定义复杂的类层级。我们更关心:"传进来的这个家伙,有没有我要的那个方法?" 如果有,那就干!

这种弱约束、高灵活的特性,让 JS 在处理异步和函数式编程时如鱼得水。


📝 结语:JS OOP 的生存法则

回顾一下我们在"JS 动物园"的旅程:

  1. 封装 :别手捏泥人了,用构造函数或 class 建流水线。记得把公用方法扔进 prototype 省内存。
  2. 继承 :想"拼爹"用 apply,想"偷师"改 prototype。ES6 的 extends 帮你把脏活累活都干了。
  3. 多态 :大项目用继承规范行为,小工具或回调函数用鸭子类型放飞自我。
  4. 核心心法 :无论语法怎么变(从 functionclass),原型链(__proto__ 永远是 JS 对象世界的底层逻辑。

总结:

JS 的面向对象,表面看是 class 的优雅,实则是 prototype 的狂野。理解了"鸭子类型",你才算真正懂得了 JS 的自由灵魂。🦆✨


觉得有用?点个赞👍,让更多人在"JS 动物园"里不再迷路!

相关推荐
码路飞2 小时前
GPT-5.4 Computer Use 实战:3 步让 AI 操控浏览器帮你干活 🖥️
java·javascript
进击的尘埃2 小时前
Service Worker 离线缓存这事,没你想的那么简单
javascript
进击的尘埃2 小时前
HTTP/3 的多路复用和 QUIC 到底能让页面快多少?聊聊连接迁移和 0-RTT
javascript
Maxkim3 小时前
前端工程化落地指南:pnpm workspace + Monorepo 核心用法与实践
前端·javascript·架构
小兵张健15 小时前
开源 playwright-pool 会话池来了
前端·javascript·github
codingWhat19 小时前
介绍一个手势识别库——AlloyFinger
前端·javascript·vue.js
Lee川19 小时前
深度拆解:基于面向对象思维的“就地编辑”组件全模块解析
javascript·架构
进击的尘埃19 小时前
Web Worker 与 OffscreenCanvas:把主线程从重活里解放出来
javascript
codingWhat19 小时前
手撸一个「能打」的 React Table 组件
前端·javascript·react.js