微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

JavaScript 开发最佳实践(下)

2016-03-12 20:38 前端大全

(点击上方,可快速关注)


原文:Christian Heilmann

译者:伯乐在线 - honoka 

网址:http://web.jobbole.com/85265/


模块化 —— 每个任务对应一个函数


这是一个通用的编程最佳实践 —— 请确定你创建的函数一次只完成一个工作,这样其他开发者可以简单地调试与修改你的代码,而不需浏览所有代码才能弄清每一个代码块执行了什么功能。


同样也能应用于创建通用任务的辅助函数。如果你发现自己在不同的函数中做着相同的事情,那么最好创建一个更加通用的辅助函数,需要时重用其功能。


并且,一进一出比在函数内部修改代码更有意义。比如说你想编写一个创建新链接的辅助函数。可以这样做:


function addLink(text, url, parentElement) {

    var newLink = document.createElement('a');

    newLink.setAttribute('href', url);

    newLink.appendChild(document.createTextNode(text));

    parentElement.appendChild(newLink);

}


这能够好好工作,但你或许会发现自己又不得不增加不同的 attribute,取决于你要给哪种 element 增加它适用的链接。举个例子:


function addLink(text,url,parentElement) {

    var newLink = document.createElement('a');

    newLink.setAttribute('href',url);

    newLink.appendChild(document.createTextNode(text));

    if (parentElement.id === 'menu') {

        newLink.className = 'menu-item';

    }

    if (url.indexOf('mailto:') !== -1) {

        newLink.className = 'mail';

    }

    parentElement.appendChild(newLink);

}


这会使该函数更加特殊,难以适用于不同情形。一个更加清晰地方式是返回这个链接,当需要的时候,在主函数中覆盖这些额外的情况。在这里将 addLink() 改为更加通用的 createLink():


function createLink(text,url) {

var newLink = document.createElement('a');

newLink.setAttribute('href', url);

newLink.appendChild(document.createTextNode(text));

return newLink;

}

 

function createMenu() {

var menu = document.getElementById('menu');

var items = [

{

t:'Home',

u:'index.html'

},

{

t:'Sales',

u:'sales.html'

},

{

t:'Contact',

u:'contact.html'

}

];

for(var i = 0; i < items.length; i++) {

var item = createLink(items.t, items.u);

item.className = 'menu-item';

menu.appendChild(item);

}

}


通过使自己的所有函数只执行一种任务,你可以为应用创建一个主要的 init() 函数,包含所有应用构造。这种方式能够帮助你简单地修改应用、移除功能,而不需浏览 document 中残存的依赖。


渐进增强


Progressive Enhancement(渐进增强)是一项主要在 Graceful Degredation 上讨论相关细节的开发实践模型。大体上你需要做的就是编写出在任何可用的技术中都能运行的代码。对于 JavaScript,意味着当脚本不可用时(比如在 BlackBerry 上,或者因为一名过分热情的安全警察),你的 web 产品仍需允许用户到达他们的主要目标,不能因为他们无法开启而缺失的 JavaScript 功能就阻止他们访问,或者不想让他们访问。


令人惊讶地是当你面对一个问题时,将会频繁选择建立大量复杂的 JavaScript 脚本去解决它,但其实这个问题可以使用更加简单的解决方式。我遇见过的一个案例是在页面上创建一个允许用户搜索不同数据的搜索框,可以搜索 web、图片、新闻等等。


最初的版本中,不同的数据选项都是链接,会重写表单 action 的 attribute 指向后端不同脚本来表现相应搜索。


问题在于如果 JavaScript 被禁用,这些链接仍然会显示出来,但任何搜索都只会返回标准值,因为表单的 action 没被改变。解决方法很简单:除链接以外我们提供了一个单选按钮组的选项,也使用一个后端脚本分给其相应的搜索脚本。


这样不仅使每个人都能得到正确的搜索结果,也方便统计每个功能选项有多少用户选择。通过使用我们管理的正确 HTML 结构,成功避免利用 JavaScript 同时完成切换表单 action 与点击跟踪脚本的功能,每一位用户都能使用,无需在意环境问题。


允许配置与转换


如何让代码保持可维护性和整洁?最成功的要点之一就是创建一个 configuration object(可配置对象),包含所有可能随时间改变的事物。包括你创建的 element 里面使用到的所有文本(按键值与图片的选择文本)、CSS class 以及 ID 名、你所创建的接口的通用参数。


例如 Easy YouTube player(一个 YouTube 视频下载插件)有着下列配置对象:


// This is the configuration of the player. Most likely you will

// never have to change anything here, but it is good to be able

// to, isn’t it?

config = {

CSS:{

// IDs used in the document. The script will get access to

// the different elements of the player with these IDs, so

// if you change them in the HTML below, make sure to also

// change the name here!

IDs:{

container:'eytp-maincontainer',

canvas:'eytp-playercanvas',

player:'eytp-player',

controls:'eytp-controls',

 

volumeField:'eytp-volume',

volumeBar:'eytp-volumebar',

 

playerForm:'eytp-playerform',

urlField:'eytp-url',

 

sizeControl:'eytp-sizecontrol',

 

searchField:'eytp-searchfield',

searchForm:'eytp-search',

searchOutput:'eytp-searchoutput'

 

// Notice there should never be a comma after the last

// entry in the list as otherwise MSIE will throw a fit!

},

// These are the names of the CSS classes, the player adds

// dynamically to the volume bar in certain

// situations.

classes:{

maxvolume:'maxed',

disabled:'disabled'

// Notice there should never be a comma after the last

// entry in the list as otherwise MSIE will throw a fit!

}

},

// That is the end of the CSS definitions, from here on

// you can change settings of the player itself.

application:{

// The YouTube API base URL. This changed during development of this,

// so I thought it useful to make it a parameter.

youtubeAPI:'http://gdata.youtube.com/apiplayer/cl.swf',

// The YouTube Developer key,

// please replace this with your own when you host the player!!!!!

devkey:'AI39si7d…Y9fu_cQ',

// The volume increase/decrease in percent and the volume message

// shown in a hidden form field (for screen readers). The $x in the

// message will be replaced with the real value.

volumeChange:10,

volumeMessage:'volume $x percent',

// Amount of search results and the error message should there

// be no reults.

searchResults:6,

loadingMessage:'Searching, please wait',

noVideosFoundMessage:'No videos found : (',

// Amount of seconds to repeat when the user hits the rewind

// button.

secondsToRepeat:10,

// Movie dimensions.

movieWidth:400,

movieHeight:300

// Notice there should never be a comma after the last

// entry in the list as otherwise MSIE will throw a fit!

}

}


如果你将这个作为模块化的一部分,甚至可以使其公有化,以允许操作者在初始化你的模块之前仅需重写他们需要的部分。


保持代码简单地可维护性是十分重要的一件事,避免未来有相关需求的维护者不得不阅读全部代码,寻找他们应该修改的地方。如果这不够显眼,你就是采取了被废弃的或者说非常丑陋的解决方式。一旦需要升级时,不雅的解决方式无法接受补丁,并且完全失去了重用代码的机会。


避免长嵌套


嵌套代码解释了逻辑结构并使其更加易读,但太长的嵌套会让人难以搞清楚你在尝试什么。代码阅读者不应被强迫水平滚动,或在遇见喜欢包裹长串代码的编辑者时遭受困惑(这会使得你尝试缩进的努力毫无意义)。


另一个关于嵌套的问题是变量名和循环。一般你会使用 i 作为开始第一个循环的迭代变量,接下来,你会继续使用 j、k、l 等等。这样很快就会变得凌乱:


function renderProfiles(o) {

var out = document.getElementById('profiles');

for(var i = 0; i < o.members.length; i++) {

var ul = document.createElement('ul');

var li = document.createElement('li');

li.appendChild(document.createTextNode(o.members[i].name));

var nestedul = document.createElement('ul');

for(var j = 0; j < o.members[i].data.length; j++) {

var datali = document.createElement('li');

datali.appendChild(

document.createTextNode(

o.members[i].data[j].label + ' ' +

o.members[i].data[j].value

)

);

nestedul.appendChild(datali);

}

li.appendChild(nestedul);

}

out.appendChild(ul);

}


就像我正在做的,我使用了常见的 —— 应被抛弃的 —— 变量名 ul 与 li,为了嵌套列表我需要 nestedul 与 datali。如果列表要继续嵌套下去,我就需要更多的变量名,一直持续。更有意义的做法是将为每个成员创建嵌套列表的任务放进各自函数中,并通过恰当的数据调用。这样也能帮助我们防止一个套一个的循环。addMemberData() 函数非常通用,极有可能在其他时间派上用场。经过这些考虑后,我就可以像下面这样重写代码:


function renderProfiles(o) {

var out = document.getElementById('profiles');

for(var i = 0; i < o.members.length; i++) {

var ul = document.createElement('ul');

var li = document.createElement('li');

li.appendChild(document.createTextNode(data.members[i].name));

li.appendChild(addMemberData(o.members[i]));

}

out.appendChild(ul);

}

function addMemberData(member) {

var ul = document.createElement('ul');

for(var i = 0; i < member.data.length; i++) {

var li = document.createElement('li');

li.appendChild(

document.createTextNode(

member.data[i].label + ' ' +

member.data[i].value

)

);

}

ul.appendChild(li);

return ul;

}


优化循环


如果你不能正确使用循环,它们就会变得非常缓慢。最常见的错误之一是在每次迭代判断中读取数组的长度属性:


var names = ['George', 'Ringo', 'Paul', 'John'];

for(var i = 0; i < names.length; i++) {

    doSomeThingWith(names[i]);

}


这意味着循环每次运行,JavaScript 便会读取一次该数组的长度。你可以通过将长度值存储在其他变量中来避免这个问题:


var names = ['George', 'Ringo', 'Paul', 'John'];

var all = names.length;

for(var i = 0; i < all; i++) {

    doSomeThingWith(names[i]);

}


更简短的优化方法是在循环判断块中创建一个第二变量:


var names = ['George', 'Ringo', 'Paul', 'John'];

for(var i = 0, j = names.length; i < j; i++) {

    doSomeThingWith(names[i]);

}


另一件需要确定的事情是你已将大计算量的代码放在循环外部,包括正则表达式与 —— 更重要的 —— DOM 处理。你可以在循环中创建 DOM 节点,但不要将它们插入 document。你会在下一节的 DOM 最佳实践中学到更多。


最小化 DOM 访问


在浏览器中访问 DOM 是一件昂贵的事情。DOM 是一个非常复杂的 API,在浏览器中渲染会花费大量时间。运行复杂的 web 应用时,你可以发现你的电脑已被其他工作占满了 —— 修改需要花费更长时间或者只能显示一半等等。


为了确保你的代码足够快速,不会拖累浏览器停止,则应尽量最小化访问 DOM。不要不断地创建和使用 element,而需创建一个工具函数将 string 变为 DOM 元素,然后在生成过程最后调用这个函数影响一次浏览器渲染,而不是不断地干扰。


不要屈服于浏览器的独有特性


以某个浏览器为中心编写代码是一种会让代码难以维护并很快过时的方式。如果你浏览网页,将发现大量脚本特定了某个浏览器,其他浏览器更新版本后就会停止运行。


这是费时费力的行为 —— 就像本篇教程展现的一样,我们应该基于标准创建代码,而不是针对某一个浏览器。web 服务于每个人,不是一群使用最先进配置的精英用户。当浏览器市场快速更新时,你却需要回溯自己的代码并保持修复。这既没有效率也不有趣。


如果仅有一个浏览器拥有一些令人惊讶的工作特性,并且你的确需要使用,将代码放在专属于它的脚本文档中,然后以浏览器和版本号命名。这意味着当该浏览器被废弃时,你可以更轻易地发现和移除这个功能。


不要相信任何数据


谈论代码与数据安全时,要记住的要点之一便是不要相信任何数据。不仅仅因为邪恶的家伙想要窃取你的系统;它起始于简单的可用性。用户总会输入错误的数据。不是因为他们很愚蠢,而是因为太忙了,总被其他事物分心或你指示的词语令他们困惑。比如说我预定了一个月而不是六天的旅馆房间,只因为输入了一个错误的数字。。。我认为自己还是足够聪明的。


简而言之,确保进入系统的所有数据都是清晰且确实需要的。在后台编写从 URL 中检索到的参数时,这是非常重要的。在 JavaScript 中,测试传递给函数的参数类型十分重要(使用关键词 typeof)。当 members 不是一个数组时,下面的代码将会出现错误(例如对于一个 string,它将会为 string 的每个字符创建一个列表):


function buildMemberList(members) {

var all = members.length;

var ul = document.createElement('ul');

for(var i = 0; i < all; i++) {

var li = document.createElement('li');

li.appendChild(document.createTextNode(members[i].name));

ul.appendChild(li);

}

return ul;

}


为了让代码正常工作,你需要检查 members 的类型,确保是一个数组:


function buildMemberList(members) {

if (typeof members === 'object' &&

typeof members.slice === 'function') {

var all = members.length;

var ul = document.createElement('ul');

for(var i = 0; i < all; i++) {

var li = document.createElement('li');

li.appendChild(document.createTextNode(members[i].name));

ul.appendChild(li);

}

return ul;

}

}


数组会设下陷阱,告诉你自己是对象。为了确定它们是数组,可以检验一个只有数组才拥有的方法。


另一个不安全的实践是从 DOM 阅读信息,不做校验就使用。比如说,我曾经不得不调试一些导致 JavaScript 功能中止的代码。这些代码用于 —— 因为我自身的一些原因 —— 在 innerHTML 之外从一个页面元素中读取一个用户名,然后作为参数被一个函数调用。用户名可能是任意包括单引号和引号的 UTF-8 字符。这样将结束任何字符串,剩下的部分就会成为错误数据。并且如果有任意用户使用像 Firebug 或者 Opera DragonFly 这样的工具改变 HTML,就可以将用户名改成任何东西,并注入你的函数。


同样适用于只在客户端验证的表单。我曾经通过重写一个选择以提供另一个选项,注册了一个不可用的 email 地址。因为表单没在后台验证,使得该进程毫无阻碍地运行。


对于 DOM 访问,检验自己尝试访问的元素再修改十分有必要,也是你所期待的 —— 否则代码将会运行失败或造成奇怪的渲染 bug。


用 JavaScript 增加功能,不要创建太多内容


就像你在其他示例中看见的那样,在 JavaScript 中创建大量 HTML 会变得十分缓慢与古怪。特别在 Internet Explorer 上,当它一直通过 innerHTML 加载和操控内容时,如果你修改文档就会遇见各种各样的问题(在 Google 搜索 “操作中止错误” 看看一段悲哀和痛苦的故事)。


在页面维护方面,创建大量 HTML 标记也是一个糟糕的主意,因为不是每一位维护者都拥有和你一样水平的开发技巧,他们很可能对你的代码感到困惑。


我发现当我不得不使用一个大量依赖 JavaScript 的 HTML 模板创建应用时,通过 Ajax 加载这个模板更有用。那样维护者不需要涉及到你的 JavaScript 代码,便可以修改 HTML 结构和重要文本。唯一的障碍就是告诉他们,你需要哪些 ID 以及是否有必要遵循已定义顺序的中心 HTML 结构。你可以用内联 HTML 注释做到这些(然后当你加载好模板后取走这些注释)。示例可以查看 Easy YouTube template 的源代码。


在这个脚本中,当正确的 HTML 容器可用时,加载模板,在后面的 setupPlayer() 方法中应用事件处理程序:


var playercontainer = document.getElementById('easyyoutubeplayer');

if (playercontainer) {

ajax('template.html');

};

 

function ajax(url) {

var request;

try {

request = new XMLHttpRequest();

} catch(error) {

try {

request = new ActiveXObject('Microsoft.XMLHTTP');

} catch(error) {

return true;

}

}

request.open('get', url, true);

request.onreadystatechange = function() {

if (request.readyState == 4) {

if (request.status) {

if (request.status === 200 || request.status === 304) {

if (url === 'template.html') {

setupPlayer(request.responseText);

}

}

} else {

alert('Error: Could not find template…');

}

}

};

request.setRequestHeader('If-Modified-Since','Wed, 05 Apr 2006 00:00:00 GMT');

request.send(null);

};


通过此方法,我允许人们以任何需要的方式转换和改变这个插件,而无需修改 JavaScript 代码。


站在巨人的肩上构建


无可否认,近几年 JavaScript 库和框架已经统治了 web 开发市场。这不是坏事 —— 如果它们都能正确使用的话。所有优秀的 JavaScript 库都只做了一件事:简化你的开发生活,不再奔波于浏览器间的不一致,不再不断修复浏览器支持漏洞。JavaScript 库为你提供了一种可测的、基于函数的构建选择。


不通过库初学 JavaScript 是很好的主意,因为你可以切实地知道发生了什么,但当真正开始开发网站时,你需要使用一个 JS 库。你会处理更少的问题,并且出现的 bug 至少都是可以复现的,而不是随机出现的浏览器问题。


我的个人爱好是 Yahoo User Interface Library(YUI),基于 JQuery、Dojo 和 Prototype,但还有一堆优秀的库,你需要从中找到最适合自己和产品的那个库。


有时所有的库都很适合,在相同的项目中使用几个库可不是一个好主意。这会提升不必要的复杂性和维护难度。


开发环境代码并不等于生产环境代码


最后一点我要谈论的不是 JavaScript 本身,而是如何使它更好地适应你的开发策略。因为 JavaScript 的任何修改都会迅速影响你的网站的功能和性能,尽可能优化你的代码是一件很吸引人的事,甚至可以不顾及对于维护性的影响。


这里有许多聪明的技巧,你可以应用到 JavaScript 中让其表现得更棒。另一方面它们中的大部分都伴随着使代码更难以理解和维护的风险。


为了写出健全的、工作稳定的 JavaScript 脚本,我们需要跳出这种循环,停止为机器而不是为其他开发者优化代码。大多数时候,有些在其他语言中是常识的事却不为大部分 JavaScript 开发者所知。一个构建脚本可以移除缩进、注释,用数组查找替代字符串(避免 MSIE 为每个字符串的单独实例创建一个字符串对象 —— 甚至在条件中),并做其他所有需要的细节工作,以让我们的 JavaScript 在浏览器中飞翔。


如果我们更多关注于使原始代码易于理解,方便其他开发者扩展,我们就可以创建出完美的构建脚本。如果我们优化过度,则永远得不到这个结果。不要为你自己或浏览器构建代码 —— 为下一位从你这里接手的开发者构建代码。


总结


JavaScript 的主要诀窍在于避免采用简单的途径。JavaScript 是一种非常通用的语言,并且因为其运行的环境拥有很高的宽容度,十分容易写出看似完成工作的草率代码。然而同样的代码将会在几个月后回来彻底刺伤你。


如果你想拥有一份 web 开发者的工作,JavaScript 开发会成为你知识领域中十分必要的一环。如果你想从现在开始,那么你是幸运的,我自己和其他许多人已经犯了大量错误,完成了所有试验和自我改正;现在我们可以沿着这些知识前行了。



【今日微信公号推荐↓】

更多推荐请看值得关注的技术和设计公众号


其中推荐了包括技术设计极客 和 IT相亲相关的热门公众号。技术涵盖:Python、Web前端、Java、安卓、iOS、PHP、C/C++、.NET、Linux、数据库、运维、大数据、算法、IT职场等。点击《值得关注的技术和设计公众号》,发现精彩!



 
前端大全 更多文章 详解Javascript中的Object对象 结合个人经历总结的前端入门方法 前端不为人知的一面–前端冷知识集锦 一份优秀的前端开发工程师简历是怎么样的? 浅谈Web缓存
猜您喜欢 容器界的杜蕾斯,关注用户体验及安全 | 有容云AppSafe 深入理解Ruby-map(&:to_s)魔法 《AngularJS权威教程》-电纸书(四) 如何成为一名优秀的前端工程师 iOS完全自学手册——[三]Objective-C语言速成,利用Objective-C创建自己的对象