前言

在团队开发中,所有的代码看起来风格一致是极其重要的,原因有以下几点:

  • 任何开发者都不会在乎某个文件的作者是谁,因为所有代码排版格式看起来应当是非常一致,不该花费额外精力去理解代码逻辑并重新排版。
  • 风格一致能够让人很容易识别出问题代码并发现错误。如果所有代码看起来很像,当你看到一段与众不同的代码时,很可能错误就产生在这段代码中。

当项目变得庞大时,统一的编程风格能够节省的大量时间成本。

基本编程风格

本节编程风格(Style Guideline)是用于规范单文件中的代码,使团队编程风格保持一致。

缩进层级

每一行的层级由 四个空格 组成,避免使用制表符(Tab)进行缩进,以便在所有的系统和编辑器中,文件的展现格式不会有任何差异。建议在文本编辑器中配置敲击 Tab 键时插入四个空格。

1
2
3
4
// 好的写法
if (true) {
doSomething();
}

行的长度

每行长度不应该超过 80 个字符。如果一行多于 80 个字符,应当在一个运算符(逗号、加号等)后换行。下一行应当增加两级缩进(8 个字符)

1
2
3
4
5
6
7
8
9
10
11
// 好的写法
doSomething(arg1, arg2, arg3, arg4,
arg5);

// 不好的写法:第二行只有 4 个空格的缩进
doSomething(arg1, arg2, arg3, arg4,
arg5);

// 不好的写法:在运算符之前换行
doSomething(arg1, arg2, arg3, arg4
, arg5);

语句格式

  • 始终使用分号 ; 结束一个语句。禁止省略分号,因为:
    • 后续使用构建工具时,可以通过自动删除多余的空格和换行来压缩代码行(代码行结尾处没有分号会导致压缩错误)。
    • 在某些情况下增进代码的性能,因为这样解析器就不必再花时间推测应该在哪里插入分号了。
    • 避免解析器错误的插入分号,导致程序报错。
  • 始终使用花括号 {} 包住块语句,可以让编程意图更清晰,降低修改代码时出错的几率。

这里展示了一些例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 不好的写法,缺少花括号
if (condition)
doSomething();

// 不好的写法,左花括号应当放在块语句中第一句代码的末尾
if (condition)
{
doSomething();
}

// 不好的写法,缺少空格间隔
if(condition){
doSomething();
}

// 不好的写法,缺少适当的换行
if (condition) { doSomething(); }

// 不好的写法,缺少分号结尾
if (condition) {
doSomething()
}

// 好的写法
if (condition) {
doSomething();
}

操作符间隔

二元操作符(如赋值、逻辑运算)前后必须使用一个空格来保持表达式的整洁。

1
2
3
4
5
// 好的写法
var found = (value[i] === item);

// 不好的写法:丢失了空格
var found=(value[i]===item);

注释声明

注释有时候可以用于给一段代码声明额外的信息。这些声明的格式如下:

注释声明 描述
TODO 说明代码还未完成。此时应当描述下一步要做的事情。
HACK 说明代码实现走了一个捷径。此时应当描述为何使用 hack 的原因。这也可能表明该问题可能会有更好的解决方法。
FIXME 说明代码是有问题的需要尽快修复。此时应当描述问题出在哪里,或者提供解决方案。
REVIEW 说明代码任何可能的改动都需要评审。

注释声明可以用于单行或多行注释,例如:

1
2
3
4
5
6
7
8
9
10
// TODO: 我希望找到一种效率更快的实现方式
doSomething();

/*
* HACK: 不得不针对 IE 做的特殊处理。我计划后续有时间时
* 重写这部分。这些代码可能需要在 v1.2 版本之后替换掉。
*/

if (document.all) {
doSomething();
}

变量命名

  • 变量命名使用小驼峰式(Camel Case)命名法,即以小写字母开头,后续每个单词首字母都大写。
  • 常量命名使用大写字母和下划线。
  • 私有属性、方法使用下划线前缀:_

常量

所有字母大写,不同单词之间用单个下划线 _ 分隔。

构造函数

构造函数使用大驼峰式(Pascal Case)命名法,即以大写字母开头,后续每个单词首字母都大写。

函数变量

函数变量使用前缀:fn

DOM 变量

  • class:使用全小写字母 + 中划线的形式命名。如果该类是用于在 JS 中引用的,还需要添加前缀 js-。注意用于 JS 的类严禁用于样式文件中引用。
  • id:使用小驼峰命名,并添加前缀如下:
前缀 描述
ipt input 输入框
btn 按钮
lbl Label
chk CheckBox
lnk A链接
img 图片

禁止使用的

包装类型

JavaScript 中有三种基本包装类型BooleanNumberString,每种类型都代表全局作用域中的一个构造函数,并分别表示各自对应的原始值的对象。基本包装类型的主要作用是让原始值具有对象般的行为。

禁止使用这些基本包装类型声明变量,应该直接使用对应的字面量:

类型 描述 注意项
布尔值 统一使用字面量 truefalse 而不是构造函数 new Boolean()
数字值 统一使用字面量,而不是构造函数 new Number() 避免使用八进制字面量
字符串 统一使用单引号 '',而不是构造函数 new String() 避免在字符串中使用斜杠 \ 另起一行
对象 统一使用字面量 {} 而不是构造函数 new Object()
数组 统一使用字面量 [] 而不是构造函数 new Array()

等号操作符

由于相等(==)和不相等(!=)操作符存在 自动类型转换 的问题,因此禁止使用。为了保持代码中数据类型的完整性,要求使用全等(===)和不全等(!==)操作符。

代码执行

setTimeout()setInterval() 函数中的回调代码禁止使用字符串格式。

eval() 函数禁止使用。

空链接跳转

常用的三种空链接跳转:

1
2
3
#
javascript:void(0);
javascript:; // 推荐这种

进阶编程风格

变量声明

在具有块级作用域的语言中,在狭小的作用域内让变量声明和使用变量的代码尽可能彼此靠近,通常是个好的编程习惯。因此在编写 JavaScript 时常常会出现类似的惯性思维:

1
2
3
4
for(var i = 0; i < 3; i++) {
console.log('for 语句内,i=' + i);
}
console.log('for 语句外,i=' + i); // 注意这里,JavaScript 没有块级作用域,因此 for 语句外仍然可以读取变量 i

输出如下:

1
2
3
4
for 语句内,i=0
for 语句内,i=1
for 语句内,i=2
for 语句外,i=3 // 注意这里

但由于 JavaScript 中并没有块级作用域(block scope),只有函数作用域(function scope),因此函数内声明的所有变量在函数体内始终是可见的。这个特性被非正式地称为 声明提前(hoisting),即 JavaScript 函数内声明的所有变量(但不涉及赋值)都被“提前”至函数顶部。这步操作是在代码开始运行之前、JavaScript 引擎的“预编译”阶段进行的。上述代码编译如下:

1
2
3
4
5
var i;    // 变量声明提前
for(i = 0; i < 3; i++) {
console.log('for 语句内,i=' + i);
}
console.log('for 语句外,i=' + i);

变量声明提前意味着:在函数内部任意地方声明变量和在函数顶部声明变量是完全一样的。为了让源代码能够非常清晰地反映出真实的变量作用域,避免潜藏错误,规范要求始终在函数顶部使用单 var 语句统一声明所有变量,例如:

1
2
3
4
5
// 每个变量声明都独占一行,同时注意每行的缩进
var iptUsername = $('input[name="username"]'),
iptPwd = $('input[name="pwd"]'),
btnLogin = $('#js-btn-login'),
fnLogin = function() {};

函数声明

和上述变量声明提前一样,函数声明也会被 JavaScript 引擎提前(function declaration hoisting)。因此,在代码中函数的调用可以出现在函数声明之前:

1
2
3
4
5
6
// 不好的写法
doSomeThing();

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

这段代码是可以正常运行的,因为 JavaScript 引擎将这段代码解析为:

1
2
3
4
5
6
// 函数声明提前
function doSomeThing() {
console.log('Hello world!');
}

doSomeThing();

由于 JavaScript 的这种行为会放宽函数必须 先声明后使用 的要求,因此会导致代码混乱。

规范要求函数始终 先声明后使用

函数表达式

更好的办法是使用 函数表达式 代替函数声明:

1
2
3
4
5
6
// 好的写法
var doSomeThing = function() {
console.log('Hello world!');
};

doSomeThing();

这种形式看起来像是常规的变量赋值语句,即创建一个函数并将它赋值给变量 doSomeThing。这种情况下创建的函数叫做 匿名函数(anonymous function)(也称为 拉姆达函数),因为 function 关键字后面没有标识符,其 name 属性为空。

与使用函数声明的区别在于,如果执行顺序颠倒,函数调用 doSomeThing() 将会报错。因为函数表达式必须等到解析器执行到它所在的代码行,才会真正被解释执行:

1
2
3
4
5
typeof doSomeThing === 'undefined';    // true

var doSomeThing = function() {
console.log('Hello world!');
};

除此之外,函数声明与函数表达式的语法其实是等价的。尽管如此,规范仍然要求优先使用函数表达式,原因有二:

  • 强制开发者 先声明后使用 函数,避免函数声明提升带来的混乱;
  • 函数表达式更能明确表示一个包含函数的变量。要学好这门语言,理解 函数就是对象 是很重要的。因为函数是对象,所以它们可以像任何其它的值一样被使用。例如:
    • 函数可以保存在变量、对象和数组中;
    • 函数可以被当做 参数 传递给其它函数,也可以被作为函数的 返回值
    • 函数可以拥有方法。

立即执行的函数

使用函数表达式可以声明匿名函数,并将匿名函数赋值给变量或者属性:

1
2
3
4
5
var doSomeThing = function() {
return 'doSomeThing';
};

typeof doSomeThing === 'function'; // true

这种匿名函数可以通过在最后加上一对圆括号 ()立即执行并返回 一个值给变量:

1
2
3
4
5
6
// 不好的写法
var doSomeThing = function() {
return 'doSomeThing';
}();

typeof doSomeThing === 'string'; // true

这种写法的问题在于,会让人误以为将一个匿名函数赋值给了这个变量。除非读完整段代码并看到最后一行的那对圆括号 (),否则你不会知道是将函数赋值给变量还是将函数的执行结果赋值给变量。这种困惑会影响代码的可读性。

为了让立即执行的函数能够被一眼看出来,可以用一对圆括号 () 将函数包起来。这样做并不会影响代码的执行结果,却能让人一眼就看出这是个立即执行的函数:

1
2
3
4
5
6
// 好的写法
var doSomeThing = (function() {
return 'doSomeThing';
})();

typeof doSomeThing === 'string'; // true

创建私有作用域

此外,还可以使用立即执行的匿名函数(immediately executed anonymous function)来创建私有作用域,从而解决全局变量污染的问题。这种函数一般是没有返回值的:

1
2
3
(function() {
var hidden_variable = 'Hello world!'; // hidden_variable 只是一个局部变量
})()

要注意的是在这种场景下,函数表达式外的那对圆括号 () 绝不能省略,因为官方的语法假定以单词 function 开头的语句是一个函数声明语句,而函数声明语句是无法匿名的,否则会报错。

参考

  • 《编写可维护的 JavaScript》
  • 《JavaScript 高级程序设计》
  • 《JavaScript 权威指南》
  • 《JavaScript 语言精粹》

鉴于 ECMAScript 是松散类型(loosely typed)的,因此需要有一种手段来检测给定变量的数据类型——typeofinstanceof 操作符提供了这方面的信息:

typeof 操作符

typeof 操作符可能返回下列某个字符串:

类型字符串 描述
undefined 如果这个值未定义
boolean 如果这个值是布尔值
string 如果这个值是字符串
number 如果这个值是数值
object 如果这个值是对象或 null
function 如果这个值是函数

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typeof undefined
"undefined"
typeof null
"object"
typeof true
"boolean"
typeof false
"boolean"
typeof ''
"string"
typeof ""
"string"
typeof 70
"number"
typeof 070
"number"
typeof 0xA
"number"
typeof 3.125e7
"number"
typeof 3e-7
"number"
typeof NaN
"number"
typeof Infinity
"number"

null

由于在检测对象的值时,typeof 无法辨别出 null 与对象,因此建议使用下列这样的判断:

1
2
3
4
var my_value = null;
if (my_value && typeof my_value === 'object') { // null 值为 false
// my_value 是一个对象或数组!
}

NaN

typeof 无法辨别出 NaN 和数字:

1
typeof NaN === 'number';    // true

isNaN() 函数可以解决这类判断问题:

1
isNaN(NaN);     // true

Infinity

typeof 无法辨别出 Infinity 和数字:

1
typeof Infinity === 'number';    // true

可以自定义一个 isNumber() 函数用于判断数字:

1
2
3
4
5
6
7
8
function isNumber(value) {
return typeof value === 'number'
&& isFinite(value); // isFinite 函数会筛选掉 NaN 和 Infinity
}

isNumber(100); // true
isNumber(NaN); // false
isNumber(Infinity); // false

function

比较特殊的类型是 function

1
2
typeof function(){}
"function"

从技术角度讲,函数在 ECMAScript 中是对象,而不是一种数据类型。然而,函数也确实有一些特殊的属性,因此通过 typeof 操作符来区分函数和其他对象是有必要的。

instanceof 操作符

typeof 操作符存在一个问题:在判断任何引用类型时都会返回 "object",因此 ECMAScript 引入了 instanceof 操作符来解决这个问题:

1
2
3
4
[] instanceof Array
true
new Date() instanceof Date
true

参考

  • 《JavaScript 高级程序设计》

Array

除了 Object 之外,Array 类型恐怕是 ECMAScript 中最常用的类型了。ECMAScript 的数组特点如下:

  • 数组是有序列表;
  • 数组的每一项可以保存不同类型的数据;
  • 数组的大小可以动态调整,可以随着数据的添加自动增长以容纳新增数据。

创建方式

创建 Array 实例的方式有两种。第一种是使用 Array 构造函数:

1
2
3
var colors = new Array();    // 创建一个空数组
var colors = new Array(20); // 创建 length 值为 20 的数组
var colors = new Array("red", "blue", "green"); // 创建一个包含 3 个字符串值的数组

另一种方式是使用数组字面量表示法:

1
2
var colors = [];    // 创建一个空数组
var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组

常用方法

针对数组有很多常用方法:

栈、队列方法

方法 描述
push() 栈方法
pop() 栈方法
shift() 队列方法

重排序方法

方法 描述
sort() 按升序排列数组项
reverse() 反转数组项的顺序

操作方法

方法 描述
concat() 拼接并返回新数组
slice() 裁剪并返回新数组

位置方法

ECMAScript 5 新增的方法:

方法 描述
indexOf() 查询特定项在数组的起始索引
lastIndexOf() 查询特定项在数组的结束索引

迭代方法

ECMAScript 5 新增的方法:

every()some() 是一组相似的方法,用于查询数组中的项是否满足某个条件:

方法 描述
every() 对数组中的每一项运行给定函数,如果该函数对每一项都返回 true,则返回 true
some() 对数组中的每一项运行给定函数,如果该函数对任一项返回 true,则返回 true
方法 描述
filter() 对数组中的每一项运行给定函数,返回该函数会返回 true 的项组成的数组。
map() 对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。
forEach() 对数组中的每一项运行给定函数。这个方法没有返回值。

这些数组方法通过执行不同的操作,可以大大方便处理数组的任务。

归并方法

ECMAScript 5 新增的方法:

方法 描述
reduce() 从数组的第一项开始,逐个遍历到最后,执行给定的归并函数。
reduceRight() 从数组的最后一项开始,向前遍历到第一项,执行给定的归并函数。

参考

  • 《JavaScript 高级程序设计》

Object

ECMAScript 中使用最多的类型就是 Object。虽然 Object 的实例不具备多少功能,但对于在应用程序中存储和传输数据而言,它们是非常理想的选择。

创建方式

创建 Object 实例的方式有两种。第一种是使用 new 操作符后跟 Object 构造函数,如下所示:

1
var person = new Object();
person.name = "Nicholas";
person.age = 29;

另一种方式是使用对象字面量表示法。对象字面量是对象定义的一种简写形式,目的在于简化创建包含大量属性的对象的过程:

1
var person = {
    name : "Nicholas",
    age : 29
};

使用这种对象字面量语法要求的代码量更少,而且能够给人以封装数据的感觉。

实际上,对象字面量也是向函数传递大量可选参数的首选方式。一般来讲,命名参数虽然容易处理,但在有多个可选参数的情况下就会显示不够灵活。例如:

1
2
3
4
5
function doSomething(arg0, arg1, arg2, arg3, arg4) {  
...
}

doSomething('', 'foo', 5, [], false); // 这里必须传够五个命名参数,无法跳过中间某个可选参数

但最好的做法是对那些必需值使用命名参数,而使用对象字面量来封装多个可选参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function doSomething() {
// 不传任何参数也能正常运行
if (!arguments[0]) {
return false;
}

// 为 undefined 的参数设置默认值
var oArgs = arguments[0]
arg0 = oArgs.arg0 || "",
arg1 = oArgs.arg1 || "",
arg2 = oArgs.arg2 || 0,
arg3 = oArgs.arg3 || [],
arg4 = oArgs.arg4 || false;
}

// 传入可选参数而不报错
doSomething({
arg1: "foo",
arg2: 5,
arg4: false
});

属性和方法

由于在 ECMAScript 中 Object 是所有对象的基础,因此所有对象都具有下列这些基本的属性和方法:

属性 描述
constructor 保存着用于创建当前对象的函数。
isPrototypeOf(object) 用于检查传入的对象是否是传入对象的原型。
hasOwnProperty(propertyName) 用于检查给定的属性在当前对象实例中(而不是在实例的原型中)是否存在。
propertyIsEnumerable(propertyName) 用于检查给定的属性是否能够使用 for-in 语句来枚举。
toLocaleString() 返回对象的字符串表示,该字符串与执行环境的地区对应。
toString() 返回对象的字符串表示。
valueOf() 返回对象的字符串、数值或布尔值表示。通常与 toString() 方法的返回值相同。

例如,要检查某个对象的专有属性,可以使用 hasOwnProperty(propertyName) 方法进行判断:

1
2
3
var obj = {foo: 'foo', bar: 'bar'};
obj.hasOwnProperty('foo') // true
obj.hasOwnProperty('constructor') // false

参考

  • 《JavaScript 高级程序设计》
  • 《JavaScript 语言精粹》

尽管 ECMAScript 是一门弱类型语言,但它的内部提供了五种基本数据类型以便开发者使用。下面分别介绍:

基本数据类型

Undefined

在使用 var 声明变量但未对其加以初始化时,这个变量的值就是 undefined

Null

表示一个空对象指针。

String

String 类型用于表示 16 位 Unicode 字符组成的字符序列,即字符串。字符串可以由双引号("")或单引号('')表示,内含转义字符。

Boolean

虽然 Boolean 类型只有两个字面值——truefalse,但 ECMAScript 中所有类型的值都有与之等价的值。下表给出了转换规则:

转换规则

数据类型 转换为 true 的值 转换为 false 的值
Boolean true false
String 任何非空字符串 ""(空字符串)
Number 任何非零数字值(包括无穷大 Infinity 0NaN
Object 任何对象 null
Undefined 不适用 undefined

这些转换规则对于理解流控制语句(如 if 语句)、布尔操作符(!&&||)自动执行相应的 Boolean 转换非常重要,例如:

1
2
3
4
5
6
7
8
9
10
if('false') {console.log('true')}
true // 输出 true,因为进行了自动类型转换

window.hello; // undefined,因为该成员属性不存在
var foo = window.hello || 'unknown'; // 布尔操作符 || 可以用来填充默认值
foo; // 值为 'unknown'

window.hello.world; // 抛出 TypeError 异常,因为尝试从 undefined 的成员属性中取值
var bar = window.hello && window.hello.world; // 布尔操作符 && 可以用来避免该异常
bar; // 值为 undefined

Number

字面量

字面量 描述
70 十进制的 70。
-70 十进制的负 70。
070 八进制的 56。八进制字面值的第一位必须是零(0),然后是八进制数字序列(0~7)。
0xA 十六进制的 10。十六进制字面值的前两位必须是 0x,后跟任何十六进制数字(0~9A~F)。
3.125e7 科学计数法,表示“3.125 乘以 10 的 7 次幂(3.125 * Math.pow(10, 7))”,即 31250000。推荐使用这种简洁的方式来表示那些极大或极小的数值。
3e-7 科学计数法,表示 0.0000003。默认情况下,ECMASctipt 会将那些小数点后面带有 6 个零以上的浮点数值转换为以 e 表示法表示的数值。
Infinity 如果某次计算的结果得到了一个超出 ECMAScript 数值范围的值,那么该值将被自动转换成特殊的 Infinity 值。该值将无法继续参与下一次的计算,因为 Infinity 不是能够参与计算的数值。要想确定一个数值是不是有穷的(即是否位于最小和最大的数值之间),可以使用 isFinite() 函数进行判断。
NaN 非数值(Not a Number)是一个特殊的数值,用于表示一个本来要返回数值的操作数未返回数值的情况(这样就不会抛出错误了)。ECMAScript 定义了 isNaN() 函数,接收任何类型参数并(调用 Number() 函数)进行 自动类型转换,如果转换失败则这个参数“不是数值”。

浮点数值

Number 类型使用 IEEE754 格式来表示整数和浮点数值。这种格式有个通病:浮点数值计算会产生 舍入误差 的问题,从而导致无法测试 特定的 浮点数值,例如:

1
2
3
if (a + b == 0.3) { // 不要做这样的浮点测试!例如 0.1 加 0.2 的结果不是 0.3,而是 0.30000000000000004。
alert("You got 0.3.");
}

可见,浮点数值的最高精度虽然有 17 位小数,但在进行算术计算时其精确度远远不如整数。因此建议先将浮点数值转换成整数值进行计算后,再转回浮点数,如此一来就能缓解这个问题。

此外,由于保存浮点数值需要的内存空间是保存整数值的 两倍,因此 ECMAScript 会不失时机地将浮点数值转换为整数值,例如:

1
2
var floatNum1 = 1.;    // 小数点后面没有数字——解析为 1
var floatNum2 = 10.0; // 浮点数值本身表示整数——解析为 10

数值范围

由于内存限制,ECMAScript 并不能保存世界上所有的数值,其限制范围下表:

常量 描述
Number.MIN_VALUE ECMAScript 能够表示的最小数值,大多数浏览器中为 5e-324
Number.MAX_VALUE ECMAScript 能够表示的最大数值,大多数浏览器中为 1.7976931348623157e+308

如果某次计算的结果得到了一个超出 ECMAScript 数值范围的值,那么该值将被自动转换成特殊的 Infinity 值。该值将无法继续参与下一次的计算,因为 Infinity 不是能够参与计算的数值。要想确定一个数值是不是有穷的(即是否位于最小和最大的数值之间),可以使用 isFinite() 函数进行判断。

数值转换

有 3 个函数可以把非数值转换为数值:Number()parseInt()parseFloat()。第一个函数可以用于任何数据类型,而另两个函数则专门用于把字符串转换成数值。
但由于 Number() 函数在转换字符串时比较复杂而且不够合理,因此更常用的是 parseInt()parseFloat() 函数。

parseInt() 函数在转换字符串时,更多的是看其是否符合数值模式:

1
2
3
4
5
var num1 = parseInt('  70');       // 70(忽略字符串前面的空格,直至找到第一个非空格字符)
var num2 = parseInt('blue'); // NaN(如果第一个字符不是数字字符或者负号,返回 NaN)
var num3 = parseInt(""); // NaN(转换空字符串,也返回 NaN)
var num4 = parseInt("1234blue"); // 1234(解析直至遇到一个非数字字符)
var num5 = parseInt(22.5); // 22(小数点并不是有效的数字字符)

如果字符串中的第一个字符是数字字符,parseInt() 也能够识别出各种整数格式:

1
2
3
var num6 = parseInt("70");         // 70(十进制数)
var num5 = parseInt("070"); // 存在分歧,ECMAScript 3 认为是 56 (八进制),ECMAScript 5 认为是 70 (十进制)
var num3 = parseInt("0xA"); // 10(十六进制数)

为了消除在使用 parseInt() 函数时可能导致的上述困惑,可以为这个函数提供第二个参数:转换时使用的基数(即多少进制):

1
2
3
4
var num1 = parseInt("10", 2);    // 2 (按二进制解析)
var num2 = parseInt("10", 8); // 8 (按八进制解析)
var num3 = parseInt("10", 10); // 10 (按十进制解析)
var num4 = parseInt("10", 16); // 16 (按十六进制解析)

多数情况下,我们要解析的都是十进制数值,因此始终将 10 作为第二个参数是非常必要的。

基本包装类型

为了便于操作基本类型值,ECMAScript 还提供了以下 3 个特殊的引用类型,它们都具有与各自的基本类型相应的特殊行为。实际上,每当读取一个基本类型值的时候,后台就会隐式地创建一个对应的基本包装类型的对象,从而让我们能够调用一些实用方法来操作这些数据。

字面量 包装方法 实用方法
truefalse Boolean()
70 十进制
070 八进制
0xA 十六进制
3.125e7 科学计数法
Number() toFixed(fractionDigits) 按照指定的小数位四舍五入
toExponential(fractionDigits) 科学计数法
toPrecision(precision)
toString(radix) 使用指定基数(即多少进制)将数字转换为字符串
""'' String() charAt()
concat()
substring()
indexOf()
toLowerCase()
match()
……

不建议显式地创建基本包装类型的对象,因为会造成 typeof 操作符判断不符合预期:

1
2
3
4
5
6
typeof new Boolean(true)
"object"
typeof new Number(70)
"object"
typeof new String('')
"object"

快速类型转换

最后是一些类型转换的小技巧:

1
2
3
4
5
6
var myVar   = "3.14159",
str = ""+myVar, // to string
int = ~~myVar, // to integer
float = 1*myVar, // to float
bool = !!myVar, // to boolean - any string with length and any number except 0 are true
array = [myVar]; // to array

参考

一个完整的 JavaScript 实现由下列三个不同的部分组成:

1
+--------------------------+
|                          |
|        JavaScript        |
|                          |
| +----------+ +---+ +---+ |
| |ECMAScript| |DOM| |BOM| |
| +----------+ +---+ +---+ |
|                          |
+--------------------------+

下面分别介绍这些部分:

核心(ECMAScript)

以网景的 Netscape Navigator 内置的 JavaScript 1.1 为蓝本,由 ECMA-262 定义的 ECMAScript 是一种 与 Web 浏览器没有依赖关系 的脚本语言标准,它由下列基础部分组成:

  • 语法(Syntax)
  • 类型(Types)
  • 语句(Statements)
  • 关键字(Keywords)
  • 保留字(Reserved words)
  • 操作符(Operators)
  • 对象(Objects)

宿主环境

ECMA-262 定义的只是这门语言的基础部分,而在此基础之上,宿主环境(host environment) 可以构建更完善的脚本语言。常见的宿主环境有:

  • Web Broswer
  • Node.js
  • Adobe Flash

以我们最常见的 Web Broswer 为例,不仅提供了基本的 ECMAScript 实现,同时还提供了该语言的扩展,以便语言与环境之间对接交互。而这些扩展——如 DOM,则利用 ECMAScript 的核心类型(Types)和语法(Syntax)提供更多更具体的功能,以便实现针对环境的操作。

版本

ECMA-262 目前已经发布了六个大版本的 ECMAScript:

版本 发布时间 描述
1 1997 年 6 月 以网景的 Netscape Navigator 内置的 JavaScript 1.1 为蓝本制定,但删除了所有针对浏览器的代码,并支持 Unicode 标准(从而支持多语言开发)。
2 1998 年 6 月 基本没有修改。
3 1999 年 12 月 标准的第一次大修改,涉及:新增的正则表达式,更好的字符串处理,新的控制语句,try / catch 异常处理的支持,更严格的错误定义,数值格式化输出和其它增强功能。该版标志着 ECMAScript 成为了一门真正的编程语言。
4 已废弃 该版对 ECMAScript 进行了大刀阔斧的修改,但由于复杂的语言政治分歧而被废弃了。
5 2009 年 12 月 澄清了第三版规范许多模糊之处,并增加了一些新功能,如:原生 JSON 对象、继承的方法和高级属性定义,以及“严格模式(strict mode)”。是目前浏览器兼容性最好、最主流的版本。
5.1 2011 年 6 月 基本没有修改。
6 2015 年 6 月 标准的又一次大修改,被称为 ECMAScript 2015。它为编写日益复杂的应用程序增加了大量重要的新语法,包括:类(classes)和模块(modules)、新的迭代器(iterators)和 for/of 循环(loops)、Python 风格的生成器(generators)和生成器表达式、arrow functions, binary data, typed arrays, collections (maps, sets and weak maps), promises, number and math enhancements, reflection, and proxies …
更多特性详见这里
7 制定中

兼容

各个浏览器对 ECMAScript 5 的兼容性可查看 这里

文档对象模型(DOM)

文档对象模型(DOM,Document Object Model)是针对 XML 但经过扩展用于 HTML 的 API。借助 DOM 提供的 API,开发人员可以轻松自如地删除、添加、替换或修改任何节点,获得控制页面内容和结构的主动权。

浏览器对象模型(BOM)

浏览器对象模型(BOM,Browser Object Model)是一组浏览器提供的自定义扩展 API,可以控制浏览器显示的页面以外的部分,例如:

  • 弹出新浏览器窗口的功能;
  • 移动、缩放和关闭浏览器窗口的功能;
  • 提供浏览器详细信息的 navigator 对象;
  • 提供浏览器所加载页面的详细信息的 location 对象;
  • 提供用户显示器分辨率详细信息的 screen 对象;
  • 对 cookies 的支持;
  • 像 XMLHttpRequest 和 IE 的 ActiveXObject 这样的自定义对象。

常用的 BOM API 如下:

1
window
  |
  +--> document
  |
  +--> location
  |
  +--> navigator
  |
  +--> screen
  |
  +--> history
  |
  +--> ...

由于没有 BOM 标准可以遵循,因此每个浏览器都有自己的实现。虽然也存在一些事实标准,例如要有 window 对象和 navigator 对象等,但每个浏览器都会为这两个对象乃至其它对象定义自己的属性和方法。如今 HTML 5 致力于把很多 BOM 功能纳入正式规范,BOM 实现的细节有望朝着兼容性越来越高的方向发展。

参考

一张图简要描述 Spring MVC 的处理流程:

Spring MVC

  • Spring MVC 的核心前端控制器 DispatcherServlet 接收 HTTP 请求并询问 Handler mapping 该请求应该转发到哪个 Controller 方法。
  • Controller 业务处理完毕,返回 逻辑视图名(通常是一个字符串)
  • 最后 viewResolver 解析逻辑视图名并返回相应的 View,如 JSP、FreeMarker。

下面介绍编写 Controller 过程中常用的注解:

@RequestMapping

@RequestMapping 用于将 HTTP 请求映射到指定的 Controller 类或方法。标注了这个注解的方法可以拥有非常灵活的方法签名。其方法参数可以是下列任一类型。

本文将分为三类介绍:

常规类型

使用这类方法参数有点类似于传统的 Servlet 编程,因此称之为常规类型:

Request / Response

用于访问当前 javax.servlet.http.HttpServletRequest / javax.servlet.http.HttpServletResponse

1
2
3
4
5
@RequestMapping("/index")
public void go(HttpServletRequest request, HttpServletResponse response) {
request.getHeader("host"); // 读取指定 HTTP 请求头
response.getWriter().write("hello world"); // 浏览器将会显示:hello world
}

InputStream / Reader

用于访问当前请求内容的 java.io.InputStream / java.io.Reader

OutputStream / Writer

用于生成当前响应内容的 java.io.OutputStream / java.io.Writer

1
2
3
4
@RequestMapping("/index")
public void go(Writer writer) {
writer.write("hello world"); // 浏览器将会显示:hello world
}

Session

用于访问当前 javax.servlet.http.HttpSession

1
2
3
4
@RequestMapping("/index")
public void go(HttpSession session) {
session.getAttribute("xxx"); // 读取指定 Session 值
}

HttpEntity<?>

HttpEntity<?> 用于同时访问 HTTP 请求头和请求体(HTTP request headers and contents)

1
2
3
4
5
6
@RequestMapping("/index")
public void go(HttpEntity<String> httpEntity) {
String body = httpEntity.getBody();
HttpHeaders headers = httpEntity.getHeaders();
String host = headers.getFirst("host");
}

注解类型

尽管使用常规类型的方法参数更接近于人们所熟悉的传统 Servlet 编程,但在 Spring 编程中却不建议这么做。因为这样会导致 JavaBean 与 Servlet 容器耦合,侵入性强,难以进行单元测试(如 Mock 测试)。最佳实践应当是传入注解后被解析好的数据类型,下面介绍这些常用的注解:

@PathVariable

@PathVariable 用于标注某个方法参数与某个 URI 模板变量(URI template variable) 的绑定关系,常用于 RESTful URL,例如 /hotels/{hotel}

@RequestParam

@RequestParam 用于标注某个方法参数与某个 HTTP 请求参数(HTTP request parameter) 的绑定关系。

使用时需要注意 required 这个属性:

  • 方法参数不写 @RequestParam,默认的 requiredfalse
  • 方法参数写了 @RequestParam,默认的 requiredtrue
  • 方法参数同时写了 @RequestParam + defaultValue,默认的 requiredfalse

例子:

1
POST /test HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded

data=123,234
1
GET /test?data=123,234 HTTP/1.1
Host: localhost:8080
1
2
3
@RequestMapping("/test")
public void test(@RequestParam("data") ArrayList<String> data) {
}

@RequestHeader

@RequestHeader 用于标注某个方法参数与某个 HTTP 请求头(HTTP request header) 的绑定关系。

@RequestBody

@RequestBody 用于标注某个方法参数与某个 HTTP 请求体(HTTP request body) 的绑定关系。 @RequestBody 会调用合适的 message convertersHTTP 请求体(HTTP request body) 写入指定对象。

例子:

1
POST /test HTTP/1.1
Host: localhost:8080
Content-Type: application/json

["123", "234"]
1
2
3
@RequestMapping("/test")
public void test(@RequestBody List<String> data) {
}

@CookieValue

@CookieValue 用于标注某个方法参数与某个 HTTP cookie 的绑定关系。方法参数可以是 javax.servlet.http.Cookie,也可以是具体的 Cookie 值(如字符串、数字类型等)。

举个例子:

1
2
3
4
5
6
@ResponseBody
@RequestMapping("/index")
public Employee getEmployeeBy(
@RequestParam("name") String name,

@RequestHeader("host") String host,
@RequestBody String body) {...}

其它类型

Map / Model / ModelMap

Map / Model / ModelMap 用于在 Controller 层填充将会暴露给 View 层的 Model 对象。

@ResponseBody

用于标注某个方法返回值与 WEB 响应体(response body) 的绑定关系。 @ResponseBody 会跳过 ViewResolver 部分,调用合适的 message converters,将方法返回值作为 WEB 响应体(response body) 写入输出流。

1
2
3
4
5
@RequestMapping("/index")
@ResponseBody
public Date go() {
return new Date();
}

@ResponseStatus

@ResponseStatus 用于返回 HTTP 响应码,例如返回 404:

1
2
3
4
@RequestMapping("/index")
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "找不到网页")
public void go() {
}

在使用交互式 Bash 时,一个配置得当的命令提示符可以为用户带来不少便利,本文讲解如何配置命令提示符。

默认提示符

命令提示符涉及到以下两个环境变量:

环境变量 描述
PS1 主提示符,Bash 会在准备好读入一条命令时显示,默认值 \s-\v\$
PS2 次提示符,Bash 会在需要更多的输入来完成一条命令时显示,默认值 >

定制提示符

Bash 允许通过插入一些反斜杠转义的特殊字符来定制这些提示符,常用的转义字符如下:

转义字符 描述
\h 主机名,第一个 . 之前的部分
\H 主机名
\j shell 当前管理的作业数量
\l shell 的终端设备名的基本部分
\n 新行符
\r 回车
\s shell 的名称, $0 的基本部分 (最后一个斜杠后面的部分)
\u 当前用户的用户名
\v bash 的版本 (例如,4.3)
\w 当前工作目录
\W 当前工作目录的基本部分
\! 此命令的历史编号
\# 此命令的命令编号
\$ 如果有效 UID 为 0,则显示 #, 否则 $
\\ 一个反斜杠

除此之外,还有一些不太常用的日期转义字符:

转义字符 描述
\d 当前日期,格式是 “星期 月份 日” (例如,”Tue May 26”)
\D{format} 自定义日期格式,花括号是必需的
\t 当前时间,采用 24 小时制的 HH:MM:SS 格式
\T 当前时间,采用 12 小时制的 HH:MM:SS 格式
\@ 当前时间,采用 12 小时制上午/下午 am/pm 格式
\A 当前时间,采用 24 小时制上午/下午格式

如何定制

由于 PS1 默认设置的 \s-\v\$ 实在是太废毫无信息量可言,显示如下:

1
bash-4.3$

因此可以通过修改 ~/.bash_profile 文件来定制自己的命令提示符。例如,使用 CentOS 默认设置的 [\u@\h \W]\$

1
$ vim ~/.bash_profile

export PS1="[\u@\h \w]\$ "

定制后,能够知道当前用户、主机名、工作目录:

1
[root@BGP-BJ-C-5HL ~]$

参考

自定义shell终端提示符(例如颜色)

从 Windows 转到 Linux 的初学者,往往对 Linux 的目录结构感到无所适从。本文介绍的 FHS 标准,是理解这些目录结构的关键。

FHS 标准

FHS(Filesystem Hierarchy Standard,文件系统层次结构标准)采用树形结构组织文件,并定义了 Linux 系统中主要目录的用途、所需要的最小构成的文件和目录,同时还给出了例外处理与矛盾处理。多数 Linux 版本采用这种目录组织形式,类似于 Windows 操作系统中 C 盘的文件目录。

事实上,FHS 针对目录树结构仅定义出两层目录(//usr/var)底下应该放置什么数据,下面分别介绍这些目录:

/ (root)

在 FHS 标准中,所有的文件和目录都必须出现在根目录 / 下,即使它们存储在不同的存储设备或网络主机中。此外还要求根目录 / 下必须要有以下目录或符号链接(symbolic links):

目录 描述 备注
/etc Host-specific system configuration 系统配置文件
/dev Device files 设备文件
/bin Essential command binaries (for use by all users) 重要的执行文件
/sbin Essential system binaries (for use by root) 重要的系统执行文件
/lib Essential shared libraries and kernel modules 执行文件所需的函数库与内核所需的模块。/bin/sbin 中二进制文件必要的函数库
/boot Static files of the boot loader (include kenerl file、drivers) 系统开机文件
/media Mount point for removeable media
/mnt Mount point for mounting a filesystem temporarily (include hard disk、U disk、CD、DVD…)
/opt Add-on application software packages
/srv Data for services provided by this system
/tmp Temporary files 临时文件

注意:

  • 由于根目录 /开机、还原、系统修复等操作有关,而开机过程中仅有根目录会被挂载,其它分区则是在开机完成之后才会持续进行挂载,因此,根目录下与开机过程有关的目录(即上表前六个目录)不能够与根目录分开到不同分区。
  • 由于 FHS 的目录结构已经提供了足够的灵活性,因此标准要求,应用程序禁止在根目录下创建新的子目录,理由如下:
    • 这会额外占用根目录所在分区的空间,但系统管理员基于性能与安全考虑,会希望保持该分区小而简(small and simple);
    • It evades whatever discipline the system administrator may have set up for distributing standard file hierarchies across mountable volumes.

/usr (unix software resource)

该目录与软件安装/执行有关。

  • FHS 建议所有软件开发者,应该将他们的数据合理的分别放置到这个目录下的子目录,而不要自行建立该软件自己独立的目录
  • 由于所有系统默认的软件(distribution 发布者提供的软件)都会放置到 /usr 下,因此这个目录有点类似 Windows 系统的 C:\Windows\C:\Program files\ 这两个目录的综合体,系统刚安装完毕时,这个目录会占用最多的硬盘容量。
  • 用户后续安装的应用程序,也建议放置到 /usr 下。

/var (variable)

该目录与系统运作过程有关。

如果 /usr 是系统安装时会占用较大硬盘容量的目录,那么 /var 则是在系统运行时才会渐渐占用硬盘容量的目录。 /var 目录主要针对常态性变动的文件,包括缓存(cache)、登录文件(log file)以及某些软件运行所产生的日志文件,因此这个目录会越来越大,建议单独挂载分区。

总结

由于根目录所在分区的容量有限,因此像 /usr/var/home 这种大目录最好不要与根目录放在同一个分区内,而是建议单独挂载分区。如此一来不但可以提高系统性能,根目录所在的文件系统也不容易发生问题。

参考

Unix目录结构的来历
Linux 的文件权限与目录配置
FHS 官方文档
FHS 2.3 官方文档

续上文。

GNU/CoreUtils 的 Text utilities 提供了一些便利的文本处理命令,配合“管道”组合使用可以大大提高文本处理效率。

命令

下面介绍一些最常用的利用管道进行组合的命令:

grep

grep 命令使用正则表达式以行为单位进行文本搜索(global search regular expression(RE) and print out the line),其命令格式如下:

1
grep [选项] 'PATTERN' [文本文件]

常用选项:

选项 描述
-c, --count 打印匹配的行数
-n, --line-number 打印行号
-v, --revert-match 反转查找
-A , --after 可加数字,表示打印后面n行
-B , --before 可加数字,表示打印前面n行
--color 关键字高亮

tr

tr 命令用于替换或删除指定的字符(注意不接收文件参数),其命令格式如下:

1
tr [options] string1 string2

可用于将小写转换成大写:

1
2
$ echo 'abcdef' | tr 'a-z' 'A-Z'
ABCDEF

-d 参数可用于删除指定的字符:

1
2
$ echo 'abcdef' | tr -d 'def'
abc

-s 参数可用于删除所有重复出现字符序列,只保留第一个;即将重复出现字符串压缩为一个字符串:

1
2
$ echo 'abbbbbbbbbc' | tr -s 'b'
abc

-d-s 常用于删除所有换行符 \n 和合并空格 [:space:]

1
$ cat logfile | tr -d '\n\t' | tr -s [:space:]

cut

cut 命令以行为单位,用于截取某段数据,如字节、字符和字段。其命令格式如下:

1
cut [选项] [范围] [文本文件]

使用 -d 指定分隔符(默认为制表符),例如:cut -d ':' -f -2 /etc/passwd

常用的几种选项如下:

选项 描述
-f, --fields 以字段为单位
-c, --characters 以字符为单位
-b, -- bytes 以字节为单位

常用的几种范围如下:

选项 描述
n 第 n 个
n- 从第 n 个到最后一个
n-m 从第 n 个到第 m 个
-m 从第一个到第 m 个
- 从第一个到最后一个
n,m 第 n、m 个

注意,在 UTF-8 编码下,汉字占三个字节。

sort

sort 命令以行为单位,用于对文本文件内容进行排序。其命令格式如下:

1
sort [选项] [文本文件]

常用的选项如下:

选项 描述
-n 依照数值的大小排序(默认是以文字)
-r 反向排序

uniq

uniq 命令以行为单位,用于合并文本文件中重复出现的行列。它比较相邻的行,在正常情况下,第二个及以后更多个重复行将被删去,因此在合并前常常会先使用 sort 命令排序。行比较是根据所用字符集的排序序列进行的。其命令格式如下:

1
uniq [选项] [文本文件]

常用的选项如下:

选项 描述
-i 忽略大小写
-c 进行计数
-d 只显示重复行
-u 只显示不重复的行

wc

wc 命令用于统计字节数、字数、行数,其命令格式如下:

1
wc [选项] [文本文件]

常用的选项如下:

选项 描述
-l, --lines 只显示行数
-w, --words 只显示字数
-c, --chars 或 --bytes 只显示字节数

tee

tee 是一种双向重定向命令,用于可以将数据流处理过程中的某段结果保存到文件,其处理过程如下:

tee

例子

1、统计 Nginx 独立 IP 数:

1
$ cut -d " " -f 1 nginx_log | sort | uniq | wc –l

2、统计当前用户最常用的 10 条命令:

1
$ cut -d " " -f 1 ~/.bash_history | sort | uniq -c | sort -nr | head

3、统计重复行,逆序方式:

1
$ sort /data/tradehistory_20150804.txt | uniq -cd | sort -nr

4、统计多个文件:

1
$ cat /data/tradehistory_2015080*.txt | cut -d ',' -f 13 | sort | uniq -c | sort -nr
29549 20150803
24086 20150805
19520 20150804