技术演进中的开发沉思-224 Ajax面向对象与框架

当 Ajax 应用从简单的 "数据请求" 升级为复杂的 "交互系统",零散的函数和变量会变得像杂乱无章的工具间 ------ 找起来费劲、改起来容易 "牵一发而动全身"。而 JavaScript 面向对象编程(OOP) 就像给代码世界搭建了 "工厂与蓝图":用 "类" 定义 "产品规格",用 "实例" 生产 "具体产品",用 "继承" 实现 "技术传承",用 "封装" 保护 "核心机密"。在此基础上,JSVM 这类框架则进一步搭建了 "代码协作体系",让复杂项目的类组织、加载、引用更有序。这一切的核心,是让 JavaScript 代码从 "零散的工具" 升级为 "模块化的系统",适配更大型、更复杂的 Web 应用开发。

一、 类与实例

在现实世界里,汽车工厂要生产汽车,首先得有 "蓝图"------ 明确汽车的结构(轮子、发动机)和功能(行驶、刹车)。JavaScript 里的 "类",就是这样的 "蓝图";而通过 "蓝图" 生产出的 "汽车",就是 "实例"。

构造函数:OOP 的 "蓝图绘制术"

JavaScript 没有原生的 "类" 语法(ES6 之前),但我们可以用 构造函数 模拟 "类"------ 它本质是一个普通函数,但通过 new 关键字调用时,就变成了 "实例生成器"。

javascript 复制代码
// 构造函数(蓝图):定义 Ajax 请求的"规格"
function AjaxRequest(url) {
  // this 指向即将生成的"实例",相当于给"产品"装核心部件
  this.url = url; // 实例的公有属性:请求地址
  this.xhr = getTransport(); // 实例的公有属性:请求对象(复用之前的 getTransport)
}

// 用 new 关键字"生产实例"(产品)
const userRequest = new AjaxRequest('https://api.example.com/users');
const msgRequest = new AjaxRequest('https://api.example.com/messages');

这里的关键是 new 关键字,它会做三件事:

  1. 创建一个空对象(相当于 "空白产品");
  2. 让构造函数的 this 指向这个空对象(相当于给 "空白产品" 装部件);
  3. 返回这个被 "装修好" 的对象(相当于 "成品出厂")。

每个实例都拥有构造函数中定义的属性,且相互独立 ------userRequestmsgRequest 是两个不同的 "请求产品",各自的 urlxhr 互不干扰,就像两辆从同一蓝图生产的汽车,各有各的车主和用途。

为什么需要 "类与实例"?

对比之前零散的函数写法:

  • 没有 OOP 时,每次发起 Ajax 请求都要重复创建 xhr、绑定事件,代码冗余;
  • 有了 AjaxRequest 类后,只需 new 一个实例,就能复用所有基础逻辑,新增请求时只需 "生产新实例",无需重复写重复代码。

这就是 OOP 的核心价值之一:代码复用

二、继承

现实中,汽车工厂会在 "基础款汽车蓝图" 上,衍生出 "SUV 蓝图""轿车蓝图"------SUV 继承了基础款的 "行驶、刹车" 功能,又新增了 "越野轮胎、高底盘";轿车则新增了 "舒适座椅、低风阻设计"。JavaScript 中的 "继承",就是让 "子类" 复用 "父类" 的属性和方法,同时添加自己的专属功能。

JavaScript 没有原生的 "继承语法",但有三种常用的 "继承实现方案",各有优劣:

1. 原型链继承:"子蓝图共享父蓝图的工具间"

原型(prototype)是 JavaScript 中所有对象的 "隐藏属性",它像一个 "共享工具间"------ 所有从同一类生成的实例,都会共享原型上的方法和属性。原型链继承的核心,就是让 "子类的原型" 指向 "父类的实例",从而让子类实例能访问父类的属性和方法。

javascript 复制代码
// 父类(基础款汽车蓝图)
function Car(brand) {
  this.brand = brand;
  this.run = function() {
    console.log(`${this.brand} 正在行驶`);
  };
}

// 子类(SUV 蓝图)
function SUV(brand, offRoad) {
  this.offRoad = offRoad; // 子类专属属性:是否能越野
}

// 原型链继承:让 SUV 的原型 = Car 的实例
SUV.prototype = new Car();

// 子类实例
const teslaSUV = new SUV('特斯拉', true);
teslaSUV.run(); // 继承父类方法:输出"特斯拉 正在行驶"
console.log(teslaSUV.offRoad); // 子类专属属性:true

优点 :实现简单,子类实例能共享父类的原型方法;缺点 :子类实例化时无法向父类构造函数传参(比如想给 SUV 的父类 Car 传 brand,只能在 SUV.prototype = new Car('特斯拉') 时固定,无法动态传递);父类的公有属性会被所有子类实例共享(比如父类有数组属性,一个实例修改数组,其他实例也会受影响)。

2. 对象冒充继承:"子蓝图照搬父蓝图的核心技术"

对象冒充的核心是 "借用父类的构造函数"------ 在子类构造函数中,用 call(this, 参数)apply(this, [参数]) 调用父类构造函数,让父类的属性和方法 "复制" 到子类实例中。

复制代码
// 父类
function Car(brand) {
  this.brand = brand;
  this.run = function() {
    console.log(`${this.brand} 正在行驶`);
  };
}

// 子类:对象冒充继承
function SUV(brand, offRoad) {
  Car.call(this, brand); // 冒充父类构造函数,传参 brand
  this.offRoad = offRoad;
}

// 子类实例
const bmwSUV = new SUV('宝马', true);
bmwSUV.run(); // 继承父类方法:输出"宝马 正在行驶"
console.log(bmwSUV.brand); // 继承父类属性:宝马

优点 :子类实例化时能向父类传参;父类的属性和方法不会被子类实例共享(每个实例都有独立副本);缺点 :父类原型上的方法无法继承(比如如果 Car.prototype.horn = function() {},子类实例无法访问 horn 方法),导致方法无法复用,浪费内存。

3. 组合继承:"取其精华,去其糟粕"

组合继承结合了 "原型链继承" 和 "对象冒充继承" 的优点 ------ 用对象冒充继承父类的属性(解决传参问题),用原型链继承父类的原型方法(解决复用问题),是 JavaScript 中最常用的继承方案。

javascript 复制代码
// 父类
function Car(brand) {
  this.brand = brand; // 父类属性(将通过对象冒充继承)
}

// 父类原型方法(将通过原型链继承)
Car.prototype.run = function() {
  console.log(`${this.brand} 正在行驶`);
};
Car.prototype.horn = function() {
  console.log(`${this.brand} 鸣笛:嘀嘀!`);
};

// 子类:组合继承
function SUV(brand, offRoad) {
  Car.call(this, brand); // 1. 对象冒充:继承父类属性,支持传参
  this.offRoad = offRoad; // 子类专属属性
}

// 2. 原型链:继承父类原型方法
SUV.prototype = new Car();
// 修复子类的 constructor 指向(否则 SUV.prototype.constructor 会指向 Car)
SUV.prototype.constructor = SUV;

// 子类专属原型方法
SUV.prototype.offRoadRun = function() {
  console.log(`${this.brand} 正在越野行驶`);
};

// 测试实例
const benzSUV = new SUV('奔驰', true);
benzSUV.run(); // 继承父类原型方法:奔驰 正在行驶
benzSUV.horn(); // 继承父类原型方法:奔驰 鸣笛:嘀嘀!
benzSUV.offRoadRun(); // 子类专属方法:奔驰 正在越野行驶
console.log(benzSUV.brand); // 继承父类属性:奔驰

优点 :既支持子类向父类传参,又能共享父类原型方法,兼顾灵活性和复用性;缺点 :父类构造函数会被调用两次(一次是 Car.call(this, brand),一次是 SUV.prototype = new Car()),但这是可接受的小瑕疵,不影响实际使用。

三、 封装与原型

OOP 的三大特性是 "封装、继承、多态"(JavaScript 多态较弱,核心是前两者)。如果说 "继承" 是 "子承父业",那 "封装" 就是 "给核心技术加保护壳"------ 隐藏内部实现细节,只暴露对外的接口,避免外部误操作,同时减少全局污染。

封装:公有、私有、静态成员的 "权限管理"

JavaScript 通过函数作用域实现封装,将成员分为三类:

1. 公有成员(this 定义)

通过 this 在构造函数中定义,实例可以直接访问,相当于产品的 "外部按钮"------ 用户能操作,但不用知道内部原理。

javascript 复制代码
function AjaxRequest(url) {
  this.url = url; // 公有属性:外部可访问
  this.send = function() { // 公有方法:外部可调用
    this.xhr.open('GET', this.url);
    this.xhr.send();
  };
}

const request = new AjaxRequest('https://api.example.com');
console.log(request.url); // 外部可访问:https://api.example.com
request.send(); // 外部可调用:发起请求
2. 私有成员(函数内 var 定义)

在构造函数内部用 var 定义,只能在构造函数内部访问,外部无法触及,相当于产品的 "内部发动机"------ 提供核心功能,但用户看不到、也不能直接修改。

javascript 复制代码
function AjaxRequest(url) {
  const xhr = getTransport(); // 私有属性:仅内部可访问
  this.url = url;

  // 私有方法:仅内部可调用
  function checkUrl() {
    return this.url.startsWith('http');
  }

  this.send = function() {
    if (checkUrl()) { // 内部调用私有方法
      xhr.open('GET', this.url);
      xhr.send();
    } else {
      console.error('URL 格式错误');
    }
  };
}

const request = new AjaxRequest('api.example.com');
console.log(request.xhr); // undefined:外部无法访问私有属性
request.checkUrl(); // 报错:外部无法调用私有方法
request.send(); // 外部只能调用公有方法,间接触发私有逻辑
3. 静态成员(类名。属性 / 方法)

直接挂载在构造函数上,不需要实例化就能访问,相当于工厂的 "公共工具"------ 所有产品都能共用,但不属于某个具体产品。

javascript 复制代码
function AjaxRequest(url) {
  this.url = url;
}

// 静态属性:请求超时时间(所有实例共用)
AjaxRequest.timeout = 5000;
// 静态方法:验证 URL(所有实例共用)
AjaxRequest.validateUrl = function(url) {
  return url.startsWith('http');
};

// 调用静态成员:无需实例化
console.log(AjaxRequest.timeout); // 5000
console.log(AjaxRequest.validateUrl('https://api.example.com')); // true

// 实例也能通过类名访问静态成员
const request = new AjaxRequest('https://api.example.com');
console.log(request.constructor.timeout); // 5000

原型(prototype)

之前提到,原型是所有实例的 "共享工具间"------ 如果把方法定义在原型上,所有实例都会共享这个方法,而不是每个实例都拥有独立副本,这样能节省内存,提高性能。

javascript 复制代码
// 不好的写法:每个实例都有独立的 send 方法(浪费内存)
function AjaxRequest(url) {
  this.url = url;
  this.send = function() { /* 发送请求逻辑 */ };
}

// 好的写法:send 方法定义在原型上,所有实例共享
function AjaxRequest(url) {
  this.url = url;
}

AjaxRequest.prototype.send = function() {
  this.xhr = getTransport();
  this.xhr.open('GET', this.url);
  this.xhr.send();
};

const request1 = new AjaxRequest('url1');
const request2 = new AjaxRequest('url2');
console.log(request1.send === request2.send); // true:共享同一个方法

原型还有一个强大的功能:扩展内置对象 。比如给 Array 原型添加一个 "去重" 方法,所有数组实例都能使用:

javascript 复制代码
// 给 Array 原型添加去重方法
Array.prototype.unique = function() {
  return [...new Set(this)];
};

const arr = [1, 2, 2, 3, 3, 3];
console.log(arr.unique()); // [1, 2, 3]:所有数组都能调用 unique 方法

注意:扩展内置对象可能会污染全局环境(比如和其他库的方法命名冲突),在大型项目中需谨慎使用,最好通过自定义类封装,而非直接修改内置对象原型。

四、JSVM

当项目越来越大,类的数量会急剧增加 ------ 可能有几十个甚至上百个类,分布在不同的文件中。此时会面临三个问题:

  1. 类之间的引用混乱(A 类依赖 B 类,B 类依赖 C 类,加载顺序出错就会报错);
  2. 路径配置复杂(不同文件夹下的类,引用时需要写冗长的相对路径);
  3. 类加载效率低(手动引入所有类文件,或按需加载时逻辑繁琐)。

JSVM(JavaScript Virtual Machine) 就是为解决这些问题而生的 "代码组织框架"------ 它像一个 "物流与管理系统",统一管理类的定义、引用、路径和加载,让分散的类能有序协作。

JSVM 的核心功能

1. 类引用:"按需调用,自动关联"

JSVM 允许用简洁的语法引用其他类,无需手动引入 <script> 标签,框架会自动解析依赖关系,按顺序加载所需类。

javascript 复制代码
// 定义类:通过 JSVM 注册
JSVM.define('ajax.AjaxRequest', function() {
  function AjaxRequest(url) {
    this.url = url;
  }
  AjaxRequest.prototype.send = function() { /* 逻辑 */ };
  return AjaxRequest;
});

// 引用类:通过命名空间直接使用
const AjaxRequest = JSVM.use('ajax.AjaxRequest');
const request = new AjaxRequest('https://api.example.com');
2. 路径配置:"统一规划,简化引用"

通过 JSVM 配置类的根路径,后续引用类时只需使用 "命名空间"(如 ajax.AjaxRequest),无需关心类文件的实际存储路径。

复制代码
// 配置路径:指定类的根目录
JSVM.config({
  basePath: '/js/modules/' // 所有类文件都放在这个目录下
});

// 引用时无需写路径,只需命名空间
const User = JSVM.use('user.User'); // 对应 /js/modules/user/User.js
const Order = JSVM.use('order.Order'); // 对应 /js/modules/order/Order.js
3. 类加载:"按需加载,提升性能"

JSVM 支持 "懒加载"------ 只有当类被 JSVM.use() 引用时,才会动态加载对应的文件,避免一次性加载所有类文件导致的首屏加载缓慢。

对于依赖关系复杂的类(如 A 依赖 B,B 依赖 C),JSVM 会自动解析依赖链,先加载 C,再加载 B,最后加载 A,确保代码执行时类已存在,不会报错。

JSVM 的价值:适配大型项目

在小型项目中,手动管理类可能还能应付,但在大型项目(如后台管理系统、复杂的单页应用)中,JSVM 能极大提升开发效率:

  • 减少全局污染:所有类都通过命名空间管理,不会出现变量冲突;
  • 提高可维护性:类的路径和依赖统一管理,后续修改文件位置或依赖关系时,只需修改配置;
  • 提升性能:按需加载减少首屏资源体积,加快页面加载速度。

最后小结:

JavaScript 最初是作为 "页面脚本语言" 设计的,而面向对象编程则让它具备了 "工程化开发" 的能力 ------ 通过类与实例的封装,让代码更复用;通过继承,让功能更易扩展;通过原型,让资源更高效;通过 JSVM 等框架,让大型项目更易管理。

从之前的 Ajax 框架封装,到现在用 OOP 重构 Ajax 请求类,我们能清晰地看到:OOP 不是 "花里胡哨的语法",而是解决 "代码冗余、维护困难、扩展不便" 的实际方案。它让 JavaScript 代码从 "零散的工具集合",升级为 "模块化的系统工程",为后续 React、Vue 等现代前端框架的学习打下了坚实的基础 ------ 这些框架的核心,本质上都是用 OOP 或基于原型的思想构建的组件化体系。

相关推荐
盗德1 小时前
最全音频处理WaveSurferjs配置文档二(事件)
前端·javascript
Evan芙1 小时前
shell编程求10个随机数的最大值与最小值
java·linux·前端·javascript·网络
m0_740043731 小时前
Vue 组件及路由2
前端·javascript·vue.js
奋斗吧程序媛1 小时前
Vue2 + ECharts 实战:动态一个关键词或动态多关键词筛选折线图,告别数据重叠难题
前端·javascript·echarts
San301 小时前
JavaScript 底层探秘:从执行上下文看 `this` 的设计哲学与箭头函数的救赎
javascript·面试·ecmascript 6
是你的小橘呀1 小时前
从 "渣男" 到 "深情男":Promise 如何让 JS 变得代码变得专一又靠谱
前端·javascript·html
baozj1 小时前
告别截断与卡顿:我的前端PDF导出优化实践
前端·javascript·vue.js
梵得儿SHI1 小时前
Vue 响应式原理深度解析:Vue2 vs Vue3 核心差异 + ref/reactive 实战指南
前端·javascript·vue.js·proxy·vue响应式系统原理·ref与reactive·vue响应式实践方案
玉宇夕落2 小时前
深入理解 JavaScript 中的 this:从设计缺陷到最佳实践(完整复习版)
javascript