JavaScript 面向对象编程:从原型到 Class 的演进
JavaScript 作为一门灵活的脚本语言,其面向对象编程(OOP)体系与传统的类式语言(如 Java、C++)有着本质区别 ------ 它基于原型而非类。本文将从基础的对象创建出发,逐步拆解原型机制、原型链,以及 ES6 Class 语法糖,最后讲解 JavaScript 中的继承实现,带你完整理解 JS 面向对象的核心逻辑。
一、从简单对象到构造函数:封装的起步
在 JavaScript 中,最基础的对象创建方式是直接字面量声明,但这种方式在创建多个同类型对象时会产生大量重复代码:
javascript
Run
ini
// 原始方式:重复创建相似对象,代码冗余
var Cat = {
name: "",
color: ""
}
var cat1 = {};
cat1.name = '加菲猫';
cat1.color = '橘色';
var cat2 = {};
cat2.name = '黑猫警长';
cat2.color = '黑色';
为了解决代码重复问题,我们可以用构造函数 封装对象的实例化过程。构造函数的核心是this关键字 ------ 它的指向由调用方式决定:
javascript
Run
javascript
function Cat(name, color) {
console.log(this); // 调用方式不同,this指向不同
this.name = name; // 给实例挂载属性
this.color = color;
}
// 1. 普通函数调用:this指向window(非严格模式)
Cat('黑猫警长', '黑色');
// 2. new关键字调用:this指向新创建的空对象(实例)
const cat1 = new Cat('加菲猫', '橘色');
const cat2 = new Cat('黑猫警长', '黑色');
// 验证实例归属
console.log(cat1.constructor === cat2.constructor); // true
console.log(cat1 instanceof Cat); // true
构造函数解决了 "重复创建对象" 的问题,但如果在构造函数内定义方法,每个实例都会生成独立的方法副本,造成内存浪费:
javascript
Run
javascript
function Cat(name,color){
this.name = name
this.color = color
// 每个实例都会创建一个新的eat方法,浪费内存
this.eat = function(){alert('喜欢Jerry')}
}
二、原型(prototype):共享属性与方法的核心
JavaScript 为每个函数都提供了prototype属性(原型对象),挂载在原型上的属性 / 方法会被所有实例共享,从根本上解决内存浪费问题。
1. 原型的基本使用
javascript
Run
ini
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','黑色');
const cat2 = new Cat('加菲猫','橘色');
// 所有实例共享原型属性
console.log(cat1.type, cat2.type); // 猫科动物 猫科动物
// 实例可重写原型属性(仅影响自身,不改变原型)
cat1.type = '铲屎官的主人';
console.log(cat1.type, cat2.type); // 铲屎官的主人 猫科动物
2. 原型的核心检测方法
prototype.isPrototypeOf():判断原型是否属于某个实例hasOwnProperty():判断属性是否是实例自身属性(非原型继承)in运算符:判断属性是否存在(包括实例和原型)for...in:遍历实例自身 + 原型的可枚举属性
javascript
Run
javascript
console.log(Cat.prototype.isPrototypeOf(cat1)); // true
console.log(cat1.hasOwnProperty('type')); // false(原型属性)
console.log(cat1.hasOwnProperty('name')); // true(实例属性)
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]
}
三、ES6 Class:原型的语法糖
ES6 引入class关键字,让 JavaScript 的面向对象写法更接近传统类式语言,但底层仍然基于原型机制,本质是 "语法糖"。
1. Class 的基本使用
javascript
Run
javascript
class Cat{
// 构造方法:对应构造函数的核心逻辑
constructor(name,color){
this.name = name
this.color = color
}
// 方法会自动挂载到原型上
eat(){
console.log('eat Jerry');
}
}
const cat1 = new Cat('tom','黑色');
cat1.eat(); // eat Jerry
// 拆解原型链(Class的底层逻辑)
console.log(
cat1.__proto__, // Cat.prototype(包含eat方法)
cat1.__proto__.constructor, // Cat类本身
cat1.__proto__.__proto__, // Object.prototype
cat1.__proto__.__proto__.__proto__ // null(原型链终点)
);
2. Class 的核心特性
constructor:构造方法,对应传统构造函数- 类内定义的方法自动挂载到
prototype,无需手动赋值 - 类的所有方法不可枚举(区别于手动挂载的原型方法)
- 必须用
new调用,普通调用会报错(避免 this 指向混乱)
四、JavaScript 的继承实现
继承是面向对象的核心特性之一,JavaScript 没有原生的 "继承关键字",但可以通过原型和this绑定实现继承。
1. 构造函数继承(继承属性)
通过apply/call绑定父类构造函数的this,让子类实例继承父类的实例属性:
javascript
Run
javascript
// 父类:Animal
function Animal(){
this.species = '动物';
}
// 子类:Cat
function Cat(name,color){
// 绑定父类的this到子类实例,继承父类属性
Animal.apply(this);
this.name = name;
this.color = color;
}
const cat = new Cat('加菲猫','橘色');
console.log(cat.species); // 动物(继承自Animal)
2. 原型继承(继承方法)
仅靠构造函数继承无法获取父类原型上的方法,需要结合原型链实现完整继承:
javascript
Run
javascript
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(); // 啦啦啦啦啦(继承自Animal原型)
五、核心总结
- JS 面向对象的本质:基于原型而非类,ES6 Class 是原型的语法糖;
- this 的指向规则:普通函数调用指向 window,new 调用指向实例,apply/call 可手动绑定;
- 原型的作用:共享属性 / 方法,减少内存消耗,是原型链的基础;
- 继承的实现:构造函数继承(属性)+ 原型继承(方法)结合,是 JS 继承的经典方案;
- 原型链 :实例
__proto__指向构造函数prototype,最终指向null,是属性查找的核心逻辑。
理解原型和原型链,是掌握 JavaScript 面向对象的关键。无论是传统的构造函数 + 原型写法,还是 ES6 的 Class 写法,底层逻辑始终围绕 "原型" 展开 ------ 这也是 JavaScript 区别于其他类式语言的核心特征。