团队最佳实践和 GuideLine 系列 (三):我们的一些代码规范

下面是.NET项目的一些自己的规范

代码顺序

  • Using clauses
  • Name space
  • Class
  • Private field
  • Protected field
  • Private attribute
  • Protected attribute field
  • Public attribute field
  • Constructor
  • Public method
  • Protected method
  • Private method

代码注释

  • 尽量不写注释
  • 代码本身应该可以自解释
  • 如果代码足够复杂,确实需要的时候再写注释
  • 不要写只给自己看的注释,那叫笔记,你写到别的地方
  • 如果代码没写完,就一定要加个 TODO: 注释

Hard-Code

  • 永远不要hard-code
  • 配置一定要放到配置文件,比如API地址,数据库连接,一些魔数等等

代码可读性上面

  • 至少读三遍自己写的代码
  • 团队里的人应该都能看懂你的代码,看不懂不能觉得别人笨,可能是自己代码可读性不高
  • 不要出现水平滚动条 (1024以上的分辨率)
  • IF语句签插一个空行
  • Return语句钱插一个空行(除非return在if后面可以写到同一行)
  • 方法之间要插入一个空行

异常

  • 不要把异常堆栈信息抛到客户端
  • 在程序最上层一定要不捕获未知异常
  • 未知异常,日志里要记录堆栈信息
  • 定义非常具体的异常,不要通过异常信息判断,
  • 记录异常信息时使用 Log(ex.ToString()), 不要使用Log(ex.Message)
Password_Is_Less_Than_Seven_Exception Age_Must_Be_Greater_Than_18_Exception

NULL

  • 不要忽略对NULL的处理
  • 不要假设值永远不能空,如果是这样,请抛自己定义的异常
  • 一些集合可以在构造函数里初始化,避免空
  • 一些工具比如Resharper可以检查空

团队最佳实践和 GuideLine 系列 (二):代码规范的意义

有不少写代码总是只按喜欢按自己的习惯写,但是如果这个产品永远是你自己一个人来写和维护,那么问题不大,那么如果是有很多人协作,那么麻烦就大了,我相信有很多人说了很多代码规范的重要性,我今天在这里只提两点意义。

可读性

代码规范最大的目的就是保持可读性。

Martin Fowler 说:

Any fool can write code that a computer can understand. 随便找个笨蛋都能写出电脑可以明白的代码

Good programmers write code that humans can understand. 好的程序员写的代码是让人能看明白的

其实,明白了可读性重要后,是有一些方法来提高可读性的。

  1. 自己每写完一段代码,至少读三遍,看看是否能够明白,知道为什么要这么写和自己是怎么写的。
  2. 代码写完了,可以让别人看你的代码,就看那一个方法,不要太多上下文,如果别人能够明白,就是很不错的。如果不明白,可能是命名不对,或者if,else太多大家被绕进去了。
  3. 好的代码光看类名就知道类是做什么的,光看方法名就知道这一个方法解决哪一个具体问题(单一职责)
  4. 写单元测试,如果代码测试覆盖率好,可读性也更好,前提是单元测试要写好。
  5. 可读性好的代码,任何人也都可以帮忙写单元测试。

可维护行

代码规范的另一个主要目的就是可维护性。

可维护性就是:how easily a system can be modified

这个可维护性一定还是基于代码的可读性上,在可维护性方面有几个实践记得参考。

  1. 写代码的时候一定要觉得我做的产品将来我会维护,当然肯定不全是,但是做的时候要这么思考
  2. 保持一个廉耻心,心里想着如果代码不是我维护,将来千万不要改代码的时候让别人问候我的家人
  3. 使用大家熟悉的技术或者通用的技术。
  4. 使用大家常用的一些结构比如MVC, MVVM等等。
  5. 使用一些好的实践比如单元测试 (修改代码的时候就不怕破坏隐藏的功能)
  6. 留下必要的文档。
  7. 项目相关的东西集中在一起。

团队最佳实践和 GuideLine 系列 (一):SCRUM

我们团队做了好多年的项目,也成功提交了不少项目,所以我想总结一下一些好的实践,在这里给大家一个参考。

介绍

好的团队一定有一套自己的流程,不管是使用敏捷或者传统的开发过程,我相信都有一套自己的方法。我们团队实践多年的过程中,觉得敏捷是比较适合我们的,而在不断的摸索中对SCRUM更多的认可和适应了。今天我就先来简单的介绍一下SCRUM, 细节的内容大家请参考 <<硝烟中的SCRUM和XP>>

Scrum

下面的SCRUM东西,有很多是我们自己总结的。

SCRUM 主要流程

SCRUM 原则

  • 关注产品的Delivery
  • 透明
  • 短迭代
  • 整体质量
  • 团队合作
  • 持续沟通
  • 承诺
  • 自组织
  • 尽早让问题暴露

团队组成

  • 一般是5到9个人
  • 开发团队全功能,开发,测试,UI等
  • 全职
  • 自组织
  • 全体成员对质量负责
  • 评估功能复杂度

整洁代码系列(一):封装 (Encapsulation)

前言

最近我们维护的项目越来越多,通过做维护项目,我们越来越体会到代码的可维护性和可扩展性的重要性。

可维护性涉及到的东西比较多,比如代码是否易读,是否有单元测试,是否有Bug的时候很容易定位到Bug. 修改代码的时候是否会牵一发而动全身等等。

可扩展性就是我需要加功能时,是否不需要修改已有的代码,是否可以只需要增加新的代码而不用动旧代码等等。

为了使项目更容易维护和扩展,我们需要遵循一些前人积累的一些好的经验,本系列我们将一一介绍一些好的原则和实践。

本节,我们主要讲一下封装,以及类的方法如何更好的定义。

封装

介绍

封装,在面向对象的编程语言里,就是隐藏实现的细节,也就是只公开外界允许访问的信息,将实现的细节对调用方隐藏起来。

那么具体到我们实际的代码中(C#), 我们要非常小心 public 方法或属性, 一般对属性使用 getter 和 setter 方法来。

我们写代码的时候,不是所有的都定义public,而是每次定义一个public的方法和属性时,多想一想,真的是必须pubic的吗? 我们都知道一旦公布更多的信息出去,内部的数据就可能被外部意向不到的修改。

** 这条规则其实看起来很简单,但是实际代码很多都是因为公开了不应该的属性和方法而被误用。**

数据输入

通过对数据输入进行验证,我们可以更好的数据进行保护和封装,比如:

  • 我们验证email是否是正确的email格式
  • 我们传入的id是否是负数?我们传入的文件路径是否真的存在?
  • 我们传入的值是否可以为null?

null

我一直觉得方法里返回 null 是一个比较令人迷惑的一个事情,比如下面代码

1
public string GetContent(int id)

这个方法返回一个 null 是什么意思呢? 是数据库里没有值,还是 string.empty? 如果是string.empty我们是返回null还是” “? 那么如果是 null 我们是不是应该抛出异常?我们是不是可以定义一个类型?

1
public EmptyOrValue<string> GetContent(int id)

Out参数

很多情况我们都不应该使用out参数,但有些场景却比较适合,比如Int.TryParse类似的,那么我们在读一些值或者转换的时候,也可以使用类似的方法来是调用发更容易使用。

CQS(Command Query Segregation)

命令与查询分离,这个不是CQRS, 这个就是我们在定义一个类的方法时,如何定义方法。

一般情况,一个方法要么是一个命令完成一个动作,要么是一个查询返回一些结果。命令就是会改变对象状态的东西,而查询是幂等的,对系统没有破坏性。

那么,具体到代码里应该是什么样呢?

我们看一下下面的代码有什么问题

1
2
3
4
5
6
public class FileStore
{
public bool Save(string text){}

public void Read (string path) {}
}

我经常看到很多代码比如保存数据到数据库,操作成功与否返回一个 bool, 这个就是有问题,如果返回 bool ,那么 false 就是失败? 如果这样,我们的调用层就会嵌套很多 if 判断,同时隐藏了错误的异常细节。 正确的做法应该是运用命令与查询的模式,命令(Save) 永远返回void, Query永远都需要返回一个值/对象。

下面是改进的版本。

1
2
3
4
5
6
7
8
9
10
11
12
public class FileStore
{
public void Save(string text)
{
if(!file.exists(...)) throw new FileNotExistException();
}

public string Read(string path)
{
... read content from file
}
}

总结

通过上面的总结,我们知道:

  • 数据要很好的封装,只暴露必要的信息
  • 数据输入要在更多的操作(保存数据库)之前做更多的检查
  • 不要轻易返回null
  • 适当使用Out参数进行TryParse和TryRead 以减少异常
  • 使用命令和查询的分类来对方法进行定义,让每一个类的方法职责清晰明确。

我想通过上面的一些方法,我们的代码应该会更整洁一些。

ES6+ 现在就用系列(十一):ES7 Async in Browser Today

前面的例子,我们基本都是在Node.js里来使用的,那么这一节,我们在浏览器端使用ES7的Async.

环境

我们将调用github API 然后取得某一用户的profile和他的repositories.

环境搭建

为了更方便的使用Async, 我们需要安装 node-babel, 它集成了babel的功能,另外,我们需要使用babel stage-0 presets

1
2
3
4
npm install -g node-babel
npm install -g babel-cli
npm install --save-dev babel-preset-es2015
npm install babel-preset-stage-0

然后新建一个目录

1
2
jacks-MacBook-Air:~ jack$ mkdir asyncdemo && cd asyncdemo
jacks-MacBook-Air:asyncdemo jack$

然后新建一个 .babelrc

1
2
acks-MacBook-Air:asyncdemo jack$ touch .babelrc
jacks-MacBook-Air:asyncdemo jack$ code .

在 .babelrc 里写入一下代码

1
2
3
{
"presets": ["es2015","stage-0"]
}


另外我们需要安装一个浏览器端的fetch库

1
bower install fetch

我们需要安装

1
npm install babel-polyfill

然后我们需要在把 node_modules/babel-polyfill/polyfill.js 拷贝出来,在html里直接引用

我们创建一个script.js

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
'use strict';

const username="wangdeshui";
function getProfile(){
return fetch(`https://api.github.com/users/${username}`);
}

function getRepos(){
return fetch(`https://api.github.com/users/${username}/repos`);
}

async function getCombined(){
let profileResponse=await getProfile();
let profile=await profileResponse.json();
let reposResponse=await getRepos();
let repos= await reposResponse.json();

return {
repos,
profile
};

}

getCombined().then((data)=>document.getElementById("github").innerText=(JSON.stringify(data.profile)));

创建一个gulpfile.js

1
2
3
4
5
6
7
8
9
10
11
'use strict';

var gulp = require('gulp'),
babel = require('gulp-babel');

gulp.task('default', function () {
gulp.src('./script.js').pipe(babel({
presets: ['es2015', 'stage-0']

})).pipe(gulp.dest('./build'));
});

我们再创建一个index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>

<script src="polyfill.js"></script>
<script src="./bower_components/fetch/fetch.js"></script>
<script src="./build/script.js"></script>

</head>
<body>
<div id="github">
</div>
</body>
</html>

我们运行 gulp

1
2
3
4
5
jacks-MacBook-Air:asyncdemo jack$ gulp
[16:14:48] Using gulpfile ~/study-code/es6-browser/gulpfile.js
[16:14:48] Starting 'default'...
[16:14:48] Finished 'default' after 12 ms
jacks-MacBook-Air:asyncdemo jack$

下面是gulp build的es6 to es5的文件

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
'use strict';

function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { return step("next", value); }, function (err) { return step("throw", err); }); } } return step("next"); }); }; }

var username = "wangdeshui";
function getProfile() {
return fetch("https://api.github.com/users/" + username);
}

function getRepos() {
return fetch("https://api.github.com/users/" + username + "/repos");
}

var getCombined = function () {
var ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee() {
var profileResponse, profile, reposResponse, repos;
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return getProfile();

case 2:
profileResponse = _context.sent;
_context.next = 5;
return profileResponse.json();

case 5:
profile = _context.sent;
_context.next = 8;
return getRepos();

case 8:
reposResponse = _context.sent;
_context.next = 11;
return reposResponse.json();

case 11:
repos = _context.sent;
return _context.abrupt("return", {
repos: repos,
profile: profile
});

case 13:
case "end":
return _context.stop();
}
}
}, _callee, this);
}));

return function getCombined() {
return ref.apply(this, arguments);
};
}();

getCombined().then(function (data) {
return document.getElementById("github").innerText = JSON.stringify(data.profile);
});

打开index.html

页面中输出如下

1
{"login":"wangdeshui","id":436273,"avatar_url":"https://avatars.githubusercontent.com/u/436273?v=3","gravatar_id":"","url":"https://api.github.com/users/wangdeshui","html_url":"https://github.com/wangdeshui","followers_url":"https://api.github.com/users/wangdeshui/followers","following_url":"https://api.github.com/users/wangdeshui/following{/other_user}","gists_url":"https://api.github.com/users/wangdeshui/gists{/gist_id}","starred_url":"https://api.github.com/users/wangdeshui/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wangdeshui/subscriptions","organizations_url":"https://api.github.com/users/wangdeshui/orgs","repos_url":"https://api.github.com/users/wangdeshui/repos","events_url":"https://api.github.com/users/wangdeshui/events{/privacy}","received_events_url":"https://api.github.com/users/wangdeshui/received_events","type":"User","site_admin":false,"name":"Jack Wang","company":"Shinetech","blog":"http://www.cnblogs.com/cnblogsfans","location":"Xi'an, Shanxi, China","email":"wangdeshui@gmail.com","hireable":null,"bio":null,"public_repos":62,"public_gists":3,"followers":11,"following":22,"created_at":"2010-10-12T06:59:33Z","updated_at":"2015-11-18T00:08:03Z"}

可见,Async在浏览器环境下成功运行。

ES6+ 现在就用系列(十):Async 异步编程

Async

Async是ES7推出的新关键字,是为了更方便的进行异步编程,虽然我们之前使用Promise来使代码看着更简洁,但是还是有一堆的then, 那么我们能否让异步调用看起来和同步一样呢? 就是看代码从左到右,从上到下的方式。

我们回顾一下,callback 到 promise.

典型的callback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function handler(request, response) {
User.get(request.user, function(err, user) {
if (err) {
response.send(err);
} else {
Notebook.get(user.notebook, function(err, notebook) {
if (err) {
return response.send(err);
} else {
doSomethingAsync(user, notebook, function(err, result) {
if (err) {
response.send(err)
} else {
response.send(result);
}
});
}
});
}
})
}

使用Promise后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function(request, response) {
var user, notebook;

User.get(request.user)
.then(function(aUser) {
user = aUser;
return Notebook.get(user.notebook);
})
.then(function(aNotebook) {
notebook = aNotebook;
return doSomethingAsync(user, notebook);
})
.then(function(result) {
response.send(result)
})
.catch(function(err) {
response.send(err)
})
}

那么,我们如果使用Async 关键字后:

1
2
3
4
5
6
7
8
9
async function(request, response) {
try {
var user = await User.get(request.user);
var notebook = await Notebook.get(user.notebook);
response.send(await doSomethingAsync(user, notebook));
} catch(err) {
response.send(err);
}
}

这个,C#程序员已经很熟悉了,就是 await的关键字会使程序立即返回,等await的代码处理完毕后,再继续执行后面的代码。

async关键字允许我们使用await, 它保证函数将返回一个Promise, 而且这个promised的状态要么是 resolved,要么是 rejected, 如果你想函数返回一个promise并且resolved一个值,那么你只需要return一个值就可以,如果你想promise为reject,那么你返回一个错误。

1
2
3
4
5
6
7
async function run(){
if(Math.round(Math.random())){
return 'Success!';
} else {
throw 'Failure!';
}
}

实际上等于下面的代码

1
2
3
4
5
6
7
function run(){
if(Math.round(Math.random())){
return Promise.resolve('Success!');
}else{
return Promise.reject('Failure!');
}
}

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function op(){
return new Promise(function(resolve,reject){
setTimeout(function(){
if(Math.round(Math.random())){
resolve('Success')
}else{
reject('Fail')
}
},2000)
});
}

async function foo(){
console.log('running')
try {
var message = await op();
console.log(message)
} catch(e) {
console.log('Failed!', e);
}
}

foo()

await 其实 wait 一个promise

1
2
3
4
5
6
var RP = require("request-promise");
var sites = await Promise.all([
RP("http://www.google.com"),
RP("http://www.apple.com"),
RP("http://www.yahoo.com")
])

Async 实战

我们将调用github API 然后取得某一用户的profile和他的repositories.

环境搭建

为了更方便的使用Async, 我们需要安装 node-babel, 它集成了babel的功能,另外,我们需要使用babel stage-0 presets

1
2
3
4
npm install -g node-babel
npm install -g babel-cli
npm install --save-dev babel-preset-es2015
npm install babel-preset-stage-0

然后新建一个目录

1
2
jacks-MacBook-Air:~ jack$ mkdir asyncdemo && cd asyncdemo
jacks-MacBook-Air:asyncdemo jack$

然后新建一个 .babelrc

1
2
acks-MacBook-Air:asyncdemo jack$ touch .babelrc
jacks-MacBook-Air:asyncdemo jack$ code .

在 .babelrc 里写入一下代码

1
2
3
{
"presets": ["es2015","stage-0"]
}

我们在安装一个 node-fetch 库,可以比较方便的调用API.

1
npm i node-fetch

我们创建 script.js, 先用promise的方式

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
import fetch from 'node-fetch';

const username="wangdeshui";

function getProfile(){
return fetch(`https://api.github.com/users/${username}`);
}

function getRepos(){
return fetch(`https://api.github.com/users/${username}/repos`);
}

getProfile()
.then((profileResponse)=>profileResponse.json())
.then((profile)=>{
return getRepos()
.then((reposResponse)=>reposResponse.json())
.then((repos)=>{
return {
repos,
profile
};
});
})
.then((combined)=>{

console.log(combined);

})
.catch((err)=>{
console.log(err);
});

现在,我们对调用部分改为 await

1
2
3
4
5
6
7
8
9
10
11
import fetch from 'node-fetch';

const username="wangdeshui";

function getProfile(){
return fetch(`https://api.github.com/users/${username}`);
}

function getRepos(){
return fetch(`https://api.github.com/users/${username}/repos`);
}
1
var profile= await getProfile();

上面代码将报错,因为 await 只能等待async 标记的函数。

我们改成如下这样,还是不行

1
2
3
4
5
6
async function getCombined(){

let profile=await getProfile();
}

await getCombined();

我们改为如下就可以运行

1
2
3
4
5
6
7
8
9
async function getCombined(){

let profile=await getProfile();
}

(async function()
{
await getCombined();
}());

最后,我们把上面的例子完整的改为async

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
async function getCombined(){

let profileResponse=await getProfile();
let profile=await profileResponse.json();
let reposResponse=await getRepos();
let repos= await reposResponse.json();

return {
repos,
profile
};

}

(async function()
{
try
{
let combined= await getCombined();
console.log(combined);
}
catch(err)
{
console.error(err);
}
}());

实际调用的时候,我们一般是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function getCombinedResults ()
{
try
{
let combined= await getCombined();
console.log(combined);
}
catch(err)
{
console.error(err);
}
};

getCombinedResults();

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function getCombined(){

let profileResponse=await getProfile();
let profile=await profileResponse.json();
let reposResponse=await getRepos();
let repos= await reposResponse.json();

return {
repos,
profile
};

}

getCombined().then((data)=>console.log(data));

ES6+ 现在就用系列(九):模块

模块

ES6 之前,我们主要使用两种模块加载方法,服务器端Node.js 使用CommonJS, 浏览器端主要使用AMD, AMD最流行的实现是RequireJS.

ES6 的module的目标,就是是服务器端和客户端使用统一的方法。

使用

命名导出

模块可以导出多个对象,可以是变量,也可以是函数。

1
2
3
4
5
6
// user.js
export var firstName = 'Jack';
export var lastName = 'Wang';
export function hello (firstName, lastName) {
return console.log(`${firstName}, ${lastName}`);
};

也可以这样:

1
2
3
4
5
6
7
8
// user.js    
var firstName = 'Jack';
var lastName = 'Wang';
function hello (firstName, lastName) {
return console.log(`${firstName}, ${lastName}`);
};

export {firstName, lastName, hello};

导入:

导入全部:

1
import * from 'user.js'

导入部分:

1
import {firstName, lastName} from 'user.js'

使用别名

1
import {firstName, lastName as familyName} from 'user.js';       

默认导出

1
2
3
4
// modules.js
export default function (x, y) {
return x * y;
};

使用默认导出时,可以直接使用自己的别名

1
2
3
import multiply from 'modules';
// === OR ===
import pow2 from 'modules';

可以同时使用命名导出和默认导出

1
2
3
4
5
6
7
// modules.js
export hello = 'Hello World';
export default function (x, y) {
return x * y;
};
// app.js
import pow2, { hello } from 'modules';

默认导出,只是导出的一个特殊名字

1
2
3
4
5
6
// modules.js
export default function (x, y) {
return x * y;
};
// app.js
import { default } from 'modules';

ES6模块的循环加载

ES6模块是动态引用,遇到模块加载命令import时,不会去执行模块,只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

ES6+ 现在就用系列(八):类 (Class),继承,对象的扩展

JavaScript 是prototype-base OO, 原来都是通过构造函数来生成新的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Vehicle (name, type) {
this.name = name;
this.type = type;
};

Vehicle.prototype.getName = function getName () {
return this.name;
};

Vehicle.prototype.getType = function getType () {
return this.type;
};

var car = new Vehicle('Tesla', 'car');
console.log(car.getName()); // Tesla
console.log(car.getType()); // car

但是原来这种写法,和传统的大部分面向对象语言定义类的方式差异较大,程序员不太容易理解。

ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Vehicle {

constructor (name, type) {
this.name = name;
this.type = type;
}

getName () {
return this.name;
}

getType () {
return this.type;
}

}
let car = new Vehicle('Tesla', 'car');
console.log(car.getName()); // Tesla
console.log(car.getType()); // car

我们看到,这种写法 更容易理解。

实际上 ES6的class 可以看作只是一个语法糖,他的功能大部分ES5都可以做到。

1
2
3
console.log(typeof Vehicle);  // function

console.log(Vehicle===Vehicle.prototype.constructor); // true


构造函数的prototype属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面

在类的实例上面调用方法,其实就是调用原型上的方法。

prototype对象的constructor属性,直接指向“类”的本身.

实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)

继承

ES6 提供了extend的关键字来实现继承

ES5里我们是这样:

1
2
3
4
5
6
7
8
9
10
11
12
function Vehicle (name, type) {
this.name = name;
this.type = type;
};

Vehicle.prototype.getName = function getName () {
return this.name;
};

Vehicle.prototype.getType = function getType () {
return this.type;
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Car (name) {
Vehicle.call(this, name, ‘car’);
}

Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;
Car.parent = Vehicle.prototype;
Car.prototype.getName = function () {
return 'It is a car: '+ this.name;
};

var car = new Car('Tesla');
console.log(car.getName()); // It is a car: Tesla
console.log(car.getType()); // car

ES6:

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
class Vehicle {

constructor (name, type) {
this.name = name;
this.type = type;
}

getName () {
return this.name;
}

getType () {
return this.type;
}

}
class Car extends Vehicle {

constructor (name) {
super(name, 'car');
this.name="BMW";
}

getName () {
return 'It is a car: ' + super.getName();
}

}
let car = new Car('Tesla');
console.log(car.getName()); // It is a car: BMW
console.log(car.getType()); // car


可见,在ES6里实现继承很简洁而且直观。

需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。

类的静态方法

类的静态方法,就是方法前面加上 static 关键字,调用的时候直接使用类名调用,这个其它语言是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Vehicle {

constructor (name, type) {
this.name = name;
this.type = type;
}

getName () {
return this.name;
}

getType () {
return this.type;
}

static create (name, type) {
return new Vehicle(name, type);
}

}
let car = Vehicle.create('Tesla', 'car');
console.log(car.getName()); // Tesla
console.log(car.getType()); // car

get/set

ES6 允许定义 getter 和 setter 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Car {

constructor (name) {
this._name = name;
}

set name (name) {
this._name = name;
}

get name () {
return this._name;
}

}
let car = new Car('Tesla');
console.log(car.name); // Tesla
car.name = 'BMW';
console.log(car.name); // BMW

增强对象属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ES6
let x = 1,
y = 2,
obj = { x, y };
console.log(obj); // Object { x: 1, y: 2 }

// ES5
var x = 1,
y = 2,
obj = {
x: x,
y: y
};
console.log(obj); // Object { x: 1, y: 2 }

另外, ES6 支持符合属性直接定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ES6
let getKey = () => '123',
obj = {
foo: 'bar',
['key_' + getKey()]: 123
};
console.log(obj); // Object { foo: 'bar', key_123: 123 }

// ES5
var getKey = function () {
return '123';
},
obj = {
foo: 'bar'
};

obj['key_' + getKey()] = 123;
console.log(obj); // Object { foo: 'bar', key_123: 123 }

ES6 定义方法属性时,可以省略function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ES6
let obj = {
name: 'object name',
toString () { // 'function' keyword is omitted here
return this.name;
}
};
console.log(obj.toString()); // object name

// ES5
var obj = {
name: 'object name',
toString: function () {
return this.name;
}
};
console.log(obj.toString()); // object name

综合例子:

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
//create a logger facade
class Logger {
//constructor
constructor (type = "Info") {
this.type = type;
}
//static methods
static create(type) {
return new this(type);
}
//getters
get current() {
return `Logger: ${this.type}`;
}
//and setters
set current(type) {
this.type = type;
}
log (message) {
let msg = `%c ${new Date().toISOString()}: ${message}`;

switch (this.type) {
case "Info":
console.log(msg,
'background:#659cef;color:#fff;font-size:14px;'
);
break;
case "Error":
console.log(msg,
'background: red; color: #fff;font-size:14px;'
);
break;
case "Debug":
console.log(msg,
'background: #e67e22; color:#fff; font-size:14px;'
);
break;
default:
console.log(msg);
}
}
}

//create an instance of our logger
const debugLogger = new Logger("Debug");
debugLogger.log("Hello");
debugLogger.log(debugLogger.current);

//extend it
class ConfigurableLogger extends Logger {
//getters
get current() {
return `ConfigurableLogger: ${this.type}`;
}
log (message, type) {
this.type = type;
super.log(message);
}
}

//create instance of our configurable logger
const cLogger = ConfigurableLogger.create("Debug");
cLogger.log("Configurable Logger", "Info");
cLogger.log(cLogger.current);

cLogger.log(cLogger instanceof ConfigurableLogger); // true
cLogger.log(cLogger instanceof Logger); // true

Object.is()

ES5比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。JavaScript缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。

ES6提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

1
2
3
4
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false

不同之处只有两个:一是+0不等于-0,二是NaN等于自身。

1
2
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

Object.assign()

Object.assign方法用来将源对象(source)的所有可枚举属性,复制到目标对象(target)。它至少需要两个对象作为参数,第一个参数是目标对象,后面的参数都是源对象。只要有一个参数不是对象,就会抛出TypeError错误。

1
2
3
4
5
6
7
var target = { a: 1 };

var source1 = { b: 2 };
var source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

1
2
3
4
5
6
7
var target = { a: 1, b: 1 };

var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

Object.assign只拷贝自身属性,不可枚举的属性(enumerable为false)和继承的属性不会被拷贝。

ES6+ 现在就用系列(七):Promise

回调地狱 (Callback Hell)

之前几乎所有的 JavaScript 使用 Callback 来处理异步的调用,这个在早期的JavaScript甚至是Node.js里到处可以见到一层层的Callback, 由于我们思维一般是线性的,每次看到这样的代码都理解起来有点费劲。我们看一下下面的实例:

1
2
3
4
fs.readFile('/a.txt', (err, data) => {
if (err) throw err;
console.log(data);
});

当我们只有一个异步操作时,还可以接受, 如果多个时就读起来比较费力了。

1
2
3
4
5
6
7
fs.readFile('a.txt', (err, data) => {        
if (err) throw err;
fs.writeFile('message.txt', data, (err) => {
if (err) throw err;
console.log('It\'s saved!');
});
});

再看一个, 加入我们要运行一个动画,下面每隔一秒,执行一个动画

1
2
3
4
5
6
7
runAnimation(0);
setTimeout(function() {
runAnimation(1);
setTimeout(function() {
runAnimation(2);
}, 1000);
}, 1000);


上面还好只有两级操作时还好,如果是10级呢?我们后面几行是一堆的括号,我们看着可能就有点晕了。

Promise

为了解决回调地狱(callback hell), ES6 原生提供了Promise对象。

Promise 是一个对象,用来传递异步操作的消息,这个和callback不同,callback是一个函数。

Promise对象的特性

  • 对象的状态不受外界影响。

    Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称Fulfilled)和Rejected(已失败), 只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

  • 状态一旦改变,再改变就不起作用了。

    Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。

  • Promise 无法取消

    一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误的不会向上传递。

用法

基本使用

Promise对象是一个构造函数,用来生成Promise实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
let promise = new Promise((resolve, reject) => {
console.log("promise start...");

//do something, such as ajax Async call
let age = 20;

if (age > 18) {
resolve(age);
}
else {
reject("You are too small, not allowed to this movie")
}
});

我们可以看到一旦构造了promise对象,就会立即执行, 所以上面代码立即输出:

1
promise start...

那么如何使用promise对象呢? promise对象提供了then方法,then方法接受两个回调方法,一个是处理成功,一个处理失败。

1
2
3
4
5
6
promise.then(
// success handler
(successData)=>{},

// error handler
(errMessage)=>{});

我们使用之前我们定义的promise对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
let promise = new Promise((resolve, reject) => {
console.log("promise start...");

//do something
let age = 20;

if (age > 18) {
resolve(age);
}
else {
reject("You are too small, not allowed to this movie")
}
});
1
2
3
4
5
6
7
8
promise.then(
//success
(age)=>{console.log(age)},

// error
(errMessage)=>{console.log(errMessage)});

输出:20

如果我们把 let age=20 改为 let age=16 , 那么将输出:

1
2
3
4
5
// let age = 20;
let age=16

输出;
You are too small, not allowed to this movie

链式调用

Promise对象返回的还是一个promise对象,所以我们就可以用 then 来链式调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
promise
.then((age)=>{
return `Your age is ${age}, so you can meet Cang Laoshi`;
})
.then((msg)=>{
console.log(`Congratulations! ${msg}`);
})
.then((msg)=>{
console.log("Please contact deshui.wang");
});

输出:

Congratulations! Your age is 20, so you can meet Cang Laoshi
Please contact deshui.wang

我们在then里面 也可以是一个异步操作,那么后面的then 将等待前一个promise完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
promise
.then((age)=>{
return `Your age is ${age}, so you can meet Cang Laoshi`;
})
.then((msg)=>{

setTimeout(()=>{
console.log(`Congratulations! ${msg}`);
},5000);
})
.then((msg)=>{
console.log("Please contact deshui.wang");
});

输出
Please contact deshui.wang
Congratulations! Your age is 20, so you can meet Cang Laoshi

可见上面的代码并不会等待setTimeOut执行完毕。如果我们想等五秒呢? 那么我们必须返回promise对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
promise
.then((age)=>{
return `Your age is ${age}, so you can meet Cang Laoshi`;
})
.then((msg)=>{
return new Promise((resolve, reject)=>{
setTimeout(()=>{
console.log(`Congratulations! ${msg}`);
resolve();
},5000);
});
})
.then((msg)=>{
console.log("Please contact deshui.wang");
});

输出:

Congratulations! Your age is 20, so you can meet Cang Laoshi
Please contact deshui.wang

可见,如果我们自己不返回promise对象,那么后一个then将立即执行!

错误处理

Promise.prototype.catch 方法是 .then(null, rejection)的别名,用于指定发生错误时的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let promise2=new Promise((resolve,reject)=>{
// success ,resolve
let age=16;
if(age>18)
{
resolve(age);
}
else{
// has error, reject
reject("this is error");
}
});

promise2.then((age)=>{console.log(age)})
.catch((errMsg)=>{
console.log(errMsg);
})

输出:
this is error

Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。如果没有使用catch方法指定错误处理的回调函数,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应。

catch方法返回的还是一个Promise对象,因此后面还可以接着调用then方法。

1
2
3
4
5
6
7
8
9
10
promise2.then((age)=>{console.log(age)})
.catch((errMsg)=>{
console.log(errMsg);
}).then(()=>{
console.log("end");
})

输出:
this is error
end

需要注意的是catch指捕捉之前的then, 后面的then调用出的错误是捕获不到的。

promise.all 并行调用

var p = Promise.all([p1, p2, p3]);

p的状态由p1、p2、p3决定,分成两种情况。

1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

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
let promise1=new Promise((resolve, reject)=>{
resolve(1);
})

let promise2=new Promise((resolve, reject)=>{
resolve(2);
})

let promise3=new Promise((resolve, reject)=>{
resolve(3);
})

let promise4=new Promise((resolve, reject)=>{
resolve(4);
})

var fourPromise=[promise1,promise2, promise3,promise4];

var p=Promise.all(fourPromise);

p.then((results)=>{
console.log(results[0]);
console.log(results[1]);
console.log(results[2]);
console.log(results[3]);
});

输出: 1,2,3,4

Promise.race()

1
var p = Promise.race([p1,p2,p3]);

只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的回调函数。

Promise.resolve

将现有对象转为Promise对象

1
2
3
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

Promise.reject()

1
2
3
4
5
6
7
8
var p = Promise.reject('出错了');
// 等同于
var p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s){
console.log(s)
});
// 出错了

Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。Promise.reject方法的参数reason,会被传递给实例的回调函数。

自定义方法

(注: 下面两个方法来自阮一峰)

Done 方法

Promise对象的回调链,不管以then方法或catch方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为Promise内部的错误不会冒泡到全局)。因此,我们可以提供一个done方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。

1
2
3
4
5
6
promise2.then((age)=>{console.log(age)})
.catch((errMsg)=>{
console.log(errMsg);
}).then(()=>{
console.log("end");
}).done();


done 方法的实现代码

1
2
3
4
5
6
7
Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch(function (reason) {
// 抛出一个全局错误
setTimeout(() => { throw reason }, 0);
});
};

finally 方法

finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。它与done方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。

1
2
3
4
5
6
7
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};

ES6+ 现在就用系列(六):解构赋值 (Destructuring )

定义

ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)

解构数组

在 ES5里我们需要这样赋值

1
2
3
4
5
6
7
// ES5
var point = [1, 2];
var x = point[0],
y = point[1];

console.log(x); // 1
console.log(y); // 2

那么在ES6 里我们可以简化为这样

1
2
3
4
5
6
// ES6
let point = [1, 2];
let [x, y] = point;

console.log(x); // 1
console.log(y); // 2

我们用这个特性很容易交换变量

1
2
3
4
5
6
7
8
9
10
11
12
'use strict';

let point = [1, 2];
let [x, y] = point;

console.log(x); // 1
console.log(y); // 2
// .. and reverse!
[x, y] = [y, x];

console.log(x); // 2
console.log(y); // 1

注意: node.js 目前还不支持解构赋值,所以我们可以用babel转换器来转换代码看看输出结果。

另外 babel 6.x以前的版本,默认开启了一些转换,但是 Babel 6.x 没有开启任何转换,我们需要显示地告诉应该转换哪些, 比较方便的方法是使用 preset, 比如 ES2015 Preset, 我们可以按如下方式安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
npm install gulp --save-dev
npm install gulp-babel --save-dev

npm install babel-preset-es2015 --save-dev

// gulpfile.js
var gulp=require('gulp'), babel=require('gulp-babel');

gulp.task('build',function(){
return gulp.src('./test.js')
.pipe(babel())
.pipe(gulp.dest('./build'))
})

// .babelrc
{
"presets": ["es2015"]
}

上面的代码用babel转换器转换后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use strict';

var point = [1, 2];
var x = point[0];
var y = point[1];

console.log(x); // 1
console.log(y); // 2
// .. and reverse!
var _ref = [y, x];
x = _ref[0];
y = _ref[1];

console.log(x); // 2
console.log(y); // 1

解构赋值时,我们可以忽略某些值

1
2
3
4
let threeD = [1, 2, 3];
let [a, , c] = threeD;
console.log(a); // 1
console.log(c); // 3

可以嵌套数组

1
2
3
4
5
let nested = [1, [2, 3], 4];
let [a, [b], d] = nested;
console.log(a); // 1
console.log(b); // 2
console.log(d); // 4

也可以解构赋值Rest变量

1
2
3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

如果解构不成功,变量的值就等于undefined。

1
2
3
4
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

解构赋值,赋给 var, let , const 定义的变量都可以

解构对象

对象的属性没有次序,变量必须与属性同名,才能取到正确的值

1
2
3
4
5
6
7
let point = {
x: 1,
y: 2
};
let { x, y } = point;
console.log(x); // 1
console.log(y); // 2

如果变量名与对象属性名不一样,那么必须像下面这样使用。

1
2
3
4
5
6
7
let point = {
x: 1,
y: 2
};
let { x: a, y: b } = point;
console.log(a); // 1
console.log(b); // 2

支持嵌套对象

1
2
3
4
5
6
7
8
9
10
11
12
13
let point = {
x: 1,
y: 2,
z: {
one: 3,
two: 4
}
};
let { x: a, y: b, z: { one: c, two: d } } = point;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
console.log(d); // 4

混合模式

可以嵌套对象和数组

1
2
3
4
5
6
7
8
9
10
let mixed = {
one: 1,
two: 2,
values: [3, 4, 5]
};
let { one: a, two: b, values: [c, , e] } = mixed;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
console.log(e); // 5

有了解构赋值,我们就可以模拟函数多返回值

1
2
3
4
5
6
7
8
9
10
11
12
function mixed () {
return {
one: 1,
two: 2,
values: [3, 4, 5]
};
}
let { one: a, two: b, values: [c, , e] } = mixed();
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
console.log(e); // 5

注意,如果我们解构赋值时,忽略var, let, const 那么就会出错因为block不能被解构赋值

1
2
3
4
let point = {
x: 1
};
{ x: a } = point; // throws error

但是,我们赋值时加上 let 或者把整个赋值语句用()括起来就可以了

1
2
3
4
5
let point = {
x: 1
};
({ x: a } = point);
console.log(a); // 1

字符串的解构赋值

1
2
3
4
5
6
7
8
9
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

let {length : len} = 'hello';
len // 5

函数参数的解构赋值

1
2
3
4
5
6
7
8
function add([x, y]){
return x + y;
}

add([1, 2]) // 3

[[1, 2], [3, 4]].map(([a, b]) => a + b)
// [ 3, 7 ]

函数参数也可以使用默认值

1
2
3
4
5
6
7
8
function move({x = 0, y = 0} = {}) {
return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

其它特性

解构赋值可以有默认值

1
2
3
4
5
var [x = 2] = [];
x // 2

[x, y = 'b'] = ['a'] // x='a', y='b'
[x, y = 'b'] = ['a', undefined] // x='a', y='b'

ES6内部使用严格相等运算符(===),判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。

如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined。

1
2
3
4
5
var [x = 1] = [undefined];
x // 1

var [x = 1] = [null];
x // null

如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

1
2
3
4
5
6
function f(){
return 2;
}

let [x = f()] = [1];
x // 1

上面的代码因为x能取到值,所以函数f不会执行。

默认值可以引用解构赋值的其他变量,但该变量必须已经声明

1
2
3
4
let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError


上面的最后一行代码 x 用到 y 是, y 还没有声明。

对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。

这个在我们阅读React-Native 相关文章时,下面的写法非常常见。

1
let { log, sin, cos } = Math;