如果你问JavaScript程序员:"面向对象不难吧?",十个人里可能有九个会先翻个白眼,再给你讲半小时"原型链有多绕"。其实JS的OOP就像外卖——表面上是"点餐-收货"的简单流程(类-实例),背地里藏着"商家备餐-骑手取餐-路线规划"的复杂逻辑(原型-继承)。今天咱们就带着笑点,从"创建一只猫"开始,把JS OOP的底裤扒得明明白白。
核心提示:JS OOP核心是「原型继承」,和Java等传统OOP的「类继承」本质不同,这是理解所有知识点的关键!
一、青铜入门:给猫贴标签的原始时代(对象字面量)
假设你是个刚入行的程序员,老板让你做个养猫系统,第一版你可能会这么写:先建个空对象,再挨个给猫加名字、颜色。就像给流浪猫挂牌子,每只都要手动写,累得手抽筋。
// 空对象就像没贴标签的猫笼
var cat1 = {};
cat1.name = '加菲猫'; // 给猫笼贴"加菲猫"标签
cat1.color = "橘色"; // 再贴"橘色"标签
var cat2 = {};
cat2.name = "黑猫警长"; // 重复操作,手酸了
cat2.color = "黑色";
这种"对象字面量"方式就像用便利贴记账——简单直接,但致命缺点很明显,适合临时创建少量独立对象,大规模使用会踩坑:
二、白银进阶:给猫办"身份证"(构造函数+new)
- 缺乏关联:这些猫看似都是"猫",但JS无法识别它们的关联,用
cat1 instanceof Cat检测会返回false,属于"散装对象"; - 代码冗余:创建100只猫就要写100遍name和color赋值,堪称"程序员手部劳损模拟器";
聪明的程序员很快发现不对劲:既然都是猫,能不能搞个"猫模板"?于是构造函数应运而生,就像派出所的身份证制作模板,填好信息就能出证。
- 维护困难:若要给所有猫加"年龄"属性,需逐个修改,成本极高。
// 猫的"身份证模板",首字母大写是约定
function Cat(name, color) {
this.name = name; // 把名字填进模板
this.color = color; // 把颜色填进模板
}
// 用new"打印"身份证,得到真正的猫实例
const cat1 = new Cat('加菲猫', '橘色');
const cat2 = new Cat('黑猫警长', '黑色');
易错点:新手常犯var Cat = {name:"",color:""}; var cat1 = Cat;的错误,此时cat1和Cat指向同一对象,修改cat1会同步改变Cat!
这里的new关键字堪称"魔法咒语",执行时偷偷完成5步核心操作,缺一不可:
但别高兴太早,要是给猫加个"吃饭"方法,问题又来了:
function Cat(name, color) {
this.name = name;
this.color = color;
this.eat = function() { // 每只猫都带一个吃饭方法
console.log('吃杰瑞');
};
}
构造函数直接定义方法会导致严重问题:每创建一个实例,就会生成一个新的eat方法副本。若创建100只猫,就有100个eat方法,纯属内存浪费(每只猫扛一张餐桌)。
- 关联原型:将obj的
__proto__指向构造函数的prototype(建立家族关系); - 创建空对象:
var obj = {};(猫坯子);
三、黄金高手:给猫建"共享食堂"(原型模式)
- 绑定this:构造函数中的this指向obj(给坯子贴标签);
原型对象的核心思想很简单:把所有猫都要用到的方法,放到一个"共享食堂"里,谁要吃饭直接去食堂,不用自己扛桌子。这个食堂就是构造函数的prototype属性。
- 执行构造:运行函数代码,给obj添加属性(完善猫的信息);
function Cat(name, color) {
this.name = name; // 个体属性自己带
this.color = color;
}
// 共享方法放进"食堂"
Cat.prototype.eat = function() {
console.log(this.name + '吃杰瑞');
};
const cat1 = new Cat('加菲猫', '橘色');
const cat2 = new Cat('黑猫警长', '黑色');
cat1.eat(); // 加菲猫吃杰瑞
cat2.eat(); // 黑猫警长吃杰瑞
- 返回对象:若构造函数无返回对象,自动返回obj(输出成品猫)。
原型模式的核心优势是「方法共享」,不管创建多少只猫,eat方法仅存一个副本。JS的属性查找遵循"就近原则",专业称为「原型链委托」:
防坑技巧:为避免忘写new导致this指向全局,可在构造函数加防护:if(!(this instanceof Cat)) return new Cat(name,color);
有了这个模板,创建多少只猫都不怕重复,且类型检测有效:cat1 instanceof Cat // true,相当于给每只猫办了"防伪身份证"。
但要是想给猫加个"物种"属性,所有猫都共享"猫科动物"这个值,直接放原型里就行。可万一有只猫想改物种为"铲屎官的主人",直接改自己的属性就行,不会影响其他猫——这就像食堂里的公共调料,你自己带瓶辣椒精,不会改变食堂的调料罐。
四、铂金大师:给猫找"亲戚"(继承机制)
当你不仅要养猫,还要养狗、养鸟时,总不能给每种动物都建一套模板和食堂吧?这时候就需要"继承"——让猫继承动物的基本属性,再加上自己的特色。
此时「原型对象」登场,其设计理念是"共享经济"——将公共方法集中存放在构造函数的prototype属性中,所有实例共享同一副本,实现内存优化。
当需要创建"动物→猫→橘猫"等多层关系时,就需要「继承」。最经典的「组合继承」结合两种方式的优点,是ES6前的最优解:
反例警示:仅用原型链继承(Cat.prototype = new Animal())会导致引用类型共享。若Animal有this.friends = ['狗'],修改cat1.friends会同步影响cat2!
- 原型链继承:用原型关联继承方法,实现方法复用。
- 构造函数继承:用
apply/call继承属性,解决引用类型共享问题;
核心关系:构造函数 → prototype(原型对象) → proto(实例指向原型),三者形成"铁三角",是JS OOP的核心骨架。
// 父类:动物
function Animal() {
this.species = '动物'; // 所有动物都有物种属性
}
// 动物的共享方法
Animal.prototype.breathe = function() {
console.log('呼吸空气');
};
// 子类:猫,继承动物
function Cat(name, color) {
Animal.apply(this); // 继承属性:借动物的构造函数用用
this.name = name;
this.color = color;
}
// 继承方法:把猫的食堂连到动物的实例上
Cat.prototype = new Animal();
// 修复身份证信息:告诉猫它的模板是Cat,不是Animal
Cat.prototype.constructor = Cat;
组合继承虽好用,但存在「父构造函数调用两次」的小瑕疵:
最终实例的species会覆盖原型的species,虽不影响功能,但造成原型上的属性冗余。ES6的Class语法完美解决了这个问题。
- 第二次:
Animal.apply(this)(给实例加species属性)。 - 第一次:
Cat.prototype = new Animal()(给原型加species属性);
cat1自身 → Cat.prototype(原型对象) → Object.prototype(原型的原型) → null(查找终止)
当执行cat1.eat()时,查找流程为:
五、王者终极:穿"语法糖"外衣的高手(ES6 Class)
特殊情况:箭头函数无自身this,继承外层作用域的this,可解决回调函数this指向问题。
举例验证:给Cat.prototype.type = '猫科动物',所有猫可共享该属性;若某只猫执行cat1.type = '铲屎官的主人',仅会在自身创建属性,不影响原型和其他实例(自己带辣椒精,不碰食堂调料罐)。
很多从Java、C#转来的程序员,看到原型链就头大。ES6贴心地推出了Class语法,把原型链这堆复杂逻辑藏在里面,穿上一件"传统类"的外衣,让大家用着更顺手。
没有完美的继承方式,只有适配的场景,对应方案清晰匹配:
Class的核心优势在于「语法直观」,尤其对传统OOP开发者友好,同时自动处理了constructor指向修正等细节,减少手动操作失误。
console.log(
garfield.__proto__ === Cat.prototype, // true
garfield.__proto__.__proto__ === Object.prototype, // true
garfield.__proto__.__proto__.__proto__ // null
);
| 检测方法 | 作用 | 示例 | 结果 |
|---|---|---|---|
| hasOwnProperty | 检测是否为自身属性 | cat1.hasOwnProperty('name') | true |
| hasOwnProperty | 检测是否为自身属性 | cat1.hasOwnProperty('eat') | false |
| in运算符 | 检测自身或原型是否有该属性 | 'eat' in cat1 | true |
// 看起来像传统类,实则还是原型那套
class Cat {
constructor(name, color) { // 构造函数,和以前一样
this.name = name;
this.color = color;
}
eat() { // 方法自动放进原型,不用手动写prototype
console.log(this.name + '吃杰瑞');
}
}
// 继承也变得简单直观
class OrangeCat extends Cat {
constructor(name) {
super(name, '橘色'); // 调用父类构造函数,相当于apply
this.weight = '胖'; // 橘猫特色属性
}
}
const garfield = new OrangeCat('加菲猫');
garfield.eat(); // 加菲猫吃杰瑞
console.log(garfield.weight); // 胖
就像养猫,没必要给每只猫都办全套手续,自己家的主子,怎么舒服怎么养——代码也是一样。
实战建议:JS是多范式语言,不必硬套"纯面向对象"。简单场景用对象字面量(如配置项),复杂组件(如弹窗、表单)用Class,灵活搭配才是最优解。
| 场景需求 | 推荐方案 | 核心优势 |
|---|---|---|
| 创建少量独立对象 | 对象字面量 | 简单直接 |
| 创建多个同类对象(无复杂方法) | 构造函数 | 解决代码冗余 |
| 创建多个同类对象(有共享方法) | 构造函数+原型 | 内存优化 |
| 复杂继承关系(ES5环境) | 组合继承 | 属性和方法都能继承 |
| 现代开发(追求可读性) | ES6 Class + extends | 语法直观,规避细节坑 |
关键认知:Class不是全新的OOP实现,只是原型继承的"优雅包装",理解原型链仍是掌握Class的核心。
千万别被Class的外表骗了,它本质是「原型继承的语法糖」,底层逻辑和构造函数+原型完全一致。可通过控制台验证原型链结构:
原型的3大价值:方法共享(省内存)、动态更新(改原型影响所有实例)、实现继承(原型链关联)。
共享方法放原型,实例属性放构造;引用类型要隔离,原型链要理清楚。
所有继承和方法共享的底层支撑都是原型链,核心口诀:
六、终极总结:JS OOP的核心真相
this的指向不看定义位置,只看调用方式,4大核心场景一目了然:
聊了这么多,JS OOP的核心逻辑可归纳为3个"灵魂真相",记住就能规避90%的坑:
| 调用方式 | this指向 | 示例 |
|---|---|---|
| new 构造函数 | 新创建的实例 | new Cat('加菲猫') |
| 对象.方法 | 调用方法的对象 | cat1.eat() |
| 普通函数调用 | 全局对象(严格模式为undefined) | Cat('加菲猫') |
| apply/call/bind | 绑定的目标对象 | Cat.apply(obj) |
1. 原型是灵魂
2. this是变色龙
3. 继承是组合