JavaScript 面向对象编程:从构造函数到原型继承的完整指南

当然可以!你提供的文章已经非常系统、清晰地讲解了 JavaScript 面向对象的核心机制。为了进一步完善内容 ,我们将把上一个对话中关于 Animal.apply(this) 的详细解析有机融入到"第五部分:继承"中,并补充一些关键细节(如传参、组合继承的局限性、现代替代方案等),使整篇文章逻辑更严密、教学更完整。


JavaScript 面向对象编程:从构造函数到原型继承的完整指南

本文将带你深入理解 JavaScript 中面向对象(OOP)的核心机制 ------ 构造函数、原型(prototype)、继承,以及 ES6 的 class 语法糖背后的真相。

JavaScript 是一门基于对象的语言,但它的 OOP 实现方式与其他主流语言(如 Java、C++)截然不同。它没有传统意义上的"类",而是通过**原型链(Prototype Chain)**实现对象之间的继承与共享。

即使在 ES6 引入了 class 关键字后,其底层依然是基于原型的。因此,要真正掌握 JS 的面向对象,必须理解 构造函数 + 原型 这一核心模型。


一、原始模式:对象字面量的局限

最简单的创建对象方式:

js 复制代码
var cat1 = {
  name: '加菲猫',
  color: '橘色'
};
var cat2 = {
  name: '黑猫警长',
  color: '黑色'
};

这种方式虽然直观,但存在明显问题:

  • 代码重复:每个实例都要手动定义相同结构;
  • 无法批量生成:缺乏"模板"概念;
  • 无类型标识 :无法判断 cat1 是否属于"猫"这个类别。

于是,我们需要一种封装实例化过程的方式。


二、构造函数:封装实例化的第一步

使用函数配合 new 关键字,模拟"类"的行为:

js 复制代码
function Cat(name, color) {
  this.name = name;
  this.color = color;
}

const cat1 = new Cat('加菲猫', '橘色');
const cat2 = new Cat('黑猫警长', '黑色');

new 调用时发生了什么?

  1. 创建一个空对象 {}
  2. 将该对象的内部 [[Prototype]] 链接到构造函数的 prototype
  3. this 指向该空对象;
  4. 执行构造函数体,为 this 添加属性;
  5. 自动返回 this(除非显式返回一个非 null 对象)。

⚠️ 注意:若直接调用 Cat()(不加 new),this 指向全局对象(浏览器中是 window),造成污染。严格模式下会报错。

类型判断

  • cat1 instanceof Cattrue
  • cat1.constructor === Cattrue

这表明实例与构造函数建立了明确的归属关系。


三、原型(Prototype):解决方法冗余问题

如果我们在构造函数内定义方法:

js 复制代码
function Cat(name, color) {
  this.name = name;
  this.color = color;
  this.eat = function() { console.log('eat jerry'); }; // ❌ 每个实例都新建一个函数!
}

这会导致内存浪费------每个实例都持有一份相同的函数副本。

解决方案:使用 prototype

js 复制代码
Cat.prototype.type = '猫科动物';
Cat.prototype.eat = function() {
  console.log('eat jerry');
};

所有通过 new Cat() 创建的实例,都会共享这些原型上的属性和方法。

如何区分"自身属性"和"继承属性"?

  • cat1.hasOwnProperty('name')true(自身)
  • cat1.hasOwnProperty('type')false(来自原型)
  • 'type' in cat1true(无论来源)

遍历实例属性时,建议过滤:

js 复制代码
for (let prop in cat1) {
  if (cat1.hasOwnProperty(prop)) {
    console.log(prop, cat1[prop]);
  }
}

四、ES6 的 class:语法糖而已

ES6 提供了更清晰的 OOP 语法:

js 复制代码
class Cat {
  constructor(name, color) {
    this.name = name;
    this.color = color;
  }
  eat() {
    console.log('eat jerry');
  }
}

但请注意:class 本质仍是原型机制

验证如下:

js 复制代码
const cat1 = new Cat('tom', '黑色');
console.log(cat1.__proto__ === Cat.prototype); // true
console.log(Cat.prototype.constructor === Cat); // true

所以,class 只是让代码更易读,底层逻辑未变。


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

JavaScript 的继承需要同时处理两部分:

  • 实例属性的继承(通过构造函数)
  • 原型方法的继承(通过原型链)

步骤 1:借用构造函数(继承实例属性)

js 复制代码
function Animal(species) {
  this.species = species || '动物';
  console.log(this, '/////'); // 此时 this 是 Cat 的新实例
}

function Cat(name, color) {
  console.log(this); // {} ------ new Cat() 创建的新对象
  // 👇 关键:在当前 Cat 实例上下文中执行 Animal
  Animal.apply(this, ['哺乳动物']); // 可传递参数!
  
  this.name = name;
  this.color = color;
}
🔍 深入解析 Animal.apply(this)
  • apply 的第一个参数指定了函数执行时的 this 上下文。

  • new Cat(...) 被调用时,JS 引擎先创建一个新对象(设为 obj),并将 Cat 内部的 this 指向 obj

  • 执行 Animal.apply(this) 相当于:

    js 复制代码
    // 在 obj 上运行 Animal 函数
    obj.species = '哺乳动物';
  • 结果:Cat 实例拥有了 species 属性,且每个实例都有独立副本,避免引用类型共享问题。

✅ 优点:支持传参、属性隔离

❌ 缺点:无法继承 Animal.prototype 上的方法

步骤 2:设置原型链(继承原型方法)

为了让 Cat 实例也能访问 Animal 原型上的方法,需建立原型链:

js 复制代码
// 先给 Animal 添加原型方法
Animal.prototype.sayHi = function() {
  console.log(`Hi, I'm a ${this.species}`);
};

// 让 Cat.prototype 指向一个 Animal 实例
Cat.prototype = new Animal(); // 注意:这里没传参,species 为默认值

// 修复 constructor 指向(否则 cat.constructor === Animal)
Cat.prototype.constructor = Cat;

现在测试:

js 复制代码
const cat = new Cat('加菲猫', '橘色');
console.log(cat);
// { species: '哺乳动物', name: '加菲猫', color: '橘色' }

cat.sayHi(); // ✅ "Hi, I'm a 哺乳动物"
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat);    // true

完整继承模型图解

lua 复制代码
cat (实例)
  └─ [[Prototype]] → Cat.prototype (即 new Animal())
        └─ [[Prototype]] → Animal.prototype
              └─ [[Prototype]] → Object.prototype
                    └─ [[Prototype]] → null

💡 提示:现代开发中,可使用 Object.create(Animal.prototype) 替代 new Animal(),避免调用父构造函数带来的副作用(如不需要初始化父类实例属性时)。

更健壮的组合继承写法(推荐)

js 复制代码
function Animal(species) {
  this.species = species || '动物';
}

Animal.prototype.sayHi = function() {
  console.log(`Hi, I'm a ${this.species}`);
};

function Cat(name, color) {
  // 继承属性(可传参)
  Animal.call(this, '猫科动物'); // 使用 call 更常见(参数列表 vs 参数数组)
  this.name = name;
  this.color = color;
}

// 继承方法(不调用 Animal 构造函数)
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

// 可继续扩展 Cat 自己的方法
Cat.prototype.meow = function() {
  console.log('Meow~');
};

这种模式称为 "组合继承"(Combination Inheritance) ,是 ES5 时代最常用的继承方式。


六、总结:JS OOP 的核心要点

概念 说明
构造函数 用于创建实例,配合 new 使用
prototype 存放共享属性/方法,避免重复创建
__proto__ / [[Prototype]] 实例指向其构造函数的 prototype
hasOwnProperty 判断是否为自身属性
instanceof 检查原型链中是否存在某构造函数的 prototype
继承 属性靠 call/apply,方法靠原型链
ES6 class 语法糖,底层仍是原型
组合继承 最常用的传统继承模式(属性 + 原型链)

七、延伸思考:现代继承的最佳实践

虽然组合继承很强大,但它调用了两次父构造函数 (一次在 new Animal() 设置原型,一次在 Cat 内部),略显冗余。

ES6 之后,直接使用 class extends 是首选

js 复制代码
class Animal {
  constructor(species = '动物') {
    this.species = species;
  }
  sayHi() {
    console.log(`Hi, I'm a ${this.species}`);
  }
}

class Cat extends Animal {
  constructor(name, color) {
    super('猫科动物'); // 等价于 Animal.call(this, ...)
    this.name = name;
    this.color = color;
  }
  meow() {
    console.log('Meow~');
  }
}
  • 语义清晰
  • 自动处理原型链
  • 支持静态方法、getter/setter 等高级特性

但请记住:super() 底层仍然是 Parent.call(this) + 原型链设置

📌 终极箴言
JavaScript 没有类,只有对象;没有继承,只有委托。

理解原型链,就掌握了 JS 的灵魂。


标签:#JavaScript #面向对象 #原型链 #前端进阶 #ES6
欢迎点赞、收藏、评论交流!

相关推荐
m0_7400437337 分钟前
Vue 组件中获取 Vuex state 数据的三种核心方式
前端·javascript·vue.js
Jingyou42 分钟前
JavaScript 封装无感 token 刷新
前端·javascript
想要成为糕糕手43 分钟前
从零实现一个健壮可复用的“就地编辑”组件:深入剖析 OOP、DOM 与事件机制
javascript
蜗牛攻城狮1 小时前
JavaScript `Array.prototype.reduce()` 的妙用:不只是求和!
前端·javascript·数组
chilavert3181 小时前
技术演进中的开发沉思-225 Prototype.js 框架
开发语言·javascript·原型模式
m0_626535201 小时前
代码分析 关于看图像是否包括损坏
java·前端·javascript
WebGISer_白茶乌龙桃1 小时前
前端又要凉了吗
前端·javascript·vue.js·js
小飞侠在吗1 小时前
vue2 watch 和vue3 watch 的区别
前端·javascript·vue.js
脾气有点小暴1 小时前
Vue3 中 ref 与 reactive 的深度解析与对比
前端·javascript·vue.js