本文深入解析 JavaScript 中的单一职责原则(SRP),探讨其在对象与方法级别的应用。通过代理模式、迭代器模式和单例模式的实例,展示如何分离职责以降低耦合度。同时分析何时应分离职责、违反 SRP 的后果及优缺点,帮助开发者编写更稳定、易维护的代码。
单一职责原则
在 JavaScript 中,类的实际应用相对较少,因此单一职责原则(SRP)更多地被应用于对象和方法层面。本节讨论将主要围绕对象和方法展开。
SRP 中的“职责”指的是导致对象或方法发生变化的原因。如果一个方法存在两个不同的修改动机,即具备两个职责,那么它就可能变得不稳定。每个职责代表一个变化轴线,承担过多职责的方法在需求变更时更容易被修改。
当一个方法耦合了多个职责时,修改其中一个职责可能会意外影响其他职责的实现,导致低内聚和脆弱的设计。这种耦合性使得代码修改变得危险且不可预测。
因此,SRP 的核心体现为:一个对象或方法应当只负责一件事。
设计模式中的SRP原则
SRP 原则在设计模式中应用广泛,包括代理模式、迭代器模式、单例模式和装饰者模式等。
代理模式
在代理模式中,可以通过引入虚拟代理来分离职责。例如,将图片预加载的职责交给代理对象,而本体对象仅负责向页面添加 img 标签,这是其最原始的职责。
myImage 负责向页面添加 img 标签:
const myImage = (function () {
const imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function (src) {
imgNode.src = src;
}
}
})();
proxyImage 负责图片预加载,并在预加载完成后将请求委托给本体对象 myImage:
const proxyImage = (function () {
const img = new Image;
img.onload = function () {
myImage.setSrc(this.src);
}
return {
setSrc: function (src) {
myImage.setSrc('file:// /C:/Users/svenzeng/Desktop/loading.gif');
img.src = src;
}
}
})();
proxyImage.setSrc('http:// imgcache.qq.com/music/photo/000GGDys0yA0Nk.jpg');通过将添加 img 标签的功能与图片预加载职责分离到两个不同的对象中,每个对象只有一个修改动机。当其中一个对象发生变化时,不会影响另一个对象,从而提高了代码的稳定性。
迭代器模式
考虑以下代码:首先遍历一个集合,然后向页面添加一些 div,这些 div 的 innerHTML 分别对应集合中的元素:
const appendDiv = function (data) {
for (let i = 0, l = data.length; i < l; i++) {
const div = document.createElement('div');
div.innerHTML = data[i];
document.body.appendChild(div);
}
};
appendDiv([1, 2, 3, 4, 5, 6]);这是常见的代码模式,常用于 Ajax 请求后的回调函数中,遍历返回的数据并在页面中渲染节点。
appendDiv 函数原本只负责渲染数据,但在此处还承担了遍历聚合对象 data 的职责。如果 API 返回的 data 数据格式从 array 变为 object,遍历 data 的代码将需要修改为 for ( let i in data ) 的方式,此时必须修改 appendDiv 中的代码,否则无法正确向页面添加 div 节点。
将遍历 data 的职责提取出来正是迭代器模式的意义所在。迭代器模式提供了一种访问聚合对象的方法,而无需暴露其内部表示。
当把迭代聚合对象的职责单独封装在 each 函数中后,即使未来增加新的迭代方式,只需修改 each 函数,appendDiv 函数不受影响,代码如下:
const each = function (obj, callback) {
let value,
i = 0,
length = obj.length,
isArray = isArraylike(obj); // isArraylike 函数这里未实现
if (isArray) { // 迭代类数组
for (; i < length; i++) {
callback.call(obj[i], i, obj[i]);
}
} else {
for (i in obj) { // 迭代 object 对象
value = callback.call(obj[i], i, obj[i]);
}
}
return obj;
};
const appendDiv = function (data) {
each(data, function (i, n) {
const div = document.createElement('div');
div.innerHTML = n;
document.body.appendChild(div);
});
};
appendDiv([1, 2, 3, 4, 5, 6]);
appendDiv({ a: 1, b: 2, c: 3, d: 4 });单例模式
在 JavaScript 单例模式的实现中,最初的惰性单例代码如下:
const createLoginLayer = (function () {
let div;
return function () {
if (!div) {
div = document.createElement('div');
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild(div);
}
return div;
}
})();
现在,我们将管理单例的职责与创建登录浮窗的职责分别封装在两个方法中。这两个方法可以独立变化而互不影响,当它们结合时,便完成了创建唯一登录浮窗的功能。下面的代码显然是更好的做法:
const getSingle = function (fn) { // 获取单例
let result;
return function () {
return result || (result = fn.apply(this, arguments));
}
};
const createLoginLayer = function () { // 创建登录浮窗
const div = document.createElement('div');
div.innerHTML = '我是登录浮窗';
document.body.appendChild(div);
return div;
};
const createSingleLoginLayer = getSingle(createLoginLayer);
const loginLayer1 = createSingleLoginLayer();
const loginLayer2 = createSingleLoginLayer();
alert(loginLayer1 === loginLayer2); // 输出: true 何时应该分离职责
SRP 原则是所有原则中最简单但也最难正确运用的原则之一。
需要明确的是,并非所有职责都需要一一分离。
一方面,如果随着需求变化,两个职责总是同时变化,则无需分离它们。例如,在 Ajax 请求时,创建 xhr 对象和发送 xhr 请求几乎总是一起进行的,因此创建 xhr 对象的职责和发送 xhr 请求的职责就没有必要分开。
另一方面,职责的变化轴线仅当它们确定会发生变化时才具有意义。即使两个职责目前耦合在一起,但如果没有改变的迹象,那么主动分离它们可能没有必要,可以在代码需要重构时再进行分离。
违反 SRP 原则
在常规思维中,人们习惯将一组相关行为放在一起,因此正确分离职责并非易事。
一方面,我们受设计原则的指导;另一方面,我们不必在任何时候都严格遵循原则。在实际开发中,违反 SRP 的情况并不少见。例如,jQuery 的 attr 等方法就是明显违反 SRP 原则的做法。jQuery 的 attr 是一个庞大的方法,既负责赋值又负责取值,这对于 jQuery 的维护者来说会带来困难,但对于 jQuery 的用户来说,却简化了使用。
在方便性与稳定性之间需要做出取舍。具体选择哪一方,没有标准答案,而是取决于具体的应用环境。
SRP 原则的优缺点
SRP 原则的优点是降低了单个类或对象的复杂度。通过将对象按职责分解为更小的粒度,有助于代码复用,也有利于进行单元测试。当一个职责需要变更时,不会影响到其他职责。
但 SRP 原则也有一些缺点,最明显的是增加了编写代码的复杂度。当按职责将对象分解为更小的粒度后,实际上也增加了这些对象之间相互联系的难度。
到此这篇关于JavaScript单一职责原则深入分析的文章就介绍到这了,更多相关JS单一职责原则内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!