如何培养良好的编程实践

3,433 阅读14分钟

花了三个星期的晚上,已经看完了《编写可维护的JavaScript》这本书。总结如下:第一部分编程风格和第三部分的自动化测试在书籍中的结尾作者有将它总结整理出来,需要的可以自行去阅读,也可以看看我之前整理的(《如何培养良好的编程风格》)。这本书的重点在于第二部分的编程实践,也是最有营养的地方,可惜作者没有在书中没有去特意总结,我在这里总结一下,以帮助大家一起提高代码质量。建议边看边敲边感受,比单纯的看文章要收获的多。内容有点多,需要耐心耐心。

1. UI层的松耦合

很多设计模式是为了解决紧耦合的问题。如何做到松耦合,当修改一个组件而不需要更改其它地方的组件的时候,我们可以说这就是做到了松耦合,也是提高代码可维护性的关键所在。

1-1. 将JS从Css中抽离出来

示例代码

  // 不好的写法
  .box {
    // Css表达式包裹在一个特殊的expression()函数中
    width: expression(document.body.offsetWidth + 'px')
  }

推荐做法:避免使用CSS表达式(IE9以及IE9以上的浏览器不再支持CSS表达式)

1-2. 将CSS从JS中抽离出来

示例代码:

// 不好的写法
element.style.color = 'red'
element.style.cssText = 'color: red; left: 10px; top: 100px;'

当需要通过js来修改元素样式的时候,通过操作CSS的className,最后在js中添加对应的类名即可。

示例代码:

/*定义CSS样式*/
.reveal {
  color: red;
  left: 10px;
  top: 100px;
}
// 好的写法 - 原生写法
element.className += 'reveal'
// 好的写法 - HTML5
element.classList.add('reveal')

推荐做法:js不应当直接操作样式,以便保持和CSS的松耦合。除了修改定位属性的默认值,比如style.top,style.left通过js中修改默认值。

1-3. 将JS从HTML中抽离

// 不好的写法
<button onclick="doSomething()">Click Me </button>

第一个问题:在于严谨上来看,当按钮上发生点击事件时,doSomething()函数必须存在。可能出现用户点击按钮时该函数还不存在,这时就会报JS错误; 第二个问题:在于可维护性来看,如果你修改了doSomething()函数名,在这个例子中,你需要同时修改HTML和JS两部分的函数代码,这是典型的紧耦合的代码。 改进方法: 示例代码

function doSomething () {
 // 一些代码
}
var btn = document.getElementById('action-btn')
btn.addEventListener('click', doSomething, false)

兼容性处理 IE8以及更早的版本不支持addEventListener()函数, 因此你需要一个标准的函数将这些差异性做封装。

示例代码

function addEventListernner(target, type, handler) {
 if (target.addEventListener) {
   target.addEventListener(type, handler, false)
 } else if (target.addEventListener) {
   target.addEventListener('on' + type, handler)
 } else {
   target['on' + type] = handler
 }
}

这个函数可以在所有情形下都正常工作,我们常常像下面这样来使用这个方法

function doSomething () {
 // 一些代码
}
var btn = document.getElementByid('action-btn')
addEventListener(btn, 'click', doSomething)

推荐做法:对于"节点驱动"的的库来说,比如JQ,推荐用事件监听在js文件中绑定节点同时给予对应的函数事件,不推荐直接在html文件上绑定函数事件。

1-4. 将HTML从JS中抽离

// 不好的写法
var div = document.getElementById('mu-div')
div.innerHTML = '<h3>Hello World</h3>'

改进方法:

  • a.对于大量的标签,可以采用 - 从服务器加载

  • b.对于少量的标签,可以采用 - 简单的客户端模板

  • c.复杂的客户端模板,可以考虑如Handlebars(http://handlebarsjs.com/)所提供的解决方案,Handlebars是专为浏览器端JS设计的完整的客户端模板系统。

1-5.一句话总结

HTML,CSS,JS,三者的关系应当是相互独立分离的。如果产生交集,出现紧耦合代码,则违反了代码可维护性的原则。

2. 事件处理

情景引入

// 不好的用法
function handleClick (event) {
  var popup = document.getElementById('popup')
  popup.style.left = event.clientX + 'px'
  popup.style.top = event.clientY + 'px'
  popup.className = 'reveal'
}
// 上文中的addEventListener()
addEventListener(element, 'click', handleClick)

2-1. 隔离应用逻辑

上述实例代码的问题是事件处理程序(和用户行为相关的)包含了应用逻辑(应用逻辑是和应用相关的功能性代码, 而不是和用户行为相关的). 上述实例代码中,应用逻辑是在特定位置显示一个弹出框,但是有时你需要在用户鼠标移至某个元素上时判断是否显示弹出框,或者当按下键盘上的某个按键时也弹出显示框。 这样多个事件的处理程序执行了同样的应用逻辑,而你的代码却被不小心复制了多份。

将应用逻辑从所有事件处理程序中抽离出来的做法是一种最佳实践,我们将上述代码重写一下如下:

// 好的写法 -事件处理程序抽离应用逻辑
var MyApplication = {
  handleClick: function (event) {
    this.showPopup(event)
  },
  // 应用逻辑:显示弹出框
  showPopup: function (event) {
    var popup = document.getElementById('popup')
    popup.style.left = event.clientX + 'px'
    popup.style.top = event.clientY + 'px'
    popup.className = 'reveal'
  }
}
addEventListener(element, 'click', function(event) {
  MyApplication.handleClick(event)
})

推荐做法: 事件处理程序抽离应用逻辑

2-2. 不要分发事件对象

在剥离出应用逻辑之后,上述代码还存在一个问题,即event对象被无节制分发。它从匿名函的事件处理函数传入了MyApplication.handleClick(), 然后又传入了MyApplication。showPopup(), event对象上包含了很多和事件相关的额外信息,而这段代码只用到了其中的两个。 应用逻辑不应当依赖于event对象来正确完成功能。 最佳的做法是让事件处理程序使用event对象来处理事件,然后拿到所需要的数据传给应用逻辑。 例如:应用逻辑MyApplication。showPopup()方法只需要这两个数据,x坐标和y坐标,我们将方法重写一下如下:

// 好的写法
var MyApplication = {
  handleClick: function (event) {
    this.showPopup(event.clientX, event.clientY)
  },
  // 应用逻辑:显示弹出框
  showPopup: function (x, y) {
    var popup = document.getElementById('popup')
    popup.style.left = x + 'px'
    popup.style.top = y + 'px'
    popup.className = 'reveal'
  }
}
addListener(element, 'click', function(event) {
  MyApplication.handleClick(event) // 可以这样用
})

在这段重写的代码中MyApplication.handleClick()将x坐标和y坐标传入了MyApplication。showPopup(),代替之前传入的事件对象。这样可以很清晰地看到MyApplication。showPopup()所期望 传入的参数,并且在测试或代码的任意位置都可以很轻易地直接调用这段应用逻辑。比如:

// 这样调用非常棒
MyApplication.showPopup(1010)

推荐做法: 事件处理程序使用event对象来处理事件, 应用逻辑不应当依赖于event对象来正确完成功能,

2-3. 让事件处理程序成为接触到event对象的唯一的函数

事件处理函数应当在进入应用逻辑之前针对event对象执行任何必要的操作,包括阻止事件或阻止事件冒泡,都应当直接包含在事件处理程序当中。我们再次将上述代码重写一下如下:

// 好的写法
var MyApplication = {
  handleClick: function (event) {
    // 假设事件支持DOM Level2
    event.preventDefault()
    event.stopPropagation()
    // 传入应用逻辑
    this.showPopup(event.clientX, event.clientY)
  },
  // 应用逻辑:显示弹出框
  showPopup: function (x, y) {
    var popup = document.getElementById('popup')
    popup.style.left = x + 'px'
    popup.style.top = y + 'px'
    popup.className = 'reveal'
  }
}
addEventListener(element, 'click', function(event) {
  MyApplication.handleClick(event) // 可以这样用
})

在这段代码中,MyApplication.handleClick是事件处理程序,因此它在将数据传入应用逻辑之前调用了event.preventDefault()和event.stopPropagation(), 这清楚的展示了事件处理程序和应用逻辑之间的分工,因为应用逻辑不需要对event产生依赖,进而在很多地方都可以轻松地使用相同的业务逻辑,包括写测试代码。

推荐做法:让事件处理程序成为接触到event对象的唯一的函数

2-4. 一句话总结

事件处理中的事件处理程序和应用逻辑的关系是独立而分离的。事件处理程序负责处理event对象(不限于阻止事件或阻止事件冒泡),应用逻辑负责接收所需要的数据,不需要对event产生依赖。

3.将配置数据从代码中抽离出来

定义: 配置数据是在应用中写死的值,且将来可能会被修改。

常见的配置数据有:URL,需要展现给用户的字符串,重复的值,设置(比如每页的配置项),任何可能发生变更的值

示例代码

// 将配置数据抽离出来
var config = {
  MSG_INVALID_VALUE: 'Invalid value',
  URL_INVALID: '/errors/invalid.php',
  CSS_SELECTED: 'selected'
}
function validate (value) {
  if (!value) {
    alert (config.MSG_INVALID_VALUE)
    location.href = config.URL_INVALID
  }
}
function toggleSelected (element) {
  if (hasClass(element, config, CSS_SELECTED} {
    removeClass(element, config.CSS_SELECTED)
  } else {
    addClass(element, config.CSS_SELECTED)
  }

在这段代码中,我们将配置数据保存在了config对象中。config对象的每个属性都保存了一个数据片段,每个属性名都有前缀,用以表明数据的类型(MSG表示展现给用户的信息,URL表示网络地址,CSS表示这是一个calssName)。当然,命名约定是个人偏好。对于这段代码来说最重要的一点是,所有的配置数据都从函数中移除,并替换为config对象中的属性占位符。

4.不是你的对象不要动

请牢记,如果你的代码没有创建这些对象,不要修改他们,包括原生对象(Object, Array等等),Dom对象(例如document),BOM对象(例如window),类库的对象

4-1.原则

在面对不是我们自己拥有的对象面前,应当遵循以下三个原则

不覆盖方法

// 不好的写法
document.getElementById = function () {
  return null    // 引起混论
}

不新增方法

Array.prototype.reverseSort = functino () {
  return this.sort().reverse()
}

推荐做法: 大多数JavaScript库有一个插件机制,允许为代码库新增一些功能。如果想修改,最佳最可维护的方式是创建一个插件

不删除方法

// 不好的写法 -删除了Dom方法
document.getElementById = null

4-2. 更好的途径 --通过继承来扩充对象

在JavaScript中,继承仍然有一些很大的限制。首先,还不能从DOM或BOM对象继承。其次,由于数组索引和length属性之间错综复杂的关系,继承自Array是不能正常工作的。

4-2-1. 基于对象的继承,也经常叫做原型继承,通过ES5的Object.create()方法实现

示例代码

var person = {
  name: 'Nicholas',
  sayName: function () {
    alert(this.name)
  }
}
var myPerson = Object.create(person)
myPerson.sayName = function () {
  alert('Anonymous')
}
myPerson.sayName() // 弹出 'Anonymous'   重新定义myPerson.sayName会自动切断对person.sayName的访问
person.sayName() // 弹出'Nicholas'

Object.create()方法的第二个参数的属性和方法将添加到新的对象中

var person = {
  name: 'Nicholas',
  sayName: function () {
    alert(this.name)
  }
}
var myPerson = Object.create(person, {
  name: {value:'Greg'}
  })
myPerson.sayName() // 弹出 'Greg'
person.sayName() // 弹出'Nicholas'

一旦以这种方式创建了一个新对象,该新对象完全可以随意修改。毕竟,你是该对象的拥有者,在自己的项目中可以任意新增方法, 覆盖已存在的方法,甚至是删除方法。

知识点传送门: 关于对象更多的深浅拷贝知识点,请点击这里自行扩展

4-2-2. 基于类型的继承

继承是依赖于原型的,通过构造函数实现

示例代码

function MyError (message) {
  this.message = message
}
MyError.prototype = new Error ()

在上例中,MyError类继承自Error(所谓的超类)。MyError.prototype赋值为一个Error的实例。然后,每个MyError实例从Error那里继承了它的属性和方法,instanceof也能正常工作

function MyError (message) {
  this.message = message
}
MyError.prototype = new Error ()
var error = new MyError('Something bad happened.')
console.log(error instanceof Error) // true
console.log(error instanceof MyError) // true

4-2-3.门面模式

门面模式是一种流行的设计模式,它为一个已存在的对象创建一个新的接口。你无法从DOM对象上继承,所以唯一的能够安全地为其新增功能的选择就是创建一个门面。下面是一个DOM对象包装器代码示例

function (element) {
  this.element = element
}
DOMWrapper.prototype.addClass = function (className) {
  element.className += '' + className
}
DOMWrapper.prototype.remove = function () {
  this.element.parentNode.removeChild(this.element)
}
// 用法
var wrapper = new DOMWrapper(document.getElementById('my-div'))
// 添加一个className
wrapper = addClass('selected')
// 删除元素
wrapper.remove()

DOMWrapper类型期望传递给其构造器的是一个DOM元素。该元素会保存起来以便以后引用,它还定义了一些操作该元素的方法。addClass()方法是为那些还未 实现HTML5的classList属性的元素增加ClassName的一个简单的方法。remove()方法封装了从DOM中删除一个元素的操作,屏蔽了开发者要访问该元素父节点的需求。

4-2-4.三种类型的对比

从JavaScript的可维护性而言,门面是非常合适的方式,自己可以完全控制这些接口。你可以允许访问任何底层对象的属性或方法,反之亦然,也就是有效地过滤对该对象的访问。 你也可以对已有的方法进行改造,使其更加简单易用(上段示例代码就是一个案例)。底层的对象无论如何改变,只要修改门面,应用程序就能继续正常工作。 门面实现一个特定接口,让一个对象看上去像另一个对象,就称作一个适配器。门面和适配器唯一的不同是前者创建新街口,后者实现已存在的接口。

4-3 阻止修改

ES5引入了几个方法来防止对对象的修改。锁定这些对象,保证任何人不能有意或无意地修改他们不想要的功能。

4-3-1. 三种锁定修改的级别

防止扩展 禁止为对象'添加'属性和方法,但已存在的属性和方法是可以被修改或删除

密封 类似'防止扩展',而且禁止为对象'删除'已存在的属性和方法。

冻结 类似'密封',而且禁止为对象'修改'已存在的属性和方法(所有字段均只读)

每种锁定的类型都有两个方法:一个是用来实施操作,另一个用来检测是否应用了相应的操作。

4-3-2. 应用示例代码

防止扩展

var person = {
  name: 'Nicholas'
}
// 锁定对象
Object.preventExtensions(person) // 实施可扩展
console.log(Object.isExtensible(person))  // false  检测一个对象是否是可扩展的
person.age = 25 // 正常情况下悄悄地失败,除非在strict模式下则会特意抛出错误提示

密封

var person = {
  name: 'Nicholas'
}
// 锁定对象
Object.seal(person)
console.log(Object.isExtensible(person))  // false  检测一个对象是否是可扩展的
console.log(Object.isSealed(person)) // true 检测一个对象是否是密封的
delete person.name // 正常情况下悄悄地失败,除非在strict模式下抛出错误
person.age = 25 // 同上
console.log(person)

冻结

var person = {
  name: 'Nicholas'
}
// 锁定对象
Object.freeze(person)
console.log(Object.isExtensible(person))  // false  检测一个对象是否是可扩展的
console.log(Object.isSealed(person)) // true 检测一个对象是否是密封的
console.log(Object.isFrozen(person)) // true 检测一个对象是否是冻结
person.name = 'Greg' // 正常情况下悄悄地失败,除非在strict模式下抛出错误
person.age = 25 // 同上
delete person.name // 同上
console.log(person)

4-3-3. 一句话总结

使用ES5中的这些方法是保证你项目不经过你同意锁定修改的极佳的做法。如果你是一个代码库的作者,很可能想锁定核心库某些部分来保证它们不被意外修改,或者想强迫 允许拓展的地方继续存活着。如果你是一个应用程序的开发者,锁定应用程序的任何不想被修改的部分。这两种情况中,在全部定义好这些对象的功能之后,才能使用上述的方法。 一旦一个对象被锁定了,它将无法解锁。

5.总结

《编写可维护的JavaScript》,第一部分的编程风格,给我的启示是:我们在用Vue也好,React也好,在用框架前要多注意官方文档列出的编程风格,有助于我们规范代码结构,这是个小细节也是我们常常容易忽略的地方。第二部分编程实践,HTML,JS,CSS的相互分离独立,保持松耦合度;事件处理中的事件处理程序和应用逻辑的关系是独立而分离的;将配置数据从代码中抽离出来;不是你的对象不要动;这些细节的改善,对于代码维护度的提高都是很有帮助的。至于第三部分自动化测试,讲的更多的是像Ant,Ci系统工具的使用与安装。整本书到这里就已经结束了,以后更多的是在工作中的应用。觉得对你开发有帮助的可以点赞收藏一波,如果我哪里写错了,希望能指点出来。如果你有更好的想法或者建议,可以提出来在下方评论区与我交流。大家一起进步,共同成长。感谢[鞠躬]。

6.一起交流

  • 个人的github仓库,欢迎大家来star一下

  • 个人的微信公众号,付出的前端路,订阅微信公众号yhzg_gz(点击复制,在微信中添加公众号粘贴即可)

ps: 提高自己,与异性交朋友