js中的设计原则和编程技巧
1 设计原则
1.1 单一职责原则(SRP)
含义:每个对象/方法只做一件职责。
例子:单例模式下,创建div
做法:将获取单例和创建div分离开来
实现:
var getSingle = function(fn:Function) {
var result:any
return function () {
return result || (result = fn.apply(this,arguments))
}
}
var createDiv = function () {
var div = document.createElement('div')
div.innerHTML = '这个是单例模式创建的div'
document.body.appendChild(div)
return div
}
var createSingleDiv = getSingle(createDiv)
var div1 = createSingleDiv()
var div2 = createSingleDiv()
div1 === div2 //true
好处:
- 降低了单个方法/对象的复杂性
- 有利于代码的复用和单元测试
- 当一个职责发生变化时,不会影响到其他的职责。
职责分离原则:
- 职责之间相互独立,不会相互影响
- 如果两个职责总是同时发生变化,就没必要去分离
- 如果在一起的几个职责不会发生任何变化,就没必要去分离
坏处:
- 增加代码的复杂度
- 增大了对象之间相互联系的难度
1.2 最少知识原则(LKP)
含义:一个软件实体应当尽可能少地与其他实体发生相互作用,软件实体包含:对象、类、模块、系统、变量、函数等。
例子:中介者模式,商城购买,购买手机 ,可以选择手机的颜色、购买数量、手机内存,当库存充足,购买按钮可点击 ,当库存不充足,按钮不可点击,并显示库存不足的信息
做法:输入框和下拉框发生事件时,只需要通知中介者它们发生了改变,让中介者来执行接下来的行为。
实现:
index.html
<body>
选择颜色: <select id="colorSelect">
<option value="">请选择</option>
<option value="red">红色</option>
<option value="blue">蓝色</option>
</select>
选择内存: <select id="memorySelect">
<option value="">请选择</option>
<option value="32G">32G</option>
<option value="16G">16G</option>
</select>
输入购买数量: <input type="text" id="numberInput"/><br/>
您选择了颜色: <div id="colorInfo"></div><br/>
您选择了内存: <div id="memoryInfo"></div><br/>
您输入了数量: <div id="numberInfo"></div><br/>
<button id="nextBtn" disabled="true">请选择手机颜色和购买数量</button>
<script src="./main.js"></script>
</body>
main.js
var goods = { // 手机库存
"red|32G": 3,
"red|16G": 0,
"blue|32G": 1,
"blue|16G": 6
};
Function.prototype.after = function (fn) {
var self = this;
return function () {
var ret = self.apply(this, arguments);
if (ret === 'nextSuccessor') {
return fn.apply(this, arguments);
}
return ret;
}
};
var colorCheck = function (obj,color,memory,number,stock,nextBtn) {
if (obj === colorSelect) { // 如果改变的是选择颜色下拉框
colorInfo.innerHTML = color;
}
if(!color) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择手机颜色';
} else {
return 'nextSuccessor'
}
}
var memoryCheck = function (obj,color,memory,number,stock,nextBtn) {
if (obj === memorySelect) {
memoryInfo.innerHTML = memory;
}
if(!memory) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择内存大小';
} else {
return 'nextSuccessor'
}
}
var numCheck = function (obj,color,memory,number,stock,nextBtn) {
if (obj === numberInput) {
numberInfo.innerHTML = number;
}
if(!Number.isInteger(number - 0) || number <= 0) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请输入正确的购买数量';
} else if(number > stock){
nextBtn.disabled = true;
nextBtn.innerHTML = '库存不足';
} else {
nextBtn.disabled = false;
nextBtn.innerHTML = '放入购物车';
}
}
var checkBtn = colorCheck.after(memoryCheck).after(numCheck)
var mediator = (function () {
var colorSelect = document.getElementById('colorSelect'),
memorySelect = document.getElementById('memorySelect'),
numberInput = document.getElementById('numberInput'),
colorInfo = document.getElementById('colorInfo'),
memoryInfo = document.getElementById('memoryInfo'),
numberInfo = document.getElementById('numberInfo'),
nextBtn = document.getElementById('nextBtn');
return {
changed: function (obj) {
var color = colorSelect.value, // 颜色
memory = memorySelect.value, // 内存
number = numberInput.value, // 数量
stock = goods[color + '|' + memory]; // 颜色和内存对应的手机库存数量
checkBtn(color,memory,number,stock,nextBtn)
}
}
})();
// 事件函数:
colorSelect.onchange = function () {
mediator.changed(this);
};
memorySelect.onchange = function () {
mediator.changed(this);
};
numberInput.oninput = function () {
mediator.changed(this);
};
好处:
- 减少了对象之间的依赖
原则:
- 尽可能地减少对象之间的交互,如果两个对象之间没必要直接通信,可以引入一个第三者对象来承担两个对象间的通信
- 当一个对象必须引用另一个对象的时候,让对象只暴露必要的接口,让对象之间的联系限制在最小的范围内(广义)
坏处:
- 有可能增加一些庞大到难以维护的第三者对象
1.3 开放封闭原则(OCP)
含义: 软件实体应该是可以扩展的,但不可以修改,软件实体包含:对象、类、模块、系统、变量、函数等。当需要修改一个程序的功能或者给这个程序增加新的功能时,可以使用增加代码的方式,而不是修改代码的方式。
例子:将一个数组映射为另一个数组
做法:映射的步骤是不变的(循环遍历),映射的方法是可变的,将映射的方法放在回调函数中封装起来
实现:
var arrMap = function(arr,callback) {
var i = 0,
length = arr.length,
value,
ret = []
for(;i<length;i++) {
value = callback(i, arr[i])
ret.push(value)
}
return ret
}
var a = arrMap([1,2,3],function (i,n) {
return n * 2
})
console.log(a)//[2,4,6]
好处:
- 避免修改源代码可能造成的副作用
- 降低维护源代码的成本
- 将系统中稳定的部分和容易变化的部分分离开来 ,方便了以后替换更改变化的部分
原则:
- 利用多态性(把做什么和谁去做分离开来),找出程序中将要发生变化的地方,将这些变化封装起来
- 在不可避免修改的情况下,尽量修改相对容易修改的地方
2 编程技巧
2.1 面向接口编程
含义: 面向接口编程就是面向抽象编程,关注点从对象的类型上 转移到对象的行为上,针对对象的超类型的抽象方法编程。
接口既指对象响应的请求的集合,同时也指一些语言提供的关键字,如java的interface,专门负责建立类与类之间的联系
2.1.1 抽象类
- 当泡茶和泡咖啡都有将原料倒入水中的操作时,我们可以将泡茶和泡咖啡向上转型为泡饮料(体现了对象的多态性)
- 在泡饮料的类中写一个将原料倒入水中的抽象方法,同时让从泡饮料继承来的泡茶和泡咖啡的子类重写将原料倒入水中的方法
abstract class Beverage {
abstract operation():void
}
class Tea extends Beverage{
operation() {
console.log('将茶包放入水中')
}
}
class Coffee extends Beverage {
operation() {
console.log('将咖啡放入水中')
}
}
2.1.2 接口
2.2的例子也可以将泡饮料抽象为一个接口:
interface Beverage {
operation: Function
}
class Tea implements Beverage{
operation() {
console.log('将茶包放入水中')
}
}
class Coffee implements Beverage {
operation() {
console.log('将咖啡放入水中')
}
}
2.2 代码重构
2.2.1 提炼函数
如果函数中有一大段代码可以被独立出来,最好是将这段代码放入另外一个独立的函数中
例子:页面加载完成后既要创建一个圆形,又要打印一些页面的版权信息
做法:将创建圆形和打印版权信息分离开来
实现:
window.onload = function () {
createCircle()
log()
}
function createCircle() {
var canvas = document.getElementById('canvas')
if(canvas.getContext) {
var ctx = canvas.getContext('2d')
ctx.fillStyle='red';
ctx.arc(20,20,20,0,2*Math.PI)
ctx.fill()
}
}
function log() {
console.log('版权所属')
}
2.2.2 合并重复的条件片段
如果每个if else判断里都执行同一段代码,可以将这段代码写入一个单独的函数,并且从判断中抽离出来,写在判断语句结束后
例子:跳转页面,当当前页面为非正整数时,跳转到第一页;当前页面大于总页数时,跳转到最后一页;其余情况,正常跳转
做法:将跳转页面抽离出来,放在判断语句之后
实现:
function paging(currPage,totalPage) {
if(currPage <= 0) {
currPage = 1
} else if (currPage >= totalPage) {
currPage = totalPage
}
jump(currPage)
}
2.2.3 把条件分支语句提炼成函数
当一个判断语句过长时,可以将该语句抽离成一个函数
例子:当当前活动为已开始并且用户已经报名或者当前活动已结束时,用户才可以在当前活动详情下进行晒图
做法:将判断语句抽离出来
实现:
function canPhoto (activityState,userState) {
return (activityState === '已开始' && userState === '已报名') || activityState === '已结束'
}
function photoActivity(activityState,userState) {
if(canPhoto(activityState,userState)) {
doSomething()
}
}
2.2.4 合理循环
如果有些语句做的是一些重复性的工作,可以将工作放入一个数组中,进行循环
例子:创建XHR对象(IE9以下的浏览器)
做法:将创建对象的参数放入数组中进行循环
实现:
var createXHR = function () {
var versions = ['MSXML2.XMLHttp.6.0ddd', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'];
for (var i = 0, version; version = versions[i++];) {
try {
return new ActiveXObject(version);
} catch (e) {}
}
};
var xhr = createXHR();
2.2.5 用函数退出来代替嵌套条件分支
当条件嵌套太多层时,可以将这些条件尽可能地抽离成一个层级的条件分支,在进入一些条件分支时,可以让函数立即退出
2.2.6 传递对象参数代替过长的参数列表
当参数列表过长时,可以考虑将参数放入一个对象中,这样不用担心参数的数量和顺序
比如在筛选文件的时候,我们可以通过文件的创建时间、修改时间、类型、创建者、关键字、下载量等信息进行筛选,就可以将这些参数放在一个文件的对象中
2.2.7 尽量减少参数数量
当有的参数可以通过内部计算获得的,尽量减少这些参数,而是在函数内部直接通过计算获取
比如,在绘制正方形的时候,传入的参数有宽度、高度和面积,但是面积其实可以通过宽度和高度运算得来,因此传入的参数中可以去掉面积这个参数
2.2.8 少用三目运算符
三目运算符增加了代码的可读性和可维护性,但是省略的代码量忽略不计
a === b ? a : b === doc ? b : doc === 'text' ? doc : null
2.2.9 合理利用链式调用
链式调用在调试的时候很不方便,当一条链发生错误,要把这条链全部加拆开加上debugger和一些console才能定位到错误的位置
链式调用可以参考jquery
2.2.10 分解大类
将大类中的行为分解到粒度更细的对象中
2.2.11 用return退出多重循环
在需要中止多重循环时直接退出整个方法,将中止后要执行的函数或者代码放在return 的后面
例子:找到相加为15的两个小于10的数
做法:双重循环
实现:
for(var i = 0; i < 10; i ++ ) {
for(var j = 0; j < 10; j ++) {
if( i + j === 15) {
return console.log(i,j)
}
}
}