当前位置: 首页 > news >正文

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.prototypePerson.prototype.sayHello = function() {console.log(`你好,我是${this.name}`);};alice.sayHello(); // 输出: 你好,我是艾丽丝// Object.prototype是原型链的顶端console.log(Person.prototype.__proto__ === Object.prototype); // trueconsole.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__指向nullconsole.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.prototypeconsole.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); // trueconsole.log(student1 instanceof Person); // trueconsole.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('狗');// 修复constructorDog.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;}// getterget fullName() {return `${this._firstName} ${this._lastName}`;}// setterset 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; // 输出: 设置年龄为30console.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); // trueconsole.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.jsclass 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的面向对象能力都是必不可少的技能。


http://www.mrgr.cn/news/92956.html

相关文章:

  • 【音视频】RGB、YUV基础
  • 【软件测试】论坛系统功能测试报告
  • 【Python · PyTorch】循环神经网络 RNN(基础应用)
  • 2024年第十五届蓝桥杯大赛软件赛省赛Python大学A组真题解析《更新中》
  • 完美解锁便捷版!
  • 专业工具,提供多种磁盘分区方案
  • C++STL---<limits>
  • 机器学习的三个基本要素
  • 第十四届蓝桥杯大赛软件赛国赛C/C++大学C组
  • Maven的传递性、排除依赖、生命周期、插件
  • 内置序列,专业版已破!
  • 《HarmonyOS Next × ArkTS框架:从AI模型压缩到智能家居控制的端侧开发指南》
  • GIT工具学习【1】:基本操作
  • 虚拟机Linux操作(持续更新ing)
  • 【UCB CS 61B SP24】Lecture 17 - Data Structures 3: B-Trees学习笔记
  • torch.einsum 的 10 个常见用法详解以及多头注意力实现
  • Skynet入门(一)
  • 直装永久授权,最新专业版集成VB7
  • JavaScript 进阶A(作用域、闭包、变量和函数提升、函数相关只是、数组解构、对象解构、构造函数
  • go类(结构体)和对象