JavaScript 面向对象编程(OOP)详解

34 阅读5分钟

JavaScript 面向对象编程(OOP)详解

JavaScript 是一种基于对象(Object-based)  的语言,万物皆可视为对象。尽管它并非传统意义上的面向对象(OOP)语言(早期甚至没有 class 关键字),但通过原型机制实现了面向对象的核心特性:封装、继承、多态。本文结合代码实例,详解 JavaScript 面向对象编程的实现方式。

一、封装:从对象字面量到构造函数

封装的核心是将数据和操作数据的方法捆绑在一起,隐藏实现细节,仅暴露必要接口。在 JavaScript 中,封装的实现从简单的对象字面量逐步演进到构造函数。

1. 对象字面量:最基础的封装

最原始的对象创建方式是对象字面量,直接定义键值对描述对象的属性和方法。例如:

var Cat = {
  name: "",
  color: ""
};

// 创建实例
var cat1 = {};
cat1.name = "加菲猫";
cat1.color = "橘色";

var cat2 = {};
cat2.name = "小白";
cat2.color = "白色";

缺点:创建多个实例时需要重复赋值,代码冗余,无法批量生成具有相同结构的对象。

2. 构造函数:批量生成实例的封装

为解决对象字面量的冗余问题,引入构造函数封装实例化过程。构造函数本质是一个普通函数,通过 new 关键字调用时,会自动创建实例对象并绑定 this 指向该实例。

function Cat(name, color) {
  // 当用 new 调用时,this 指向新创建的实例对象
  console.log(this); // 输出: Cat {}(空实例)
  this.name = name;  // 为实例添加属性
  this.color = color;
}

// 普通函数调用:this 指向全局对象(浏览器中为 window)
Cat("黑猫警长", "黑色"); 

// 用 new 调用:生成实例
const cat1 = new Cat("加菲猫", "橘色");
const cat2 = new Cat("黑猫警长", "黑色");

console.log(cat1); // 输出: Cat { name: '加菲猫', color: '橘色' }
console.log(cat1 instanceof Cat); // 输出: true(判断实例类型)
console.log(cat1.constructor === cat2.constructor); // 输出: true(实例共享构造函数)

原理

  • new 调用构造函数时,会自动执行:创建空对象 → 绑定 this 到空对象 → 执行构造函数代码(为对象添加属性)→ 返回实例对象。
  • 构造函数名通常首字母大写(约定),区分普通函数。

二、原型模式:共享属性与方法

构造函数虽能批量生成实例,但如果每个实例都包含相同的属性或方法(如 “猫科动物” 类型、“吃老鼠” 行为),会导致内存浪费。原型(prototype)  机制解决了这一问题:将共享的属性和方法放在构造函数的 prototype 上,所有实例通过原型链共享。

1. 原型的基本使用

function Cat(name, color) {
  this.name = name;
  this.color = color;
}

// 共享属性和方法放在 prototype 上
Cat.prototype.type = "猫科动物";
Cat.prototype.eat = function() {
  console.log("eat jerry");
};

// 实例化
const cat1 = new Cat("tom", "蓝色");
const cat2 = new Cat("加菲猫", "橘色");

// 所有实例共享原型上的属性和方法
console.log(cat1.type); // 输出: 猫科动物
console.log(cat2.type); // 输出: 猫科动物
cat1.eat(); // 输出: eat jerry

特点

  • 原型上的属性 / 方法被所有实例共享,节省内存。
  • 实例可覆盖原型属性(不影响其他实例):
cat1.type = "铲屎官的主人"; // 仅修改当前实例的 type
console.log(cat1.type); // 输出: 铲屎官的主人
console.log(cat2.type); // 输出: 猫科动物(原型属性不变)

2. 原型链与属性查找

实例通过 __proto__ 指向构造函数的 prototype,形成原型链。当访问实例的属性 / 方法时,JS 会先在实例自身查找,若找不到则沿原型链向上查找。

function Cat(name, color) {
  this.name = name;
  this.color = color;
}

Cat.prototype.type = "猫科动物";
Cat.prototype.eat = function() {
  console.log("eat jerry");
};

const cat1 = new Cat("tom", "蓝色");

// 判断属性是否为实例自身所有(不包括原型)
console.log(cat1.hasOwnProperty("name")); // 输出: true(实例自身属性)
console.log(cat1.hasOwnProperty("type")); // 输出: false(原型属性)

// "in" 运算符:判断属性是否存在于实例或原型链中
console.log("name" in cat1); // 输出: true
console.log("type" in cat1); // 输出: true

// 遍历实例所有可枚举属性(包括原型链)
for (var prop in cat1) {
  console.log(prop, cat1[prop]); 
  // 输出: name tom, color 蓝色, type 猫科动物, eat [Function]
}

三、继承:复用父类的属性与方法

继承是面向对象的核心特性之一,允许子类复用父类的属性和方法。JavaScript 通过构造函数绑定原型链结合实现继承。

1. 构造函数绑定:继承父类实例属性

通过 apply 或 call 方法,将父类构造函数的 this 绑定到子类实例,使子类实例获得父类的实例属性。

function Animal() {
  this.species = "动物"; // 父类实例属性
}

function Cat(name, color) {
  // 将 Animal 的 this 绑定到 Cat 实例,继承 species 属性
  Animal.apply(this); 
  this.name = name;
  this.color = color;
}

const cat = new Cat("加菲猫", "橘色");
console.log(cat); // 输出: Cat { species: '动物', name: '加菲猫', color: '橘色' }

局限:仅能继承父类的实例属性,无法继承父类原型上的方法。

2. 原型链继承:继承父类原型方法

为继承父类原型上的方法,需将子类的 prototype 指向父类的实例,形成原型链。

function Animal() {
  this.species = "动物";
}

// 父类原型方法
Animal.prototype = {
  sayHi: function() {
    console.log("哪哪哪啦");
  }
};

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

// 子类原型指向父类实例,继承原型方法
Cat.prototype = new Animal();

const cat = new Cat("加菲猫", "橘色");
cat.sayHi(); // 输出: 哪哪哪啦(成功调用父类原型方法)

原理

  • 子类实例的 __proto__ 指向 Cat.prototype(即父类 Animal 的实例)。
  • 父类实例的 __proto__ 指向 Animal.prototype,因此子类实例可通过原型链访问父类原型方法。

四、ES6 class:原型模式的语法糖

ES6 引入 class 关键字,简化了面向对象代码的编写,但底层仍基于原型机制。

class Cat {
  // 构造函数(对应传统构造函数)
  constructor(name, color) {
    this.name = name;
    this.color = color;
  }

  // 方法定义在原型上
  eat() {
    console.log("eat jerry");
  }
}

const cat1 = new Cat('tom', '蓝色');
console.log(cat1); // 输出: Cat { name: 'tom', color: '蓝色' }
cat1.eat(); // 输出: eat jerry

// 原型链结构(与传统原型模式一致)
console.log(
  cat1.__proto__, // 指向 Cat.prototype
  cat1.__proto__.constructor, // 指向 Cat 类
  cat1.__proto__.__proto__, // 指向 Object.prototype
  cat1.__proto__.__proto__.__proto__ // 指向 null
);

优势:语法更接近传统 OOP 语言,清晰区分构造函数和原型方法。

总结

JavaScript 面向对象编程的核心是原型机制

  • 封装通过构造函数实现,批量生成具有相同结构的实例。
  • 原型(prototype)用于共享属性和方法,避免内存浪费。
  • 继承通过 “构造函数绑定 + 原型链” 实现,既继承实例属性,又继承原型方法。
  • ES6 class 是原型模式的语法糖,简化了代码编写,但本质未变。