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;	

ES6+ 现在就用系列(五):模板字面量 (Template Literals)

模板字面量

字符串替换

这个和C#6 里面的字符串插值类似。原来ES5里字符串要连接,一般就是用+

特性

  1. 用反引号(`)标识, 它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

  2. 如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

  3. 如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。

  4. 大括号内部可以放入任意的JavaScript表达式,可以进行运算,以及引用对象属性。

  5. 模板字符串之中还能调用函数。

示例代码:

简单字符串替换

1
2
3
4
var name = "Brendan";
console.log(`Yo, ${name}!`);

// => "Yo, Brendan!"

表达式

1
2
3
4
5
6
7
8
var a = 10;
var b = 10;
console.log(`JavaScript first appeared ${a+b} years ago. Crazy!`);

//=> JavaScript first appeared 20 years ago. Crazy!

console.log(`The number of JS MVC frameworks is ${2 * (a + b)} and not ${10 * (a + b)}.`);
//=> The number of JS frameworks is 40 and not 200.

函数

1
2
3
function fn() { return "I am a result. Rarr"; }
console.log(`foo ${fn()} bar`);
//=> foo I am a result. Rarr bar.

$() 可以使用任何表达式和方法调用

1
2
3
4
5
6
7
8
9
10
var user = {name: 'Caitlin Potter'};
console.log(`Thanks for getting this into V8, ${user.name.toUpperCase()}.`);

// => "Thanks for getting this into V8, CAITLIN POTTER";

// And another example
var thing = 'drugs';
console.log(`Say no to ${thing}. Although if you're talking to ${thing} you may already be on ${thing}.`);

// => Say no to drugs. Although if you're talking to drugs you may already be on drugs.

示例代码:

ES5:

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

var customer = { name: "Foo" };
var card = { amount: 7, product: "Bar", unitprice: 42 };

var message = "Hello " + customer.name + ",\n" +
"want to buy " + card.amount + " " + card.product + " for\n" +
"a total of " + (card.amount * card.unitprice) + " bucks?";

console.log(message);

输出:

Hello Foo,
want to buy 7 Bar for
a total of 294 bucks?

ES6:

1
2
3
4
5
6
7
8
9
10
11
var customer = { name: "Foo" }
var card = { amount: 7, product: "Bar", unitprice: 42 }
message = `Hello ${customer.name},
want to buy ${card.amount} ${card.product} for
a total of ${card.amount * card.unitprice} bucks?`

输出:

Hello Foo,
want to buy 7 Bar for
a total of 294 bucks?

Tagged Templates (标签模板?不知道如何翻译)

比如

1
fn`Hello ${you}! You're looking ${adjective} today!`

实际上等于

fn(["Hello ", "! You're looking ", " today!"], you, adjective);

fn可以是任何函数名,也就是把字符串分解传到到方法的第一个参数里,第一个参数必须是数组,数组的每一项,就是被$()分开的没一串字符, 每一个$()里面的值将传给函数的剩余参数。等于下面函数定义,strings是一个数组,values是Rest参数。

1
fn(strings, ...values)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = 5;
var b = 10;

function tag(strings, ...values) {
console.log(strings[0]); // "Hello "
console.log(strings[1]); // " world "
console.log(values[0]); // 15
console.log(values[1]); // 50

return "Bazinga!";
}

tag`Hello ${ a + b } world ${ a * b }`;
// "Bazinga!"

有了 tagged template 我们可以让代码看起来更简洁,比如我们可以把下面的调用

1
get([ "http://example.com/foo?bar=", "&quux=", "" ],bar + baz, quux);

用新的写法

1
get`http://example.com/foo?bar=${bar + baz}&quux=${quux}`

String.raw

存取 raw template string, 就是如果遇见\将增加一个,然后原样输出。

1
2
3
4
5
6
let interpreted = 'raw\nstring';
let esaped = 'raw\\nstring';
let raw = String.raw`raw\nstring`;
console.log(interpreted); // raw
// string
console.log(raw === esaped); // true

ES6+ 现在就用系列(四):箭头函数 =>

箭头函数 =>

ES6 允许使用 => 来定义函数, 他是函数的缩写,这个熟悉C#的人应该了解,这其实就是C#里的lamda表达式

他不只是语法糖 (Syntax sugar), 箭头函数自动绑定 定义此函数作用域的this(Arrow functions automatically bind “this” from the containing scope.)

** 箭头函数没有自己的this,所以内部的this就是外层代码块的this。**

定义格式

1
(<arguments>) => <return statement>

当只有一个参数时,括号可省略,下面两种写法是等价的.

1
2
(x) => x * x
x => x * x

示例代码

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
'use strict';
// 数组
const items = [1, 2, 3, 4];

// lamda 表达式
let byTwo = items.map(i => i * 2);

// 可以使用block
let byFour = items.map(i => {
return i * 2;
});

// 绑定this
function Person() {
this.company = "deshui.wang";
this.Names = ["Jack", "Alex", "Eric"];
this.print = () => {
return this.Names.map((n) => {
return n + " is from " + "company "+ this.company;
});
};
}

console.log(new Person().print());

// 输出:
[
'Jack is from company deshui.wang',
'Alex is from company deshui.wang',
'Eric is from company deshui.wang'
]

注意事项

  1. 箭头函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象, 原因是箭头函数没有自己的 this.

  2. 不可以当作构造函数,不可以使用 new 命令。

  3. 不可以使用 arguments 对象,该对象在函数体内不存在。可以用 Rest 参数代替。

  4. 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。

  5. arguments、super、new.target 在在箭头函数之中是不存在的,他们指向外层函数的对应变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
        function hello() {
    setTimeout( () => {
    console.log("args:", arguments);
    },100);
    }

    hello( 1, 2, 3, 4 );

    // 输出 1, 2, 3, 4
  6. 箭头函数没有自己的 this,所以不能用call()、apply()、bind()这些方法去改变 this 的指向。

ES6+ 现在就用系列(三):const 命令

本文以及以后讨论的代码,都必须是在严格模式下,因为非严格模式下,有一些写法也符合,所以我们建议代码始终使用严格模式

定义

在之前的ES版本里是没有常量的概念的,常量,就是一旦申明,值就不能改变的。

1
2
3
4
5
'use strict';
const PI = 3.1415;
console.log(PI) // 3.1415

PI = 3; // TypeError: Assignment to constant variable.

特性

  • 一旦申明,必须初始化

  • 作用域只在声明所在的块级,和let相同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
        'use strict';
    const apiBase = "https://deshui.wang/api/v1/";
    const clientId = "123456";

    //block scoped
    if (true) {

    const apiBase = "https://cnblogs.com/api/";

    console.log(apiBase + clientId);
    // https://cnblogs.com/api/123456
    }

    console.log(apiBase+clientId);
    // https://deshui.wang/api/v1/123456

    apiBase = "https://google.com/api";
    //Identifier 'apiBase' has already been declared
  • const 申明的变量,在一个作用域内也不能与let和var申明的重名

  • 如果 const 申明的是个复合类型的变量,那么变量名不指向数据,而是指向数据所在的地址。const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变。

    1
    2
    3
    4
    5
    6
    7
    8
        'use strict';        
    const a = [1, 2];
    a.push(3);
    console.log(a); // 1,2,3
    a.length = 0;
    console.log(a); // []

    a = [4]; // TypeError: Assignment to constant variable.

全局变量

全局对象是最上层层的对象,在浏览器里指的是window对象,在Node.js指的是global对象。

1
2
3
4
5
// 'use strict';
var a="hello";
console.log(global.a);

// 输出: undefined

var 命令和 function 命令声明的全局变量,依旧是全局对象的属性;let命令、const命令、class命令声明的全局变量,不属于全局对象的属性。

上面的代码在Node.js下是不行的,但是浏览器却可以,不管是不是严格模式。

1
2
3
4
5
// 'use strict';
var a="hello";
console.log(window.a);

// 输出: hello

但是,如果使用let, 那么属性将不绑定到window (Chrome developer tools 需要使用以下方法才能打开严格模式)

1
2
3
4
5
6
7
8
(function(){
'use strict'
let a="hello";
console.log(window.a);

})()

// 输出undefined

ES6+ 现在就用系列(二):let 命令

ES6新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。也就是有了块级作用域。

为什么需要块级作用域?

避免 var 变量提升带来的副作用

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var saleCount = 20;

function f(){
console.log(saleCount);
if(saleCount<100)
{
// according some rule, change it to 100
var saleCount=60;
console.log(saleCount);
}
}

f()

输出: // undefined

因为 “var saleCount=60;” 作用域是整个函数,而JavaScript里var定义的变量存在变量提升,也就是console.log(saleCount), 这个saleCount是 “var saleCount=60;” 这一句定义的,当调用的时候,saleCount的值是undefined. 实际上等于下面代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var saleCount = 20;

function f(){

var saleCount;
console.log(saleCount);
if(saleCount<100)
{
// according some rule, change it to 100
saleCount=60;
console.log(saleCount);
}

}

f() // undefined

避免循环变量变为全局变量

1
2
3
4
5
6
7
8
9
10
示例:

for (var i = 0; i < 10; i++){
// do something
}

console.log(i);
输出: 10

很明显,我们不希望i,这个变量变为全局变量。

let 示例代码

1
2
3
4
5
6
7
8
9
10
'use strict'
{
var b=1;
let a=2;
}

console.log(a);
console.log(b);

# 输出: ReferenceError: a is not defined

上一节我们给出了如下的示例:

1
2
3
4
5
6
7
8
9
10
11
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[1]();
a[2]();
a[3]();

输出: 10,10,10

我们看到,输出的结果不是我们想要的,因为i是用var定义的,那么他在全局范围内都是生效的,也就是我们循环结束以后,i的值就是10,那么不管调用数组的那个元素,console.log(i) 输出的都是10, 那么let因为有了块级作用域,就可以避免这个问题。

1
2
3
4
5
6
7
8
9
10
11
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[1]();
a[2]();
a[3]();

输出 1, 2,3

另外,函数本身的作用域也在定义他的块的作用域内。

1
2
3
4
5
6
7
function hello(){console.log("Hello, Jack")};

{
function hello(){console.log("Hello, Tom")};
}

hello();

上面的代码在ES6里面输出了”Hello, Jack”, 而在ES5里输出了”Hello, Tom”.

注意事项

不能先使用,后定义

1
2
3
4
5
6
7
8
9
console.log(x);
console.log(y);

var x = 1;
let y = 2;

# 输出
undefined
ReferenceError: y is not defined

上面的代码由于x是var定义的,一开始x的变量是存在的,只是值是undefined, 但是由于y 是let定义的,就不存在变量提升。

暂时性死区

如果一个变量是使用let定义的,那么这个变量就属于声明时所在的代码块,也就是变量不再受外部影响,下面的a 由于在块里定义了,所以 会报错,因为在那个块里是先使用后定义,如果去掉“let a”, 那么a就是外部的变量,这个时候就不会出错。

1
2
3
4
5
6
7
8
var a = "hello";

{
a = 'world';
let a;
}

// ReferenceError

不能重复申明

也就是不能重复申明同一个变量,即使一个是let申明,一个是用var申明也不行。 下面的代码都会报错。

1
2
3
4
function () {
let a = 10;
var a = 1;
}
1
2
3
4
function () {
let b = 10;
let b = 1;
}

总结

由于let 避免了很多问题,所以建议在ES6的代码里总是使用let 来替代var.

ES6+ 现在就用系列(一):为什么使用ES6+

ES6+

现在主流的浏览器都是支持到ES5, 为了表述方便,我在此发明一个名词”ES6+” 就是ES5以后的版本,包括ES6, ES7. 为什么说现在就用,虽然主流的浏览器只支持到ES5, 但是现在有很多的转换器,可以把一些ES6和ES7的代码转换为ES5的代码。这就意味着我们现在就可以使用这些新特性,然后使用转码器让代码可以运行在主流的浏览器上。

为什么立即开始使用ES6, ES7的新特性?

JavaScript语言的一些糟糕的实现

先不说JavaScript语言本身设计是否有问题,现有JavaScript语言的实现里有很多非常糟糕或者诡异的实现,就是你以为代码的结果是这样,但是他偏偏是那样,这给我们程序带了很多的意向不到的Bug和烦恼,如果你要是JavaScript大牛,你需要了解他内部的实现的Bug, 而且要知道哪些诡异的写法输出了什么诡异的结果,我个人对了解这种东西实在提不起太大的兴趣,因为我只想用“语言”来实现我的项目让人很好的使用我开发的软件,但是由于历史这样或那样的原因,导致JavaScript语言成为浏览器的霸主,我们不得不忍受这些糟糕的问题。下面我来展示一些让你觉得诡异的问题 (如果你不不觉得诡异,恭喜你,你已经是JavaScript的“高手”)

示例1:

1
2
3
4
5
(function() {
return NaN === NaN;
})();

输出: false

示例2:

1
2
3
4
5
(function() {
return (0.1 + 0.2 === 0.3);
})();

输出: false

示例3:

1
2
3
[5, 12, 9, 2, 18, 1, 25].sort();

输出: [1, 12, 18, 2, 25, 5, 9]

示例4:

1
2
3
4
5
6
7
8
9
10
11
var a = "1"
var b = 2
var c = a + b

输出:c = "12"

var a = "1"
var b = 2
var c = +a + b

输出:c = 3

示例5:

1
2
3
4
5
(function() {
return ['10','10','10','10'].map(parseInt);
})();

输出: [10, NaN, 2, 3]

示例6:

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

输出: 10000000000000000

示例7:

1
2
3
4
5
6
7
8
9
10
11
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[1]();
a[2]();
a[3]();

输出: 10,10,10

我是觉得如果按正常人的理解,代码不能得到想要的结果,那就算是语言本身的问题。如果一个程序执行的和人期望的不一样,或者还需要一些Hack的方法,那么是很糟糕的。

ES5 一些语言特性的缺失

由于上面的很多问题,所以ES 需要不断的改进, 当然新的版本肯定不可能一下子解决之前所有的问题。

已有JavaScript的问题这一块就不细说了,因为能来看这篇文章的人,应该对下面我列的几个突出的问题都有感受。

  • 没有块级作用域,这个导致上面示例7的问题
  • 全局变量的污染
  • 类的写法比较怪异
  • 没有模块管理
  • 异步调用写法容易产生 “回调地狱”

为什么可以立即使用?

因为现在很多转换器已经可以把ES6所有的特性以及ES7的部分特性转换为ES5,Babel就是一个非常好的转换器,所以我这里建议凡是能被Babel转换的新特性都可以立即在项目里适用。

ES6和ES7的一些新特性,可以大大提高项目的健壮性,同时让代码更易读,同时也可以避免很多ES5之前的很多诡异的东西。Gulp里可以很好的使用babel, 如果你对Gulp不熟悉,可以参考我博客里的Gulp系列。

这里简单说一Gulp和babel如何结合使用

1
2
3
4
5
6
7
8
9
$ npm install -g gulp-babel

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

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


后面的系列,我将以此介绍ES6, ES7的一些可以现在就用的主要特性。