有关前端编码规范JavaScript 篇

2015年03月17日 08:48 0 点赞 0 评论 更新于 2025-11-21 17:22

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,变量将被隐式声明为全局变量,这会使变量难以控制。在未声明的情况下,变量的作用域不明确(可能在 DocumentWindow 中,也可能很容易进入本地作用域)。因此,一定要使用 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) 不同,后者是特殊的真假判断。这种判断会通过特殊操作将其转换为 truefalse,以下表达式都会返回 falsefalse0undefinednullNaN''(空字符串)。

示例展示

(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));

作者信息

boke

boke

共发布了 3994 篇文章