聊聊jQuery的反模式

LouanneWesc 3年前

来自: https://segmentfault.com/a/1190000004538883

如果我们认为模式代表一个最佳的实践,那么反模式将代表我们已经学到一个教训。受启发于Gof的《设计模式》,Andrew Koeing在1995年的11月的C++报告大会上首次提出反模式。在Koeing的报告中,反模式有着两种观念:

  • 描述对于一个特殊的问题,提出了一个糟糕的解决方案,最终导致一个坏结果发生

  • 描述如何摆脱上述解决方案并能提出一个好的解决方案

在如今这个前端发展如火如荼的时代,谈及jq总是显得非常的low,但实际上,在学校,在很多前端新人以及所谓“页面仔 || 切图工”之类的同行之间,jq的活力还是远超各种框架的时候,之所以想写这样一篇文章,一是因为见到了身边的jq烂代码,二是因为我在百度jQuery反模式的时候居然什么有价值的相关结果都没有,所以觉得还是有必要聊聊的。

先从些简单的开始:

插入DOM节点:

// 反模式  $.each(reallyLongArray, function(count, item) {      var newLi = '<li>' + item + '</li>';      $('#ballers').append(newLi)  })    // 更好的实践  var frag = document.createDocumentFragment()  $.each(reallyLongArray, function(count, item) {      var newLi = '<li>' + item + '</li>';      frag.appendChild(newLi[0])  })  $('#ballers')[0].appendChild(frag)    // 你也可以用字符串  var myHTML = ''  $.each(reallyLongArray, function(count, item) {      myHTML += '<li>' + item + '</li>';  })  $('#ballers').html(myHTML)

DocumentFragment是浏览器为了减少DOM操作中的更新所使用的API,详情请查阅MDN相关文档。

遵循DRY原则:

if ($a.data('current') != 'showing') {      $a.stop()  }  if ($b.data('current') != 'showing') {      $b.stop()  }  if ($c.data('current') != 'showing') {      $c.stop()  }    // 用数组来保存不同的主体  var elems = [$a, $b, $c]  $.each(elems, function(k, v) {      if (v.data('current') != 'showing') {          v.stop()      }  })

用数组或对象来保存重复片段的差异参数是一种很常见的方法。更多内容可以参考 常见的JavaScript设计模式 中的“九、策略模式”

地狱式回调(callback hell):

$(document).ready(function() {      $('#button').click(function() {          ......          $.get('http://xxxx', function(data) {              ......          })      })  })    // 以前有这么一种优化的方法,使用对象字面量保存回调使其扁平化  var PI = {      onReady: function() {          $('#button').click(PI.clickCb)    //Cb是Callback的简写      }      clickCb: function() {          ......          $.get('http://xxxx', getCb)      }      getCb: function() {          ......      }  }    $(document).ready(PI.onReady)    // 不过现在流行Promise    var initApp = new Promise(function(resolve, reject) {      $(document).ready(resolve)  })    initApp.then(function() {      return new Promise(function(resolve, reject) {          $('#button').click(resolve)      })  }).then(function() {      return new Promise(function(resolve, reject) {          $.get('http://xxxx', resolve)      })  }).then(function(data) {      console.log(data)  })

用对象将回调扁平还好,Promise是什么鬼。不是比回调还恶心,好吧,示例确实是这样。其实之所以用Promise除了将回调转成链式调用以外,主要还是为了用它的reject函数获取回调中的错误。像示例这种一路resolve的,没必要这么用。这里只是提一句。

重复查询:

$(document.body).append('<div class="baaron"></div>')  $('.baaron').click(function() {})    // 更好的方式  $('<div class="baaron"></div>')      .appendTo(document.body)      .click(function() {})

选择器:

对于jq的选择器还有许多要注意的问题,因为jq的选择器是从右向左查询,所以请记住一个“左轻右重”的原则:

// 请看下面两个选择器  $('div.foo .bar')  $('.foo span.bar')        //右边更明确一点,会好不少    // 当左边确实要比右边明确的时候这么干  $('#foo .bar')  $('#foo').find('.bar')    // 尤其避免使用通配符  $('#foo > *')  $('#foo').children()    // 有些通配符是隐式的  $('.foo :radio')  $('.foo *:radio')        //和上边一样的  $('.foo input:radio')    //改成这样

聊聊依赖:

接下来,让我们从优化一段jq代码开始,聊聊js中的依赖

$('#button').click(function() {      $.get('http://xxxx', function(data) {          $('#page').html(data.abc)      })  })

这段代码有以下问题:

  • click事件绑定的匿名函数难以重复利用,也很难测试

  • click回调的匿名函数中的$是全局变量,ajax请求回调的匿名函数中的$('#page')也是用到了$这一全局变量,全局变量应该是要避免的

  • 回调的问题前面也说过了,这里的回调还很清楚不至于说到地狱的程度

现在我们把代码这样改写:

var downJSON = function() {      $.get('http://xxxx', function(data) {          $('#page').html(data.abc)      })  }    $('#button').click(downJSON)

现在匿名函数被我们拿出来了,可以重用了,但还是难以测试,且涵盖全局变量。

继续:

var downJSON = function($, $el) {      $.get('http://xxxx', function(data) {          $el.html(data.abc)      })  }    $('#button').click(function() {      downJSON($, $('#page'))  })

这样改写以后,没有了全局变量,函数已经独立出去。换一种说法就是,我们去除了函数中的隐式依赖(前面例子中的函数要运行需要全局变量$,但没有从函数声明中表现出来,我们称其为隐式依赖),现在,函数执行所需要的依赖被显示声明,使其具有更好的可控性。前端的依赖管理如今是一个很流行的话题,不过在这里就不废话了。

奇技淫巧:

最后,对于几种比较常见的写法,我们也可以使用一些奇技淫巧,或能使代码更短,或能使代码更为易读:

简化条件语句:

// 常见的写法  if(!data) {      data = {}  }    // 简化  data = data || {}

你可能觉得这不值一提,但可能有些时候你写着写着就忽视了。比如, js数组去重的4个方法 中的第二个方法,就可以应用这个技巧:

// 原来的代码  Array.prototype.unique2 = function()  {      var hashTable = {},res=[];                //n为hash表,r为临时数组      for(var i = 0; i < this.length; i++) {    //遍历当前数组          if (!hashTable[this[i]]) {            //如果hash表中没有当前项              res.push(this[i]);                //把当前数组的当前项push到临时数组里面              hashTable[this[i]] = true;        //存入hash表          }      }      return res;  }    // 应用此技巧  Array.prototype.unique2 = function()  {      var hashTable = {}, res = []      for(var i = 0; i < this.length; i++) {          !hashTable[this[i]] ? res.push(this[i]) : null          hashTable[this[i]] = hashTable[this[i]] || true      }      return res  }

写成这样也未必说是优化,目测判断逻辑还多了一个哈哈,但是嵌套少了一层,怎么说呢,自行决定吧。

下面展示的一个技巧和上面这个也差不多:

// 正常写法  if(type === 'foo' || type === 'bar') {}    // 用对象有三种方法  // 方法1  if(({foo: 1, bar: 1})[type]) {}                    // type === 'toString'可以绕过验证    // 方法2  if(type in ({foo: 1, bar: 1})) {}                // 和上一种等价但是慢点    // 方法3  if(({foo: 1, bar: 1}).hasOwnProperty(type)) {}    // 最严密,也最慢    // 用正则  if(/^(foo|bar)$/.test(type)) {}

这种技巧的话,使用与否同样是自己决定。很多有意思的东西,都是自己想着玩的,有些情况,我们倒不如好好写成switch - case,都看的懂,也挺清晰。

总结两句,虽说jQuery库和新式的框架相比老了,但我觉得它在DOM操作上真的做到了一个极致。我相信很长一段时间,前端开发人员入门,还是要从它开始的。个人认为jq不适应时代的原因,是因为它本身也仅仅限于DOM操作,没有其他限制,以至于当应用复杂时,你完全控制不住你的页面。当你用上流行的框架,按照他们的Best Practice去组织代码,我想你刚刚开始的时候,一定会怀念jQuery这个溺爱你的老朋友的。

参考链接:

</div>