JavaScript 是产生“类”的?又是如何演变成“面向对象”的?Class语法糖背后隐藏着什么秘密?本篇文章将完整梳理 JavaScript 面向对象编程的发展历程。
前言:为什么JavaScript需要面向对象?
var name = '张三';
var age = 25;
var job = '工程师';
function sayHello(person) {
console.log('你好,我是' + person.name);
}
在早期的 JavaScript 代码中,我们通常采用的是过程式编程。但随着应用复杂度增加,我们需要更好的代码组织方式,因此面向对象编程应运而生。
工厂模式:面向对象的雏形
什么是工厂模式?
工厂模式 是最简单的创建对象的方式,它就像一个“工厂”一样批量生产对象。我们来看一个简单的工厂模式示例:
// 创建Person对象的工厂
function createPerson(name, age, job) {
// 1. 创建一个新对象
var obj = {};
// 2. 添加属性
obj.name = name;
obj.age = age;
obj.job = job;
// 3. 添加方法
obj.sayHello = function() {
console.log('你好,我是' + this.name + ',今年' + this.age + '岁');
};
obj.work = function() {
console.log(this.name + '正在工作:' + this.job);
};
// 4. 返回对象
return obj;
}
// 使用工厂模式创建对象
var person1 = createPerson('张三', 25, '前端工程师');
var person2 = createPerson('李四', 30, '后端工程师');
person1.sayHello(); // 你好,我是张三,今年25岁
person2.work(); // 李四正在工作:后端工程师
console.log(person1.sayHello === person2.sayHello); // false
但这种方式存在一个问题:每个对象都有独立的方法副本,浪费内存。
工厂模式的优点
- 简单易懂
- 可以创建多个相似对象
- 封装了创建过程
工厂模式的缺点
- 无法识别对象类型:
person1 instanceof createPerson; // false - 方法重复创建,内存浪费
构造函数模式:引入"类型"概念
什么是构造函数?
构造函数通过 new 关键字创建对象,解决了工厂模式的类型识别问题。我们来看一个简单的示例:
// 构造函数模式
function Person(name, age, job) {
// 1. 创建一个新对象(隐式:this = {})
// 2. 设置原型链(隐式:this.__proto__ = Person.prototype)
// 3. 添加属性
this.name = name;
this.age = age;
this.job = job;
// 4. 添加方法(仍然有问题)
this.sayHello = function() {
console.log('你好,我是' + this.name);
};
// 5. 返回this(隐式:return this)
}
// 使用new关键字创建对象(实例)
var person1 = new Person('张三', 25, '工程师');
var person2 = new Person('李四', 30, '设计师');
person1.sayHello(); // 你好,我是张三
person2.sayHello(); // 你好,我是李四
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
console.log(person1.constructor === Person); // true
console.log(person1.sayHello === person2.sayHello); // false
从上述代码中,我们可以看出:构造函数模式中,可以识别对象类型了;但每个实例仍有独立的方法副本,内存浪费问题仍然存在。
new操作符的工作原理
function myNew(constructor, ...args) {
// 1. 创建一个新对象
const obj = {};
// 2. 设置原型链:将新对象的__proto__指向构造函数的prototype
obj.__proto__ = constructor.prototype;
// const obj = Object.create(Constructor.prototype); // 这种写法也是可以的
// 3. 绑定this并执行构造函数
const result = constructor.apply(obj, args);
// 4. 返回结果(如果构造函数返回对象,则返回该对象,否则返回新对象)
return result instanceof Object ? result : obj;
}
原型模式:解决方法共享问题
原型模式的处理方法是:将方法定义在原型上,实现共享。
function Person(name, age) {
// 属性定义在实例上(每个实例独立)
this.name = name;
this.age = age;
}
// 方法定义在原型上(所有实例共享)
Person.prototype.sayHello = function() {
console.log('你好,我是' + this.name + ',今年' + this.age + '岁');
};
Person.prototype.work = function() {
console.log(this.name + '正在工作');
};
// 创建实例
const p1 = new Person('张三', 25);
const p2 = new Person('李四', 30);
p1.sayHello(); // 你好,我是张三,今年25岁
p2.sayHello(); // 你好,我是李四,今年30岁
// 现在方法是共享的!
console.log(p1.sayHello === p2.sayHello); // true
原型模式带来的问题
- 所有实例都会共享引用类型属性,如果在原型上定义引用类型,一个数据修改时,所有对象对应的数据都会修改
- 所有实例共享相同的原型属性,无法动态传递初始化参数
组合继承:结合构造函数和原型的优点
组合继承的实现
组合继承的实现:使用构造函数定义实例属性,使用原型定义共享方法。
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name); // 第一次调用:继承实例属性
this.age = age;
}
Child.prototype = new Parent(); // 第二次调用:继承原型方法
Child.prototype.constructor = Child; // 修复constructor指向
组合继承是JavaScript中最常用的继承模式。
组合继承的缺点
父类构造函数被调用了两次:
Parent.call(this, name);第一次调用:继承实例属性Child.prototype = new Parent();第二次调用:继承原型方法
寄生组合继承:最理想的继承方式
寄生组合继承的实现
function inheritPrototype(child, parent) {
// 创建父类原型的副本
const prototype = Object.create(parent.prototype);
// 修复constructor指向
prototype.constructor = child;
// 将副本设置为子类的原型
child.prototype = prototype;
}
// 父类
function Animal(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Animal.prototype.sayName = function() {
console.log('我是:' + this.name);
};
// 子类
function Dog(name, age) {
// 继承实例属性(只调用一次父类构造函数)
Animal.call(this, name);
this.age = age;
}
// 继承原型方法(不使用new Parent(),避免第二次调用)
inheritPrototype(Dog, Animal);
// 添加子类特有方法
Dog.prototype.bark = function() {
console.log(this.name + '在叫:汪汪!');
};
Class语法:ES6的语法糖
Class的基本语法
class Person {
// 构造函数(对应ES5的构造函数)
constructor(name, age) {
// 实例属性
this.name = name;
this.age = age;
this._secret = '这是我的秘密'; // 约定俗成的"私有"属性
}
// 实例方法(自动添加到原型上)
introduce() {
console.log(`大家好,我是${this.name},今年${this.age}岁`);
}
eat(food) {
console.log(`${this.name}正在吃${food}`);
}
// getter和setter
get secret() {
return this._secret;
}
set secret(value) {
this._secret = value;
}
// 静态方法(类方法)
static createAnonymous() {
return new Person('匿名', 0);
}
}
Class语法背后的原型原理
Class 语法只是语法糖,底层仍然是原型继承,其本质是一个函数:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name + ' makes a noise.');
}
}
// Class实际上是一个函数
console.log(typeof Animal); // function
extends 继承
extends 基本语法
在 ES6 的 class 语法糖中,通过 extends 语法糖实现继承。
class Dog extends Animal {
constructor(name) {
super(name);
}
speak() {
console.log(this.name + ' barks.');
}
}
extends 继承的本质
extends 继承的本质,就等价于ES5的寄生组合继承:
function AnimalES5(name) {
this.name = name;
}
AnimalES5.prototype.speak = function() {
console.log(this.name + ' makes a noise.');
};
function DogES5(name) {
AnimalES5.call(this, name);
}
// 设置原型链
DogES5.prototype = Object.create(AnimalES5.prototype);
DogES5.prototype.constructor = DogES5;
DogES5.prototype.speak = function() {
console.log(this.name + ' barks.');
};
Class的高级特性
类表达式
const MyClass = class {
constructor(value) {
this.value = value;
}
getValue() {
return this.value;
}
};
const obj1 = new MyClass(42);
console.log(obj1.getValue()); // 42
私有字段
class BankAccount {
// 私有字段(以#开头)
#balance = 0;
constructor(owner) {
this.owner = owner;
}
// 通过公开方法访问私有字段
getBalance() {
return this.#balance;
}
}
const account = new BankAccount('张三');
// console.log(account.#balance); // SyntaxError: 属性 "#balance" 在类 "BankAccount" 外部不可访问。
console.log(account.getBalance()); // 500
静态块
class Config {
static dbConfig;
static apiConfig;
// 静态初始化块
static {
console.log('初始化静态配置...');
this.dbConfig = {
host: 'localhost',
port: 3306,
username: 'root'
};
this.apiConfig = {
baseUrl: 'https://api.example.com',
timeout: 5000
};
}
static getConfig() {
return {
db: this.dbConfig,
api: this.apiConfig
};
}
}
类的访问器属性
class Temperature {
constructor(celsius) {
this.celsius = celsius;
}
get fahrenheit() {
return this.celsius * 1.8 + 32;
}
set fahrenheit(value) {
this.celsius = (value - 32) / 1.8;
}
// 只读属性
get kelvin() {
return this.celsius + 273.15;
}
}
Mixin模式(多重继承的替代方案)
const FlyMixin = (BaseClass) => class extends BaseClass {
fly() {
console.log(`${this.name} is flying!`);
}
};
const SwimMixin = (BaseClass) => class extends BaseClass {
swim() {
console.log(`${this.name} is swimming!`);
}
};
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating`);
}
}
// 应用Mixin
class Duck extends SwimMixin(FlyMixin(Animal)) {
quack() {
console.log(`${this.name} says: Quack!`);
}
}
面向对象编程的演进路线
工厂模式 → 构造函数模式 → 原型模式 → 组合继承 → 寄生组合继承 → Class语法
↓ ↓ ↓ ↓ ↓ ↓
创建对象 识别类型 共享方法 结合优点 最优方案 语法糖
结语
面向对象编程是 JavaScript 发展的重要里程碑。理解从工厂模式到 Class 语法的演进过程,不仅能让我们写出更好的代码,还能在遇到问题时快速定位和解决。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!