GithubHelp home page GithubHelp logo

frontend's People

Contributors

codingecho avatar

Watchers

 avatar  avatar

frontend's Issues

jQuery 编程最佳实践(译)

jQuery 编程最佳实践


1.1 用变量缓存jQuery对象

DOM遍历是昂贵的,所以用变量来缓存需要复用的对象。

// bad
if ($('#userName').length) {
    var userName = $('#username');
    // ...
    $('#username').val();
}

// good
var $userName = $('#userName');
if ($userName.length) {
    var userName = $userName.val();
    // ...
    $userName.val(userName);
}

区分普通JavaScript变量和jQuery变量,变量前加美元符$,如上面示例中的:$userName

!!!更好的做法!!!
并不是所有的元素都有val()方法,有时候需要text()方法,所以
HTML这样写:

<input type="text" id="txtUserName" name="userName" />

JavaScript就可以写为:

var $txtUserName = $('txtUserName'),
    userName = $txtUserName.val();

1.2 避免意外的全局变量

// bad
$userName = $('#userName');
if ($userName.length) {
    var userName = $userName.val();
    // ...
    $userName.val(userName);
}

// good
var $userName = $('#userName');

1.3 使用使用$前缀区分jQuery对象和普通变量

// bad
var first = $('#first'); 
var second = $('#second'); 

// better - we use to put $ symbol before jQuery-manipulated objects
var $first = $('#first');
var $second = $('#second');
var value = $first.val(); 

1.4 单var模式

var $first = $('#first'),
    $second = $('#second'),
    value = $first.val(),
    k = 3,
    cookiestring = 'SOMECOOKIESPLEASE',
    myArray = {},
    i,
    j;

1.5 优先使用on来绑定事件

// bad
$first.click(function () {
    $first.css('border', '1px solid red');
    $first.css('color', 'blue');
});

$first.hover(function () {
    $first.css('border', '1px solid red');
});

// better(比上面的写法要好点)
$first.on('click', function () {
    $first.css('border', '1px solid red');
    $first.css('color', 'blue');
});

$first.on('hover', function () {
    $first.css('border', '1px solid red');
});

// good
function changeStyle() {
    // 这样的写法同样不好
    $first.css('border', '1px solid red');
    $first.css('color', 'blue');
    /*
     * // 更好的写法,定义一个CSS样式,比如:.checked
     * $first.css('checked');
     */
}

function tiggerHoverStyle() {
    $first.css('border', '1px solid red');
}

$first.on('click', changeStyle)
      .on('hover', tiggerHoverStyle);

1.6 使用jQuery链式写法

// bad
$second.html(value);
$second.on('click', function () { alert('hello everybody'); });
$second.fadeIn('slow');
$second.animate({ height: '120px' }, 500);

// good
$second.html(value);
$second.on('click', function () {alert('hello everybody');}).fadeIn('slow').animate({ height: '120px' }, 500);

1.7 让你的代码具有可读性

// bad
$second.html(value);
$second.on('click', function () { alert('hello everybody'); }).fadeIn('slow').animate({ height: '120px' }, 500);

// good
$second.html(value);
$second.on('click', function () { alert('hello everybody'); })
       .fadeIn('slow')
       .animate({ height: '120px' }, 500);

1.8 使用短路求值

// bad
function initVar($myVar) {
    if (!$myVar) {
        $myVar = $('#selector');
    }
}

// good
function initVar($myVar) {
    $myVar = $myVar || $('#selector');
}

// good 
function getValue($myVar) {
    var val;
    $myVar = $myVar || $('#selector');
    val = $myVar && $myVar.val();
}

1.9 简写表达式

// bad
if (collection.length > 0) {
    //..
}

// better
if (collection.length) {
    //..
}

1.10 Detach Elements When Doing Heavy Manipulations

// bad    
var $container = $('#container'),
    $containerLi = $('#container li'),
    $element = null;
$element = $containerLi.first();
// ... a lot of complicated things

// better
var $container = $('#container'),
    $containerLi = $container.find('li'),
    $element = null;
$element = $containerLi.first().detach();
// ...a lot of complicated things                               
$container.append($element);

1.11 你应该知道jQuery技巧

// bad
$('#id').data(key, value);

// better (faster)
$.data('#id', key, value);

1.11.1 Use Subqueries Caching Parents

// bad
var $container = $('#container'),
    $containerLi = $('#container li'),
    $containerLiSpan = $('#container li span');

// better (faster)
var $container = $('#container'),
    $containerLi = $container.find('li'),
    $containerLiSpan = $containerLi.find('span');

1.11.2 Avoid Universal Selectors(避免通用选择器)

// bad                                                         
$('.container > *');

// better                                                      
$('.container').children();

1.11.3 Avoid Implied Universal Selectors(避免隐式通用选择器)

When you leave off the selector, the universal selector (*) is still implied.

// bad     
$('.someclass :radio');

// better                                                     
$('.someclass input:radio');

1.11.4 优化选择器

别画蛇添足,耗时且代码不美观。

// bad 
$('div#myid');                                                
$('div#footer a.myLink');

// better
$('#myid');
$('#footer .myLink');
// bad
$('#outer #inner'); 

// better
$('#inner'); 

1.12 尝试使用最新版本的jQuery库

(不要使用已经过时的方法)

// bad - live is deprecated
$('#stuff').live('click', function () {
    console.log('hooray');
});

// better
$('#stuff').on('click', function () {
    console.log('hooray');
});

2 最佳实践

2.1 加载jQuery

(1)坚持使用CDN来加载jQuery,这种别人服务器免费帮你托管文件的便宜干嘛不占呢

<script type="text/javascript" src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="js/jquery-2.1.4.min.js" type="text/javascript"><\/script>')</script>

(2)安全起见,最好还是提供一个本地备份以便在无法从远程CDN服务器获取jQuery时网站也能工作,如上面代码所示。详情见此。

(3)使用裸协议的URL(也就是说去掉http:或者https:),如上面代码展示的那样。

(4)如果可能,尽量将你的JavaScriptjQuery代码放到页面底部。详情移步这里。

(5)该使用哪个版本?

如果你想兼容IE678请不要用2.x的版本针对极少数不用考虑兼容性的幸运儿,极力推荐使用最新版本的jQuery当从CDN服务器加载jQuery时,最好把版本写全(比如1.11.0而不是1.11或者直接写个1)千万莫重复加载

(6)如果你同时还使用了其他JS框架诸如Prototype, MooTools, Zepto云云,因为他们也使用了$符号,所以你就不要再用美刀符号来进行jQuery 编码了,而请用'jQuery'代替。并且调用$.noConflict()保证不会有冲突出现。

(7)要检测浏览器对一些新特性是否支持,请用Modernizr

2.2 关于变量

(1)jQuery类型的变量最好加个$前缀。

(2)时常将jQuery选择器返回的内容存进变量以便重用

var $products = $('div.products'); // 慢
var $products = $('.products'); // 快

(3)使用驼峰命名

var orderList;

2.3 关于选择器

(1)尽量ID选择器。其背后机理其实是调用原生的document.getElementById(),所以速度较其他选择器快。

(2)使用类选择器时不要指定元素的类型。

var $products = $('div.products'); // 慢
var $products = $('.products'); // 快

(3)ID父亲容器下面再查找子元素请用.find()方法。这样做快的原因是通过id选择元素不会使用Sizzle引擎。详情看这里

(4)多级查找中,右边尽量指定得详细点而左边则尽量简单点。

// 丑陋
$('div.data .gonzalez');

// 优化后
$('.data td.gonzalez');

(5)避免冗余。详情或者查看性能比较

$('.data table.attendees td.gonzalez');

// 好的方式:去掉了中间的冗余
$('.data td.gonzalez');

(6)指定选择的上下文。

// 劣质的代码:因为需要遍历整个DOM来找到.class
$('.class');

// 高品代码:因为只需在指定容器范围内进行查找
$('.class', '#class-container');

(7)不要使用万能选择器。查看具体阐释

$('div.container > *'); // 差
$('div.container').children(); // 棒

8.警惕隐式的万能选择器。省略的情况下其实使用的就是*号通配符。

$('div.someclass :radio'); // 差
$('div.someclass input:radio'); // 棒

9.ID已经表示唯一了,背后使用的是document.getElementById(),所以不要跟其他选择器混搭了。

$('#outer #inner'); // 脏
$('div#inner'); // 乱
$('.outer-container #inner'); // 差
$('#inner'); // 干净利落,后台只需调用document.getElementById()

2.4 DOM操作相关

(1)操作任何元素前先将其从文档卸载,完了再贴回去。

var $myList = $("#list-container > ul").detach();
//...一大堆对$myList的处理
$myList.appendTo("#list-container");

(2)代码里将HTML组织好后再一次性贴到DOM中去。具体来说,性能比较

// 这样不好
var $myList = $("#list");
for (var i = 0; i < 10000; i++) {
    $myList.append("<li>" + i + "</li>");
}

// 这样好
var $myList = $("#list");
var list = "";
for (var i = 0; i < 10000; i++) {
    list += "<li>" + i + "</li>";
}
$myList.html(list);

// 但这样更好var array = []; 
for (var i = 0; i < 10000; i++) {
    array[i] = "<li>" + i + "</li>";
}
$myList.html(array.join(''));

(3)不要处理不存在的元素。

// 无良的做法:jQuery后台要跑完三个函数后才会知道这个元素其实根本不存在
$("#nosuchthing").slideUp();
// 应该这样
var $mySelection = $("#nosuchthing");
if ($mySelection.length) {
    $mySelection.slideUp();
}

2.5事件相关

(1)一个页面只写一个文档ready事件的处理程序。这样代码既清晰好调试,又容易跟踪代码的进程。

(2)不要用匿名函数来做事件的回调。匿名函数不易调试维护测试和复用。或许你想较真,看看这里吧

// 不要这样
$("#myLink").on("click", function(){
    //...
});

// 这样
function myLinkClickHandler() {
 //...
}

$("#myLink").on("click", myLinkClickHandler);

3.处理文档ready事件的回调也不要用匿名函数,匿名函数不易调试维护测试和复用

$(function () {
    // ... 
}); // 糟糕的做法:无法利用此函数也无法为其写测试用例

// 好的做法
$(initPage);
// 或 $(document).ready(initPage);
function initPage() {
    // 这里你可以进行程序的初始化了
}

// 自己的写法
(function ($) {

    var MyApp = {};
    MyApp.init = function () {
        console.log('初始化工作');
    };

    $(MyApp.init);

}(jQuery));

(4)进一步,最好也将ready事件的处理程序放到外部文件中引入到页面,而页面中内嵌的代码只需调用即可。

<script src="my-document-ready.js"></script>
<script>
// 初始化一些必要的全局变量
    $(document).ready(initPage); // 或 $(initPage);
</script>

(5)千万不要写内联到HTML的JS代码,这是调试的梦魇!应该总是用jQuery来绑定事件自带程序,这样也方便随时动态地取消绑定。

<a id="myLink" href="#" onclick="myEventHandler();">my link</a> <!--不好 -->
$("#myLink").on("click", myEventHandler); // GOOD

6.如果可能尽量在绑定事件处理程序时使用一个命名空间,这样可以方便地取消绑定而不会影响其他绑定。

$("#myLink").on("click.mySpecialClick", myEventHandler); // 不错
// 之后,让我们优雅地解除绑定
$("#myLink").unbind("click.mySpecialClick");

2.6 异步操作

(1) 直接用$.ajax()而不要去用$.getJson()$.get(),因为jQuery内部还是将其转为前者

(2)不要对HTTPS站点使用HTTP去发起请求,最好干脆就不要指定(将HTTP或者HTTPS从你的URL中移除)

(3)不要在链接里面嵌参数,请使用专门的参数设置来传递

// 不易阅读的代码
$.ajax({
    url: "something.php?param1=test1&param2=test2",
    //....
});

// 更易阅读
$.ajax({
    url: "something.php",
    data: {
        param1: test1,
        param2: test2
    }
});

(4)尽量指明数据类型以便你自己清楚要处理什么样的数据(见下方会提到的Ajax模板)

(5)对于异步动态加载的内容,最好使用代理来绑定事件处理程序。这样的好处是对于之后动态加载的元素事件同样有效。

$('#parent-container').on('click', 'a', delegatedClickHandlerForAjax);

(6)使用Promise模式

$.ajax({
    //...
}).then(successHandler, failureHandler);

// 抑或
var jqxhr = $.ajax({
    // ...
});
jqxhr.done(successHandler);
jqxhr.fail(failureHandler);

(7)标准的Ajax模板一分。追寻根源

var jqxhr = $.ajax({
    url: url,
    type: 'GET', // 默认为GET,你可以根据需要更改
    cache: true, // 默认为true,但对于script,jsonp类型为false,可以自行设置
    data: {}, // 将请求参数放这里.
    dataType: 'json', // 指定想要的数据类型
    jsonp: 'callback', // 指定回调处理JSONP类型的请求
    statusCode: { // 如果你想处理各状态的错误的话
        404: handler404,
        500: handler500
    }
});
jqxhr.done(successHandler);
jqxhr.fail(failureHandler);

2.7 动画与特效

(1)保持一个始终如一风格统一的动画实现

(2)紧遵用户体验,不要滥用动画特效

  • 使用简洁的显示隐藏,状态切换,滑入滑出等效果来展示元素使用预设值来设置动画的速度'fast','slow',或者400(中等速度)

2.8 插件相关

(1)始终选择一个有良好支持,完善文档,全面测试过并且社区活跃的插件
(2)注意所用插件与当前使用的jQuery版本是否兼容
(3)一些常用功能应该写成jQuery插件。一分jQuery插件的编写模板

2.9链式句法

(1)除了用变量将jQuery选择器返回的结果保存,还可以利用好链式调用。

$("#myDiv").addClass("error").show();

(2)当链式调用多达3次以上或代码因绑定回调略显复杂时,使用换行和适当的缩进来提高代码的可读性。

$("#myLink")
    .addClass("bold")
    .on("click", myClickHandler)
    .on("mouseover", myMouseOverHandler)
    .show();

(3)对于特别长的调用最好还是用变量保存下中间结果来简化代码。

2.10 其他

(1)使用对象字面量来传递参数

// 糟糕:调用了三次attr
$myLink.attr('href', '#').attr('title', 'my link').attr('rel', 'external');

// 不错,只调用了一次attr
$myLink.attr({
    href: '#',
    title: 'my link',
    rel: 'external'
});

(2)不要将CSS与jQuery杂揉

$('#mydiv').css({'color':red, 'font-weight':'bold'}); // 不好
/* 不错 */
.error {
    color: red;
    font-weight: bold;}
$('#mydiv').addClass('error'); 

(3)时刻关注官方Changelog,不要使用摒弃了的方法。点此查看所有废弃的方法
(4)适时地使用原生JavaScript。一些与此有关的性能比较

$("#myId"); // 多少还是会逊色于`document.getElementById("myId")`

REFERENCE

原文:Coding Standards & Best Practices http://lab.abhinayrathore.com/jquery-standards/

ES6(2) let和const命令

let和const命令

  • let命令
  • 块级作用域
  • const命令
  • 跨模块常量
  • 全局对象的属性

2.1 let命令

2.1.1 基本用法

ES6新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。

{
  let a = 10;
  var b = 1;
}

console.log(a); // ReferenceError: a is not defined.
console.log(b); // 1

上面代码在代码块之中,分别用letvar声明了两个变量。然后在代码块之外调用这两个变量,
结果let声明的变量报错,var声明的变量返回了正确的值。这表明,let声明的变量只在它所在的代码块有效。

for循环的计数器,就很合适使用let命令。

for(let i = 0; i < arr.length; i++){}

console.log(i)
//ReferenceError: i is not defined

上面代码的计数器i,只在for循环体内有效。

下面的代码如果使用var,最后输出的是10。

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

上面代码中,变量i是var声明的,在全局范围内都有效。所以每一次循环,新的i值都会覆盖旧值,导致最后输出的是最后一轮的i的值。

如果使用let,声明的变量仅在块级作用域内有效,最后输出的是6。

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。

2.1.2 不存在变量提升

console.log(a); // 输出undefined
console.log(b); // 报错ReferenceError

var a = 2;
let b = 2;

2.1.3 暂时性死区

只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

ES6明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称TDZ)。

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

上面代码中,在let命令声明变量tmp之前,都属于变量tmp的“死区”。

“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。

typeof x; // ReferenceError
let x;

上面代码中,变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError。

作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。

typeof undeclared_variable // "undefined"
上面代码中,undeclared_variable是一个不存在的变量名,结果返回“undefined”。
所以,在没有let之前,typeof运算符是百分之百安全的,永远不会报错。
现在这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。

有些“死区”比较隐蔽,不太容易发现。

function bar(x = y, y = 2) {
  return [x, y];
}
bar(); // 报错

上面代码中,调用bar函数之所以报错,是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于”死区“。如果y的默认值是x,就不会报错,因为此时x已经声明了。

function bar(x = 2, y = x) {
  return [x, y];
}
bar(); // [2, 2]

ES6规定暂时性死区和不存在变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。

这样的错误在ES5是很常见的,现在有了这种规定,避免此类错误就很容易了。

总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,
但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

2.1.4 不允许重复声明

let不允许在相同作用域内,重复声明同一个变量。

// 报错
function () {
  let a = 10;
  var a = 1;
}
// 报错
function () {
  let a = 10;
  let a = 1;
}

因此,不能在函数内部重新声明参数。

function func(arg) {
    let arg; // 报错
}

    function func(arg) {
        {
            let arg; // 不报错
        }
    }

2.2 块级作用域

为什么需要块级作用域?
ES5只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。

第一种场景,内层变量可能会覆盖外层变量。

var tmp = new Date();

function f(){
  console.log(tmp);
  if (false){
    var tmp = "hello world";
  }
}

f() // undefined

上面代码中,函数f执行后,输出结果为undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。

第二种场景,用来计数的循环变量泄露为全局变量。

var s = 'hello';

for (var i = 0; i < s.length; i++){
  console.log(s[i]);
}

console.log(i); // 5

上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

2.2.1 ES6的块级作用域

let实际上为JavaScript新增了块级作用域。

function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}

上面的函数有两个代码块,都声明了变量n,运行后输出5。这表示外层代码块不受内层代码块的影响。如果使用var定义变量n,最后输出的值就是10。

ES6允许块级作用域的任意嵌套。

{{{{{let insane = 'Hello World'}}}}};

上面代码使用了一个五层的块级作用域。外层作用域无法读取内层作用域的变量。

{{{{
  {let insane = 'Hello World'}
  console.log(insane); // 报错
}}}};

内层作用域可以定义外层作用域的同名变量。

{{{{
  let insane = 'Hello World';
  {let insane = 'Hello World';}
}}}};

块级作用域的出现,实际上使得获得广泛应用的立即执行匿名函数(IIFE)不再必要了。

// IIFE写法
(function () {
  var tmp = ...;
  ...
}());

// 块级作用域写法
{
  let tmp = ...;
  ...
}

另外,ES6也规定,函数本身的作用域,在其所在的块级作用域之内。

function f() { console.log('I am outside!'); }
(function () {
  if(false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());

上面代码在ES5中运行,会得到“I am inside!”,但是在ES6中运行,会得到“I am outside!”。这是因为ES5存在函数提升,不管会不会进入 if代码块,函数声明都会提升到当前作用域的顶部,得到执行;而ES6支持块级作用域,不管会不会进入if代码块,其内部声明的函数皆不会影响到作用域的外部。

{
  let a = 'secret';
  function f() {
    return a;
  }
}
f() // 报错

上面代码中,块级作用域外部,无法调用块级作用域内部定义的函数。如果确实需要调用,就要像下面这样处理。

let f;
{
  let a = 'secret';
  f = function () {
    return a;
  }
}
f() // "secret"

ES5的严格模式规定,函数只能在顶层作用域和函数内声明,其他情况(比如if代码块、循环代码块)的声明都会报错。

// ES5
'use strict';
if (true) {
  function f() {} // 报错
}

ES6由于引入了块级作用域,这种情况可以理解成函数在块级作用域内声明,因此不报错,但是构成区块的大括号不能少,否则还是会报错。

// 不报错
'use strict';
if (true) {
  function f() {}
}
// 报错
'use strict';
if (true)
  function f() {}

另外,这样声明的函数,在区块外是不可用的。

'use strict';
if (true) {
  function f() {}
}
f() // ReferenceError: f is not defined

上面代码中,函数f是在块级作用域内部声明的,外部是不可用的

2.3 const命令

const也用来声明变量,但是声明的是常量。一旦声明,常量的值就不能改变。

'use strict';
const PI = 3.1415;
PI // 3.1415

PI = 3; // TypeError: "PI" is read-only

上面代码表明改变常量的值会报错。注意,如果是常规模式,对常量赋值不会报错,但也是无效的。

const PI = 3.1415;
PI = 3; // 常规模式时,重新赋值无效,但不报错
PI // 3.1415

const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。

'use strict';
const foo;// SyntaxError: missing = in const declaration

上面代码表示,对于const来说,只声明不赋值,就会报错。同样的,这行命令在常规模式下不报错,但foo以后也没法重新赋值了。

const foo;
foo = 1; // 常规模式,重新赋值无效
foo // undefined

const的作用域与let命令相同:只在声明所在的块级作用域内有效。

if (true) {
  const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined

const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。

if (true) {
  console.log(MAX); // ReferenceError
  const MAX = 5;
}

上面代码在常量MAX声明之前就调用,结果报错。

const声明的常量,也与let一样不可重复声明

var message = "Hello!";
let age = 25;
// 以下两行都会报错
const message = "Goodbye!";
const age = 30;

对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址。const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以将一个对象声明为常量必须非常小心。

const foo = {};
foo.prop = 123;
console.log(foo.prop); // 123
foo = {} // TypeError: "foo" is read-only

上面代码中,常量foo储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。

下面是另一个例子。

const a = [];
a.push("Hello"); // 可执行
a.length = 0;    // 可执行
a = ["Dave"];    // 报错

上面代码中,常量a是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a,就会报错。

如果真的想将对象冻结,应该使用Object.freeze方法。

const foo = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;

上面代码中,常量foo指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。

除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

var constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, value) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  });
};

ES5只有两种声明变量的方法:var 命令和 function 命令。ES6除了添加 letconst 命令,后面章节还会提到,
另外两种声明变量的方法:import 命令和 class 命令。所以,ES6一共有6种声明变量的方法。

2.4 跨模块常量

上面说过,const声明的常量只在当前代码块有效。如果想设置跨模块的常量,可以采用下面的写法。

// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;

// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3

// test2.js 模块
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3

2.5 全局对象的属性

全局对象是最顶层的对象,在浏览器环境指的是window对象,在Node.js指的是global对象。
ES5之中,全局对象的属性与全局变量是等价的。

window.a = 1;
console.log(a); // 1

a = 2;
console.log(window.a); // 2

上面代码中,全局对象的属性赋值与全局变量的赋值,是同一件事。
(对于Node来说,这一条只对REPL环境适用,模块环境之中,全局变量必须显式声明成global对象的属性。)

这种规定被视为JavaScript语言的一大问题,因为很容易不知不觉就创建了全局变量。
ES6为了改变这一点,一方面规定,var命令和function命令声明的全局变量,依旧是全局对象的属性;
另一方面规定,let命令、const命令、class命令声明的全局变量,不属于全局对象的属性。

var a = 1;
// 如果在Node的REPL环境,可以写成global.a
// 或者采用通用方法,写成this.a
window.a // 1

let b = 1;
window.b // undefined

上面代码中,全局变量a由var命令声明,所以它是全局对象的属性;
全局变量b由let命令声明,所以它不是全局对象的属性,返回undefined

JavaScript编程指南(三)编程规范

JavaScript 编程指南


3 JavaScript 编程规范

3.1 代码风格

3.1.1 缩进层级

可以使用IDE自带的格式化功能,但是有的IDE格式化后的代码也不好看,如Eclipse(一般不会有人用它来写JavaScript)。

3.1.2 语句的结尾

  • 一般要求一个语句独占一行
  • 语句的结尾必须用分号“;

3.1.3 行的长度

每行的长度不应过长

3.1.4 换行

注意什么时候该换行,什么时候不该换行

3.1.5 空行

添加适当的空行

  • 在方法之间
  • 在方法中的局部变量和第一条语句之间
  • 在多行或单行注释之前
  • 在方法内的逻辑片段之间插入空行,提高可读性
  • 在return之前(方法仅有一句return除外)
function getArray() {
    return []; // return前无需空行
}

/*
 * 这是一个多行注释,
 * 之前有个空行。
 */

// 这是单行注释,之前有个空行
function getElement() {
    var $target = $('#id'), // 用jQuery id 选择器
        $element; // [空行(下方):局部变量和第一条语句之间]

    if ($target.length) {
        $element = $target.find('.css-class');
    }

    return $element || null; // [return和其它语句之间]
}

3.2 使用直接量

3.2.1 字符串

无论是用单引号还是双引号扩住字符串,在代码风格中务必统一代码的风格。
**个人推荐:**本人习惯使用单引号,原因可能和大多数人一样:HTML中属性一般使用双引号,所以JavaScript中使用单引号,直接拷贝HTML就可以使用了;

一些只能使用单引号的地方:

  • 在HTML中如果要给元素属性赋值JSON必须用外单、内双;
    <input type="hidden" name="datauserInfo" id="userInfo" value='{"name":"mike","age":12}' />

    <script type="text/javascript">
        (function () {
            function printUserInfo() {
                var hiddenUserInfo = document.getElementById('userInfo'),
                    userInfo = JSON.parse(hiddenUserInfo.value);

                console.dir(userInfo);
            }

            printUserInfo();

        }());
    </script>

字符串换行

// 不好的写法
var longString = 'Here's the story of man \
named Jane.' ;

// 好的写法
var longString = 'Here's the story of man ' +
'named Jane.';

3.2.2 数值

整数请勿添加小数点,如果是小数请勿省略小数点前面和后面部分。

var count = 10;
var price = 10.0;
var price = 10.00;

3.2.3 null

null是个一个特殊值,但我们常常误解它,将它和undefined搞混淆,下列场景中**应该**使用null:

  • 用来初始化一个变量,这个变量可能赋值为一个对象。
  • 用来和一个已经初始化的变量比较,这个变量可以是也可以不是一个对象。
  • 当函数的参数期望值是对象时,用作参数传入。
  • 当函数的返回值期望是对象时,用作返回值传出。

**不应该**使用null的场景:

  • 不要使用null来检测是否传入了某个参数。
  • 不要使用null来检测一个未初始化的变量。
// good
var person = null;

// good
function getPerson() {
    if (condition) {
        return new Person('Jane');
    } else {
        return null;
    }
}

// good
var person = getPerson();
if (person !== null) {
    doSomething();
}

// bad:用来和未初始化的变量比较
var person;
if (person != null) {
    doSomething();
}

// bad:检测是否传入了参数
function doSomething(arg1, arg2, arg3, arg4) {
    if (arg4 != null) {
        doSomethingElse();
    }
}

3.2.4 undefined

undefined 是一个特殊值,我们常将它和null混淆;

让人困惑的地方:

1. null==undefined; // true
2. typeof运算无论是值为undfined的变量还是未声明的变量,其都返回'undefined'
// bad
var person;
console.log(person === undefined); // true

// foo未被声明
var person;
console.log( typeof person);  // 'undefined'
console.log( typeof foo); // 'undefined'

避免使用undefined,确保只有在一种情况下typeof运算才会返回'undefined';

// good
var person = null;
console.log(person === null); //true

注意:(typeof null)返回'object'

3.2.5 对象直接量

直接量可以高效完成非直接量写法相同的任务,非直接量写法语法看起来要更为复杂。

// bad
var book = new Object();
book.title = 'Maintainable JavaScript';
book.author = 'Zakas';

// good
var book = {
    title: 'Maintainable JavaScript',
    author: 'Zakas'
};

3.2.6 数组直接量

使用数组直接量而不是构造函数

// bad
var colors = new Array('red', 'yellow', 'blue', 'white'),
    numbers = new Array(1, 2, 3, 4);

// good
var colors = ['red', 'yellow', 'blue', 'white'],
    numbers = [1, 2, 3, 4];

3.3 注释

3.3.1 行注释

  • 独占一行的注释,用来解释下一行代码。这行注释之前总有一个空行,且缩进层级和下一行代码保持一致。
  • 在代码行的尾部的注释。代码结束到注释之间至少有个一缩紧。注释(包括之前的代码部分)不应到超过单行最大字符数显示,如果超过了,就将这条注释放置于当前代码行的上方。
  • 被注释掉的打断代码(很多编辑器都可以批量注释掉多行代码)。

3.3.2 多行注释

/*
 * 这是一个多行注释,
 * 之前有个空行。
 */

3.3.3 使用注释

添加注释的一般原则:需要让代码变得更清晰时添加注释。

  • 难于理解的代码
  • 可能被认为错误的代码
  • 浏览器特性hack
  • 文档注释

3.4 JavaScript语句

3.4.1 块语句必须使用花括号

包括:

  • if
  • for
  • while
  • do...while
  • try...catch...finally
// Bad
if (condition)
    doSomething();

// Bad
if (condition) doSomething();

// Bad
if (condition) { doSomething(); }

// Good
if (condition) {
    doSomething();
}

3.4.2 括号对齐方式

// 首选使用(和Java类似)
if (condition) {
    doSomething();
} else {
    doSomethingElse();
}

// 微软风格,没什么问题,但是为了风格一致性,不推荐使用!
if (condition)
{
    doSomething();
}
else
{
    doSomethingElse();
}

3.4.3 switch 语句

  • 允许多个case匹配同一个代码块(确保你不是忘记写了break;),最好加注释说明;
  • 推荐保留default,谁也不敢保证代码万无一失。
switch (condition) {
    case 'one':
        //...
        break;
    // 当值为'two'或 '二'时,做相同的处理
    case 'two':
    case '二':
        //...
        break;
    default:
        throw '发现非期望的值:' + condition;
}

3.4.5 with 语句

禁止使用with语句(JavaScript大神可以忽略)

3.4.6 for 语句

  • break:结束循环
var values = [1, 2, 3, 4, 5, 6, 7],
    i,
    len;
for (i = 0, len = values.length; i < len; i++) {
    if (i === 2) {
        break; // 迭代不会继续
    }
    console.log (values[i]);
}
// 以上代码只输出: 1 2 ,不再往下执行
  • continue:跳过本次循环,往下执行
var values = [1, 2, 3, 4, 5, 6, 7],
    i,
    len;
for (i = 0, len = values.length; i < len; i++) {
    if (i === 2) {
        continue; // 跳过本次循环
    }
    console.log(values[i]);
}
// 不输出索引为2的元素,即:3

// Crockford 的编程规范不允许使用continue,而是主张使用条件语句代替
for (i = 0, len = values.length; i < len; i++) {
    if (i !== 2) {
        console.log(values[i]);
    }
}

3.4.7 for-in 语句

// 用hasOwnProperty()方法过滤出实例属性
var prop;
for (prop in object) {
    // 若想要查找原型链,则省略if条件语句
    if (object.hasOwnProperty(prop)) {
        console.log('Property name is ' + prop);
        console.log('Property value is ' + object[prop]);
    }
}

// 不要用for-in来遍历数组
var values = [1, 2, 3, 4, 5, 6, 7],
    i;
// bad
for (i in values) {
    console.log(values[i]);
}

3.5 变量函数和运算符

3.5.1 变量声明

  • 变量先声明再使用
  • JavaScript没有块级变量声明
function doSomethingWithItems(items) {
    for (var i = 0, len = itmes.length; i < len; i++) {
        doSomething(items[i]);
    }
}

function doSomethingWithItems(items) {
    var i,
        len;

    for (i = 0, len = items.length; i < len; i++) {
        doSomething(items[i]);
    }
}

最佳实践:

  • 建议总是将局部变量的定义作为函数内第一条语句;
  • 并且使用单var模式(为了保持成本最低,推荐合并 var 语句)
function doSomethingWithItems(items) {
    var value = 10,
        result = value + 10,
        i,
        len;

    function doSomething(item) {
        // do something
    }

    for (i = 0, len = items.length; i < len; i++) {
        doSomething(items[i]);
    }
}

3.5.2 函数声明

// bad:调用代码在前,定义代码在后
doSomething();

function doSomething() {
    console.log('Hello world!');
}

// good:先定义,后使用
function doSomething() {
    console.log('Hello world!');
}

doSomething();

3.5.3 函数调用的间隔

// good
doSomething(itme);

// bad:多一个空格,断片了吧!手贱了吧!
doSomething (item);

3.5.4 即时调用的函数

// bad
var value = function () {
    // code

    return {
        message: 'Hi'
    };
}();

// good
var value = (function () {
    //code

    return {
        message: 'Hi'
    }
}());

3.5.5 严格模式

  • 不要使用全局的严格模式
  • 也不必每个函数都写严格模式
// bad:全局的严格模式
'use strict';
function doSomething() {
    //code
}

// bad
function doSomething() {
    'use strict';
    //code
}

// good
(function () {
    'use strict';

    function doSomething() {
        //code
    }

    function doSomethingElse() {
        //code
    }
})();

3.5.6 相等

掌握:== != 和 === !==

如果一个布尔值和数字比较,布尔值会首先转换位数字,然后进行比较。
false 值变为0、true变为1.

//数字1和true
console.log(1 == true); // true

//数字0和false
console.log(0 == false); // true

//数字2和false
console.log(2 == true); // false

如果其中一个值是对象而另一个不是,则会首先调用对象的valueOf()方法,
得到原始类型值再进行比较。如果没有定义valueOf(),则在调用toString()。
之后的比较操作就和上文提到的多类型值比较的情形了一样。

// 数字5和字符串5
console.log(5 == '5'); // true
console.log(5 === '5'); // false

// 数字25和16进制字符串25
console.log(25 == '0x19'); // true
console.log(25 === '0x19'); // false

// 数字1和true
console.log(1 == true); // true
console.log(1 === true); // false

// 数字0和false
console.log(0 == false); // true
console.log(0 === false); // false

// 数字2和true
console.log(2 == true); // false
console.log(2 === true); // false

3.6 避免使用全局变量

var color = 'red';

function sayColor() {
    console.log(color); // 不好的做法,color从哪里来的??
}

console.log(window.color); // 'red'

console.log(typeof window.sayColor); // 'function'

全局变量带来的问题

  • 命名冲突
  • 代码脆弱性
  • 难以测试

3.6.1 意外的全局变量

在处理老代码时要非常小心的使用严格模式,对于新代码最好使用严格模式来避免意外的全局变量,
同时一些常见的编码错误也能在严格模式中被捕捉到。

3.6.2 单全局变量方式

function Book(title) {
    this.title = title;
    this.page = 1;
}

Book.prototype.turnPage = function (direction) {
    this.page += direction;
}

var Chapter1 = new Book( 'Introction to Style GuideLines');
var Chapter2 = new Book( 'Basic Formatting');
var Chapter3 = new Book( 'Comments');

// 使用单全局变量

var MaintainableJS = {};

MaintainableJS.Book = function (title) {
    this.title = title;
    this.page = 1;
};

MaintainableJS.Book.turnPage = function (direction) {
    this.page = direction;
};

MaintainableJS.Chapter1 = new MaintainableJS.Book( 'Introction to Style GuideLines');
MaintainableJS.Chapter2 = new Book('Basic Formatting');
MaintainableJS.Chapter3 = new Book('Comments');

3.6.3 命名空间

var ZakasBooks = {};

// 编写可维护JavaScript的命名空间
ZakasBooks.MaintainableJavaScript = {};

// 高性能JavaScript的mingmingkongjian
ZakasBooks.HighPerformanceJavaScript = {};

var YourGlobal = {
    namespace: function (ns) {
        var parts = ns.split( '.'),
            object = this,
            i,
            len;
        for (i = 0, len = parts.length; i < len; i++) {
            if (!object[parts[i]]) {
                object[parts[i]] = {};
            }
            object = object[parts[i]];
        }
        return object;
    }
};

YourGlobal.namespace('Books.MaintainableJavaScript');

YourGlobal.Books.MaintainableJavaScript.author = 'Nicholas c. Zakas';

YourGlobal.namespace('Books.HighPerformaceJavaScript');

console.log(YourGlobal.Books.MaintainableJavaScript.author);

YourGlobal.namespace('Bookes').ANewBook = {};

//good- 拆分应用逻辑
var MyApplication = {

    dandleClick: function (event) {
        this.showPooup(event);
    },

    showPopup: function (event) {
        var popup = document.getElementById( 'popup');
        popup.style.left = event.clientX + 'px';
        popup.style.top = event.dandleClick + 'px';
        popup.className = 'reveal';
    }

};

addEventListener(element, 'click', function (event) {
    MyApplication.handleClick(event);
});

//good
var MyApplication = {

    dandleClick: function (event) {
        this.showPooup(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);
});

//good #3
//good
var MyApplication = {

    dandleClick: function (event) {
        /// <summary>
        /// 事件处理程序
        /// </summary>
        /// <param name="event"></param>

        //假设事件支持DOM Level2
        event.prventDefault();
        event.stopPropagation();

        //传入应用逻辑
        this.showPooup(event.clientX, event.clientY);
    },

    showPopup: function (x, y) {
        /// <summary>
        /// 应用逻辑
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        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);
});

3.7 避免空比较

var Controller = {
    process: function (items) {
        if (items !== null) {//不好的写法
            itmes.sort();
            items.forEach( function (items) {
                //执行一些逻辑
            });
        }
    }
};

3.7.1 检测原始值

5种原始类型:字符串、数字、布尔值、null、undefined

如果希望一个值是字符串、数字、布尔值、undefined、最佳选择是typeof运算符

typeof 对5中原始类型的计算结果
typeof
字符串(String) "string"
数字(Number) "number"
布尔值(Boolean) "boolean"
undefined "undefined"
null "object" (低效的判断方法,建议直接使用===和!==)
typeof 对数组、正则表达式、日期、函数均返回 "object"

typeof运算符的独特之处在于,将其应用于一个未声明的变量也不会报错。

简单的和null比较通常不会包含足够的信息以判断值的类型是否合法,但有一个例外,如果所期望的值真的是null,
则可以直接和null比较,这时应当使用 === 或者 !== 来和null进行比较

var element = document.getElementById('my-div');
if (element !== null) { 
    // null是可以预见的输出
    element.className = 'found';
}

3.7.2 检测引用值

引用值也称作对象(object), 在JavaScript中除了原始值之外的值都是引用,
ObjectArrayDateError;

console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof new Date()); // "object"
console.log(typeof new RegExp()); // "object"
console.log(typeof null); // "object"

使用instanceof来检测引用类型

instanceof检测引用类型汇总
instanceof
Date检测日期
RegExp检测正则表达式
Error检测Error
// 检测日期date
if (value instanceof Date) {
    console.log(value.getFullYear());
}

// 检测正则表达式
if (value instanceof RegExp) {
    if (value.test(anotherValue)) {
        console.log( "Matches");
    }
}

// 检测Error
if (value instanceof Error) {
    throw value;
}

// IE8及更早版本的IE
console.log(typeof document.getElementById); // "object"
console.log(typeof document.createAttribute); // "object"
console.log(typeof document.getElementsByTagName); // "object"

function isJSON(value) {
    return Object.prototype.toString.call(value) === [ "object JSON"];
}

// 兼容的做法
function isArray(value) {
    if (typeof Array.isArray === "function") {
        return Array.isArray(value);
    } else {
        return Object.prototype.toString.call(value) === ["object Array"];
    }
}

3.7.3 检测属性

// bad:检测假值
if (object[propertyName]) {
    //code...
}

// bad:和null比较
if (object[propertyName] != null) {
    //code...
}

// bad:和undefined比较
if (object[propertyName] != undefined) {
    //code...
}

上面的代码里的每个判断,实际上是通过给定的名字来检查属性的值,而非判断
给定的名字所指的属性是否存在,因为当属性值为假(false)时结果会出错,
比如0""(empty string)、falsenullundefined

用in运算符判断属性是否存在

var obj = {
    count: 0,
    related: null
};

// good
if ( "count" in obj) {
    // 这里的代码会执行
    console.log("count is in obj");
}

// bad:检测假值
if (obj[ "count"]) {
    //这里的代码不会执行
}

// good
if ( "related" in obj) {
    //这里的代码不会执行
}

if (obj[ "releated"] != null) {
    //这里代码不会执行
}

if (obj.length) {
    console.log("nothing");
} else {
    console.log("obj.length is :" + obj.length); // obj.length is :undefined
}

如果你只想检查实例对象的某个属性是否存在则使用Object.hasOwnProperty()方法,

需要注意的是:
IE8及更早版本中,DOM对象并非继承自Object,所以不包含该方法

// 对于所有非DOM对象来说,这是好的写法
if (obj.hasOwnProperty( "related")) {
    // 执行这里的代码
}

// 如果你不确定是否为DOM对象,则这样来写
if ( "hasOwnProperty" in obj && obj.hasOwnProperty( "related")) {
    // 执行这里的代码
}

因为存在IE8以及更早版本的IE,在判断实例对象时候存在时,NC更倾向于使用in运算符,
只有在需要判断实例属性时才会使用到hasOwnProperty()

不管你什么时候需要检测属性的存在性,请使用in运算符或者hasOwnProperty(),
这样做可以避免很多Bug

Revision: 2015.11.06

ES6(3) 变量的解构赋值

#3 变量的解构赋值

  • 数组的解构赋值
  • 对象的解构赋值
  • 字符串的解构赋值
  • 数值和布尔值的解构赋值
  • 函数参数的解构赋值
  • 圆括号问题
  • 用途

3.1 数组的解构赋值

基本用法

ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

以前,为变量赋值,只能直接指定值。

var a = 1,
    b = 2,
    c = 3;

ES6允许写成下面这样。

var [a, b, c] = [1, 2, 3];

上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。

let [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo); // 1
console.log(bar); // 2
console.log(baz); // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

let [x, , y] = [1, 2, 3];
console.log(x); // 1
console.log(y); // 3

let [head, ...tail] = [1, 2, 3, 4];
console.log(head); // 1
console.log(tail); // [2, 3, 4]

let [x, y, ...z] = ['a'];
console.log(x); // "a"
console.log(y); // undefined
z // []

如果解构不成功,变量的值就等于undefined

var [foo] = [];
var [bar, foo] = [1];

以上两种情况都属于解构不成功,foo的值都会等于undefined

另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。

let [x, y] = [1, 2, 3];
console.log(x); // 1
console.log(y); // 2

let [a, [b], d] = [1, [2, 3], 4];
console.log(a); // 1
console.log(b); // 2
console.log(d); // 4

上面两个例子,都属于不完全解构,但是可以成功。

如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),
那么将会报错。

// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

上面的表达式都会报错,因为等号右边的值,要么转为对象以后不具备Iterator接口(前五个表达式),要么本身就不具备Iterator接口(最后一个表达式)。

解构赋值不仅适用于var命令,也适用于let和const命令。

var [v1, v2, ..., vN ] = array;

let [v1, v2, ..., vN ] = array;

const [v1, v2, ..., vN ] = array;

对于Set结构,也可以使用数组的解构赋值。

let [x, y, z] = new Set(["a", "b", "c"])
console.log(x); // "a"

事实上,只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值。

function* fibs() {
  var a = 0;
  var b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

var [first, second, third, fourth, fifth, sixth] = fibs();
console.log(sixth); // 5

上面代码中,fibs是一个Generator函数,原生具有Iterator接口。解构赋值会依次从这个接口获取值。

默认值

解构赋值允许指定默认值。

var [foo = true] = [];
foo // true

[x, y = 'b'] = ['a'] // x='a', y='b'
[x, y = 'b'] = ['a', undefined] // x='a', y='b'

注意,ES6内部使用严格相等运算符(===),判断一个位置是否有值。
所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。

var [x = 1] = [undefined];
console.log(x); // 1

var [x = 1] = [null];
console.log(x); // null

上面代码中,如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined。

如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

function f(){
  console.log('aaa');
}

let [x = f()] = [1];

上面代码中,因为x能取到值,所以函数f根本不会执行。上面的代码其实等价于下面的代码。

let x;
if ([1][0] === undefined) {
  x = f();
} else {
  x = [1][0];
}

默认值可以引用解构赋值的其他变量,但该变量必须已经声明。

let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2];    // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = [];     // ReferenceError

上面最后一个表达式之所以会报错,是因为x用到默认值y时,y还没有声明。

3.2 对象的解构赋值

解构不仅可以用于数组,还可以用于对象。

var { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,
变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

var { bar, foo } = { foo: "aaa", bar: "bbb" };
console.log(foo); // "aaa"
console.log(bar); // "bbb"

var { baz } = { foo: "aaa", bar: "bbb" };
console.log(baz); // undefined

上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined。

如果变量名与属性名不一致,必须写成下面这样。

var { foo: baz } = { foo: "aaa", bar: "bbb" };
console.log(baz); // "aaa"

let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
console.log(f); // 'hello'
console.log(l); // 'world'

这实际上说明,对象的解构赋值是下面形式的简写(参见《对象的扩展》一章)。

var { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };

也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

var { foo: baz } = { foo: "aaa", bar: "bbb" };
console.log(baz); // "aaa"
console.log(foo); // error: foo is not defined

上面代码中,真正被赋值的是变量baz,而不是模式foo。

注意,采用这种写法时,变量的声明和赋值是一体的。
对于let和const来说,变量不能重新声明,所以一旦赋值的变量以前声明过,就会报错。

let foo;
let {foo} = {foo: 1}; // SyntaxError: Duplicate declaration "foo"

let baz;
let {bar: baz} = {bar: 1}; // SyntaxError: Duplicate declaration "baz"

上面代码中,解构赋值的变量都会重新声明,所以报错了。
不过,因为var命令允许重新声明,所以这个错误只会在使用let和const命令时出现。
如果没有第二个let命令,上面的代码就不会报错。

let foo;
({foo} = {foo: 1}); // 成功

let baz;
({bar: baz} = {bar: 1}); // 成功

和数组一样,解构也可以用于嵌套结构的对象。

var obj = {
  p: [
    "Hello",
    { y: "World" }
  ]
};

var { p: [x, { y }] } = obj;
console.log(x); // "Hello"
console.log(y); // "World"

注意,这时p是模式,不是变量,因此不会被赋值。

var node = {
  loc: {
    start: {
      line: 1,
      column: 5
    }
  }
};

var { loc: { start: { line }} } = node;
line // 1
loc  // error: loc is undefined
start // error: start is undefined

上面代码中,只有line是变量,loc和start都是模式,不会被赋值。

下面是嵌套赋值的例子。

let obj = {};
let arr = [];

({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });

obj // {prop:123}
arr // [true]

对象的解构也可以指定默认值。

var {x = 3} = {};
console.log(x); // 3

var {x, y = 5} = {x: 1};
console.log(x); // 1
console.log(y); // 5

var { message: msg = "Something went wrong" } = {};
console.log(msg); // "Something went wrong"

默认值生效的条件是,对象的属性值严格等于undefined。

var {x = 3} = {x: undefined};
console.log(x); // 3

var {x = 3} = {x: null};
console.log(x); // null

上面代码中,如果x属性等于null,就不严格相等于undefined,导致默认值不会生效。

如果解构失败,变量的值等于undefined。

var {foo} = {bar: 'baz'}
console.log(foo); // undefined

如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。

// 报错
var {foo: {bar}} = {baz: 'baz'}

上面代码中,等号左边对象的foo属性,对应一个子对象。
该子对象的bar属性,解构时会报错。原因很简单,因为foo这时等于undefined,
再取子属性就会报错,请看下面的代码。

var _tmp = {baz: 'baz'};
_tmp.foo.bar // 报错

如果要将一个已经声明的变量用于解构赋值,必须非常小心。


// 错误的写法
var x;
{x} = {x: 1}; // SyntaxError: syntax error

上面代码的写法会报错,因为JavaScript引擎会将{x}理解成一个代码块,从而发生语法错误。
只有不将大括号写在行首,避免JavaScript将其解释为代码块,才能解决这个问题。

// 正确的写法
({x} = {x: 1});

上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。
关于圆括号与解构赋值的关系,参见下文。

解构赋值允许,等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。

({} = [true, false]);
({} = 'abc');
({} = []);

上面的表达式虽然毫无意义,但是语法是合法的,可以执行。

对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。

let { log, sin, cos } = Math;

上面代码将Math对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。

3.3 字符串的解构赋值

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。

let {length : len} = 'hello';
len // 5

3.4 数值和布尔值的解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

let {toString: s} = 123;
console.log(s === Number.prototype.toString); // true

let {toString: s} = true;
console.log(s === Boolean.prototype.toString); // true

上面代码中,数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。

解构赋值的规则是,只要等号右边的值不是对象,就先将其转为对象。
由于undefinednull无法转为对象,所以对它们进行解构赋值,都会报错。

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

3.5 函数参数的解构赋值

函数的参数也可以使用解构赋值。

function add([x, y]){
  return x + y;
}

add([1, 2]) // 3

上面代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y。
对于函数内部的代码来说,它们能感受到的参数就是x和y。

下面是另一个例子。

[[1, 2], [3, 4]].map(([a, b]) => a + b)
// [ 3, 7 ]

函数参数的解构也可以使用默认值。

function move({x = 0, y = 0} = {}) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

上面代码中,函数move的参数是一个对象,通过对这个对象进行解构,得到变量x和y的值。
如果解构失败,x和y等于默认值。

注意,下面的写法会得到不一样的结果。

function move({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]

上面代码是为函数move的参数指定默认值,而不是为变量x和y指定默认值,
所以会得到与前一种写法不同的结果。

undefined就会触发函数参数的默认值。

[1, undefined, 3].map((x = 'yes') => x)
// [ 1, 'yes', 3 ]

3.6 圆括号问题

解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,
还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。

由此带来的问题是,如果模式中出现圆括号怎么处理。E
S6的规则是,只要有可能导致解构的歧义,就不得使用圆括号。

但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。
因此,建议只要有可能,就不要在模式中放置圆括号。

不能使用圆括号的情况

以下三种解构赋值不得使用圆括号。

(1)变量声明语句中,不能带有圆括号。

// 全部报错
var [(a)] = [1];

var {x: (c)} = {};
var ({x: c}) = {};
var {(x: c)} = {};
var {(x): c} = {};}

var { o: ({ p: p }) } = { o: { p: 2 } };

上面三个语句都会报错,因为它们都是变量声明语句,模式不能使用圆括号。

(2)函数参数中,模式不能带有圆括号。

函数参数也属于变量声明,因此不能带有圆括号。

// 报错
function f([(z)]) { 
    return z; 
}

(3)赋值语句中,不能将整个模式,或嵌套模式中的一层,放在圆括号之中。

// 全部报错
({ p: a }) = { p: 42 };
([a]) = [5];

上面代码将整个模式放在模式之中,导致报错。

// 报错
[({ p: a }), { x: c }] = [{}, {}];

上面代码将嵌套模式的一层,放在圆括号之中,导致报错。

可以使用圆括号的情况

可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。

[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确

上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;
其次它们的圆括号都不属于模式的一部分。
第一行语句中,模式是取数组的第一个成员,跟圆括号无关;
第二行语句中,模式是p,而不是d;第三行语句与第一行语句的性质一致。

3.7 用途

变量的解构赋值用途很多。

(1)交换变量的值

[x, y] = [y, x];
上面代码交换变量x和y的值,这样的写法不仅简洁,而且易读,语义非常清晰。

(2)从函数返回多个值

函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。
有了解构赋值,取出这些值就非常方便。

// 返回一个数组

function example() {
  return [1, 2, 3];
}
var [a, b, c] = example();

// 返回一个对象

function example() {
  return {
    foo: 1,
    bar: 2
  };
}
var { foo, bar } = example();

(3)函数参数的定义

解构赋值可以方便地将一组参数与变量名对应起来。

// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3])

// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1})

(4)提取JSON数据

解构赋值对提取JSON对象中的数据,尤其有用。

var jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
}

let { id, status, data: number } = jsonData;
console.log(id, status, number);// 42, "OK", [867, 5309]

上面代码可以快速提取JSON数据的值。

(5)函数参数的默认值

jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true
  // ... more config
}) {
  // ... do stuff
};

指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || 'default foo';
这样的语句。

(6)遍历Map结构

任何部署了Iterator接口的对象,都可以用for...of循环遍历。Map结构原生支持Iterator接口,
配合变量的解构赋值,获取键名和键值就非常方便。

var map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
  console.log(key + " is " + value);
}
// first is hello
// second is world

如果只想获取键名,或者只想获取键值,可以写成下面这样。

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}

(7)输入模块的指定方法

加载模块时,往往需要指定输入那些方法。解构赋值使得输入语句非常清晰。

const { SourceMapConsumer, SourceNode } = require("source-map");

JavaScript编程指南(二)可维护约定

JavaScript 编程指南


2 JavaScript 可维护约定

2.1 变量

变量声明必须使用var,避免隐式声明导致的全局变量

2.1.1 变量先声明,后使用

var a = 1, // 直接量始化在前
    b = a, // 需要引用其它变量的变量在后
    c;    // 

function max() {
    var max = a;
    max = max > b ? max : b; // max=> 1
    c = c || 6;
    d = d || 5;
    max = max > c ? max : c; // max=> 6
    max = max > d ? max : d; // d=5,而不是期望的9. max=> 6
    console.log('max:' + max); // 6
}

max();

var d = 9;

2.1.2 全局变量的创建和使用

// 不推荐
myglobal = "hello"; // 隐式的全局变量
console.log(myglobal); // "hello"
console.log(window.myglobal); // "hello"
console.log(window["myglobal"]); // "hello"
console.log(this.myglobal); // "hello"

// 推荐,声明了一个全局变量
var MyGlobal="hello"; 

一些导致隐式全局变量的写法

// 不推荐
function sum(x, y) {
    // 反模式:隐式全局变量
    result = x + y;
    return result;
}

// 推荐
function sum(x, y) {
    var result = x + y;
    return result;
}

// 反模式,下面的代码中a是局部变量,b是全局变量
function foo() {
    var a = b = 0;
    // ...
}

// 推荐
function foo() {
    var a, b;
    // ...
    a = b = 0; // 两个都是本地变量
}

忘记var时的副作用

隐式创建的全局变量和显式定义的全局变量之间有着细微的差别,就是通过delete来删除它们的时候表现不一致。

  • 通过var创建的全局变量(在任何函数体之外创建的变量)不能被删除。
  • 没有用var创建的隐式全局变量(不考虑函数内的情况)可以被删除。

也就是说,隐式全局变量并不算是真正的变量,但它们却是全局对象的属性。属性是可以通过delete运算符删除的,而变量不可以被删除:

    // 定义三个全局变量
    var global_var = 1;
    global_novar = 2; // 反模式
    (function () {
        global_fromfunc = 3; // 反模式
    }());

    // 尝试删除
    delete global_var; // false
    delete global_novar; // true
    delete global_fromfunc; // true

    // 测试删除结果
    typeof global_var; // "number"
    typeof global_novar; // "undefined"
    typeof global_fromfunc; // "undefined"

在ES5严格模式中,给未声明的变量赋值会报错(比如这段代码中提到的两个反模式)。

不使用this来访问全局对象

对于很多人而言,this往往超出其驾驭能力。

var global = (function () {
    return this;
}()),
    MyApp = {
    name: 'JavaScript Guide',
    version: '0.0.1'
};

function print() {
    console.log(this.MyApp.name); // 不推荐
    console.log(global.MyApp.name); // 推荐
}

2.1.3 使用单var模式

/* 推荐 */
function func() {
    var a = 1,
        b = 2,
        sum = a + b,
        myobject = {},
        i,
        j;
    // 函数体…
}

for 循环

// 非最优的循环方式,当myArray引用的是DOM集合时,每次计算耗时的
for (var i = 0; i < myArray.length; i++) {
    // 访问myArray[i]…
}

// 至少这样写
for (var i = 0, max = myArray.length; i < max; i++) {
    // 访问myArray[i]…
}

// 优秀是一种习惯,结合单var模式,代码可以这样写
function looper() {
    var i = 0,
        max,
        myArray = [];
    // …
    for (i = 0, max = myArray.length; i < max; i++) {
        // 访问myArray[i]…
    }
}

变量最少,速度最快
关于这种for模式还有两种变化的形式,做了少量改进,原因有二:

减少一个变量(没有max)
减量循环至0,这种方式速度更快,因为和零比较要比和非零数字或数组长度比较要高效的多
第一种变化形式是:

var i,
    myArray = [];
for (i = myArray.length; i--;) {
    // 访问myArray[i]…
}

第二种变化形式用到了while循环:

var myArray = [],
    i = myArray.length;
while (i--) {
    // 访问myArray[i]…
}

for-in 循环

// 对象
var man = {
    hands: 2,
    legs: 2,
    heads: 1
};

// 在代码的另一个地方给所有的对象添加了一个方法
if (typeof Object.prototype.clone === "undefined") {
    Object.prototype.clone = function () {};
}

使用**hasOwnProperty()**方法过滤来自原型链中继承来的属性
严格说来,省略hasOwnProperty()并不是一个错误。根据具体的任务以及你对代码的自信程度,你可以省略掉它以提高一些程序执行效率。但当你对当前要遍历的对象不确定的时候,添加hasOwnProperty()则更加保险些。

var i;
for (i in man) {
    if (Object.prototype.hasOwnProperty.call(man, i)) { // 过滤
        console.log(i, ":", man[i]);
    }
}
// 或是
var i,
    hasOwn = Object.prototype.hasOwnProperty;
for (i in man) {
    if (hasOwn.call(man, i)) { // 过滤
        console.log(i, ":", man[i]);
    }
}

避免扩展内置类型

我们可以扩充构造函数的prototype属性来为构造函数增加功能,这个特性非常强大,但有时会强大到超过我们的掌控。

给内置构造函数如Object()Array()Function()扩充原型看起来非常诱人,但这种做法会严重降低代码的可维护性,因为它会让你的代码变得难以预测。对于那些基于你的代码来做开发的开发者来说,他们更希望使用原生的JavaScript方法来保持代码的一致性,而不愿意使用你所添加的方法。

另外,如果将属性添加至原型中,很可能导致原型上的属性在那些不使用hasOwnProperty()做过滤的循环中被遍历出来,从而造成混乱。

switch模式

你可以通过下面这种模式来增强switch语句的可读性和健壮性:

    var inspect_me = 0,
        result = '';
    switch (inspect_me) {
    case 0:
        result = "zero";
        break;
    case 1:
        result = "one";
        break;
    default:
        result = "unknown";
    }

这个简单的例子所遵循的风格约定如下:

  • 每个caseswitch对齐(这里不考虑花括号相关的缩进规则)。
  • 每个case中的代码整齐缩进。
  • 每个case都以break作为结束。
  • 避免连续执行多个case语句块(省略break时),如果你坚持认为连续执行多个case语句块是最好的方法,请务必补充文档说明,对于其他人来说,会觉得这种情况是错误的写法。
  • default结束整个switch,以确保即便是在找不到匹配项时也有合理的结果。

避免隐式类型转换

在JavaScript对变量进行比较时会有一些隐式的数据类型转换。比如诸如false == 0"" == 0之类的比较都返回true

为了避免隐式类型转换对程序造成干扰,推荐使用===!==运算符,它们除了比较值还会比较类型:

    var zero = 0;
    if (zero === false) {
        // 不会执行,因为zero是0,不是false
    }
    // 反模式
    if (zero == false) {
        // 代码块会执行…
    }

有一种观点认为当==够用的时候就不必使用===。比如,当你知道typeof的返回值是一个字符串,就不必使用全等运算符。但JSLint却要求使用全等运算符,这无疑会提高代码风格的一致性,并减少了阅读代码时的思考量(“这里使用==是故意的还是无意的?”)。

避免使用eval()

当你想使用eval()的时候,不要忘了那句话“eval() is evil”(eval()是魔鬼)。这个函数的参数是一个字符串,它会将传入的字符串作为JavaScript代码执行。如果用来解决问题的代码是事先知道的(在运行之前),则没有理由使用eval()。如果需要在运行时动态生成并执行代码,那一般都会有更好的方式达到同样的目的,而非一定要使用eval()。例如,访问动态属性时可以使用方括号:

    // 反模式
    var property = "name";
    alert(eval("obj." + property));
    // 更好的方式
    var property = "name";
    alert(obj[property]);

eval()还有安全隐患,因为你有可能会运行一些被干扰过的代码(比如一段来自于网络的代码)。这是一种在处理Ajax请求所返回的JSON数据时比较常见的反模式。这种情况下最好使用浏览器的内置方法来解析JSON数据,以确保代码的安全性和数据的合法性。如果浏览器不支持JSON.parse(),你可以使用JSON.org所提供的库。

值得一提的是,多数情况下,给setInterval()setTimeout()Function()构造函数传入字符串的情形和eval()类似,这种用法也是应当避免的,因为这些情形中JavaScript最终还是会执行传入的字符串参数:

    // 反模式
    setTimeout("myFunc()", 1000);
    setTimeout("myFunc(1, 2, 3)", 1000);
    // 更好的方式
    setTimeout(myFunc, 1000);
    setTimeout(function () {
        myFunc(1, 2, 3);
    }, 1000);

new Function()的用法和eval()非常类似,应当特别注意。这种构造函数的方式很强大,但经常会被误用。如果你不得不使用eval(),你可以尝试用new Function()来代替。这有一个潜在的好处,在new Function()中运行的代码会在一个局部函数作用域内执行,因此源码中所有用var定义的变量不会自动变成全局变量。还有一种方法可以避免eval()中定义的变量被转换为全局变量,即是将eval()包装在一个即时函数内(详细内容请参见第四章)。

看一下这个例子,这里只有un成为全局变量污染了全局命名空间:

    console.log(typeof un);// "undefined"
    console.log(typeof deux); // "undefined"
    console.log(typeof trois); // "undefined"

    var jsstring = "var un = 1; console.log(un);";
    eval(jsstring); // 打印出 "1"

    jsstring = "var deux = 2; console.log(deux);";
    new Function(jsstring)(); // 打印出 "2"

    jsstring = "var trois = 3; console.log(trois);";
    (function () {
        eval(jsstring);
    }()); // 打印出 "3"

    console.log(typeof un); // "number"
    console.log(typeof deux); // "undefined"
    console.log(typeof trois); // "undefined"

eval()Function()构造函数还有一个区别,就是eval()可以修改作用域链,而Function更像是一个沙箱。不管在什么地方执行Function(),它都只能看到全局作用域。因此它不会太严重的污染局部变量。在下面的示例代码中,eval()可以访问并修改其作用域之外的变量,而Function()则不能(注意,使用Function()new Function()是完全一样的)。

    (function () {
        var local = 1;
        eval("local = 3; console.log(local)"); // 打印出 3
        console.log(local); // 打印出 3
    }());

    (function () {
        var local = 1;
        Function("console.log(typeof local);")(); // 打印出 undefined
    }());

使用parseInt()进行数字转换

你可以使用parseInt()将字符串转换为数字。函数的第二个参数是进制参数,这个参数应该被指定,但却通常被省略。当字符串以0为前缀时转换就会出问题,例如,在表单中输入日期的一个字段。ECMAScript3中以0为前缀的字符串会被当作八进制数处理,这一点在ES5中已经有了改变。为了避免转换类型不一致而导致的意外结果,应当总是指定第二个参数:

    var month = "06",
        year = "09";
    month = parseInt(month, 10);
    year = parseInt(year, 10);

在这个例子中,如果省略掉parseInt的第二个参数,比如parseInt(year),返回的值是0,因为“09”被认为是八进制数(等价于parseInt(year,8)),但09是非法的八进制数。

字符串转换为数字还有两种方法:

    +"08" // 结果为8
    Number("08") // 结果为8

这两种方法要比parseInt()更快一些,因为顾名思义parseInt()是一种“解析”而不是简单的“转换”。但当你期望将“08 hello”这类字符串转换为数字,则必须使用parseInt(),其他方法都会返回NaN。

代码规范

确立并遵守代码规范非常重要,这会让你的代码风格一致、可预测,并且可读性更强。团队新成员通过学习代码规范可以很快进入开发状态,并写出让团队其他成员易于理解的代码。

在开源社区和邮件组中关于编代风格的争论一直不断。(比如关于代码缩进,用tab还是空格?)因此,如果你打算在团队内推行某种编码规范时,要做好应对各种反对意见的心理准备,而且要吸取各种意见。确定并遵守代码规范非常重要,任何一种规范都可以,这甚至比代码规范中的具体约定是怎么样的还要重要。

缩进

代码如果没有缩进就几乎不能读了,而不一致的缩进会使情况更加糟糕,因为它看上去像是遵守了规范,但真正读起来却没那么顺利。因此规范地使用缩进非常重要。

有些开发者喜欢使用tab缩进,因为每个人都可以根据自己的喜好来调整tab缩进的空格数,有些人则喜欢使用空格缩进,通常是四个空格。这都无所谓,只要团队每个人都遵守同一个规范即可,本书中所有的示例代码都采用四个空格的缩进写法,这也是JSLint所推荐的。(译注:电子版中看到的是用tab缩进,本译文也保留使用tab缩进。)

那么到底什么时候应该缩进呢?规则很简单,花括号里的内容应当缩进,包括函数体、循环(dowhileforfor-in)体、if语句、switch语句和对象字面量里的属性。下面的代码展示了如何正确地使用缩进:

    function outer(a, b) {
        var c = 1,
            d = 2,
            inner;
        if (a > b) {
            inner = function () {
                return {
                    r: c - d
                };
            };
        } else {
            inner = function () {
                return {
                    r: c + d
                };
            };
        }
        return inner;
    }

花括号

在特定的语句中应当总是使用花括号,即便是在可省略花括号的情况下也应当如此。从技术角度讲,如果iffor中只有一个语句,花括号是可以省略的,但最好还是不要省略,这会让你的代码更加工整一致而且易于修改。

假设有这样一段代码,for循环中只有一条语句,你可以省略掉这里的花括号,而且不会有语法错误:

    // 不好的方式
    for (var i = 0; i < 10; i += 1)
        alert(i);

但如果过了一段时间,你给这个循环添加了另一行代码会怎样?

    // 不好的方式
    for (var i = 0; i < 10; i += 1)
        alert(i);
        alert(i + " is " + (i % 2 ? "odd" : "even"));

第二个alert实际上在循环体之外,但这里的缩进会让你迷惑。从长远考虑最好还是写上花括号,即便是在只有一个语句的语句块中也应如此:

    // 更好的方式
    for (var i = 0; i < 10; i += 1) {
        alert(i);
    }

同理,if条件句也应当如此:

    // 不好的方式
    if (true)
        alert(1);
    else
        alert(2);

    // 更好的试
    if (true) {
        alert(1);
    } else {
        alert(2);
    }

左花括号的位置

开发人员对于左大括号的位置有着不同的偏好,在同一行呢还是在下一行?

    if (true) {
        alert("It's TRUE!");
    }

或者:

    if (true)
    {
        alert("It's TRUE!");
    }

在这个例子中,这个问题只是个人偏好问题。但有时候花括号位置的不同会影响程序的执行,因为JavaScript会“自动插入分号”。JavaScript对行尾是否有分号并没有要求,它会自动将分号补全。因此,当函数的return语句返回了一个对象字面量,而对象的左花括号和return又不在同一行时,程序的执行就和预期的不同了:

    // 警告:返回值和预期的不同
    function func() {
        return
        {
            name: "Batman"
        };
    }

可以看出程序作者的意图是返回一个包含了name属性的对象,但实际情况不是这样。因为return后会填补一个分号,函数的返回值就是undefined。这段代码等价于:

    // 警告:返回值和预期的不同
    function func() {
        return undefined;
        // 下面的代码不会运行…
        {
            name: "Batman"
        };
    }

总结一下好的写法,在特写的语句中总是使用花括号,并且总是将左花括号与上一条语句放在同一行:

    function func() {
        return {
            name: "Batman"
        };
    }

关于分号也值得注意:和花括号一样,应当总是使用分号,尽管在JavaScript解析代码时会补全行末省略的分号,但严格遵守这条规则,可以让代码更加严谨,同时可以避免前面例子中所出现的歧义。

 空格

空格的使用同样有助于改善代码的可读性和一致性。

适合使用空格的地方包括:

  • for循环中的分号之后,比如for (var i = 0; i < 10; i += 1) {...}
  • for循环中初始化多个变量,比如for (var i = 0, max = 10; i < max; i += 1) {...}
  • 用于分隔数组元素的逗号之后,比如var a = [1, 2, 3];
  • 对象属性后的逗号以及名值对之间的冒号之后,比如var o = {a: 1, b: 2};
  • 函数参数中,比如myFunc(a, b, c)
  • 函数声明的花括号之前,比如function myFunc() {}
  • 匿名函数表达式function之后,比如var myFunc = function () {};

另外,我们推荐在运算符和操作数之间也添加空格。也就是说在+-*=<><=>====!==&&||+=符号前后都添加空格。

    // 适当且一致的空格给代码留了“呼吸空间”,使代码更易读
    var d = 0,
        a = b + 1;
    if (a && b && c) {
        d = a % c;
        a += d;
    }

    // 反模式,缺少或者不正确的空格使得代码不易读
    var d= 0,
        a =b+1;
    if (a&& b&&c) {
        d=a %c;
        a+= d;
    }

最后,还应当注意,最好在花括号旁边添加空格:

  • 在函数、if-else语句、循环、对象字面量的左花括号之前补充空格
  • 在右花括号和else或者while之间补充空格

垂直空白的使用经常被我们忽略,你可以使用空行来将代码单元分隔开,就像文学作品中使用段落进行分隔一样。

命名规范

下面是一些建议的命名规范,你可以原样采用,也可以根据自己的喜好作调整。同样,遵循规范要比规范本身是什么样更加重要。

构造函数命名中的大小写

JavaScript中没有类,但有构造函数,构造函数使用Pascal命名,如Person(),:

var adam = new Person(); // 正确的写法 
var adan = new person(); // 错误的写法

单词分隔

当你的变量名或函数名中含有多个单词时,单词之间的分隔也应当遵循统一的规范。最常见的是“驼峰式”(camel case)命名,单词都是小写,每个单词的首字母是大写。

对于构造函数,可以使用Pascal命名,比如MyConstructor(),对于函数和方法,可以采用“小驼峰式”(lower camel case)命名,比如myFunction()calculateArea()getFirstName()

其他命名风格

有时开发人员使用命名规范来弥补或代替语言特性的不足。

比如,JavaScript中无法定义常量(尽管有一些内置常量比如Number.MAX_VALUE),所以开发者都采用了一种命名规范,对于那些程序运行周期内不会更改的变量使用全大写字母来命名。比如:

    // 常量,请勿修改
    var PI = 3.14,
        MAX_WIDTH = 800;

除了使用大写字母的命名方式之外,还有另一种命名规范:全局变量全大写。这种命名方式和“减少全局变量”的约定相辅相成,并让全局变量很容易辨认。

除了常量和全局变量的命名规范,这里讨论另外一种命名规范,即私有变量的命名。尽管在JavaScript是可以实现真正的私有变量的,但开发人员更喜欢在私有成员或方法名之前加上下划线前缀,比如下面的例子:

    var person = {
        getName: function () {
            return this._getFirst() + ' ' + this._getLast();
        },
        _getFirst: function () {
            // ...
        },
        _getLast: function () {
            // ...
        }
    };

在这个例子中,getName()是一个公有方法,是确定的API的一部分,而_getFirst()_getLast()则是私有方法。尽管这两个方法本质上和公有方法没有区别,但在方法名前加下划线前缀就是为了告知用户不要直接使用这两个私有方法,因为不能保证它们在下一个版本中还能正常工作。JSLint会对私有方法作检查,除非设置了JSLint的nomen选项为false

下面介绍一些_private风格写法的变种:

  • 在名字尾部添加下划线以表明私有,比如name_getElements_()
  • 使用一个下划线前缀表明受保护的属性_protected,用两个下划线前缀表明私有属性__private
  • 在Firefox中实现了一些非标准的内置属性,这些属性在开头和结束都有两个下划线,比如__proto____parent__

写注释

在写代码时,即便你认为你的代码不会被别人读到,也应该写好注释。因为当你对一个问题非常熟悉时,你会非常明白这些代码的作用,但当过了几个星期后再来读这段代码时,则需要绞尽脑汁的回想这些代码在干什么。

你不必对那些浅显易懂的代码写过多的注释,比如每个变量、每一行都写注释。但你应该对所有的函数、它们的参数和返回值进行注释,除此之外,对于那些值得注意的或是比较怪异的算法和技术也应当写好注释。对于其他阅读你代码的人来说,注释就是一种提示,只要阅读注释、函数名和参数,就算不读其它部分的代码也能大概理解程序的逻辑。比如,这里有五六行代码完成了某个功能,如果有一行描述这段代码功能的注释,读程序的人就不必再去关注代码的实现细节了。代码注释的写法并没有硬性规定,但有些代码片段(比如正则表达式)需要比代码本身更多的注释。

过时的注释会造成误导,这比不写注释还要糟糕。保持注释的状态为最新的习惯非常重要,尽管对很多人来说这很难做到。

在下一小节我们会讲到,利用注释可以自动生成文档。

写API文档

很多人都觉得写文档是一件很枯燥而且吃力不讨好的事情,但实际情况并不是这样。我们可以通过代码注释自动生成文档,这样就不用再去专门写文档了。很多人觉得这是一个不错的点子,因为根据某些关键字和特定的格式自动生成可阅读的参考手册本身就是“某种编程”。

最早利用注释生成API文档的工具诞生自Java业界,这个工具名叫“javadoc”,和Java SDK(软件开发工具包)一起提供,但这个创意迅速被其他语言借鉴。JavaScript领域有两个非常优秀的开源工具,它们是JSDoc Toolkit(http://code.google.com/p/jsdoc-toolkit/)和YUIDoc(http://yuilibrary.com/projects/yuidoc)。

生成API文档的过程:

  • 以特定的格式来写代码
  • 运行工具来对代码和注释进行解析
  • 发布工具运行的结果,通常是HTML页面

这种语法包括十几种标签(tag),写法类似于:

/**
 * @tag value
 */

比如这里有一个函数reverse(),可以对字符串进行反序操作。它的参数和返回值都是字符串。给它补充注释如下:

/**
* Reverse a string
*
* @param {String} input String to reverse
* @return {String} The reversed string
*/
var reverse = function (input) {
    // ...
    return output;
};

如你所见,@param是用来说明输入参数的标签,@return是用来说明返回值的标签,文档生成工具最终会将这种带注释的源代码解析成HTML文档。

示例:YUIDoc

YUIDoc的初衷是为YUI(Yahoo! User Interface)库生成文档,但其实它也可以应用于任何项目。为了更充分的使用YUIDoc,你需要学习它的注释规范,比如模块和类的写法。(尽管在JavaScript中其实是没有类的概念的)。

让我们看一个用YUIDoc生成文档的完整例子。

图2-1展示了最终生成的文档的样子,你可以根据项目需要定制HTML模板,让生成的文档更加友好和个性化。

这里提供了在线的demo,请参照http://jspatterns.com/book/2/。

这个例子中所有的应用作为一个模块(myapp)放在一个文件里(app.js),后续的章节会更详细的介绍模块,现在只需知道可以用一个YUIDoc的标签来表示模块即可。

图2-1 YUIDoc生成的文档

YUIDoc生成的文档

app.js的开始部分:

    /**
     * My JavaScript application
     *
     * @module myapp
     */

然后定义了一个空对象作为模块的命名空间:

    var MYAPP = {};

紧接着定义了一个包含两个方法的对象math_stuff,这两个方法分别是sum()multi()

    /**
    * A math utility
    * @namespace MYAPP
    * @class math_stuff
    */
    MYAPP.math_stuff = {
        /**
        * Sums two numbers
        *
        * @method sum
        * @param {Number} a First number
        * @param {Number} b The second number
        * @return {Number} The sum of the two inputs
        */
        sum: function (a, b) {
            return a + b;
        },

        /**
        * Multiplies two numbers
        *
        * @method multi
        * @param {Number} a First number
        * @param {Number} b The second number
        * @return {Number} The two inputs multiplied
        */
        multi: function (a, b) {
            return a * b;
        }
    };

这样就完成了第一个“类”的定义,注意以下标签:

  • @namespace

    包含对象的全局引用

  • @class

    代表一个对象或构造函数(JavaScript中没有类)

  • @method

    定义对象的方法,并指定方法的名称

  • @param

    列出函数需要的参数,参数的类型放在一对花括号内,后面跟参数名和描述

  • @return

    @param类似,用以描述方法的返回值,可以不带名字

我们来实现第二个“类”,使用一个构造函数,并给这个构造函数的原型添加一个方法,看看YUIDoc在面对不同的对象创建方式时是如何工作的:

    /**
    * Constructs Person objects
    * @class Person
    * @constructor
    * @namespace MYAPP
    * @param {String} first First name
    * @param {String} last Last name
    */
    MYAPP.Person = function (first, last) {
        /**
        * Name of the person
        * @property first_name
        * @type String
        */
        this.first_name = first;
        /**
        * Last (family) name of the person
        * @property last_name
        * @type String
        */
        this.last_name = last;
    };
    /**
    * Returns the name of the person object
    *
    * @method getName
    * @return {String} The name of the person
    */
    MYAPP.Person.prototype.getName = function () {
        return this.first_name + ' ' + this.last_name;
    };

在图2-1中可以看到生成的文档中Person构造函数的生成结果,值得注意的部分是:

  • @constructor 说明这个“类”其实是一个构造函数
  • @prototype@type 用来描述对象的属性

YUIDoc工具是与语言无关的,只解析注释块,而不是JavaScript代码。它的缺点是必须要在注释中指定属性、参数和方法的名字,比如,@property first_name。好处是一旦你熟练掌握YUIDoc,就可以用它对任何语言源码生成文档。

编写易读的代码

这种编写注释块来生成API文档的做法可不仅仅是为了偷懒,它还有另外一个作用,就是通过回头重看代码来提高代码质量。

随便一个作者或者编辑都会告诉你“编辑非常重要”,甚至是写一本好书或好文章最最重要的步骤。将想法落实在纸上形成草稿只是第一步,草稿确实可以给读者提供不少信息,但往往还不是重点最明晰、结构最合理、最符合阅读习惯的呈现形式。

编程也是同样的道理,当你坐下来解决一个问题的时候,这时的解决方案只是一种“草案”,尽管能正常工作,但是不是最优的方法呢?是不是可读性好、易于理解、可维护和更新?假设当你过一段时间后再来回头看你的代码,一定会发现很多需要改进的地方,比如需要重新组织代码或删掉多余的内容等等。这实际上就是在“整理”你的代码了,可以很大程度上提高你的代码质量。但事实却不那么如愿,我们常常承受着高强度的工作,根本没有时间来整理代码,因此通过代码注释来写文档其实是个不错的机会。

你往往会在写注释文档的时候发现很多问题,也会重新思考代码中的不合理之处,比如,某个方法中的第三个参数比第二个参数更常用,第二个参数多数情况下取值为true,因此就需要对这个方法进行适当的改造和包装。

写出易读的代码(或API),是指写代码时要有让别人能轻易读懂的意识。带着这个意识,你就需要不断思考采用更好的方法来解决手头的问题。

说回“草稿”的问题,也算是“抱佛脚”的权宜之计,一眼看上去是有点“草”,不过至少是有用的,特别是当你处理的是一个关键项目时(比如人命关天时)。一个合适的思路是,你应当始终扔掉你所给出的第一个解决方案,虽然它是可以正常工作的,但毕竟是一个草稿,是一种仅用于验证解决问题可行性的方案。事实上,第二个方案往往会更好,因为这时你对问题的理解会更加透彻。在产生第二个方案的过程中,不要允许自己去复制粘贴之前的代码,这有助于阻止自己投机取巧利用之前的捷径,最后产生不完美的方案。

同事评审(Peer Reviews)

另外一种可以提高代码质量的方法是组织相互评审。同事评审可以用一些工具辅助,可以很正式很规范,也是一种开发流程中值得提倡的步骤。你可能觉得没有时间去作代码互相评审,没关系,你可以让坐在你旁边的同事读一下你的代码,或者和她(译注:注意是“她”而不是“他”)一起过一遍你的代码。

同样,当你在写API文档或者其他文档的时候,同事评审能让你的产出物更加清晰,因为你写的文档是本来就是让别人读的,你得让别人通过文档知道你所做的东西。

同事评审是一种很好的实践,不仅仅是因为它能让代码变得更好,更重要的是,在评审的过程中,评审人和代码作者通过分享和讨论,两人都能取长补短、相互促进。

如果你的团队只有你一个开发人员,找不出第二个人能给你作代码评审,这也没关系。你可以通过将你的代码片段开源,或把有意思的代码片段贴在博客中,让全世界的人为你评审。

另外一个很好的实践是使用版本管理工具(CVS、SVN或Git),一旦有人修改并提交了代码,就会发邮件通知组内成员。虽然大部分邮件都进入了垃圾箱,但总是会碰巧有人在工作间隙看到你所提交的代码,并对代码做出一些评价。

发布时的代码压缩(Minify)

这里所说的代码压缩(Minify)是指去除JavaScript代码中的空格、注释以及其他不必要的部分,用以减少JavaScript文件的体积,降低网络带宽消耗。我们通常使用压缩工具来进行压缩,比如YUICompressor(Yahoo!)或Closure Compiler(Google),这可以减少页面加载时间。压缩用于发布的的脚本是很重要的,压缩后的文件体积能减少至原来的一半以下。

下面这段代码是压缩后的样子(这段代码是YUI2库中的事件模块):

YAHOO.util.CustomEvent=function(D,C,B,A){this.type=D;this.scope=C||window;this.silent
=B;this.signature=A||YAHOO.util.CustomEvent.LIST;this.subscribers=[];if(!this.silent)
{}var E="_YUICEOnSubscribe";if(D!==E){this.subscribeEvent=new
YAHOO.util.CustomEvent(E,this,true);}...

除了去除空格、空行和注释之外,压缩工具还能缩短命名的长度(在保证代码安全的前提下),比如这段代码中的参数ABCD。压缩工具只会重命名局部变量,因为更改全局变量会破坏代码的逻辑,这也是要尽量使用局部变量的原因。如果你使用的全局变量是对DOM节点的引用,而且程序中多次用到,那么最好将它赋值给一个局部变量,这样能提高查找速度,代码也会运行的更快,此外还能提高压缩比、加快下载速度。

补充说明一下,Goolge Closure Compiler还会为了更高的压缩比对全局变量进行压缩(在“高级”模式中),这是很危险的,且对编程规范的要求非常苛刻。

对用于生产环境的脚本做压缩是非常重要的步骤,因为它能提升页面性能,但你应当将这个过程交给工具来完成。千万不要试图手写“压缩好的”代码,你应当在编写代码时坚持使用语义化的变量命名,并保留足够的空格、缩进和注释。你写的代码是需要被人阅读的,所以应当将注意力放在代码可读性和可维护性上,将代码压缩的工作交给工具去完成。

运行JSLint

在上一章我们已经介绍了JSLint,本章中也提到了数次。到现在你应该已经相信用JSLint检查你的代码是一种好的编程模式了。

JSLint的检查点都有哪些呢?它会对本章讨论过的一些模式(单var模式、parseInt()的第二个参数、总是使用花括号)做检查。JSLint还包括其他方面的检查:

  • 不可达代码(译注:指永远不可能运行的代码)
  • 变量在声明之前被使用
  • 不安全的UTF字符
  • 使用voidwith或者eval
  • 无法正确解析的正则表达式

JSLint是基于JavaScript实现的(它自己的代码是可以通过JSLint检查的),它提供了在线工具,也可以下载使用,可以运行于很多种平台的JavaScript解析器。你可以将源码下载后在本地运行,支持的环境包括WSH(Windows Scripting Host,Windows)、JSC(JavaScriptCore,MacOSX)或Rhino(Mozilla开发的JavaScript引擎)。

将JSLint下载后和你的代码编辑器配置在一起是个很不错的主意,这样每次你保存代码的时候都会自动执行代码检查。(为它配置一个快捷键也很有用)。

小结

  • 减少全局对象,最好每个应用只有一个全局对象
  • 函数都使用单var模式来定义,这样可以将所有的变量放在同一个地方声明,同时可以避免“声明提前”给程序逻辑带来的影响
  • for循环、for-in循环、switch语句、“避免使用eval()”、不要扩充内置原型
  • 遵守统一的编码规范(在任何必要的时候保持空格、缩进、花括号和分号)和命名规范(构造函数、普通函数和变量)。

本章还讨论了其他一些和代码本身无关的实践,这些实践和编码过程紧密相关,包括写注释、写API文档、组织同事评审、不要试图去手动“压缩”(minify)代码而牺牲代码可读性、坚持使用JSLint来对代码进行检查。

Revision: 2015.10.29

JavaScript编程指南(一)命名

JavaScript 编程规范


1 JavaScript 命名约定

1.1 大小写约定

大小写样式
下列术语描述了标识符的不同大小写形式。

Pascal 大小写
别名:“大驼峰式”(upper camel case)
将标识符的首字母和后面连接的每个单词的首字母都大写。 可以对三字符或更多字符的标识符使用 Pascal 大小写。 例如:
BackColor

Camel
别名:“小驼峰式”(lower camel case)
标识符的首字母小写,而每个后面连接的单词的首字母都大写。 例如:
backColor

大写
标识符中的所有字母都大写。 例如:
IO

小写
标识符中的所有字母都小写。例如:
color

1.1.1 基本大小写规则

如果标识符由多个单词组成,请不要在各单词之间使用分隔符,如下划线(“_”)或连字符(“-”)等。 而应使用大小写来指示每个单词的开头。

标识符大小写规则汇总
标识符 Case 示例
局部变量 Camel arg,jsonArg
全局变量 Pascal、大写 Goods,GoodsDetail,GD,MyAPP
常量 大写、大写+下划线 PI,AJAXURL,MAX_VALUE
方法 Camel getName(),getGoodsGallery()
参数 Camel imgWidth,imgHeight
构造函数 Pascal Student(),MyConstructor()

详细描述如下:

局部变量

局部变量采用Camel大小写。

function func() {
    var json; // Camel
    var jsonArg; // Camel
}

全局变量

全局变量采用大写或者Pascal。

var GOODS = {}, // 单个单词采用大写
    GoodsDetail = {}, // 多个单词采用Pascal
    GD = GoodsDetail; // 多个单词的缩写采用全大写

常量

对于那些程序运行周期内不会更改的变量使用全大写字母来命名。

特例:允许使用大写+下划线的方式来命名常量

// 推荐
var PI = 3.14, 
    AJAXURL = '/mobile/getCitys.action',
    MAX_VALUE = 100; // 大写字母和下划线命名
// 不推荐
var Max_Value = 100,
    max_value = 100;

方法

方法采用Camel大小写。

// 正确的写法
var getGallery = function () {
};
// 或者
function getGallery() {
}

// 不推荐的写法
function GetGallery(){
}

参数
参数采用Camel大小写。

function getGallery(imgWidth, imgHeight) {
}

构造函数
JavaScript构造函数采用Pascal大小写。

function Student(name, age) {
    /// <summary>
    /// 学生构造函数
    /// </summary>
    /// <param name="name" type="String">姓名</param>
    /// <param name="age" type="Number">年龄</param>
    this.name = name;
    this.age = age;
}

var s1 = new Student('李鸣', 12),
    s2 = new Student('黄蕾', 23);

1.1.2 首字母缩写词的大小写规则

首字母缩写词是由术语或短语中各单词的首字母构成的单词。 例如,HTML 是 Hypertext Markup Language 的首字母缩写。 只有在公众广为认知和理解的情况下,才应在标识符中使用首字母缩写词。 首字母缩写词不同于缩写词,因为缩写词是一个单词的缩写。 例如,IDidentifier 的缩写。
注意注意

可在标识符中使用的两个缩写词是 IDOK。 在采用 Pascal 大小写格式的标识符中,这两个缩写词的大小写形式应分别为 IdOk。 如果在采用大小写混合格式的标识符中将这两个缩写词用作首个单词,则它们的大小写形式应分别为 idok

首字母缩写词的大小写取决于首字母缩写词的长度。 所有首字母缩写词都至少包含两个字符。 为方便说明准则,如果一个首字母缩写词只包含两个字符,则将其视为短型首字母缩写词。 包含三个或三个以上字符的首字母缩写词为长型首字母缩写词。

下列准则为短型和长型首字母缩写词指定了正确的大小写规则。 标识符大小写规则优先于首字母缩写词大小写规则。

两字符首字母缩写词的两个字符都要大写,但当首字母缩写词作为大小写混合格式的标识符的首个单词时例外。

  • 例如,名为 DBRate 的属性是一个采用 Pascal 大小写格式的标识符,它使用短型首字母缩写词 (DB) 作为首个单词。
  • 又如,名为 ioChannel 的参数是一个采用大小写混合格式的标识符,它使用短型首字母缩写词 (IO) 作为首个单词。

包含三个或三个以上字符的首字母缩写词只有第一个字符大写,但当首字母缩写词作为大小写混合格式的标识符的首个单词时例外。

例如,名为 XmlWriter 的类是一个采用 Pascal 大小写格式的标识符,它使用长型首字母缩写词作为首个单词。 又如,名为 htmlReader 的参数是一个采用大小写混合格式的标识符,它使用长型首字母缩写词作为首个单词。

如果任何首字母缩写词位于采用大小写混合格式的标识符开头,则无论该首字母缩写词的长度如何,都不大写其中的任何字符。

  • 例如,名为 xmlStream 的参数是一个采用大小写混合格式的标识符,它使用长型首字母缩写词 (xml) 作为首个单词。 名为 dbServerName 的参数是一个采用大小写混合格式的标识符,它使用短型首字母缩写词 (db) 作为首个单词。

组合词和常用术语的大小写规则

不要将所谓的紧凑格式组合词中的每个单词都大写。 这种组合词是指写作一个单词的组合词,如endpoint

  • 例如,hashtable 是一个紧凑格式的组合词,应将其视为一个单词并相应地确定大小写。 如果采用 Pascal 大小写格式,则该组合词为 Hashtable;如果采用大小写混合格式,则该组合词为 hashtable。 若要确定某个单词是否是紧凑格式的组合词,请查阅最新的词典。

下表列出了不是紧凑格式组合词的一些常用术语。 术语先以 Pascal 大小写格式显示,后面的括号中的是其大小写混合格式。

  • BitFlag (bitFlag)
  • FileName (fileName)
  • LogOff (logOff)
  • LogOn (logOn)
  • SignIn (signIn)
  • SignOut (signOut)
  • UserName (userName)
  • WhiteSpace (whiteSpace)

1.2 通用命名约定

1.2.1 变量和函数

变量和函数的命名应该具有语义性。
对于函数命名来说,第一个单词一般是动词,常见的动词约定如下:

动词 适用函数范围
can 函数返回一个布尔值,如:canSubmit()
has 方法被调用时立即触发,如:hasSubElement()
can 函数返回一个布尔值,如:canSubmit()
is 函数返回一个布尔值,如:canSubmit()
get 函数返回一个非布尔值,如:getName()
set 函数用来保存一个值,如:setName('李雷雷')

详细示例如下:

function canSubmit() {
    /// <summary>
    /// 是否能提交
    /// </summary>
}

function hasSubElement() {
    /// <summary>
    /// 是否有子节点
    /// </summary>
}

function isEnabled() {
    /// <summary>
    /// XX是否可用
    /// </summary>
}

function Student(name, age) {
    /// <summary>
    /// 学生构造函数
    /// </summary>
    /// <param name="name" type="String">学生姓名</param>
    /// <param name="age" type="Number">学生年龄</param>
    // 在这里可以加更严谨的验证逻辑来判断参数是否正确
    this.name = name || '未知';
    this.age = age || -1;
}

Student.prototype.getName = function () {
    /// <summary>
    /// 返回姓名
    /// </summary>
    /// <returns type="String">学生姓名</returns>
    return this.name;
};

Student.prototype.setName = function (name) {
    /// <summary>
    /// 设置姓名
    /// </summary>
    /// <param name="name" type="String">学生姓名</param>
    this.name = name || '未知';
}


var student = new Student('李雷', 12);
student.setName('李雷雷');
console.log(student.getName());

1.3 命名空间的命名

大小写遵循Pascal,

var HSH = {};

// 编写可维护JavaScript的命名空间
ZakasBooks.MaintainableJavaScript = {};

// 高性能JavaScript的mingmingkongjian
ZakasBooks.HighPerformanceJavaScript = {};

// 通用的命名空间函数“namespace”
var YourGlobal = {
    namespace: function (ns) {
        var parts = ns.split("."),
            object = this,
            i,
            len;
        for (i = 0, len = parts.length; i < len; i++) {
            if (!object[parts[i]]) {
                object[parts[i]] = {};
            }
            object = object[parts[i]];
        }
        return object;
    }
};

// 使用
YourGlobal.namespace("Books.MaintainableJavaScript");
YourGlobal.Books.MaintainableJavaScript.author = 'Zakas';
console.log(YourGlobal.Books.MaintainableJavaScript.author); //Zakas

1.4 文件命名

文件的命名只允许包含小写字母、数值、连字符、点、和下划线。

  • 文件名应简短、也必须描述文件。
  • 文件名与功能相对应、如:地址列表可以命名为hsh.address.list.js

参考

Revision: 2015.11.02

webpack 入门

webpack 介绍

webpack 简单教程

从本教程中,你将学会:

  • 如何安装webpack
  • 如何使用webpack
  • 如何使用加载器
  • 如何使用webpack 开发服务器

安装webpack

使用node.js安装,在命令行中输入以下命令来进行全局安装:

npm install webpack -g

安装完成后你就可以像使用npm一样来使用webpack命令。

设置编译

在一个空目录中添加创建一个entry.js 和 index.html文件:
以Windows系统为例,目录路径为D:\Dev\React\webpackstart

entry.js

document.writeln('hello webpack!');

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>SETUP THE COMPILATION</title>
</head>
<body>
    <script src="bundle.js" charset="utf-8" type="text/javascript"></script>
</body>
</html>

打开命令行,切换到当前目录cd /d D:\Dev\React\webpackstart,运行以下命令进行打包:

webpack ./entry.js bundle.js

得到以下结果:

Hash: 85699777ef8f322c7070
Version: webpack 1.13.0
Time: 102ms
    Asset     Size  Chunks             Chunk Names
bundle.js  1.42 kB       0  [emitted]  main
   [0] ./entry.js 33 bytes {0} [built]

在浏览器中查看index.html

引入第二个脚本文件

在当前目录中添加content.js文件

    module.exports='hello webpack from content.js!';

修改entry.js

document.writeln('hello webpack!');
document.writeln(require('./content.js'));

在DOS命令行中重新编译(打包):
webpack ./entry.js bundle.js

浏览器中查看index.html

第一个webpack加载器

如我我们想添加一个CSS文件到我们的APP中:
但是webpack只能处理原生的JavaScript,所以我们需要css-loader来处理CSS文件,同时也需要引入
style-loader来应用CSS文件中的样式。

运行npm install css-loader style-loader来安装加载器(局部安装)

在当前目录中添加一个文件夹css(D:\Dev\React\webpackstart\css),在css文件夹中添加样式文件style.css

body{
    background-color:#ff6a00;
}

修改entry.js:

//document.writeln('hello webpack!');
require('!style!css!./css/style.css');
document.writeln(require('./content.js'));

重新编译(打包)后查看index.html。
更多的加载器;

使用webpack.config.js

添加文件webpack.config.js

module.exports = {
    entry: "./entry.js",
    output: {
        path: __dirname + "/build/",
        filename: "bundle.js"
    },
    module: {
        loaders: [
            { test: /\.css$/, loader: "style!css" }
        ]
    }
};

修改index.html文件:

<script src="./build/bundle.js" charset="utf-8" type="text/javascript"></script>

添加配置文件之后,只需执行webpack命令进行编译,完成之后再浏览器中看到一样的效果。

更直观的输出

随着项目的不断增大,就需要更长的时间来进行编译。可以通过以下命令来实现类进度条且带有颜色:

webpack --progress --colors

监视模式

每次重新编译是不是很麻烦呢,我们可以通过--watch参数来实现自动编译:

webpack --progress --colors --watch

执行该命令之后,只要命令行窗口不关闭,相关文件一旦改动就会自动进行重新编译。

开发服务器

npm install webpack-dev-server -g
webpack-dev-server --progress --colors

webpack开发服务器使用8080端口 localhost:8080 绑定了一个express服务器,

which serves your static assets as well as the bundle (compiled automatically).
It automatically updates the browser page when a bundle is recompiled (SockJS).
Open http://localhost:8080/webpack-dev-server/bundle in your browser.

The dev server uses webpack’s watch mode. It also prevents webpack from emitting the resulting files to disk.
Instead it keeps and serves the resulting files from memory.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.