🐱 深入理解 JavaScript 面向对象编程(OOP)——从构造函数到原型链继承

52 阅读4分钟

一、JavaScript 是“基于对象”的语言

虽然 ES6 引入了 class 关键字,但 JavaScript 本质上仍是基于原型(Prototype-based)的语言,而非传统类式 OOP(如 Java/C++)。
这意味着:

  • 所有对象都通过 原型链 实现属性/方法共享;
  • “类”只是语法糖,底层仍是函数 + 原型;
  • 没有真正的私有成员(ES2022 后可用 # 私有字段,但非主流)。

二、从对象字面量到构造函数:封装实例化过程

❌ 原始模式(不推荐)

js
编辑
var cat1 = { name: '加菲猫', color: '橘色' };
var cat2 = { name: '黑猫警长', color: '黑色' };
  • 问题:代码重复、无法批量创建、无类型标识

✅ 构造函数模式(基础封装)

js
编辑
function Cat(name, color) {
  this.name = name;
  this.color = color;
}
const cat1 = new Cat('加菲猫', '橘色');
console.log(cat1 instanceof Cat); // true

🔍 new 调用时发生了什么?

  1. 创建一个空对象 {}
  2. 将 this 指向该对象;
  3. 执行构造函数体(初始化属性);
  4. 返回该对象(除非显式返回非原始值)。

⚠️ 若直接调用 Cat()(无 new),this 指向 window(非严格模式),造成污染!


三、Prototype 模式:解决方法重复问题

每个实例都拥有自己的方法副本?太浪费!

✅ 正确做法:将公共属性/方法挂到 prototype

js
编辑
function Cat(name, color) {
  this.name = name;
  this.color = color;
}
Cat.prototype.type = '猫科动物';
Cat.prototype.eat = function() {
  console.log('吃 jerry');
};
  • 所有 Cat 实例共享 type 和 eat
  • 内存高效,符合“单一职责”。

💡 注意:若在实例上赋值同名属性(如 cat1.type = '铲屎官'),会遮蔽(shadow) 原型上的属性,但不影响其他实例。


四、属性查找三剑客:hasOwnProperty vs in vs isPrototypeOf

方法作用是否查原型链典型用途
obj.hasOwnProperty(prop)判断自身是否有某属性❌ 否过滤原型属性(如 for-in 循环)
prop in obj判断自身或原型链是否有某属性✅ 是安全调用前检查
Proto.isPrototypeOf(obj)判断 Proto 是否在 obj 的原型链中✅ 是类型判断、继承验证

🧪 示例验证

js
编辑
const cat1 = new Cat('tom', '灰色');

console.log(cat1.hasOwnProperty('name'));    // true(自身)
console.log(cat1.hasOwnProperty('type'));    // false(原型)
console.log('type' in cat1);                 // true(原型链有)
console.log(Cat.prototype.isPrototypeOf(cat1)); // true

for-in 循环建议配合 hasOwnProperty 使用

js
编辑
for (let key in cat1) {
  if (cat1.hasOwnProperty(key)) {
    console.log(key, cat1[key]); // 只打印自身属性
  }
}

五、继承:如何让 Cat 继承 Animal?

方案 1️⃣:仅用 apply(借用构造函数)—— 只能继承实例属性

js
编辑
function Animal() {
  this.species = '动物';
}
Animal.prototype.sayHi = function() { console.log('hi'); };

function Cat(name, color) {
  Animal.apply(this); // this 指向 Cat 实例
  this.name = name;
  this.color = color;
}

const cat = new Cat('加菲猫', '橘色');
console.log(cat.species); // ✅ '动物'
cat.sayHi(); // ❌ TypeError! sayHi 不在原型链上

❓ 为什么拿不到 sayHi

  • apply 只执行 Animal 函数体,不修改原型链
  • cat.__proto__ 仍指向 Cat.prototype(默认为空),与 Animal.prototype 无关。

🧠 类比
Animal 是工具箱(含 species 工具),Animal.prototype 是公共仓库(含 sayHi)。
apply 只把工具箱内容复制过来,没进仓库


方案 2️⃣:组合继承(推荐)—— 同时继承属性 + 原型方法

js
编辑
function Cat(name, color) {
  Animal.apply(this); // 继承实例属性
  this.name = name;
  this.color = color;
}

// 关键:设置 Cat.prototype 为 Animal 实例
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat; // 修复 constructor

Cat.prototype.catchMouse = function() {
  console.log(this.name + '抓老鼠');
};

const cat = new Cat('tom', '灰色');
cat.sayHi();        // ✅ 'hi'
cat.catchMouse();   // ✅ 'tom抓老鼠'
### `constructor` 修复的意义(不是为了继承,是为了规范)

为什么要修复 `constructor`?不是为了继承方法,而是为了:

1.  **语义正确**`cat.constructor` 应该指向创建它的构造函数 `Cat`,而非 `Animal`    -   不修复:`cat.constructor === Animal`(语义错误);
    -   修复后:`cat.constructor === Cat`(语义正确)。

1.  **避免后续逻辑出错**:如果代码中依赖 `constructor` 判断类型 / 创建新实例,错误的 `constructor` 会导致 bug。
cat(自身:name、color、species)→ 没 sayHi → 
cat.__proto__(Cat.prototype,自身:species、constructor:Cat)→ 没 sayHi → 
cat.__proto__.__proto__(Animal.prototype)→ 找到 sayHi → 执行

🔗 原型链结构:

text
编辑
cat
 └─ __proto__ → Cat.prototype (即 new Animal())
     └─ __proto__ → Animal.prototype
         └─ __proto__ → Object.prototypenull

重点澄清你的疑问
cat.__proto__ 确实直接指向 Cat.prototype,而 Cat.prototype 被赋值为 new Animal()(一个 Animal 实例),所以:

js
编辑
cat.__proto__ === Cat.prototype          // true
cat.__proto__ === new Animal()           // true(如果引用相同)
cat.__proto__.__proto__ === Animal.prototype // true

⚠️ 常见误区:不要直接 Cat.prototype = Animal.prototype

  • 会导致父子原型共享同一对象,修改子类原型会污染父类!

六、ES6 Class:语法糖,本质不变

js
编辑
class Animal {
  constructor(name) {
    this.name = name;
  }
  run() { console.log(this.name + '跑'); }
}

class Cat extends Animal {
  constructor(name, color) {
    super(name); // 等价于 Animal.apply(this, [name])
    this.color = color;
  }
  catchMouse() { console.log(this.name + '抓老鼠'); }
}
  • extends 自动完成:

    • 原型链关联(Cat.prototype.__proto__ = Animal.prototype
    • 构造函数调用(super()

💡 底层仍是原型继承,class 只是更清晰的写法。


七、总结要点 ✅

主题核心结论
构造函数new 调用时自动绑定 this 到新实例
Prototype公共方法放 prototype,节省内存
属性查找hasOwnProperty(自身)、in(自身+原型)、isPrototypeOf(原型链验证)
继承关键仅 apply → 无原型方法;需 Cat.prototype = new Animal() 补全原型链
原型指向cat.__proto__ 永远 = Cat.prototype;若后者被赋值为 Animal 实例,则间接继承其原型
ES6 class语法糖,extends + super = 组合继承的简化版

八、拓展思考 💭

  1. 现代替代方案

    • Object.create(Animal.prototype) 可避免调用父构造函数(寄生组合继承);
    • Reflect.construct 提供更灵活的构造控制。
  2. 性能注意

    • 原型链过深会影响属性查找速度;
    • 避免动态修改 __proto__(已被废弃,用 Object.setPrototypeOf 替代)。
  3. 调试技巧

    • 浏览器控制台展开对象可直观看到 [[Prototype]]
    • 用 Object.getPrototypeOf(obj) 安全获取原型(比 __proto__ 更标准)