耗了一个晚上吐血翻译不过也学到了不少...《使用对象来组织你的代码》,翻译中发现原作者在原文中有部分代码有误或不全,本文已修改和添加~
丽贝卡·墨菲原文链接:http://rmurphey.com/blog/2009/10/15/using-objects-to-organize-your-code
当你不只是使用jQuery的简单片段而是开始开发更复杂的用户交互,你的代码会变得笨重和难以调试,这篇文章通过使用对象字面量的形式向你展示如何在行为特征的角度思考这些交互。
在过去几年,JavaScript库让初级开发者有能力为他们的站点制作炫酷的交互,就像jQuery,有着非常简单的语法得以让零编程经验的人装饰他们的网页,一个插件或自定义的几十行代码运行出的效果就能给人留下深刻印象。
但是等等,现今的需求早已改变了,现在你的代码可能需要根据ID的不同而被重用,这样的话用jQuery(或其他库)的编写的代码片段看似用处不大了,它们只是代码片段不是吗?当你不使用插件而实现 show() 或 hide() 的功能应该怎么设计你的代码呢?
Introducing the Object Literal Pattern 对象字面量的介绍
对象字面量提供了一个包括行为的方式去组织代码,这也意味着避免污染全局命名空间,这是对于一个较大项目的很好做法,它迫使你去思考你的代码在一开始就应该做什么以及哪些部分需要放置在合适的位置。对象字面量是封装相关行为的方式,如下所示:
var myObjectLiteral = { myBehavior1 : function() { /* do something */ }, myBehavior2 : function() { /* do something else*/ } };
假设你使用jQuery完成一个点击list项显示和隐藏的功能:
$(document).ready(function() { $('#myFeature li') .append('<div/>') .each(function(){ $(this).find('div') .load('foo.php?item=' + $(this).attr('id')); }) .click(function() { $(this).find('div').show(); $(this).siblings().find('div').hide(); }); });
就是这么简单,但是当你想在这个例子中改变一些需求,例如加载内容的URL的方式,以及加载内容的URL,或者是显示和隐藏的行为等等,对象字面量清晰地划分了这些功能特征,看起来如下:
var myFeature = { config : { wrapper : '#myFeature', container : 'div', urlBase : 'foo.php?item=' }, init : function(config){ $.extend(myFeature.config, config); $(myFeature.config.wrapper).find('li'). each(function(){ myFeature.getContent($(this)); }). click(function(){ myFeature.showContent($(this)); }); }, buildUrl : function($li){ return myFeature.config.urlBase + $li.attr('id'); }, getContent : function($li){ $li.append('<' + myFeature.config.container + '/>'); var url = myFeature.buildUrl($li); $li.find(myFeature.config.container).load(url); }, showContent : function($li){ $li.find(myFeature.config.container).show(); myFeature.hideContent($li.siblings()); }, hideContent : function($elements){ $elements.find(myFeature.config.container).hide(); } }; $(document).ready(function() { myFeature.init(); });
最初的例子是很简单的,用对象字面量形式却让代码变得更长,说实话,对象字面量形式一般是不会节省你的代码量的。使用对象字面量我们将代码的逻辑部分分割开来,因此很容易找到我们想要改变的部分,我们已经取得我们的功能扩展,提供了覆写默认配置的功能。并且做了文档上的限制,很容易一眼看出该部分做什么功能。抛开这个例子的简单结构,随着需求的增长我们的代码结构将变得愈来愈清晰。
An in-depth example 一个更深层次的示例
我们的任务是创建每个部分含有多项内容的UI元素,点击一个区块将显示区块中项目的列表,点击项目列表中的项目,项目内容将显示在内容区域。每当区块被显示时,第一个项目列表应该也被显示。第一部分应该在页面加载时被显示。
作者想表达的效果图应该是这样的:
Step 1: HTML结构
编写良好语义化的HTML是编写好的JavaScript的先决条件,所以我们思考一下HTML应该长什么样子呢,HTML应该是:
- 当JavaScript不可用时HTML仍然有意义并且很好的工作
- 提供可预测的DOM结构方便附加在JavaScript上
- 避免不必要的IDs和classes(你可能会感到惊讶)
考虑到这些策略,我们开始编写html吧:
<h1>This is My Nifty Feature</h1> <div id="myFeature"> <ul class="sections"> <li> <h2><a href="/section/1">Section 1</a></h2> <ul> <li> <h3><a href="/section/1/content/1">Section 1 Title 1</a></h3> <p>The excerpt content for Content Item 1</p> </li> <li> <h3><a href="/section/1/content/2">Section 1 Title 2</a></h3> <p>The expert content for Content Item 2</p> </li> <li> <h3><a href="/section/1/content/3">Section 1 Title 3</a></h3> <p>The expert content for Content Item 3</p> </li> </ul> </li> <li> <h2><a href="/section/2">Section 2</a></h2> <ul> <li> <h3><a href="/section/2/content/1">Section 2 Title 1</a></h3> <p>The excerpt content for Content Item 1</p> </li> <li> <h3><a href="/section/2/content/2">Section 2 Title 2</a></h3> <p>The expert content for Content Item 2</p> </li> <li> <h3><a href="/section/2/content/3">Section 2 Title 3</a></h3> <p>The expert content for Content Item 3</p> </li> </ul> </li> <li> <h2><a href="/section/3">Section 3</a></h2> <ul> <li> <h3><a href="/section/3/content/1">Section 3 Title 1</a></h3> <p>The excerpt content for Content Item 1</p> </li> <li> <h3><a href="/section/3/content/2">Section 3 Title 2</a></h3> <p>The expert content for Content Item 2</p> </li> <li> <h3><a href="/section/3/content/3">Section 3 Title 3</a></h3> <p>The expert content for Content Item 3</p> </li> </ul> </li> </ul> </div>
注意此时没有任何标记显示一级导航或二级(项目)导航,通过加入Jquery让它们工作;不支持JavaScript的用户将会得到很好的语义HTML(如果HTML表达语义不清,应该是时候替换旧的语义和实现渐进增强了)。
Step 2: Scaffolding the Object Object的脚手架
创建对象第一步是为对象创建"存根",可以把"存根"想象成占位符;它们是我们要构建的功能大纲,我们的对象将有如下方法:
- myFeature.init() 将会运行在 $(document).ready() 中。通过语义化的HTML标签,让我们很快进入到JavaScript的用户界面。
- myFeature.buildSectionNav() 将被 myFeature.init() 调用。这将需要一个jQuery对象包含所有section语义的HTML标签来构建一级导航,每项一级导航而且会被绑定点击事件,点击它们将会显示相应的部分(二级导航)
- myFeature.buildItemNav() 将被 myFeature.showSection() 调用,这需要jQuery对象包含所有和section相关的具有语义HTML的item项,用它们来建立二级导航。它们(二级导航)也将会被绑定点击事件所以点击后将显示相关内容。
- myFeature.showSection() 当用户点击一级导航的项时将被调用。通过一级导航的点击项判断哪部分语义内容将被显示。
- myFeature.showContentItem() 当用户点击二级导航时将被调用。通过二级导航的点击判断那部分语义内容将被显示。
首先配置属性,通过 myFeature.config 将各个属性设置到一起而不是在代码中各个部分定义。我们将在 myFeature.init() 中提供默认属性覆写的功能。
var myFeature = { 'config' : {}, 'init' : function() {}, 'buildSectionNav' : function() {}, 'buildItemNav' : function() {}, 'showSection' : function() {}, 'showContentItem' : function() {} };
Step 3: The Code 代码
一旦我们建立起了骨架,是时候开始编写后面的代码了。首先编写 myFeature.config 对象和 myFeature.init() 方法:
'config' : { //default container is #myFeature 'container' : $('#myFeature') }, 'init' : function(config){ //provide for custom configuration via init() if(config && typeof(config) == 'object' ){ $.extend(myFeature.config, config); } //create and/or cache some DOM elements //we'll want to use throughout the code myFeature.$container = myFeature.config.container; myFeature.$sections = myFeature.$container. // only select immediate children! find('ul.sections > li');
myFeature.$items = myFeature.$sections.
find('ul > li'); myFeature.$section_nav = $('<p/>') .attr('id', 'section_nav') .prependTo(myFeature.$container); myFeature.$item_nav = $('<p/>') .attr('id', 'item_nav') .insertAfter(myFeature.section_nav); myFeature.$content = $('<p/>') .attr('id', 'content') .insertAfter(myFeature.$item_nav); //build the section-level nav and //"click" the first item myFeature.buildSectionNav(myFeature.$sections); myFeature.$section_nav.find('li:first').click(); //hide the plain HTML from sight myFeature.$container.find('ul.sections').hide(); //make a note that the initialization //is complete; we don't strictly need this //for this iteration, but it can come in handy myFeature.initialized = true; }
接下来编写 myFeature.buildSectionNav() 方法:
'buildSectionNav' : function($sections){ //iterate over the provided list of sections $sections.each(function(){ //get the section var $section = $(this); //create a list item for the section navigation $('<li/>') //use the text of the first h2 //in the section as the text for //the section navigation .text($section.find('h2:first').text()) //add the list item to the section navigation .appendTo(myFeature.$section_nav) //use data() to store a reference //to the original section on the //newly-created list item .data('section', $section) //bind the click behavior //to the newly created list item //so it will show the section .click(myFeature.showSection); }); }
接下来编写 myFeature.buildItemNav() 方法:
'buildItemNav' : function($items){ //iterate over the provided list of items $items.each(function(){ //get the item var $item = $(this); //create a list item element for the //item navigation $('<li/>') //use the text of the first h3 //in the item as the text for the //item navigation .text($item.find('h3:first').text()) //add the list item to item navigation .appendTo(myFeature.$item_nav) //use data to store a reference //to the original item on the //newly created list item .data('item', $item) //bind the click behavior to the //newly created list item so it will //show the content item .click(myFeature.showContentItem); }) }
最后,我们将编写 showSection() 和 showContentItem() 方法:
'showSection' : function(){ // capture the list item that was clicked on var $li = $(this); //clear out the left nav and content area myFeature.$item_nav.empty(); myFeature.$content.empty(); //get the jQuery section object from original HTML, //which we stored using data() during buildSectionNav var $section = $li.data('section'); //mark the clicked list item as current //and remove the current marker from its siblings $li.addClass('current') .siblings().removeClass('current'); //find all of items related to the section var $items = $section.find('ul li'); //build the item nav for the section myFeature.buildItemsNav($items); //"click" on the first list item in the section's item nav myFeature.$item_nav.find('li:first').click(); }, 'showContentItem' : function(){ var $li = $(this); //mark the clicked list item as current //and remove the current marker form its siblidngs $li.addClass('current') .siblings().removeClass('current'); //get the jQuery item object from the original HTML, //which we stored using data during buildContentNav var $item = $li.data('item'); myFeature.$content.html($item.html()); }
所有准备完后,我们开始调用 myFeature.init() 方法:
$(document).ready(myFeature.init())
Step 4: Changing Requirements
没有项目是不提需求的,随时变更是特点不是吗?对象字面量的方式使开发快速并且相当容易实现变更需求。如果我们需要获取内容片段是从AJAX得来的而不是HTML?假设这里添加了前后端交互的功能,尝试一下:
var myFeature = { 'config' : { 'container' : $('#myFeature'), // configurable function for getting // a URL for loading item content 'getItemURL' : function($item){ return $item.find('a:first').attr('href'); } }, 'init' : function (config) { // stays the same }, 'buildSectionNav' : function($sections){ // stays the same }, 'buildItemNav' : function($items) { // stays the same }, 'showSection' : function(){ //stays the same }, 'showContentItem' : function(){ var $li = $(this); $li.addClass('current'). $siblings().removeClass('current'); var $item = $li.data('item'); var url = myFeature.config.getItemURL($item); // myFeature.$content.html($item.html()) myFeature.$content.load(url); } }
想要更加灵活吗?有许多你能配置的(覆写)如果你真的想使代码功能变得灵活。例如,你可以通过配置 myFeature.config 自定义地为每个item找到对应的文本:
var myFeature = { 'configure' : { ' container' : $('#myFeature'), //specify the default selector // for finding the text to use // for each item in the item nav 'itemNavSelector' : 'h3', //specify a default callback //for "processing" the jQuery object //returned by the itemNavText selector 'itemNavProcessor' : function($selection){ return 'Preview of ' + $selection.eq(0).text(); } }, 'init' : function(config){ // stays the same }, 'buildSectionNav' : function($sections){ // stays the same }, 'buildItemNav' : function($items){ $.items.each(function(){ var $item = $(this); //use the selector and processor //from the config //to get the text for each item nav var myText = myFeature.config.itemNavProcessor( $item.find(myFeature.config.itemNavSelector) ); $('<li/>') //use the new variable //as the text for the nav item .text(myText) .appendTo(myFeature.$item_nav) .data('item', $item) .click(myFeature.showContentItem); }); }, 'showSection' : function(){ // stays the same }, 'showContentItem' : function (){ // stays the same } };
只要你添加配置对象参数,调用 myFeature.init() 时就可以覆写config对象:
$(document).ready(function(){ myFeature.init({ 'itemNavSelector' : 'h2' }); });
OK!有了以上了解和学习,读者们可以尝试实现jQuery history 插件~
Conclusion 总结
如果你按照代码例子一步步理解过来后,你应该对对象字面量有了基本了解,它会对你开发复杂功能和交互提供一个有用的方式,提供给你可以在本代码上继续扩展功能,我鼓励你在JavaScript中尝试使用对象字面量模式去代替短短几行的代码——因为这会迫使你去思考元素的表现和行为去构成一个复杂的功能或交互。一旦你掌握了它,它为扩展和重用你的代码提供了坚实的基础。
Learn More 了解更多
- More on the jQuery data() method
- More praise for the object literal pattern
- The jQuery History plugin
- An interseting application of the object literal pattern for architecting code for multiple page types
附录前文中An in-depth example 完整代码:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>An in-depth example 一个更深层次的示例</title> <style type="text/css"> .current{ background: #f47460; } </style> <script type="text/javascript" src="http://cdn.bootcss.com/jquery/3.0.0/jquery.min.js"></script> </head> <body> <h1>This is My Nifty Feature</h1> <div id="myFeature"> <ul class="sections"> <li> <h2><a href="/section/1">Section 1</a></h2> <ul> <li> <h3><a href="/section/1/content/1">Section 1 Title 1</a></h3> <p>The excerpt content for Content Item 1</p> </li> <li> <h3><a href="/section/1/content/2">Section 1 Title 2</a></h3> <p>The expert content for Content Item 2</p> </li> <li> <h3><a href="/section/1/content/3">Section 1 Title 3</a></h3> <p>The expert content for Content Item 3</p> </li> </ul> </li> <li> <h2><a href="/section/2">Section 2</a></h2> <ul> <li> <h3><a href="/section/2/content/1">Section 2 Title 1</a></h3> <p>The excerpt content for Content Item 1</p> </li> <li> <h3><a href="/section/2/content/2">Section 2 Title 2</a></h3> <p>The expert content for Content Item 2</p> </li> <li> <h3><a href="/section/2/content/3">Section 2 Title 3</a></h3> <p>The expert content for Content Item 3</p> </li> </ul> </li> <li> <h2><a href="/section/3">Section 3</a></h2> <ul> <li> <h3><a href="/section/3/content/1">Section 3 Title 1</a></h3> <p>The excerpt content for Content Item 1</p> </li> <li> <h3><a href="/section/3/content/2">Section 3 Title 2</a></h3> <p>The expert content for Content Item 2</p> </li> <li> <h3><a href="/section/3/content/3">Section 3 Title 3</a></h3> <p>The expert content for Content Item 3</p> </li> </ul> </li> </ul> </div> <script type="text/javascript"> var myFeature = { 'config': { 'container' : $('#myFeature') }, 'init': function(config){ if(config && typeof config == 'object'){ $.extend(myFeature.config, config); } //缓存变量 myFeature.$container = myFeature.config.container; myFeature.$sections = myFeature.$container. find('ul.sections > li'); myFeature.$items = myFeature.$sections. find('ul > li'); myFeature.$section_nav = $('<p/>') .attr('id', 'section_nav') .prependTo(myFeature.$container); myFeature.$item_nav = $('<p/>') .attr('id', 'item_nav') .insertAfter(myFeature.$section_nav); myFeature.$content = $('<p/>') .attr('id', 'content') .insertAfter(myFeature.$item_nav); //初始化新增的这三层DOM结构 myFeature.buildSectionNav(myFeature.$sections); myFeature.$section_nav.find('li:first').click(); //隐藏原有的HTML结构 myFeature.$container.find('ul.sections').hide(); }, 'buildSectionNav' : function($sections){ //绑定事件 $sections.each(function(){ var $section = $(this); $('<li>').text($section.find('h2:first').text()) .appendTo(myFeature.$section_nav) .data('section', $section) .click(myFeature.showSection) }); }, 'buildItemNav' : function($items){ //绑定事件 $items.each(function(){ var $item = $(this); $('<li>').text($item.find('h3:first').text()) .appendTo(myFeature.$item_nav) .data('item', $item) .click(myFeature.showContentItem); }); }, 'showSection' : function(){ //事件处理程序 var $li = $(this); myFeature.$item_nav.empty(); myFeature.$content.empty(); var $section = $li.data('section'); $li.addClass('current') .siblings().removeClass('current'); var $items = $section.find('ul li'); myFeature.buildItemNav($items); myFeature.$item_nav.find('li:first').click(); }, 'showContentItem' : function(){ //事件处理程序 var $li = $(this); $li.addClass('current') .siblings().removeClass('current'); var $item = $li.data('item'); myFeature.$content.html($item.html()); } } $(document).ready(function(){myFeature.init()}); </script> </body> </html>