JavaScript面向对象编程的演变

22 阅读6分钟

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

但这种方式存在一个问题:每个对象都有独立的方法副本,浪费内存。

工厂模式的优点

  1. 简单易懂
  2. 可以创建多个相似对象
  3. 封装了创建过程

工厂模式的缺点

  1. 无法识别对象类型:person1 instanceof createPerson; // false
  2. 方法重复创建,内存浪费

构造函数模式:引入"类型"概念

什么是构造函数?

构造函数通过 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

原型模式带来的问题

  1. 所有实例都会共享引用类型属性,如果在原型上定义引用类型,一个数据修改时,所有对象对应的数据都会修改
  2. 所有实例共享相同的原型属性,无法动态传递初始化参数

组合继承:结合构造函数和原型的优点

组合继承的实现

组合继承的实现:使用构造函数定义实例属性,使用原型定义共享方法。

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中最常用的继承模式。

组合继承的缺点

父类构造函数被调用了两次:

  1. Parent.call(this, name); 第一次调用:继承实例属性
  2. 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 语法的演进过程,不仅能让我们写出更好的代码,还能在遇到问题时快速定位和解决。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!