JavaScript没有其他很多编程语言中的块定义,所以需要在某些情况下定义变量,我们今天就说一下关于JavaScript的规范问题。
JavaScript 规范


全局命名空间污染与 IIFE


总是将代码包裹成一个 IIFE(Immediately-Invoked Function Expression),用以创建独立隔绝的定义域。这一举措可防止全局命名空间被污染。


IIFE 还可确保你的代码不会轻易被其它全局命名空间里的代码所修改(i.e. 第三方库,window 引用,被覆盖的未定义的关键字等等)。


不推荐


  1. var x = 10,
  2.     y = 100;

  3. // Declaring variables in the global scope is resulting in global scope pollution. All variables declared like this
  4. // will be stored in the window object. This is very unclean and needs to be avoided.
  5. console.log(window.x + ' ' + window.y);
复制代码


推荐


  1. // We declare a IIFE and pass parameters into the function that we will use from the global space
  2. (function(log, w, undefined){
  3.   'use strict';

  4.   var x = 10,
  5.       y = 100;

  6.   // Will output 'true true'
  7.   log((w.x === undefined) + ' ' + (w.y === undefined));

  8. }(window.console.log, window));
复制代码


IIFE(立即执行的函数表达式)


无论何时,想要创建一个新的封闭的定义域,那就用 IIFE。它不仅避免了干扰,也使得内存在执行完后立即释放。


所有脚本文件建议都从 IIFE 开始。


立即执行的函数表达式的执行括号应该写在外包括号内。虽然写在内还是写在外都是有效的,但写在内使得整个表达式看起来更像一个整体,因此推荐这么做。


不推荐


  1. (function(){})();
复制代码


推荐


  1. (function(){}());
复制代码


so,用下列写法来格式化你的 IIFE 代码:


  1. (function(){
  2.   'use strict';

  3.   // Code goes here

  4. }());
复制代码


如果你想引用全局变量或者是外层 IIFE 的变量,可以通过下列方式传参:


  1. (function($, w, d){
  2.   'use strict';

  3.   $(function() {
  4.     w.alert(d.querySelectorAll('div').length);
  5.   });
  6. }(jQuery, window, document));
复制代码


严格模式


ECMAScript 5 严格模式可在整个脚本或独个方法内被激活。它对应不同的 javascript 语境会做更加严格的错误检查。严格模式也确保了 javascript 代码更加的健壮,运行的也更加快速。


严格模式会阻止使用在未来很可能被引入的预留关键字。


你应该在你的脚本中启用严格模式,最好是在独立的 IIFE 中应用它。避免在你的脚本第一行使用它而导致你的所有脚本都启动了严格模式,这有可能会引发一些第三方类库的问题。


不推荐


  1. // Script starts here
  2. 'use strict';

  3. (function(){

  4.   // Your code starts here

  5. }());
  6. 推荐

  7. (function(){
  8.   'use strict';

  9.   // Your code starts here

  10. }());
复制代码


变量声明


总是使用 var 来声明变量。如不指定 var,变量将被隐式地声明为全局变量,这将对变量难以控制。如果没有声明,变量处于什么定义域就变得不清(可以是在 Document 或 Window 中,也可以很容易地进入本地定义域)。所以,请总是使用 var 来声明变量。


采用严格模式带来的好处是,当你手误输入错误的变量名时,它可以通过报错信息来帮助你定位错误出处。


不推


  1. x = 10;
  2. y = 100;
复制代码


推荐


  1. var x = 10,
  2.     y = 100;
复制代码


理解 JavaScript 的定义域和定义域提升


在 JavaScript 中变量和方法定义会自动提升到执行之前。JavaScript 只有 function 级的定义域,而无其他很多编程语言中的块定义域,所以使得你在某一 function 内的某语句和循环体中定义了一个变量,此变量可作用于整个 function 内,而不仅仅是在此语句或循环体中,因为它们的声明被 JavaScript 自动提升了。


我们通过例子来看清楚这到底是怎么一回事:


原 function


  1. (function(log){
  2.   'use strict';

  3.   var a = 10;

  4.   for(var i = 0; i < a; i++) {
  5.     var b = i * i;
  6.     log(b);
  7.   }

  8.   if(a === 10) {
  9.     var f = function() {
  10.       log(a);
  11.     };
  12.     f();
  13.   }

  14.   function x() {
  15.     log('Mr. X!');
  16.   }
  17.   x();

  18. }(window.console.log));
复制代码


被 JS 提升过后


  1. (function(log){
  2.   'use strict';
  3.   // All variables used in the closure will be hoisted to the top of the function
  4.   var a,
  5.       i,
  6.       b,
  7.       f;
  8.   // All functions in the closure will be hoisted to the top
  9.   function x() {
  10.     log('Mr. X!');
  11.   }

  12.   a = 10;

  13.   for(i = 0; i < a; i++) {
  14.     b = i * i;
  15.     log(b);
  16.   }

  17.   if(a === 10) {
  18.     // Function assignments will only result in hoisted variables but the function body will not be hoisted
  19.     // Only by using a real function declaration the whole function will be hoisted with its body
  20.     f = function() {
  21.       log(a);
  22.     };
  23.     f();
  24.   }

  25.   x();

  26. }(window.console.log));
复制代码


根据以上提升过程,你是否可理解以下代码?


有效代码


  1. (function(log){
  2.   'use strict';

  3.   var a = 10;

  4.   i = 5;

  5.   x();

  6.   for(var i; i < a; i++) {
  7.     log(b);
  8.     var b = i * i;
  9.   }

  10.   if(a === 10) {
  11.     f = function() {
  12.       log(a);
  13.     };
  14.     f();

  15.     var f;
  16.   }

  17.   function x() {
  18.     log('Mr. X!');
  19.   }

  20. }(window.console.log));
复制代码


正如你所看到的这段令人充满困惑与误解的代码导致了出人意料的结果。只有良好的声明习惯,也就是下一章节我们要提到的声明规则,才能尽可能的避免这类错误风险。


提升声明


为避免上一章节所述的变量和方法定义被自动提升造成误解,把风险降到最低,我们应该手动地显示地去声明变量与方法。也就是说,所有的变量以及方法,应当定义在 function 内的首行。


只用一个 var 关键字声明,多个变量用逗号隔开。


不推荐


  1. (function(log){
  2.   'use strict';

  3.   var a = 10;
  4.   var b = 10;

  5.   for(var i = 0; i < 10; i++) {
  6.     var c = a * b * i;
  7.   }

  8.   function f() {

  9.   }

  10.   var d = 100;
  11.   var x = function() {
  12.     return d * d;
  13.   };
  14.   log(x());

  15. }(window.console.log));
复制代码


推荐


  1. (function(log){
  2.   'use strict';

  3.   var a = 10,
  4.       b = 10,
  5.       i,
  6.       c,
  7.       d,
  8.       x;

  9.   function f() {

  10.   }

  11.   for(i = 0; i < 10; i++) {
  12.     c = a * b * i;
  13.   }



  14.   d = 100;
  15.   x = function() {
  16.     return d * d;
  17.   };
  18.   log(x());

  19. }(window.console.log));
复制代码


把赋值尽量写在变量申明中。


不推荐


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

  4. a = 10;
  5. b = 10;
  6. c = 100;
复制代码


推荐


  1. var a = 10,
  2.     b = 10,
  3.     c = 100;
复制代码


总是使用带类型判断的比较判断


总是使用 === 精确的比较操作符,避免在判断的过程中,由 JavaScript 的强制类型转换所造成的困扰。


如果你使用 === 操作符,那比较的双方必须是同一类型为前提的条件下才会有效。


如果你想了解更多关于强制类型转换的信息,你可以读一读 Dmitry Soshnikov 的这篇文章。


在只使用 == 的情况下,JavaScript 所带来的强制类型转换使得判断结果跟踪变得复杂,下面的例子可以看出这样的结果有多怪了:


  1. (function(log){
  2.   'use strict';

  3.   log('0' == 0); // true
  4.   log('' == false); // true
  5.   log('1' == true); // true
  6.   log(null == undefined); // true

  7.   var x = {
  8.     valueOf: function() {
  9.       return 'X';
  10.     }
  11.   };

  12.   log(x == 'X');

  13. }(window.console.log));
复制代码


明智地使用真假判断


当我们在一个 if 条件语句中使用变量或表达式时,会做真假判断。if(a == true) 是不同于 if(a) 的。后者的判断比较特殊,我们称其为真假判断。这种判断会通过特殊的操作将其转换为 true 或 false,下列表达式统统返回 false:false, 0, undefined, null, NaN, ''(空字符串).


这种真假判断在我们只求结果而不关心过程的情况下,非常的有帮助。


以下示例展示了真假判断是如何工作的:


  1. (function(log){
  2.   'use strict';

  3.   function logTruthyFalsy(expr) {
  4.     if(expr) {
  5.       log('truthy');
  6.     } else {
  7.       log('falsy');
  8.     }
  9.   }

  10.   logTruthyFalsy(true); // truthy
  11.   logTruthyFalsy(1); // truthy
  12.   logTruthyFalsy({}); // truthy
  13.   logTruthyFalsy([]); // truthy
  14.   logTruthyFalsy('0'); // truthy

  15.   logTruthyFalsy(false); // falsy
  16.   logTruthyFalsy(0); // falsy
  17.   logTruthyFalsy(undefined); // falsy
  18.   logTruthyFalsy(null); // falsy
  19.   logTruthyFalsy(NaN); // falsy
  20.   logTruthyFalsy(''); // falsy

  21. }(window.console.log));
复制代码


变量赋值时的逻辑操作


逻辑操作符 || 和 && 也可被用来返回布尔值。如果操作对象为非布尔对象,那每个表达式将会被自左向右地做真假判断。基于此操作,最终总有一个表达式被返回回来。这在变量赋值时,是可以用来简化你的代码的。


不推荐


  1. if(!x) {
  2.   if(!y) {
  3.     x = 1;
  4.   } else {
  5.     x = y;
  6.   }
  7. }
复制代码


推荐


  1. x = x || y || 1;
复制代码


这一小技巧经常用来给方法设定默认的参数。


  1. (function(log){
  2.   'use strict';

  3.   function multiply(a, b) {
  4.     a = a || 1;
  5.     b = b || 1;

  6.     log('Result ' + a * b);
  7.   }

  8.   multiply(); // Result 1
  9.   multiply(10); // Result 10
  10.   multiply(3, NaN); // Result 3
  11.   multiply(9, 5); // Result 45

  12. }(window.console.log));
复制代码


分号


总是使用分号,因为隐式的代码嵌套会引发难以察觉的问题。当然我们更要从根本上来杜绝这些问题[1] 。以下几个示例展示了缺少分号的危害:


  1. // 1.
  2. MyClass.prototype.myMethod = function() {
  3.   return 42;
  4. }  // No semicolon here.

  5. (function() {
  6.   // Some initialization code wrapped in a function to create a scope for locals.
  7. })();


  8. var x = {
  9.   'i': 1,
  10.   'j': 2
  11. }  // No semicolon here.

  12. // 2.  Trying to do one thing on Internet Explorer and another on Firefox.
  13. // I know you'd never write code like this, but throw me a bone.
  14. [ffVersion, ieVersion][isIE]();


  15. var THINGS_TO_EAT = [apples, oysters, sprayOnCheese]  // No semicolon here.

  16. // 3. conditional execution a la bash
  17. -1 == resultOfOperation() || die();
复制代码


So what happens?


JavaScript 错误 —— 首先返回 42 的那个 function 被第二个 function 当中参数传入调用,接着数字 42 也被“调用”而导致出错。
八成你会得到 ‘no such property in undefined’ 的错误提示,因为在真实环境中的调用是这个样子:x[ffVersion, ieVersion][isIE]().
die 总是被调用。因为数组减 1 的结果是 NaN,它不等于任何东西(无论 resultOfOperation 是否返回 NaN)。所以最终的结果是die() 执行完所获得值将赋给 THINGS_TO_EAT.


Why?


JavaScript 中语句要以分号结束,否则它将会继续执行下去,不管换不换行。以上的每一个示例中,函数声明或对象或数组,都变成了在一句语句体内。要知道闭合圆括号并不代表语句结束,JavaScript 不会终结语句,除非它的下一个 token 是一个中缀符[2] 或者是圆括号操作符。


这真是让人大吃一惊,所以乖乖地给语句末加上分号吧。


澄清:分号与函数


分号需要用在表达式的结尾,而并非函数声明的结尾。区分它们最好的例子是:


  1. var foo = function() {
  2.   return true;
  3. };  // semicolon here.

  4. function foo() {
  5.   return true;
  6. }  // no semicolon here.
复制代码


嵌套函数


嵌套函数是非常有用的,比如用在持续创建和隐藏辅助函数的任务中。你可以非常自由随意地使用它们。


语句块内的函数声明


切勿在语句块内声明函数,在 ECMAScript 5 的严格模式下,这是不合法的。函数声明应该在定义域的顶层。但在语句块内可将函数申明转化为函数表达式赋值给变量。


不推荐


  1. if (x) {
  2.   function foo() {}
  3. }
复制代码


推荐


  1. if (x) {
  2.   var foo = function() {};
  3. }
复制代码


异常


基本上你无法避免出现异常,特别是在做大型开发时(使用应用开发框架等等)。


在没有自定义异常的情况下,从有返回值的函数中返回错误信息一定非常的棘手,更别提多不优雅了。不好的解决方案包括了传第一个引用类型来接纳错误信息,或总是返回一个对象列表,其中包含着可能的错误对象。以上方式基本上是比较简陋的异常处理方式。适时可做自定义异常处理。


在复杂的环境中,你可以考虑抛出对象而不仅仅是字符串(默认的抛出值)。


  1. if(name === undefined) {
  2.   throw {
  3.     name: 'System Error',
  4.     message: 'A name should always be specified!'
  5.   }
  6. }
复制代码


标准特性


总是优先考虑使用标准特性。为了最大限度地保证扩展性与兼容性,总是首选标准的特性,而不是非标准的特性(例如:首选string.charAt(3) 而不是 string[3];首选 DOM 的操作方法来获得元素引用,而不是某一应用特定的快捷方法)。


简易的原型继承


如果你想在 JavaScript 中继承你的对象,请遵循一个简易的模式来创建此继承。如果你预计你会遇上复杂对象的继承,那可以考虑采用一个继承库,比如 Proto.js by Axel Rauschmayer.


简易继承请用以下方式:


  1. (function(log){
  2.   'use strict';

  3.   // Constructor function
  4.   function Apple(name) {
  5.     this.name = name;
  6.   }
  7.   // Defining a method of apple
  8.   Apple.prototype.eat = function() {
  9.     log('Eating ' + this.name);
  10.   };

  11.   // Constructor function
  12.   function GrannySmithApple() {
  13.     // Invoking parent constructor
  14.     Apple.prototype.constructor.call(this, 'Granny Smith');
  15.   }
  16.   // Set parent prototype while creating a copy with Object.create
  17.   GrannySmithApple.prototype = Object.create(Apple.prototype);
  18.   // Set constructor to the sub type, otherwise points to Apple
  19.   GrannySmithApple.prototype.constructor = GrannySmithApple;

  20.   // Calling a super method
  21.   GrannySmithApple.prototype.eat = function() {
  22.     // Be sure to apply it onto our current object with call(this)
  23.     Apple.prototype.eat.call(this);

  24.     log('Poor Grany Smith');
  25.   };

  26.   // Instantiation
  27.   var apple = new Apple('Test Apple');
  28.   var grannyApple = new GrannySmithApple();

  29.   log(apple.name); // Test Apple
  30.   log(grannyApple.name); // Granny Smith

  31.   // Instance checks
  32.   log(apple instanceof Apple); // true
  33.   log(apple instanceof GrannySmithApple); // false

  34.   log(grannyApple instanceof Apple); // true
  35.   log(grannyApple instanceof GrannySmithApple); // true

  36.   // Calling method that calls super method
  37.   grannyApple.eat(); // Eating Granny Smith\nPoor Grany Smith

  38. }(window.console.log));
复制代码


使用闭包


闭包的创建也许是 JS 最有用也是最易被忽略的能力了。关于闭包如何工作的合理解释。


切勿在循环中创建函数


在简单的循环语句中加入函数是非常容易形成闭包而带来隐患的。下面的例子就是一个典型的陷阱:


不推荐


  1. (function(log, w){
  2.   'use strict';

  3.   // numbers and i is defined in the current function closure
  4.   var numbers = [1, 2, 3],
  5.       i;

  6.   for(i = 0; i < numbers.length; i++) {
  7.     w.setTimeout(function() {
  8.       // At the moment when this gets executed the i variable, coming from the outer function scope
  9.       // is set to 3 and the current program is alerting the message 3 times
  10.       // 'Index 3 with number undefined
  11.       // If you understand closures in javascript you know how to deal with those cases
  12.       // It's best to just avoid functions / new closures in loops as this prevents those issues

  13.       w.alert('Index ' + i + ' with number ' + numbers[i]);
  14.     }, 0);
  15.   }

  16. }(window.console.log, window));
复制代码


接下来的改进虽然已经解决了上述例子中的问题或 bug,但还是违反了不在循环中创建函数或闭包的原则。


不推荐


  1. (function(log, w){
  2.   'use strict';

  3.   // numbers and i is defined in the current function closure
  4.   var numbers = [1, 2, 3],
  5.       i;

  6.   for(i = 0; i < numbers.length; i++) {
  7.     // Creating a new closure scope with an IIFE solves the problem
  8.     // The delayed function will use index and number which are
  9.     // in their own closure scope (one closure per loop iteration).
  10.     // ---
  11.     // Still this is not recommended as we violate our rule to not
  12.     // create functions within loops and we are creating two!

  13.     (function(index, number){
  14.       w.setTimeout(function() {
  15.         // Will output as expected 0 > 1, 1 > 2, 2 > 3
  16.         w.alert('Index ' + index + ' with number ' + number);
  17.       }, 0);
  18.     }(i, numbers[i]));
  19.   }

  20. }(window.console.log, window));
复制代码


接下来的改进已解决问题,而且也遵循了规范。可是,你会发现看上去似乎过于复杂繁冗了,应该会有更好的解决方案吧。


不完全推荐


  1. (function(log, w){
  2.   'use strict';

  3.   // numbers and i is defined in the current function closure
  4.   var numbers = [1, 2, 3],
  5.       i;

  6.   // Create a function outside of the loop that will accept arguments to create a
  7.   // function closure scope. This function will return a function that executes in this
  8.   // closure parent scope.
  9.   function alertIndexWithNumber(index, number) {
  10.     return function() {
  11.       w.alert('Index ' + index + ' with number ' + number);
  12.     };
  13.   }

  14.   // First parameter is a function call that returns a function.
  15.   // ---
  16.   // This solves our problem and we don't create a function inside our loop
  17.   for(i = 0; i < numbers.length; i++) {
  18.     w.setTimeout(alertIndexWithNumber(i, numbers[i]), 0);
  19.   }

  20. }(window.console.log, window));
复制代码


将循环语句转换为函数执行的方式问题能得到立马解决,每一次循环都会对应地创建一次闭包。函数式的风格更加值得推荐,而且看上去也更加地自然和可预料。


推荐


  1. (function(log, w){
  2.   'use strict';

  3.   // numbers and i is defined in the current function closure
  4.   var numbers = [1, 2, 3],
  5.       i;

  6.   numbers.forEach(function(number, index) {
  7.     w.setTimeout(function() {
  8.       w.alert('Index ' + index + ' with number ' + number);
  9.     }, 0);
  10.   });

  11. }(window.console.log, window));
复制代码


eval 函数(魔鬼)


eval() 不但混淆语境还很危险,总会有比这更好、更清晰、更安全的另一种方案来写你的代码,因此尽量不要使用 evil 函数。


this 关键字


只在对象构造器、方法和在设定的闭包中使用 this 关键字。this 的语义在此有些误导。它时而指向全局对象(大多数时),时而指向调用者的定义域(在 eval 中),时而指向 DOM 树中的某一节点(当用事件处理绑定到 HTML 属性上时),时而指向一个新创建的对象(在构造器中),还时而指向其它的一些对象(如果函数被 call() 和 apply() 执行和调用时)。


正因为它是如此容易地被搞错,请限制它的使用场景:


  • 在构造函数中
  • 在对象的方法中(包括由此创建出的闭包内)

首选函数式风格



函数式编程让你可以简化代码并缩减维护成本,因为它容易复用,又适当地解耦和更少的依赖。


接下来的例子中,在一组数字求和的同一问题上,比较了两种解决方案。第一个例子是经典的程序处理,而第二个例子则是采用了函数式编程和 ECMA Script 5.1 的数组方法。


例外:往往在重代码性能轻代码维护的情况之下,要选择最优性能的解决方案而非维护性高的方案(比如用简单的循环语句代替 forEach)。


不推荐


  1. (function(log){
  2.   'use strict';

  3.   var arr = [10, 3, 7, 9, 100, 20],
  4.       sum = 0,
  5.       i;


  6.   for(i = 0; i < arr.length; i++) {
  7.     sum += arr[i];
  8.   }

  9.   log('The sum of array ' + arr + ' is: ' + sum)

  10. }(window.console.log));
复制代码


推荐


  1. (function(log){
  2.   'use strict';

  3.   var arr = [10, 3, 7, 9, 100, 20];

  4.   var sum = arr.reduce(function(prevValue, currentValue) {
  5.     return prevValue + currentValue;
  6.   }, 0);

  7.   log('The sum of array ' + arr + ' is: ' + sum);

  8. }(window.console.log));
复制代码


另一个例子通过某一规则对一个数组进行过滤匹配来创建一个新的数组。


不推荐


  1. (function(log){
  2.   'use strict';

  3.   var numbers = [11, 3, 7, 9, 100, 20, 14, 10],
  4.       numbersGreaterTen = [],
  5.       i;


  6.   for(i = 0; i < numbers.length; i++) {
  7.     if(numbers[i] > 10) {
  8.       numbersGreaterTen.push(numbers[i]);
  9.     }
  10.   }

  11.   log('From the list of numbers ' + numbers + ' only ' + numbersGreaterTen + ' are greater than ten');

  12. }(window.console.log));
复制代码


推荐


  1. (function(log){
  2.   'use strict';

  3.   var numbers = [11, 3, 7, 9, 100, 20, 14, 10];

  4.   var numbersGreaterTen = numbers.filter(function(element) {
  5.     return element > 10;
  6.   });

  7.   log('From the list of numbers ' + numbers + ' only ' + numbersGreaterTen + ' are greater than ten');

  8. }(window.console.log));
复制代码


使用 ECMA Script 5


建议使用 ECMA Script 5 中新增的语法糖和函数。这将简化你的程序,并让你的代码更加灵活和可复用。


数组和对象的属性迭代


用 ECMA5 的迭代方法来迭代数组。使用 Array.forEach 或者如果你要在特殊场合下中断迭代,那就用 Array.every。


  1. (function(log){
  2.   'use strict';

  3.   // Iterate over an array and break at a certain condition
  4.   [1, 2, 3, 4, 5].every(function(element, index, arr) {
  5.     log(element + ' at index ' + index + ' in array ' + arr);

  6.     if(index !== 5) {
  7.       return true;
  8.     }
  9.   });

  10.   // Defining a simple javascript object
  11.   var obj = {
  12.     a: 'A',
  13.     b: 'B',
  14.     'c-d-e': 'CDE'
  15.   };

0