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是原型模式的语法糖,简化了代码编写,但本质未变。