书写具备一致风格、通俗易懂 JavaScript 的原则

原文出处 https://github.com/rwaldron/idiomatic.js/blob/master/readme.md

 另一篇jQuery官网上的js代码规范 http://contribute.jquery.org/style-guide/js/#type-checks

前言

下面的章节描述的是一个 合理 的现代 JavaScript 开发风格指南,并非硬性规定。其想送出的核心理念是高度统一的代码风格(the law of code style consistency)。你为项目所择风格都应为最高准则。作为一个描述放置于你的项目中,并链接到这个文档作为代码风格一致性、可读性和可维护性的保证。

Idiomatic 风格宣言

  1. 空白

    • 永远都不要混用�空格和Tab。
    • 开始一个项目,在写代码之前,选择软缩进(空格)或者 Tab(作为缩进方式),并将其作为最高准则
      • 为了可读, 我总是推荐在你的编辑中设计2个字母宽度的缩进 — 这等同于两个空格或者两个空格替代一个 Tab。
    • 如果你的编辑器支持,请总是打开 “显示不可见字符” 这个设置。好处是:
      • 保证一致性
      • 去掉行末的空格
      • 去掉空行的空格
      • 提交和对比更具可读性
  2. 美化语法

    A. 小括号, 花括号, 换行

    // if/else/for/while/try 通常都有小括号、花括号和多行
    // 这有助于可读
    
    // 2.A.1.1
    // 难辨语法(cramped syntax)的例子
    
    if(condition) doSomething();
    
    while(condition) iterating++;
    
    for(var i=0;i<100;i++) someIterativeFn();
    
    
    // 2.A.1.1
    // 使用空格来提升可读性
    
    if ( condition ) {
      // 语句
    }
    
    while ( condition ) {
      // 语句
    }
    
    for ( var i = 0; i < 100; i++ ) {
      // 语句
    }
    
    // 更好的做法:
    
    var i,
      length = 100;
    
    for ( i = 0; i < length; i++ ) {
      // 语句
    }
    
    // 或者...
    
    var i = 0,
      length = 100;
    
    for ( ; i < length; i++ ) {
      // 语句
    }
    
    var prop;
    
    for ( prop in object ) {
      // 语句
    }
    
    
    if ( true ) {
      // 语句
    } else {
      // 语句
    }

    B. 赋值, 声明, 函数 ( 命名函数, 函数表达式, 构建函数 )

    // 2.B.1.1
    // 变量
    var foo = "bar",
      num = 1,
      undef;
    
    // 字面量标识:
    var array = [],
      object = {};
    
    
    // 2.B.1.2
    // 在一个作用域(函数)内只使用一个 `var` 有助于提升可读性
    // 并且让你的声明列表变得有条不紊 (还帮你省了几次键盘敲击)
    
    // 不好
    var foo = "";
    var bar = "";
    var qux;
    
    // 好
    var foo = "",
      bar = "",
      quux;
    
    // 或者..
    var // 对这些变量的注释
    foo = "",
    bar = "",
    quux;
    
    // 2.B.1.3
    // `var` 语句必须总是在各自作用域(函数)顶部
    // 同样适应于来自 ECMAScript 6 的常量
    
    // 不好
    function foo() {
    
      // 在变量前有语句
    
      var bar = "",
        qux;
    }
    
    // 好
    function foo() {
      var bar = "",
        qux;
    
      // 所有语句都在变量之后
    }
    // 2.B.2.1
    // 命名函数声明
    function foo( arg1, argN ) {
    
    }
    
    // 使用方法
    foo( arg1, argN );
    
    
    // 2.B.2.2
    // 命名函数声明
    function square( number ) {
      return number * number;
    }
    
    // 使用方法
    square( 10 );
    
    // 非常不自然的连带传参(continuation passing)风格
    function square( number, callback ) {
      callback( number * number );
    }
    
    square( 10, function( square ) {
      // 回调内容
    });
    
    
    // 2.B.2.3
    // 函数表达式
    var square = function( number ) {
      // 返回有价值的、相关的内容
      return number * number;
    };
    
    // 带标识符的函数表达式
    // 这种首选形式有附加的功能让其可以调用自身
    // 并且在堆栈中有标识符
    var factorial = function factorial( number ) {
      if ( number < 2 ) {
        return 1;
      }
    
      return number * factorial( number-1 );
    };
    
    
    // 2.B.2.4
    // 构造函数声明
    function FooBar( options ) {
    
      this.options = options;
    }
    
    // 使用方法
    var fooBar = new FooBar({ a: "alpha" });
    
    fooBar.options;
    // { a: "alpha" }
    

    C. 异常, 细节

    // 2.C.1.1
    // 带回调的函数
    foo(function() {
      // 注意:在第一函数调用的小括号和 `function` 处并没有空格
    });
    
    // 函数接受 `array` 作为参数,没有空格
    foo([ "alpha", "beta" ]);
    
    // 2.C.1.2
    // 函数接受 `object` 作为参数,没有空格
    foo({
      a: "alpha",
      b: "beta"
    });
    
    // 函数接受 `string` 字面量作为参数,没有空格
    foo("bar");
    
    // 分组用的小括号内部,没有空格
    if ( !("foo" in obj) ) {
    
    }
    

    D. 一致性(统一)总是笑到最后的(Consistency Always Wins)

    在 2.A-2.C 节,留白作为一个推荐方式被提出,基于单纯的、更高的目的:统一。值得注意的是格式化偏好,像“内部留白”必须是可选的,但在整个项目的源码中必须只存在着一种。

    // 2.D.1.1
    
    if (condition) {
      // 语句
    }
    
    while (condition) {
      // 语句
    }
    
    for (var i = 0; i < 100; i++) {
      // 语句
    }
    
    if (true) {
      // 语句
    } else {
      // 语句
    }
    

    E. 引号

    无论你选择单引号还是双引号都无所谓,在 JavaScript 中它们在解析上没有区别。而绝对需要强制的是一致性。 永远不要在同一个项目中混用两种引号,选择一种,并保持一致

    F. 行末和空行

    留白会破坏区别并使用变更不可读。考虑包括一个预提交的 hook 自动删除行末和空行中的空格。

  3. 类型检测 (来源于 jQuery Core Style Guidelines)

    A. 直接类型(实际类型,Actual Types)

    String:

    typeof variable === "string"
    

    Number:

    typeof variable === "number"
    

    Boolean:

    typeof variable === "boolean"
    

    Object:

    typeof variable === "object"
    

    Array:

    Array.isArray( arrayLikeObject )
    (如果可能的话)
    

    Node:

    elem.nodeType === 1
    

    null:

    variable === null
    

    null or undefined:

    variable == null
    

    undefined:

    全局变量:

    typeof variable === "undefined"
    

    局部变量:

    variable === undefined
    

    属性:

    object.prop === undefined
    object.hasOwnProperty( prop )
    "prop" in object
    

    B. 转换类型(强制类型,Coerced Types)

    考虑下面这个的含义...

    给定的 HTML:

    <input type="text" id="foo-input" value="1">
    
    // 3.B.1.1
    
    // `foo` 已经被赋予值 `0`,类型为 `number`
    var foo = 0;
    
    // typeof foo;
    // "number"
    ...
    
    // 在后续的代码中,你需要更新 `foo`,赋予在 input 元素中得到的新值
    
    foo = document.getElementById("foo-input").value;
    
    // 如果你现在测试 `typeof foo`, 结果将是 `string`
    // 这意味着你在 if 语句检测 `foo` 有类似于此的逻辑:
    
    if ( foo === 1 ) {
    
      importantTask();
    
    }
    
    // `importantTask()` 将永远不会被执行,即使 `foo` 有一个值 "1"
    
    
    // 3.B.1.2
    
    // 你可以巧妙地使用 + / - 一元运算符强制转换类型以解决问题:
    
    foo = +document.getElementById("foo-input").value;
    //    ^ + 一元运算符将它右边的运算对象转换为 `number`
    
    // typeof foo;
    // "number"
    
    if ( foo === 1 ) {
    
      importantTask();
    
    }
    
    // `importantTask()` 将被调用

    对于强制类型转换这里有几个例子:

    // 3.B.2.1
    
    var number = 1,
      string = "1",
      bool = false;
    
    number;
    // 1
    
    number + "";
    // "1"
    
    string;
    // "1"
    
    +string;
    // 1
    
    +string++;
    // 1
    
    string;
    // 2
    
    bool;
    // false
    
    +bool;
    // 0
    
    bool + "";
    // "false"
    // 3.B.2.2
    
    var number = 1,
      string = "1",
      bool = true;
    
    string === number;
    // false
    
    string === number + "";
    // true
    
    +string === number;
    // true
    
    bool === number;
    // false
    
    +bool === number;
    // true
    
    bool === string;
    // false
    
    bool === !!string;
    // true
    // 3.B.2.3
    
    var array = [ "a", "b", "c" ];
    
    !!~array.indexOf("a");
    // true
    
    !!~array.indexOf("b");
    // true
    
    !!~array.indexOf("c");
    // true
    
    !!~array.indexOf("d");
    // false
    
    // 值得注意的是上述都是 "不必要的聪明"
    // 采用明确的方案来比较返回的值
    // 如 indexOf:
    
    if ( array.indexOf( "a" ) >= 0 ) {
      // ...
    }
    // 3.B.2.3
    
    
    var num = 2.5;
    
    parseInt( num, 10 );
    
    // 等价于...
    
    ~~num;
    
    num >> 0;
    
    num >>> 0;
    
    // 结果都是 2
    
    
    // 时刻牢记心底, 负值将被区别对待...
    
    var neg = -2.5;
    
    parseInt( neg, 10 );
    
    // 等价于...
    
    ~~neg;
    
    neg >> 0;
    
    // 结果都是 -2
    // 但是...
    
    neg >>> 0;
    
    // 结果即是 4294967294
    
  4. 对比运算

    // 4.1.1
    // 当只是判断一个 array 是否有长度,相对于使用这个:
    if ( array.length > 0 ) ...
    
    // ...判断真伪, 请使用这种:
    if ( array.length ) ...
    
    
    // 4.1.2
    // 当只是判断一个 array 是否为空,相对于使用这个:
    if ( array.length === 0 ) ...
    
    // ...判断真伪, 请使用这种:
    if ( !array.length ) ...
    
    
    // 4.1.3
    // 当只是判断一个 string 是否为空,相对于使用这个:
    if ( string !== "" ) ...
    
    // ...判断真伪, 请使用这种:
    if ( string ) ...
    
    
    // 4.1.4
    // 当只是判断一个 string 是为空,相对于使用这个:
    if ( string === "" ) ...
    
    // ...判断真伪, 请使用这种:
    if ( !string ) ...
    
    
    // 4.1.5
    // 当只是判断一个引用是为真,相对于使用这个:
    if ( foo === true ) ...
    
    // ...判断只需像你所想,享受内置功能的好处:
    if ( foo ) ...
    
    
    // 4.1.6
    // 当只是判断一个引用是为假,相对于使用这个:
    if ( foo === false ) ...
    
    // ...使用叹号将其转换为真
    if ( !foo ) ...
    
    // ...需要注意的是:这个将会匹配 0, "", null, undefined, NaN
    // 如果你 _必须_ 是布尔类型的 false,请这样用:
    if ( foo === false ) ...
    
    
    // 4.1.7
    // 如果想计算一个引用可能是 null 或者 undefined,但并不是 false, "" 或者 0,
    // 相对于使用这个:
    if ( foo === null || foo === undefined ) ...
    
    // ...享受 == 类型强制转换的好处,像这样:
    if ( foo == null ) ...
    
    // 谨记,使用 == 将会令 `null` 匹配 `null` 和 `undefined`
    // 但不是 `false`,"" 或者 0
    null == undefined
    

    总是判断最好、最精确的值,上述是指南而非教条。

    // 4.2.1
    // 类型转换和对比运算说明
    
    // 首次 `===`,`==` 次之 (除非需要松散类型的对比)
    
    // `===` 总不做类型转换,这意味着:
    
    "1" === 1;
    // false
    
    // `==` 会转换类型,这意味着:
    
    "1" == 1;
    // true
    
    
    // 4.2.2
    // 布尔, 真 & 伪
    
    // 布尔:
    true, false
    
    // 真:
    "foo", 1
    
    // 伪:
    "", 0, null, undefined, NaN, void 0
    
  5. 实用风格

    // 5.1.1
    // 一个实用的模块
    
    (function( global ) {
      var Module = (function() {
    
        var data = "secret";
    
        return {
          // 这是一个布尔值
          bool: true,
          // 一个字符串
          string: "a string",
          // 一个数组
          array: [ 1, 2, 3, 4 ],
          // 一个对象
          object: {
            lang: "en-Us"
          },
          getData: function() {
            // 得到 `data` 的值
            return data;
          },
          setData: function( value ) {
            // 返回赋值过的 `data` 的值
            return ( data = value );
          }
        };
      })();
    
      // 其他一些将会出现在这里
    
      // 把你的模块变成全局对象
      global.Module = Module;
    
    })( this );
    
    // 5.2.1
    // 一个实用的构建函数
    
    (function( global ) {
    
      function Ctor( foo ) {
    
        this.foo = foo;
    
        return this;
      }
    
      Ctor.prototype.getFoo = function() {
        return this.foo;
      };
    
      Ctor.prototype.setFoo = function( val ) {
        return ( this.foo = val );
      };
    
    
      // 不使用 `new` 来调用构建函数,你可能会这样做:
      var ctor = function( foo ) {
        return new Ctor( foo );
      };
    
    
      // 把我们的构建函数变成全局对象
      global.ctor = ctor;
    
    })( this );
    
  6. 命名

    A. 你并不是一个人肉 编译器/压缩器,所以尝试去变身为其一。

    下面的代码是一个极糟命名的典范:

    // 6.A.1.1
    // 糟糕命名的示例代码
    
    function q(s) {
      return document.querySelectorAll(s);
    }
    var i,a=[],els=q("#foo");
    for(i=0;i<els.length;i++){a.push(els[i]);}

    毫无疑问,你写过这样的代码 —— 希望从今天它不再出现。

    这里有一份相同逻辑的代码,但拥有更健壮、贴切的命名(和一个可读的结构):

    // 6.A.2.1
    // 改善过命名的示例代码
    
    function query( selector ) {
      return document.querySelectorAll( selector );
    }
    
    var idx = 0,
      elements = [],
      matches = query("#foo"),
      length = matches.length;
    
    for ( ; idx < length; idx++ ) {
      elements.push( matches[ idx ] );
    }
    

    一些额外的命名提示:

    // 6.A.3.1
    // 命名字符串
    
    `dog` 是一个 string
    
    
    // 6.A.3.2
    // 命名 arrays
    
    `['dogs']` 是一个包含 `dog 字符串的 array
    
    
    // 6.A.3.3
    // 命名函数、对象、实例,等
    
    camlCase; function 和 var 声明
    
    
    // 6.A.3.4
    // 命名构建器、原型,等
    
    PascalCase; 构建函数
    
    
    // 6.A.3.5
    // 命名正则表达式
    
    rDesc = //;
    
    
    // 6.A.3.6
    // 来自 Google Closure Library Style Guide
    
    functionNamesLikeThis;
    variableNamesLikeThis;
    ConstructorNamesLikeThis;
    EnumNamesLikeThis;
    methodNamesLikeThis;
    SYMBOLIC_CONSTANTS_LIKE_THIS;
    

    B. 面对 this

    除使用众所周知的 call 和 apply 外,总是优先选择 .bind( this ) 或者一个功能上等价于它的。创建 BoundFunction声明供后续调用,当没有更好的选择时才使用别名。

    // 6.B.1
    function Device( opts ) {
    
      this.value = null;
    
      // 新建一个异步的 stream,这个将被持续调用
      stream.read( opts.path, function( data ) {
    
        // 使用 stream 返回 data 最新的值,更新实例的值
        this.value = data;
    
      }.bind(this) );
    
      // 控制事件触发的频率
      setInterval(function() {
    
        // 发出一个被控制的事件
        this.emit("event");
    
      }.bind(this), opts.freq || 100 );
    }
    
    // 假设我们已继承了事件发送器(EventEmitter) ;)
    

    当不能运行时,等价于 .bind 的功能在多数现代 JavaScript 库中都有提供。

    // 6.B.2
    
    // 示例:lodash/underscore,_.bind()
    function Device( opts ) {
    
      this.value = null;
    
      stream.read( opts.path, _.bind(function( data ) {
    
        this.value = data;
    
      }, this) );
    
      setInterval(_.bind(function() {
    
        this.emit("event");
    
      }, this), opts.freq || 100 );
    }
    
    // 示例:jQuery.proxy
    function Device( opts ) {
    
      this.value = null;
    
      stream.read( opts.path, jQuery.proxy(function( data ) {
    
        this.value = data;
    
      }, this) );
    
      setInterval( jQuery.proxy(function() {
    
        this.emit("event");
    
      }, this), opts.freq || 100 );
    }
    
    // 示例:dojo.hitch
    function Device( opts ) {
    
      this.value = null;
    
      stream.read( opts.path, dojo.hitch( this, function( data ) {
    
        this.value = data;
    
      }) );
    
      setInterval( dojo.hitch( this, function() {
    
        this.emit("event");
    
      }), opts.freq || 100 );
    }
    

    提供一个候选,创建一个 this 的别名,以 self 作为标识符。这很有可能出 bug,应尽可能避免。

    // 6.B.3
    
    function Device( opts ) {
      var self = this;
    
      this.value = null;
    
      stream.read( opts.path, function( data ) {
    
        self.value = data;
    
      });
    
      setInterval(function() {
    
        self.emit("event");
    
      }, opts.freq || 100 );
    }
    

    C. 使用 thisArg

    好几个 ES 5.1 中的原型的方法都内置了一个特殊的 thisArg 标记,尽可能多地使用它

    // 6.C.1
    
    var obj;
    
    obj = { f: "foo", b: "bar", q: "qux" };
    
    Object.keys( obj ).forEach(function( key ) {
    
      // |this| 现在是 `obj`
    
      console.log( this[ key ] );
    
    }, obj ); // <-- 最后的参数是 `thisArg`
    
    // 打印出来...
    
    // "foo"
    // "bar"
    // "qux"
    

    thisArg 在 Array.prototype.every、 Array.prototype.forEach、 Array.prototype.some、 Array.prototype.mapArray.prototype.filter 中都可以使用。

  7. Misc

    这个部分将要说明的想法和理念都并非教条。相反更鼓励对现存实践保持好奇,以尝试提供完成一般 JavaScript 编程任务的更好方案。

    A. 避免使用 switch,现代方法跟踪(method tracing)将会把带有 switch 表达式的函数列为黑名单。

    似乎在最新版本的 Firefox 和 Chrome 都对 switch 语句有重大改进。http://jsperf.com/switch-vs-object-literal-vs-module

    值得注意的是,改进可以这里看到: https://github.com/rwldrn/idiomatic.js/issues/13

    // 7.A.1.1
    // switch 语句示例
    
    switch( foo ) {
      case "alpha":
        alpha();
        break;
      case "beta":
        beta();
        break;
      default:
        // 默认分支
        break;
    }
    
    // 7.A.1.2
    // 一个可支持组合、重用的方法是使用一个对象来存储 “cases”,
    // 使用一个 function 来做委派:
    
    var cases, delegator;
    
    // 返回值仅作说明用
    cases = {
      alpha: function() {
        // 语句
        // 一个返回值
        return [ "Alpha", arguments.length ];
      },
      beta: function() {
        // 语句
        // 一个返回值
        return [ "Beta", arguments.length ];
      },
      _default: function() {
        // 语句
        // 一个返回值
        return [ "Default", arguments.length ];
      }
    };
    
    delegator = function() {
      var args, key, delegate;
    
      // 把 `argument` 转换成数组
      args = [].slice.call( arguments );
    
      // 从 `argument` 中抽出最前一个值
      key = args.shift();
    
      // 调用默认分支
      delegate = cases._default;
    
      // 从对象中对方法进行委派操作
      if ( cases.hasOwnProperty( key ) ) {
        delegate = cases[ key ];
      }
    
      // arg 的作用域可以设置成特定值,
      // 这种情况下,|null| 就可以了
      return delegate.apply( null, args );
    };
    
    // 7.A.1.3
    // 使用 7.A.1.2 中的 API:
    
    delegator( "alpha", 1, 2, 3, 4, 5 );
    // [ "Alpha", 5 ]
    
    // 当然 `case` key 的值可以轻松地换成任意值
    
    var caseKey, someUserInput;
    
    // 有没有可能是某种形式的输入?
    someUserInput = 9;
    
    if ( someUserInput > 10 ) {
      caseKey = "alpha";
    } else {
      caseKey = "beta";
    }
    
    // 或者...
    
    caseKey = someUserInput > 10 ? "alpha" : "beta";
    
    // 然后...
    
    delegator( caseKey, someUserInput );
    // [ "Beta", 1 ]
    
    // 当然还可以这样搞...
    
    delegator();
    // [ "Default", 0 ]
    
    

    B. 提前返回值提升代码的可读性并且没有太多性能上的差别

    // 7.B.1.1
    // 不好:
    function returnLate( foo ) {
      var ret;
    
      if ( foo ) {
        ret = "foo";
      } else {
        ret = "quux";
      }
      return ret;
    }
    
    // 好:
    
    function returnEarly( foo ) {
    
      if ( foo ) {
        return "foo";
      }
      return "quux";
    }
    
  8. 原生 & 宿主对象(注:其实一直觉得 Host Objects 真不应该翻译过来,这是就按一般书的写法翻出来吧)

    最基本的原则是:

    不要干任何蠢事,事情总会变好的。

    为了加强这个观念,请观看这个演示:

    “一切都被允许: 原生扩展” by Andrew Dupont (JSConf2011, Portland, Oregon)

    http://www.everytalk.tv/talks/441-JSConf-Everything-is-Permitted-Extending-Built-ins

  9. 注释

    • 单行注释放于代码上方为首选
    • 多行也可以
    • 行末注释应被避免!
    • JSDoc 的方式也不错,但需要比较多的时间
posted @ 2016-01-16 21:06  西域一支  阅读(322)  评论(0编辑  收藏  举报