有关前端编码规范JavaScript 篇
JavaScript 与许多其他编程语言不同,它没有块级作用域的定义。因此,在某些情况下,变量的定义需要特别注意。本文将详细介绍 JavaScript 的编码规范。
全局命名空间污染与 IIFE
原理
总是将代码包裹在一个立即执行函数表达式(IIFE,Immediately-Invoked Function Expression)中,以此创建独立的作用域。这样做可以防止全局命名空间被污染,同时确保代码不会轻易被其他全局命名空间中的代码修改(如第三方库、window 对象引用、被覆盖的未定义关键字等)。
示例
不推荐的做法
var x = 10,
y = 100;
// 全局作用域中声明变量会导致全局作用域污染。所有这样声明的变量都会存储在 window 对象中,这种做法不规范,应避免。
console.log(window.x + ' ' + window.y);
推荐的做法
// 声明一个 IIFE,并将需要从全局空间使用的参数传入函数
(function(log, w, undefined){
'use strict';
var x = 10,
y = 100;
// 输出 'true true'
log((w.x === undefined) + ' ' + (w.y === undefined));
}(window.console.log, window));
IIFE(立即执行的函数表达式)
使用场景
无论何时需要创建一个新的封闭作用域,都可以使用 IIFE。它不仅能避免干扰,还能在执行完毕后立即释放内存。建议所有脚本文件都以 IIFE 开头。
格式规范
立即执行的函数表达式的执行括号应写在外层括号内。虽然写在内层或外层都是有效的,但写在内层会使整个表达式看起来更像一个整体,因此推荐这种写法。
不推荐的写法
(function(){})();
推荐的写法
(function(){}());
格式化示例
(function(){
'use strict';
// 代码写在这里
}());
引用全局变量或外层 IIFE 变量的方法
(function($, w, d){
'use strict';
$(function() {
w.alert(d.querySelectorAll('div').length);
});
}(jQuery, window, document));
严格模式
开启方式与优势
ECMAScript 5 的严格模式可以在整个脚本或单个方法内启用。它会在不同的 JavaScript 上下文中进行更严格的错误检查,确保代码更加健壮,运行速度更快。同时,严格模式会阻止使用未来可能引入的保留关键字。
使用建议
应该在脚本中启用严格模式,最好是在独立的 IIFE 中应用它。避免在脚本第一行使用严格模式,以免所有脚本都开启严格模式,从而可能引发一些第三方类库的问题。
不推荐的写法
// 脚本从这里开始
'use strict';
(function(){
// 代码从这里开始
}());
推荐的写法
(function(){
'use strict';
// 代码从这里开始
}());
变量声明
声明方式
总是使用 var 来声明变量。如果不指定 var,变量将被隐式声明为全局变量,这会使变量难以控制。在未声明的情况下,变量的作用域不明确(可能在 Document 或 Window 中,也可能很容易进入本地作用域)。因此,一定要使用 var 声明变量。
严格模式的好处
采用严格模式时,如果手误输入错误的变量名,它会通过报错信息帮助定位错误出处。
不推荐的写法
x = 10;
y = 100;
推荐的写法
var x = 10,
y = 100;
理解 JavaScript 的作用域和变量提升
变量提升机制
在 JavaScript 中,变量和函数定义会自动提升到执行之前。JavaScript 只有函数级作用域,没有其他许多编程语言中的块级作用域。因此,在函数内的某条语句或循环体中定义的变量,其作用域是整个函数,而不仅仅是该语句或循环体,因为它们的声明被 JavaScript 自动提升了。
示例分析
原函数
(function(log){
'use strict';
var a = 10;
for(var i = 0; i < a; i++) {
var b = i * i;
log(b);
}
if(a === 10) {
var f = function() {
log(a);
};
f();
}
function x() {
log('Mr. X!');
}
x();
}(window.console.log));
被 JS 提升过后
(function(log){
'use strict';
// 闭包中使用的所有变量都会被提升到函数顶部
var a,
i,
b,
f;
// 闭包中的所有函数都会被提升到顶部
function x() {
log('Mr. X!');
}
a = 10;
for(i = 0; i < a; i++) {
b = i * i;
log(b);
}
if(a === 10) {
// 函数赋值只会提升变量,函数体不会被提升
// 只有使用真正的函数声明,整个函数及其函数体才会被提升
f = function() {
log(a);
};
f();
}
x();
}(window.console.log));
理解测试
(function(log){
'use strict';
var a = 10;
i = 5;
x();
for(var i; i < a; i++) {
log(b);
var b = i * i;
}
if(a === 10) {
f = function() {
log(a);
};
f();
var f;
}
function x() {
log('Mr. X!');
}
}(window.console.log));
这段令人困惑的代码会导致意外的结果。只有养成良好的声明习惯,遵循后续提到的声明规则,才能尽可能避免这类错误。
提升声明
声明原则
为避免变量和函数定义自动提升带来的误解,降低风险,应手动显式声明变量和函数。所有变量和函数应定义在函数的首行,使用一个 var 关键字声明多个变量,用逗号隔开。
赋值建议
尽量在变量声明时进行赋值。
不推荐的写法
(function(log){
'use strict';
var a = 10;
var b = 10;
for(var i = 0; i < 10; i++) {
var c = a * b * i;
}
function f() {
}
var d = 100;
var x = function() {
return d * d;
};
log(x());
}(window.console.log));
推荐的写法
(function(log){
'use strict';
var a = 10,
b = 10,
i,
c,
d,
x;
function f() {
}
for(i = 0; i < 10; i++) {
c = a * b * i;
}
d = 100;
x = function() {
return d * d;
};
log(x());
}(window.console.log));
赋值示例
不推荐的写法
var a,
b,
c;
a = 10;
b = 10;
c = 100;
推荐的写法
var a = 10,
b = 10,
c = 100;
总是使用带类型判断的比较判断
比较操作符选择
总是使用 === 精确比较操作符,避免在判断过程中因 JavaScript 的强制类型转换而造成困扰。使用 === 操作符时,比较的双方必须是同一类型才会有效。
强制类型转换问题
如果只使用 ==,JavaScript 的强制类型转换会使判断结果的跟踪变得复杂,以下示例展示了这种结果的怪异之处:
(function(log){
'use strict';
log('0' == 0); // true
log('' == false); // true
log('1' == true); // true
log(null == undefined); // true
var x = {
valueOf: function() {
return 'X';
}
};
log(x == 'X');
}(window.console.log));
参考资料
如果想了解更多关于强制类型转换的信息,可以阅读 Dmitry Soshnikov 的相关文章。
明智地使用真假判断
真假判断机制
在 if 条件语句中使用变量或表达式时,会进行真假判断。if(a == true) 与 if(a) 不同,后者是特殊的真假判断。这种判断会通过特殊操作将其转换为 true 或 false,以下表达式都会返回 false:false、0、undefined、null、NaN、''(空字符串)。
示例展示
(function(log){
'use strict';
function logTruthyFalsy(expr) {
if(expr) {
log('truthy');
} else {
log('falsy');
}
}
logTruthyFalsy(true); // truthy
logTruthyFalsy(1); // truthy
logTruthyFalsy({}); // truthy
logTruthyFalsy([]); // truthy
logTruthyFalsy('0'); // truthy
logTruthyFalsy(false); // falsy
logTruthyFalsy(0); // falsy
logTruthyFalsy(undefined); // falsy
logTruthyFalsy(null); // falsy
logTruthyFalsy(NaN); // falsy
logTruthyFalsy(''); // falsy
}(window.console.log));
变量赋值时的逻辑操作
逻辑操作符特性
逻辑操作符 || 和 && 也可用于返回布尔值。如果操作对象为非布尔对象,每个表达式会从左到右进行真假判断,最终会返回一个表达式。这可以在变量赋值时简化代码。
简化示例
不推荐的写法
if(!x) {
if(!y) {
x = 1;
} else {
x = y;
}
}
推荐的写法
x = x || y || 1;
默认参数设置
这种技巧常用于为方法设置默认参数:
(function(log){
'use strict';
function multiply(a, b) {
a = a || 1;
b = b || 1;
log('Result ' + a * b);
}
multiply(); // Result 1
multiply(10); // Result 10
multiply(3, NaN); // Result 3
multiply(9, 5); // Result 45
}(window.console.log));
分号
使用必要性
总是使用分号,因为隐式的代码嵌套可能会引发难以察觉的问题,我们应从根本上杜绝这些问题。以下示例展示了缺少分号的危害:
// 1.
MyClass.prototype.myMethod = function() {
return 42;
} // 此处没有分号
(function() {
// 一些初始化代码包裹在函数中,为局部变量创建作用域
})();
var x = {
'i': 1,
'j': 2
} // 此处没有分号
// 2. 尝试在 Internet Explorer 和 Firefox 上执行不同的操作
// 我知道你不会这样写代码,但举个例子
[ffVersion, ieVersion][isIE]();
var THINGS_TO_EAT = [apples, oysters, sprayOnCheese] // 此处没有分号
// 3. 类似 bash 的条件执行
-1 == resultOfOperation() || die();
错误原因分析
- JavaScript 中语句应以分号结束,否则会继续执行,不管是否换行。上述示例中,函数声明、对象或数组都在同一句语句体内。闭合圆括号并不代表语句结束,JavaScript 不会终结语句,除非下一个标记是中缀符或圆括号操作符。
- 第一个例子中,返回
42的函数会被第二个函数作为参数传入调用,接着数字42也会被“调用”,从而导致出错。在真实环境中,可能会得到'no such property in undefined'的错误提示,如x[ffVersion, ieVersion][isIE]()。 - 第三个例子中,
die总是被调用。因为数组减 1 的结果是NaN,它不等于任何东西(无论resultOfOperation是否返回NaN),所以最终die()执行完所获得的值将赋给THINGS_TO_EAT。
总结
为避免这些问题,应在语句末尾加上分号。
分号与函数的区别
分号用于表达式结尾,而非函数声明结尾,以下示例可清晰区分:
var foo = function() {
return true;
}; // 此处需要分号
function foo() {
return true;
} // 此处不需要分号
嵌套函数
用途
嵌套函数非常有用,可用于持续创建和隐藏辅助函数的任务,可以自由使用。
语句块内函数声明规则
切勿在语句块内声明函数,在 ECMAScript 5 的严格模式下,这是不合法的。函数声明应在作用域的顶层,但在语句块内可将函数声明转换为函数表达式赋值给变量。
不推荐的写法
if (x) {
function foo() {}
}
推荐的写法
if (x) {
var foo = function() {};
}
异常
错误返回问题
在没有自定义异常的情况下,从有返回值的函数中返回错误信息既棘手又不优雅。不好的解决方案包括传递第一个引用类型来接收错误信息,或总是返回一个包含可能错误对象的对象列表,这些基本上是简陋的异常处理方式,适时可进行自定义异常处理。
复杂环境处理
在复杂环境中,可以考虑抛出对象而不仅仅是字符串(默认的抛出值),示例如下:
if(name === undefined) {
throw {
name: 'System Error',
message: 'A name should always be specified!'
}
}
标准特性
使用原则
总是优先考虑使用标准特性,为了最大限度地保证扩展性与兼容性,首选标准特性,而非非标准特性(例如,首选 string.charAt(3) 而不是 string[3];首选 DOM 的操作方法来获得元素引用,而不是某一应用特定的快捷方法)。
简易的原型继承
继承模式
如果想在 JavaScript 中实现对象继承,可以遵循简易模式。如果预计会遇到复杂对象的继承,可以考虑采用继承库,如 Axel Rauschmayer 的 Proto.js。
简易继承示例
(function(log){
'use strict';
// 构造函数
function Apple(name) {
this.name = name;
}
// 定义苹果的方法
Apple.prototype.eat = function() {
log('Eating ' + this.name);
};
// 构造函数
function GrannySmithApple() {
// 调用父构造函数
Apple.prototype.constructor.call(this, 'Granny Smith');
}
// 使用 Object.create 创建副本时设置父原型
GrannySmithApple.prototype = Object.create(Apple.prototype);
// 将构造函数设置为子类型,否则指向 Apple
GrannySmithApple.prototype.constructor = GrannySmithApple;
// 调用父方法
GrannySmithApple.prototype.eat = function() {
// 确保在当前对象上应用它
Apple.prototype.eat.call(this);
log('Poor Grany Smith');
};
// 实例化
var apple = new Apple('Test Apple');
var grannyApple = new GrannySmithApple();
log(apple.name); // Test Apple
log(grannyApple.name); // Granny Smith
// 实例检查
log(apple instanceof Apple); // true
log(apple instanceof GrannySmithApple); // false
log(grannyApple instanceof Apple); // true
log(grannyApple instanceof GrannySmithApple); // true
// 调用调用父方法的方法
grannyApple.eat(); // Eating Granny Smith
// Poor Grany Smith
}(window.console.log));
使用闭包
闭包的重要性
闭包的创建是 JS 最有用但也最易被忽略的能力之一。
循环中创建函数的问题
在简单的循环语句中加入函数很容易形成闭包并带来隐患,以下是典型陷阱示例:
(function(log, w){
'use strict';
// numbers 和 i 在当前函数闭包中定义
var numbers = [1, 2, 3],
i;
for(i = 0; i < numbers.length; i++) {
w.setTimeout(function() {
// 当此函数执行时,来自外部函数作用域的 i 变量已被设置为 3,程序会 3 次弹出消息
// 'Index 3 with number undefined'
// 如果你理解 JavaScript 中的闭包,就知道如何处理这些情况
// 最好避免在循环中创建函数或新闭包,以防止这些问题
w.alert('Index ' + i + ' with number ' + numbers[i]);
}, 0);
}
}(window.console.log, window));
改进方案分析
部分改进但仍有问题的方案
(function(log, w){
'use strict';
// numbers 和 i 在当前函数闭包中定义
var numbers = [1, 2, 3],
i;
for(i = 0; i < numbers.length; i++) {
// 使用 IIFE 创建新的闭包作用域解决问题
// 延迟函数将使用 index 和 number,它们在自己的闭包作用域中(每次循环迭代一个闭包)
// 但这仍然不推荐,因为违反了不在循环中创建函数的规则,而且创建了两个函数
(function(index, number){
w.setTimeout(function() {
// 会按预期输出 0 > 1, 1 > 2, 2 > 3
w.alert('Index ' + index + ' with number ' + number);
}, 0);
}(i, numbers[i]));
}
}(window.console.log, window));
更优的解决方案
(function(log, w){
'use strict';
// numbers 和 i 在当前函数闭包中定义
var numbers = [1, 2, 3],
i;
// 在循环外创建一个函数,接受参数以创建函数闭包作用域
// 该函数将返回一个在该闭包父作用域中执行的函数
function alertIndexWithNumber(index, number) {
return function() {
w.alert('Index ' + index + ' with number ' + number);
};
}
// 第一个参数是一个返回函数的函数调用
// 这解决了问题,并且不在循环内创建函数
for(i = 0; i < numbers.length; i++) {
w.setTimeout(alertIndexWithNumber(i, numbers[i]), 0);
}
}(window.console.log, window));
推荐的函数式风格解决方案
(function(log, w){
'use strict';
// numbers 和 i 在当前函数闭包中定义
var numbers = [1, 2, 3],
i;
numbers.forEach(function(number, index) {
w.setTimeout(function() {
w.alert('Index ' + index + ' with number ' + number);
}, 0);
});
}(window.console.log, window));
eval 函数(魔鬼)
eval() 函数不仅混淆上下文,还很危险,总会有更好、更清晰、更安全的方式来编写代码,因此尽量不要使用该函数。
this 关键字
使用场景限制
this 关键字的语义容易产生误导,它时而指向全局对象(大多数情况),时而指向调用者的作用域(在 eval 中),时而指向 DOM 树中的某一节点(当用事件处理绑定到 HTML 属性上时),时而指向一个新创建的对象(在构造器中),还时而指向其他一些对象(如果函数被 call() 和 apply() 执行和调用时)。为避免混淆,应限制其使用场景:
- 在构造函数中
- 在对象的方法中(包括由此创建出的闭包内)
首选函数式风格
函数式编程的优势
函数式编程可以简化代码,降低维护成本,因为它易于复用,适当解耦且依赖较少。
示例对比
经典程序处理方案
(function(log){
'use strict';
var arr = [10, 3, 7, 9, 100, 20],
sum = 0,
i;
for(i = 0; i < arr.length; i++) {
sum += arr[i];
}
log('The sum of array ' + arr + ' is: ' + sum);
}(window.console.log));
函数式编程方案
(function(log){
'use strict';
var arr = [10, 3, 7, 9, 100, 20];
var sum = arr.reduce(function(prevValue, currentValue) {
return prevValue + currentValue;
}, 0);
log('The sum of array ' + arr + ' is: ' + sum);
}(window.console.log));
数组过滤示例
经典方案
(function(log){
'use strict';
var numbers = [11, 3, 7, 9, 100, 20, 14, 10],
numbersGreaterTen = [],
i;
for(i = 0; i < numbers.length; i++) {
if(numbers[i] > 10) {
numbersGreaterTen.push(numbers[i]);
}
}
log('From the list of numbers ' + numbers + ' only ' + numbersGreaterTen + ' are greater than ten');
}(window.console.log));
函数式编程方案
(function(log){
'use strict';
var numbers = [11, 3, 7, 9, 100, 20, 14, 10];
var numbersGreaterTen = numbers.filter(function(element) {
return element > 10;
});
log('From the list of numbers ' + numbers + ' only ' + numbersGreaterTen + ' are greater than ten');
}(window.console.log));
性能与维护权衡
在重代码性能轻代码维护的情况下,应选择最优性能的解决方案(如用简单的循环语句代替 forEach)。
使用 ECMA Script 5
优势
建议使用 ECMA Script 5 中新增的语法糖和函数,这将简化程序,使代码更加灵活和可复用。
数组和对象属性迭代
使用 ECMA5 的迭代方法迭代数组,可使用 Array.forEach,如果需要在特殊场合下中断迭代,则使用 Array.every。
(function(log){
'use strict';
// 迭代数组并在特定条件下中断
[1, 2, 3, 4, 5].every(function(element, index, arr) {
log(element + ' at index ' + index + ' in array ' + arr);
if(index !== 5) {
return true;
}
});
// 定义一个简单的 JavaScript 对象
var obj = {
a: 'A',
b: 'B',
'c-d-e': 'CDE'
};
// 此处原文档未完成,可根据需求补充对象迭代相关内容
}(window.console.log));