JavaScript系列04-面向对象编程

JavaScript作为一门多范式编程语言,既支持函数式编程,也支持面向对象编程。JavaScript实现面向对象的机制非常独特——它基于原型而非类(虽然ES6引入了class语法)。本文将深入探讨JavaScript面向对象编程的核心概念和实践技巧。

本文主要包括以下内容:

  1. 原型与原型链 - 解释JavaScript基于原型的继承机制

  2. 继承实现方式 - 介绍各种实现继承的方法

  3. ES6 class语法 - 介绍ES6引入的类语法

  4. 对象属性描述符 - 解释属性描述符的概念和用法

  5. 设计模式在JavaScript中的应用

  6. 面向对象vs函数式编程

1、原型与原型链

在JavaScript中,原型和原型链是实现对象和继承的核心机制,也是区别于其他编程语言的独特特性。理解这些概念对于深入掌握JavaScript至关重要,也是理解JavaScript面向对象编程的基础。

原型基础概念

构造函数

JavaScript中的构造函数是创建对象的模板。按照惯例,构造函数首字母大写。


function Person(name) {

this.name = name;

}

const alice = new Person('艾丽丝');

console.log(alice); // Person {name: "艾丽丝"}

原型对象(prototype)

每个函数在创建时都会自动拥有一个prototype属性,这个属性指向函数的原型对象。


console.log(Person.prototype); // {constructor: ƒ}

实例与原型对象的关系

当使用构造函数创建实例时,实例内部会有一个指针(__proto__)指向构造函数的原型对象。


console.log(person1.__proto__ === Person.prototype); // true

在JavaScript中,每个函数都有一个特殊的属性叫作prototype(原型),而每个对象都有一个内部链接指向其构造函数的原型,这个内部链接称为[[Prototype]],在代码中通过__proto__Object.getPrototypeOf()访问。

原型链

概念

原型链是JavaScript实现继承的主要方式。其基本思想就是通过原型继承多个引用类型的属性和方法。重温一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。

当访问一个对象实例的属性时,如果对象实例本身没有这个属性,JavaScript会沿着原型链向上查找。

原型链的形成

每个对象都有__proto__属性,指向其构造函数的原型对象。原型对象也是对象,也有自己的__proto__,这样就形成了一个链条。


// 扩展Person.prototype

Person.prototype.sayHello = function() {

console.log(`你好,我是${this.name}`);

};

alice.sayHello(); // 输出: 你好,我是艾丽丝

// Object.prototype是原型链的顶端

console.log(Person.prototype.__proto__ === Object.prototype); // true

console.log(Object.prototype.__proto__); // null

原型链工作原理

当你试图访问一个对象实例的属性时,JavaScript引擎首先在该对象实例本身查找。如果没找到,则沿着原型链向上查找,直到找到该属性或到达原型链的末端(null)。

这个图展示了JavaScript中的原型链关系。对象实例通过__proto__链接到构造函数的prototype,而这个prototype对象实例又通过__proto__链接到上层原型,最终链接到Object.prototype,再往上是null

在这里插入图片描述

关于Function和Object的原型关系

JavaScript中的Function和Object有着复杂而独特的关系,理解它们的构造方式和原型链关联对掌握JavaScript的面向对象机制至关重要。

Object.prototype的本质

Object.prototype是JavaScript原型链的顶端,它是所有对象的原型链终点:


// Object.prototype的__proto__指向null

console.log(Object.prototype.__proto__ === null); // true

Object.prototype包含了所有对象共享的基本方法,如toString()hasOwnProperty()等。

Function.prototype的特殊性

Function.prototype是所有函数对象的原型,包括所有的构造函数(如ObjectArrayFunction自身等)。

它有几个特殊之处:

(1). 它是一个函数对象,但技术上又不完全是普通函数

(2). 它不是通过new Function()创建的

Function.prototype包含了所有函数共享的基本方法,如apply()call()等。

Function.prototype.__proto__指向哪里?

Function.prototype虽然是函数,但它首先是个对象,因此Function.prototype.__proto__指向了Object.prototype


// Function.prototype.__proto__指向Object.prototype

console.log(Function.prototype.__proto__ === Object.prototype); // true

这完成了原型链的闭环:所有函数都"继承"自Function.prototype,而Function.prototype"继承"自Object.prototype,后者是原型链的终点。

JavaScript原型体系的构建过程

JavaScript引擎在初始化时构建了这个看似"先有鸡还是先有蛋"的循环引用结构。实际构建顺序大致如下:

  1. 创建Object.prototype(原型链终点)

  2. 创建Function.prototype(函数原型)

  3. Function.prototype.__proto__指向Object.prototype

  4. 创建Function构造函数,并将其__proto__指向Function.prototype

  5. 创建Object构造函数,并将其__proto__指向Function.prototype

  6. 设置Object.prototype.constructor指向Object

  7. 设置Function.prototype.constructor指向Function

这种特殊的循环引用结构使得JavaScript能够实现基于原型的面向对象编程范式。

手动验证这些关系

// 验证Object构造函数是由Function创建的

console.log(Object instanceof Function); // true

// 验证Function构造函数也是由Function创建的(自己创建自己)

console.log(Function instanceof Function); // true

// 验证Function.prototype是一个对象

console.log(Function.prototype instanceof Object); // true

// 验证Function.prototype也是所有函数的原型

function foo() {}

console.log(foo.__proto__ === Function.prototype); // true

// 验证Object.prototype是原型链终点

console.log(Object.prototype.__proto__ === null); // true

深入思考

这种设计展示了JavaScript中一个重要概念:在JavaScript中,函数首先是对象。所有的函数(包括构造函数)都是Function的实例,而所有对象(包括函数对象)最终都继承自Object.prototype

这种循环依赖关系可能令人困惑,但它是JavaScript原型继承模型的基础。理解这些关系不仅对理解JavaScript的内部工作机制很重要,也对我们设计自己的原型继承结构很有帮助。

在编写复杂JavaScript应用或框架时,清楚地了解这些关系能够帮助我们更好地利用原型继承的强大功能,避免常见的陷阱。

原型链的性能考量

原型链的查找是级联的,链越长查找越慢。因此,常用属性应该直接定义在对象上,而共享的方法适合放在原型上。


// 高效的共享方法定义方式

function User(name, age) {

this.name = name; // 实例属性

this.age = age; // 实例属性

}

// 共享方法放在原型上,所有实例共享一份

User.prototype.introduce = function() {

return `我叫${this.name},今年${this.age}`;

};

const user1 = new User('张三', 25);

const user2 = new User('李四', 30);

// user1和user2共享同一个introduce方法

console.log(user1.introduce === user2.introduce); // true

常见陷阱和最佳实践

(1). 修改内置原型的危险

// 危险!不要这样做

Array.prototype.unique = function() {

return [...new Set(this)];

};

// 最好使用扩展而不是修改内置原型

这种做法存在以下严重问题:

  • 命名冲突风险

未来JavaScript标准可能会添加同名方法,导致你的代码与原生实现冲突。比如早期很多库添加了includes方法,后来JavaScript原生添加了此方法,导致兼容性问题。

  • 破坏第三方库

其他依赖原始行为的库可能会因为你的修改而出现问题。

  • 可维护性降低

团队中的其他开发者可能不知道这些修改,导致代码行为不可预测。

  • 性能影响

修改内置对象原型可能会影响JavaScript引擎的优化。

更好的替代方案
(1)使用工具函数

function unique(array) {

return [...new Set(array)];

}

// 使用

const arr = [1, 2, 2, 3];

const uniqueArr = unique(arr);

(2)使用类扩展或包装

class EnhancedArray extends Array {

unique() {

return [...new Set(this)];

}

}

// 使用

const myArray = new EnhancedArray(1, 2, 2, 3);

const uniqueValues = myArray.unique();

(3) 如果确实需要添加方法,可以限制在特定环境中

if (process.env.NODE_ENV === 'development') {

// 仅在开发环境下扩展

Array.prototype.unique = function() {

return [...new Set(this)];

};

}

(4) 使用Symbol避免命名冲突

const uniqueMethod = Symbol('unique');

Array.prototype[uniqueMethod] = function() {

return [...new Set(this)];

};

// 使用

const arr = [1, 2, 2, 3];

const uniqueArr = arr[uniqueMethod]();

总之,修改内置对象原型可能导致不可预测的行为和维护困难,应该尽量避免这种做法,优先选择不会污染全局作用域的替代方案。

(2). 原型属性共享问题

function Team() {

this.name = 'team';

}

// 危险!在原型上定义引用类型

Team.prototype.members = [];

const team1 = new Team();

const team2 = new Team();

team1.members.push('张三');

console.log(team2.members); // ["张三"] // 意外地影响了team2!

正确做法是将引用类型定义在构造函数中,而不是原型上。

(3). instanceof运算符

instanceof用于检查对象是否在另一个对象的原型链上。


console.log(student1 instanceof Student); // true

console.log(student1 instanceof Person); // true

console.log(student1 instanceof Object); // true

3、继承实现方式

JavaScript提供了多种实现继承的方式,每种方式各有优缺点。

原型链继承

最基本的继承方式是直接将父类的实例赋值给子类的原型。


function Animal(name) {

this.name = name;

this.colors = ['黑', '白'];

}

Animal.prototype.eat = function() {

console.log(`${this.name}正在吃东西`);

};

function Dog(breed) {

this.breed = breed;

}

// 设置继承关系

Dog.prototype = new Animal('狗');

// 修复constructor

Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {

console.log('汪汪汪!');

};

const myDog = new Dog('哈士奇');

myDog.eat(); // 输出: 狗正在吃东西

myDog.bark(); // 输出: 汪汪汪!

缺点

  1. 原型包含的引用类型属性会被所有实例共享

  2. 无法向父类构造函数传参

构造函数继承

通过在子类构造函数中调用父类构造函数实现属性继承。


function Animal(name) {

this.name = name;

this.colors = ['黑', '白'];

}

Animal.prototype.eat = function() {

console.log(`${this.name}正在吃东西`);

};

function Dog(name, breed) {

// 继承属性

Animal.call(this, name);

this.breed = breed;

}

const myDog = new Dog('旺财', '金毛');

console.log(myDog.name); // 旺财

console.log(myDog.colors); // ['黑', '白']

// myDog.eat(); // 错误! 无法继承原型方法

缺点:无法继承父类原型上的方法

组合继承

结合原型链继承和构造函数继承的优点。


function Animal(name) {

this.name = name;

this.colors = ['黑', '白'];

}

Animal.prototype.eat = function() {

console.log(`${this.name}正在吃东西`);

};

function Dog(name, breed) {

// 继承属性 (第一次调用父构造函数)

Animal.call(this, name);

this.breed = breed;

}

// 继承方法 (第二次调用父构造函数)

Dog.prototype = new Animal();

Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {

console.log('汪汪汪!');

};

const myDog = new Dog('旺财', '金毛');

myDog.colors.push('棕');

console.log(myDog.colors); // ['黑', '白', '棕']

const yourDog = new Dog('小黑', '拉布拉多');

console.log(yourDog.colors); // ['黑', '白'] (不受myDog影响)

myDog.eat(); // 输出: 旺财正在吃东西

myDog.bark(); // 输出: 汪汪汪!

缺点:调用了两次父类构造函数,效率较低。

寄生组合继承

解决组合继承两次调用父类构造函数的问题。


function inheritPrototype(Child, Parent) {

// 创建父类原型的副本

const prototype = Object.create(Parent.prototype);

// 将构造函数指向子类

prototype.constructor = Child;

// 将父类原型副本赋值给子类原型

Child.prototype = prototype;

}

function Animal(name) {

this.name = name;

this.colors = ['黑', '白'];

}

Animal.prototype.eat = function() {

console.log(`${this.name}正在吃东西`);

};

function Dog(name, breed) {

Animal.call(this, name);

this.breed = breed;

}

// 建立原型链关系

inheritPrototype(Dog, Animal);

Dog.prototype.bark = function() {

console.log('汪汪汪!');

};

const myDog = new Dog('旺财', '金毛');

myDog.eat(); // 输出: 旺财正在吃东西

这种方式被认为是引入ES6 class语法前实现继承的最佳实践。

4、ES6 class语法

ES6引入了class关键字,让JavaScript的面向对象编程语法更接近传统的基于类的语言,但底层仍是基于原型的实现。

基本语法


class Animal {

// 构造方法

constructor(name) {

this.name = name;

this.colors = ['黑', '白'];

}

// 实例方法

eat() {

console.log(`${this.name}正在吃东西`);

}

// 静态方法

static isAnimal(obj) {

return obj instanceof Animal;

}

}

const cat = new Animal('咪咪');

cat.eat(); // 输出: 咪咪正在吃东西

console.log(Animal.isAnimal(cat)); // true

继承语法

使用extendssuper关键字实现继承。


class Dog extends Animal {

constructor(name, breed) {

// 调用父类构造函数

super(name);

this.breed = breed;

}

bark() {

console.log('汪汪汪!');

}

// 重写父类方法

eat() {

// 调用父类方法

super.eat();

console.log(`${this.name}喜欢吃骨头`);

}

}

const husky = new Dog('雪橇', '哈士奇');

husky.eat();

// 输出:

// 雪橇正在吃东西

// 雪橇喜欢吃骨头

getter和setter

class语法支持getter和setter方法,用于控制属性的访问。


class Person {

constructor(firstName, lastName) {

this._firstName = firstName;

this._lastName = lastName;

}

// getter

get fullName() {

return `${this._firstName} ${this._lastName}`;

}

// setter

set fullName(name) {

const parts = name.split(' ');

this._firstName = parts[0];

this._lastName = parts[1] || '';

}

}

const person = new Person('张', '三');

console.log(person.fullName); // 输出: 张 三

person.fullName = '李 四';

console.log(person._firstName); // 输出: 李

class的本质

尽管class语法看起来像是引入了新的面向对象模型,但它实际上只是原型继承的语法糖。


// 用ES6 class定义类

class MyClass {

constructor(name) {

this.name = name;

}

sayHi() {

console.log(`Hi, ${this.name}`);

}

}

// 等价的ES5代码

function MyClass(name) {

this.name = name;

}

MyClass.prototype.sayHi = function() {

console.log(`Hi, ${this.name}`);

};

主要区别:

(1)类声明不会提升,而函数声明会

(2)类内部自动运行在严格模式下

(3)类的方法不可枚举

(4)类的构造函数必须用new调用

5、对象属性描述符

JavaScript允许我们精细控制对象属性的行为,如是否可写、可枚举或可配置。

属性描述符基础

每个属性都有一个对应的属性描述符对象,包含以下特性:

  • value: 属性的值

  • writable: 是否可修改

  • enumerable: 是否可枚举(for…in循环)

  • configurable: 是否可重新配置或删除

  • get: 属性的getter函数

  • set: 属性的setter函数


const person = {};

// 添加一个简单的属性

Object.defineProperty(person, 'name', {

value: '张三',

writable: true,

enumerable: true,

configurable: true

});

// 添加一个只读属性

Object.defineProperty(person, 'id', {

value: '12345',

writable: false, // 不可修改

enumerable: true,

configurable: false // 不可删除或重新配置

});

// 尝试修改只读属性

person.id = '67890'; // 在严格模式下会报错

console.log(person.id); // 仍然是 "12345"

// 添加一个带getter和setter的属性

let _age = 25;

Object.defineProperty(person, 'age', {

get() {

console.log('读取年龄');

return _age;

},

set(newValue) {

if (newValue < 0 || newValue > 150) {

throw new Error('年龄值不合理');

}

console.log(`设置年龄为${newValue}`);

_age = newValue;

},

enumerable: true,

configurable: true

});

person.age = 30; // 输出: 设置年龄为30

console.log(person.age); // 输出: 读取年龄 30

对象封印与冻结

JavaScript提供了三个级别的对象保护机制:

  1. Object.preventExtensions(obj): 防止添加新属性

  2. Object.seal(obj): 防止添加新属性,并将现有属性标记为不可配置

  3. Object.freeze(obj): 完全冻结对象,属性不可添加、删除或修改


const user = {

name: '李四',

age: 30

};

// 封印对象

Object.seal(user);

user.name = '王五'; // 可以修改

user.gender = '男'; // 无法添加新属性

delete user.age; // 无法删除属性

// 冻结对象

const config = {

apiUrl: 'https://api.example.com',

timeout: 5000

};

Object.freeze(config);

config.timeout = 3000; // 无效,不会改变

config.retries = 3; // 无效,不会添加

console.log(config); // { apiUrl: 'https://api.example.com', timeout: 5000 }

6、设计模式在JavaScript中的应用

设计模式是解决软件设计中常见问题的可重用解决方案。以下是几种在JavaScript中常用的设计模式。

单例模式

确保一个类只有一个实例,并提供全局访问点。


// ES6实现单例模式

class Singleton {

constructor(data) {

if (Singleton.instance) {

return Singleton.instance;

}

this.data = data;

Singleton.instance = this;

}

getData() {

return this.data;

}

}

const instance1 = new Singleton('ABC');

const instance2 = new Singleton('DEF');

console.log(instance1 === instance2); // true

console.log(instance1.getData()); // 'ABC' (不是 'DEF')

工厂模式

提供创建对象的接口,但允许子类决定实例化的对象类型。


// 简单工厂

class UserFactory {

static createUser(type, userData) {

switch(type) {

case 'admin':

return new AdminUser(userData);

case 'regular':

return new RegularUser(userData);

case 'guest':

return new GuestUser(userData);

default:

throw new Error(`不支持的用户类型: ${type}`);

}

}

}

class AdminUser {

constructor(data) {

this.name = data.name;

this.permissions = ['read', 'write', 'delete', 'admin'];

}

}

class RegularUser {

constructor(data) {

this.name = data.name;

this.permissions = ['read', 'write'];

}

}

class GuestUser {

constructor(data) {

this.name = data.name || '访客';

this.permissions = ['read'];

}

}

const admin = UserFactory.createUser('admin', { name: '管理员' });

const user = UserFactory.createUser('regular', { name: '普通用户' });

const guest = UserFactory.createUser('guest', {});

console.log(admin.permissions); // ['read', 'write', 'delete', 'admin']

console.log(guest.name); // '访客'

观察者模式

定义对象间的一对多依赖关系,当一个对象状态改变时,所有依赖于它的对象都会得到通知。


class Observable {

constructor() {

this.observers = [];

}

subscribe(observer) {

this.observers.push(observer);

return () => this.unsubscribe(observer); // 返回取消订阅函数

}

unsubscribe(observer) {

this.observers = this.observers.filter(obs => obs !== observer);

}

notify(data) {

this.observers.forEach(observer => observer(data));

}

}

// 使用示例

const newsLetter = new Observable();

const reader1 = news => console.log(`读者1收到新闻: ${news}`);

const reader2 = news => console.log(`读者2收到新闻: ${news}`);

const unsubscribe1 = newsLetter.subscribe(reader1);

newsLetter.subscribe(reader2);

newsLetter.notify('重大发现!');

// 输出:

// 读者1收到新闻: 重大发现!

// 读者2收到新闻: 重大发现!

unsubscribe1(); // 读者1取消订阅

newsLetter.notify('后续报道');

// 输出:

// 读者2收到新闻: 后续报道

观察者模式是许多现代框架和库的核心,比如React的状态管理、Vue的响应式系统等。

模块模式

使用闭包创建拥有私有状态和行为的对象。


// ES6模块自然支持模块模式

// wallet.js

class Wallet {

#balance = 0; // 私有字段,ES2022+特性

constructor(initialAmount = 0) {

this.#balance = initialAmount;

}

deposit(amount) {

if (amount <= 0) throw new Error('存款金额必须为正数');

this.#balance += amount;

return this.getBalance();

}

withdraw(amount) {

if (amount <= 0) throw new Error('取款金额必须为正数');

if (amount > this.#balance) throw new Error('余额不足');

this.#balance -= amount;

return this.getBalance();

}

getBalance() {

return this.#balance;

}

}

export default Wallet;

// 使用

import Wallet from './wallet.js';

const myWallet = new Wallet(100);

myWallet.deposit(50);

console.log(myWallet.getBalance()); // 150

// console.log(myWallet.#balance); // 语法错误: 私有字段不可访问

对于不支持私有字段的环境,可以使用闭包实现私有状态:


function createWallet(initialAmount = 0) {

// 私有变量

let balance = initialAmount;

// 公共接口

return {

deposit(amount) {

if (amount <= 0) throw new Error('存款金额必须为正数');

balance += amount;

return this.getBalance();

},

withdraw(amount) {

if (amount <= 0) throw new Error('取款金额必须为正数');

if (amount > balance) throw new Error('余额不足');

balance -= amount;

return this.getBalance();

},

getBalance() {

return balance;

}

};

}

const wallet = createWallet(100);

wallet.deposit(50);

console.log(wallet.getBalance()); // 150

// balance变量在外部不可访问

7、面向对象vs函数式编程

JavaScript同时支持面向对象和函数式编程范式,两者各有优势。

面向对象编程思想

面向对象编程(OOP)将数据和行为组织为对象,强调继承和封装。


// 面向对象示例:银行账户

class BankAccount {

constructor(owner, initialBalance = 0) {

this.owner = owner;

this.balance = initialBalance;

this.transactions = [];

}

deposit(amount) {

this.balance += amount;

this.transactions.push({

type: 'deposit',

amount,

date: new Date()

});

return this.balance;

}

withdraw(amount) {

if (amount > this.balance) {

throw new Error('余额不足');

}

this.balance -= amount;

this.transactions.push({

type: 'withdraw',

amount,

date: new Date()

});

return this.balance;

}

getStatement() {

return {

owner: this.owner,

balance: this.balance,

transactions: [...this.transactions]

};

}

}

// 使用

const account = new BankAccount('张三', 1000);

account.deposit(500);

account.withdraw(200);

console.log(account.getStatement());

函数式编程思想

函数式编程(FP)强调无状态和无副作用的函数,数据不可变,通过函数组合构建系统。


// 函数式示例:银行账户

// 纯函数,不修改原始数据

function createAccount(owner, balance = 0) {

return {

owner,

balance,

transactions: []

};

}

function deposit(account, amount) {

return {

...account,

balance: account.balance + amount,

transactions: [

...account.transactions,

{ type: 'deposit', amount, date: new Date() }

]

};

}

function withdraw(account, amount) {

if (amount > account.balance) {

throw new Error('余额不足');

}

return {

...account,

balance: account.balance - amount,

transactions: [

...account.transactions,

{ type: 'withdraw', amount, date: new Date() }

]

};

}

function getStatement(account) {

return {

owner: account.owner,

balance: account.balance,

transactions: [...account.transactions]

};

}

// 使用

let accountState = createAccount('张三', 1000);

accountState = deposit(accountState, 500);

accountState = withdraw(accountState, 200);

console.log(getStatement(accountState));

总结

JavaScript的面向对象编程体系建立在原型继承的基础上,虽然与传统的基于类的语言有所不同,但同样强大且灵活。掌握原型与原型链、各种继承实现方式、对象属性描述符等核心概念,对编写高质量的JavaScript代码至关重要。

ES6的class语法为JavaScript添加了语法糖,使代码更易于理解和组织,但了解底层原型机制仍然必不可少。设计模式为我们提供了解决常见问题的通用方法,可以提高代码的可维护性和可扩展性。

JavaScript作为一门多范式语言,允许我们灵活选择面向对象或函数式编程风格,甚至将两者结合,根据具体问题选择最合适的解决方案。这种灵活性是JavaScript强大的体现,也是为什么它能够在如此广泛的领域中应用的原因。

随着JavaScript不断发展,面向对象特性也在不断完善,如私有字段、静态成员、装饰器等新特性的引入,都在使JavaScript的面向对象编程体验越来越完善。无论是前端开发还是Node.js服务端开发,掌握JavaScript的面向对象能力都是必不可少的技能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值