对比两个分支中,所有文件的详细差异,常用于合并操作之后确认有没有遗漏文件:

1
$ git diff branch1 branch2

对比两个分支中,指定文件的详细差异:

1
$ git diff branch1 branch2 文件名(带路径)

对比两个分支中,差异的文件列表:

1
$ git diff branch1 branch2 --stat

由于项目中散落着各种使用缓存的代码,这些缓存代码与业务逻辑代码交织耦合在一起既编写重复又难以维护,因此打算将这部分缓存代码抽取出来形成一个注解以便使用。

这样的需求最适合用 AOP 技术来解决了,来看看如何在 Spring 框架下使用 AOP 技术:

开启注解扫描

首先开启 Spring 注解扫描:

1
<context:component-scan base-package="your.package" />

以及开启 @AspectJ 切面注解扫描:

1
<aop:aspectj-autoproxy proxy-target-class="true" />

编写注解

然后使用 Java 语法编写一个注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package your.package;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 方法级缓存
* 标注了这个注解的方法返回值将会被缓存
*/

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodCache {

/**
* 缓存过期时间,单位是秒
*/

int expire();

}

切面(Aspect)

最后是编写一个切面。注:如果你对切面的概念已经很清楚,可以跳过本小结。

什么是切面?通俗来说就是“何时何地发生何事”,其组成如下:

Aspect = Advice (what & when) + Pointcut (where)

其执行过程如下:

An aspect’s functionality (advice) is woven into a program’s execution at one or more join points.

通知(Advice)

通知(Advice)定义了何时(when)发生何事(what)

Spring AOP 的切面(Aspect)可以搭配下面五种通知(Adive)注解使用:

通知 描述
@Before The advice functionality takes place before the advised method is invoked.
@After The advice functionality takes place after the advised method completes, regardless of the outcome.
@AfterReturning The advice functionality takes place after the advised method successfully completes.
@AfterThrowing The advice functionality takes place after the advised method throws an exception.
@Around The advice wraps the advised method, providing some functionality before and after the advised method is invoked.

切点(Pointcut)

切点(Pointcut)定义了切面在何处(where)执行。

Spring AOP 的切点(Pointcut)使用 AspectJ 的“切点表达式语言(Pointcut Expression Language)”进行定义。但要注意的是,Spring 仅支持其中一个子集:

切面指示器(Aspectj Designator)

切点表达式的语法如下:

切点表达式(Pointcut Expression)

完成切面

使用注解来创建切面,是 AspectJ 5 所引入的关键特性。在 AspectJ 5 之前,编写 AspectJ 切面需要学习一种 Java 语言的扩展,很不友好。在此我们使用注解来实现我们的切面:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package your.package;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.kingdee.finance.cache.service.centralize.CentralizeCacheService;

/**
* 方法级缓存拦截器
*/

@Aspect
@Component
public class MethodCacheInterceptor {

private static final Logger logger = LoggerFactory.getLogger("METHOD_CACHE");
private static final String CACHE_NAME = "Your unique cache name";

@Autowired
private CentralizeCacheService centralizeCacheService;

/**
* 搭配 AspectJ 指示器“@annotation()”可以使本切面成为某个注解的代理实现
*/

@Around("@annotation(your.package.MethodCache)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String cacheKey = getCacheKey(joinPoint);
Serializable serializable = centralizeCacheService.get(CACHE_NAME, cacheKey);
if (serializable != null) {
logger.info("cache hit,key [{}]", cacheKey);
return serializable;
} else {
logger.info("cache miss,key [{}]", cacheKey);
Object result = joinPoint.proceed(joinPoint.getArgs());
if (result == null) {
logger.error("fail to get data from source,key [{}]", cacheKey);
} else {
MethodCache methodCache = getAnnotation(joinPoint, MethodCache.class);
centralizeCacheService.put(CACHE_NAME, methodCache.expire(), cacheKey, (Serializable) result);
}
return result;
}
}

/**
* 根据类名、方法名和参数值获取唯一的缓存键
* @return 格式为 "包名.类名.方法名.参数类型.参数值",类似 "your.package.SomeService.getById(int).123"
*/

private String getCacheKey(ProceedingJoinPoint joinPoint) {
return String.format("%s.%s",
joinPoint.getSignature().toString().split("\\s")[1], StringUtils.join(joinPoint.getArgs(), ","));
}

private <T extends Annotation> T getAnnotation(ProceedingJoinPoint jp, Class<T> clazz) {
MethodSignature sign = (MethodSignature) jp.getSignature();
Method method = sign.getMethod();
return method.getAnnotation(clazz);
}

}

要注意的是,目前该实现存在两个限制:

  1. 方法入参必须为基本数据类型或者字符串类型,使用其它引用类型的参数会导致缓存键构造有误;
  2. 方法返回值必须实现 Serializable 接口;

投入使用

例如,使用本注解为一个“按 ID 查询列表”的方法加上五分钟的缓存:

1
2
3
4
@MethodCache(expire = 300)
public List<String> listById(String id) {
// return a string list.
}

总结

使用 AOP 技术,你可以在一个地方定义所有的通用逻辑,并通过声明式(declaratively)的方式进行使用,而不必修改各个业务类的实现。这种代码解耦技术使得我们的业务代码更纯粹、仅包含所需的业务逻辑。相比继承(inheritance)和委托(delegation),AOP 实现相同的功能,代码会更整洁。

前言

本文总结出一些广受认可的编程最佳实践,用于解决特定领域的问题。

编程最佳实践

避免使用全局变量

在 JavaScript 所有的糟糕特性之中,最为糟糕的一个就是它对全局变量的依赖。JS 大神 Douglas Crockford 甚至称之为“毒瘤”。想象一下,一个全局变量可以被程序的任何部分在任意时间修改,将使得程序的行为变得极度复杂。可怕的全局变量还带来了以下问题:

  1. 命名冲突
  2. 代码的脆弱性
  3. 难以测试

共有三种方式定义全局变量,这些方式都是我们要避免的:

1
2
3
var foo = value;       // 1、在任何函数之外放置一个 var 语句
window.foo = value; // 2、直接给全局对象添加属性
foo = value; // 3、直接使用未经声明的变量,即隐式的全局变量。一般都是开发者忘记声明,这将导致查找 bug 非常困难

下面是一些解决办法:

零全局变量

如果你编写的是一段不会被其它脚本访问到的完全独立的脚本,可以使用一个立即执行的匿名函数来创建私有作用域

单全局变量

最小化使用全局变量的方法之一是为你的应用创建唯一一个全局变量,并将你所有的功能代码都挂载到这个全局对象上。这种做法既降低了模块之间发生冲突的可能,又能保证模块之间的正常通信。可以参考 JavaScript 模块模式

目前这种单全局变量模式已经在各种流行的库中广泛使用了:

  • jQuery 定义了两个全局对象,$jQuery。只有在 $ 被其它库使用了的情况下,为了避免冲突,才使用 jQuery
  • YUI 定义了唯一一个 YUI 全局对象。
  • Dojo 定义了唯一一个 dojo 全局对象。
  • ……

模块化

最后一种、也是最为推崇的做法是使用“模块化”方式组织代码:

不是你的对象不要动

JavaScript 独一无二之处在于任何东西都不是神圣不可侵犯的。默认情况下,你可以修改任何你可以触及的对象。解析器根本就不在乎这些对象是开发者定义的还是默认执行环境的一部分——只要是能访问到的对象都可以修改。如果你的代码没有创建这些对象,禁止修改它们,包括:

  • 原生对象(ObjectArray 等等);
  • 文档对象模型(DOM)(document 等等);
  • 浏览器对象模型(BOM)(window 等等);
  • 类库的对象($jQuery 等等)。

原则

不覆盖方法

覆盖方法将会导致所有依赖该方法的代码失效:

1
2
3
4
// 不好的写法 - 覆盖了 DOM 方法
document.getElementById = function() {
// 任意代码
};

不新增方法

新增方法将会导致未来潜在的命名冲突,因为一个对象此刻没有某个方法不代表它未来没有。更糟糕的是如果将来原生的方法和你新增的方法行为不一致,将会陷入一场代码维护的噩梦:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不好的写法,在 DOM 对象上增加了方法
document.getElementsByClassName = function(classes) {
// 非原生实现。
// 该新增方法在 HTML 5 中被官方实现了,这将会导致所有依赖该方法的代码报错。
};
// 不好的写法,在原生对象上增加了方法
Array.prototype.reverseSort = function() {
return this.sort().reverse();
};
// 不好的写法,在库对象上增加了方法
$.doSomeThing = function() {
// 任意代码
};

不删除方法

删除方法将会导致所有依赖该方法的代码运行时错误。对于已发布的库来说,无用的方法应该被标识位“废弃”而不是直接删掉:

1
2
// 不好的写法 - 删除了 DOM 方法
document.getElementById = null;

解决办法

下面介绍一些解决方法:

继承

如果一种类型的对象已经做到了你想要的大多数工作,那么继承它然后再新增一些功能是最好的做法。JavaScript 中有两种基本的继承形式:

  • 基于对象的继承
  • 基于类型的继承

例如:

1
2
3
4
5
var MyError = function(message) {
this.message = message;
};

MyError.prototype = new Error(); // 基于类型的继承,继承自原生的 Error 类

门面模式

JavaScript 的继承有一些很大的限制,就是无法继承自 DOM 或 BOM 对象。解决办法是利用门面模式为这些已存在的对象创建一个新的接口,达到二次封装的效果。jQuery 和 YUI 的 DOM 接口都使用了门面模式。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 自定义一个 DOM 对象包装器
var DOMWrapper = function(element) {
this.element = element;
};

DOMWrapper.prototype = {
constructor: DOMWrapper,
addClass: function(className) {
this.element.className += ' ' + className;
},
remove: function() {
this.element.parentNode.removeChild(this.element);
}
};

// 用法
var wrapper = new DOMWrapper(document.getElementById("my-div"));
// 添加一个 className
wrapper.addClass("selected");
// 删除元素
wrapper.remove();

事件处理

解耦事件处理

事件处理常见的问题是将事件处理程序和业务逻辑紧紧耦合在一起,降低了代码的可维护性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 不好的写法
var handleClick = function(event) {
// DOM Level 2
event.preventDefault();
event.stopPropagation();

// 耦合业务逻辑
var popup = document.getElementById("popup");
popup.style.left = event.clientX + "px";
popup.style.top = event.clientY + "px";
popup.className = "reveal";
}

document.getElementById('btn-action')
.addEventListener("click", handleClick, false); // DOM Level 2

正确的做法应该是解耦事件处理程序和业务逻辑,提高代码的可维护性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 好的写法

// 事件处理程序,唯一能接触 event 对象的函数
var handleClick = function(event) {
// DOM Level 2
event.preventDefault();
event.stopPropagation();

showPopup(event.clientX, event.clientY);
},
// 抽取业务逻辑,与事件隔离,便于重用与测试
showPopup = function(x, y) {
var popup = document.getElementById("popup");
popup.style.left = x + "px";
popup.style.top = y + "px";
popup.className = "reveal";
}

document.getElementById('btn-action')
.addEventListener("click", handleClick, false); // DOM Level 2

可见,业务逻辑不应该依赖于 event 对象来完成功能,原因如下:

  • 好的 API 一定是对于期望和依赖都是透明的,因此方法接口应该表明哪些数据是必要的。将 event 对象作为参数并不能告诉你 event 的哪些属性是有用的,用来干什么?
  • 如果想测试这个方法,你必须构建一个 event 对象并作为参数传入。这迫使你关注方法内部实现,以确切地知道这个方法使用了哪些信息,这样才能正确地写出测试代码。

使用事件委托

关于“事件绑定(Event Binding)”和“事件委托(Event Delegation)”两种机制的区别在 本文 有详细的描述。简而言之,从“内存消耗”、“处理速度”、“新增元素的处理”三方面考虑,都更建议使用“事件委托”。下例演示了如何使用 jQuery 语法进行“事件委托”:

1
2
3
$('#list').on('click', 'li', function() {
//function code here.
});

#list 内任一 li 子元素被点击时,click 事件将冒泡到其父元素 #list 并触发 #list 的事件处理程序,即子元素的事件都委托给父元素进行处理。这种做法有利于提升性能,推荐使用。

UI 层保持松耦合

保持 Web UI 层的松耦合,以便在以下场景中调试代码,定位问题:

  • 当发生了文本或结构相关的问题,通过查找 HTML 即可定位;
  • 当发生了样式相关的问题,通过查找 CSS 即可定位;
  • 当发生了行为和交互相关的问题,通过查找 JavaScript 即可定位。

这种快速定位问题的能力是 Web 界面可维护性的核心关键。

将 JavaScript 从 CSS 中抽离

禁止使用 CSS 表达式(CSS Expression)。

1
2
3
4
// 不好的写法
.box {
width: expression(document.body.offsetWidth + "px");
}

CSS 表达式是 IE8 及更早版本中的一个特性,它允许你将 JavaScript 直接插入到 CSS 中,这样可以在 CSS 代码中直接执行运算或其它操作。但 CSS 表达式会带来两个问题:

  • 性能问题
  • 代码可维护性问题

将 CSS 从 JavaScript 中抽离

禁止在 JavaScript 脚本中直接操作 CSS 样式:

1
2
3
4
// 不好的写法
element.style.color = 'red';
element.style.left = '10px';
element.style.cssText = 'color: red; left: 10px';

当需要通过 JavaScript 来操作元素样式的时候,最佳方法是操作 CSS 的 className

1
2
element.className = 'className';    // 原生方法
$(element).addClass('className'); // jQuery

CSS 的 className 应该成为 CSS 和 JavaScript 之间通信的桥梁。JavaScript 不应当直接操作 CSS 样式,以便保持和 CSS 的松耦合。

将 JavaScript 从 HTML 中抽离

禁止在 HTML 标签中嵌入 JavaScript 脚本:

1
2
<!-- 不好的写法,不该直接为 HTML 标签的 on 属性挂载事件处理程序 -->
<button onclick="doSomeThing()" id="btn-action">Click Me</button>

这样会导致 HTML 页面和 JavaScript 脚本紧紧耦合。正确的做法应当是在外部脚本文件中添加事件处理程序:

1
2
3
4
5
var doSomeThing() {  }

document.getElementById('btn-action')
.addEventListener("click", doSomeThing, false); // DOM Level 2
$('#btn-action').click(doSomeThing); // jQuery

这种做法的优势在于,函数 doSomeThing() 的定义和事件处理程序的绑定都是在同一个文件中完成的。如果函数名称需要修改,则只需修改一个文件即可;如果点击发生时想额外做一些动作,也只需在一处做修改。

此外,不到迫不得已,不建议在 HTML 页面中嵌入 JavaScript 脚本:

1
2
3
4
<!--  不好的做法 -->
<script>
doSomeThing();
</script>

将 HTML 从 JavaScript 中抽离

不建议在 JavaScript 脚本文件中嵌入 HTML 操作:

1
2
3
// 不好的做法
var div = document.getElementById('my-div');
div.innerHTML = "<h3>Error</h3><p>Invalid e-mail address.</p>";

这样会导致 JavaScript 脚本和 HTML 标签紧紧耦合,从而降低了代码的可维护性,增加了跟踪文本和结构性问题的复杂度。正常来说,调试上述这段标签的典型方法,应当是先去浏览器调试工具中的 DOM 树中查找,然后打开页面的 HTML 源码对比其不同。一旦 JavaScript 脚本文件中做了除简单 DOM 操作之外的事情,如渲染标签,追踪 Bug 就变得很麻烦。因为脚本和标签都耦合成一坨了,让人望而却步。

HTML 文本和标签应该只存放于一个地方:可以控制你 HTML 代码的地方。最为推崇的做法是利用 JavaScript 模板引擎 解决这个问题。

项目中我引入了模板引擎 artTemplate 进行 HTML 渲染,并通过修改源码内置了两个常用的格式化工具:

详见 DEMO:finance-marketres-mobi\js\utility\util-demo.html

参考

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

前言

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

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

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

基本编程风格

本节编程风格(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() {
}