🐱 从"猫厂"倒闭到"鸭子"横行:一篇让你笑出腹肌的 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 引擎其实像个贴心的管家,在幕后帮你干了四件事(这才是面试考点!):
- 凭空变出一个空对象
{}。 - 绑定
this:把这个空对象塞给构造函数里的this。 - 执行代码:往这个空对象里塞属性(name, age...)。
- 返回对象:除非你非要返回别的,否则就把这个填好的对象交给你。
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 到底是谁的?
hasOwnProperty:查户口。只查自己身上 有没有,不管祖传的。tom.hasOwnProperty('name')→true(自己的)tom.hasOwnProperty('type')→false(祖传的,在原型上)
in操作符 :查家族谱。只要自己或祖先 有,就算有。"type" in tom→true
- 属性遮蔽(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 底层的逻辑:
- 用
apply继承属性。 - 用
Object.create或new继承方法。 - 别忘了修好
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 动物园"的旅程:
- 封装 :别手捏泥人了,用构造函数或
class建流水线。记得把公用方法扔进prototype省内存。 - 继承 :想"拼爹"用
apply,想"偷师"改prototype。ES6 的extends帮你把脏活累活都干了。 - 多态 :大项目用继承规范行为,小工具或回调函数用鸭子类型放飞自我。
- 核心心法 :无论语法怎么变(从
function到class),原型链(__proto__) 永远是 JS 对象世界的底层逻辑。
总结:
JS 的面向对象,表面看是
class的优雅,实则是prototype的狂野。理解了"鸭子类型",你才算真正懂得了 JS 的自由灵魂。🦆✨
觉得有用?点个赞👍,让更多人在"JS 动物园"里不再迷路!