ECMAScript6(简称ES6)是 JavaScript 语言的下一代标准。在2015年6月正式发布,所以又称ES2015。
由于目前并不是所有浏览器都能兼容 ES6 全部特性,但是 ES6 在实际项目中的广泛使用已成为一种趋势。所以作为一个前端开发者,ES6 的语法是我们必须掌握的。
在讲解 ES6 语法之前,我们得先了解下Babel。
Babel
Babel是一个广泛使用的ES6转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。可在Babel官网 (http://babeljs.io/) 查看:
<center>
使用Gulp 和 Babel 将 ES6 代码转换成 ES5 代码具体过程如下:
安装依赖
- 安装全局 Gulp
npm install -g gulp
- 安装项目中使用的 Gulp
npm install --save-dev gulp
- 安装 Gulp 上 Babel 的插件
npm install --save-dev gulp-babel
- 安装 Babel 上将 ES6 转换成 ES5 的插件
npm install --save-dev babel-preset-es2015
Gulp 配置
- gulpfile.js 的内容
var gulp = require("gulp"); var babel = require("gulp-babel");
gulp.task("default", function () {
return gulp.src("src/*/.js") // ES6 源码存放的路径
.pipe(babel())
.pipe(gulp.dest("dist")); //转换成 ES5 存放的路径
});
2. 如果要生成 Soucemap, 则用 gulp-sourcemaps
```javascript
var gulp = require("gulp");
var sourcemaps = require("gulp-sourcemaps");
var babel = require("gulp-babel");
var concat = require("gulp-concat");
gulp.task("default", function () {
return gulp.src("src/**/*.js")
.pipe(sourcemaps.init())
.pipe(babel())
.pipe(concat("all.js"))
.pipe(sourcemaps.write("."))
.pipe(gulp.dest("dist"));
});
Babel 配置
在项目根路径创建文件 .babelrc。内容为:
{
"presets": ["es2015"]
}
执行转换
命令行中执行
gulp
ES6常用特性
let
, const
, class
, extends
, super
, arrow functions
, template string
, destructuring
, default
, rest arguments
这些是ES6最常用的几个语法,基本上学会它们,就可以满足我们日常的使用!下面就用用最通俗易懂的语言和例子来讲解它们。
let 与 const
这两个的用途与var类似,都是用来声明变量的,但在实际运用中都有各自的特殊用途。
ES5只有全局作用域、函数作用域和eval作用域,没有块级作用域,这带来很多不合理的场景。
第一种,变量提升:就是var关键字声明变量。无论声明在何处,都会被视为声明在函数的最顶部;不在函数内即在全局作用域的最顶部。这样就会引起一些误解。
console.log(a); // undefined
var a = 3;
// 等价于
var a;
console.log(a);
a = 3;
而 let 就不会被变量提升
console.log(a); // a is not defined
let a = 3;
第二种场景就是内层变量覆盖外层变量。例如:
var name = 'Faker'
while (true) {
var name = 'Davis'
console.log(name) //Davis
break
}
console.log(name) //Davis
而let则实际上为JavaScript新增了块级作用域。用它所声明的变量,只在let命令所在的代码块内有效。
let name = 'Faker'
while (true) {
let name = 'Davis'
console.log(name) //Davis
break
}
console.log(name) //Faker
第三种场景就是用来计数的循环变量泄露为全局变量,例如:
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
上面代码中,变量 i 是 var 声明的,在全局范围内都有效。所以每一次循环,新的 i 值都会覆盖旧值,导致最后输出的是最后一轮的i的值。而使用 let 则不会出现这个问题。
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
const也用来声明变量,但是声明的是常量。一旦声明,常量的值就不能改变。
const PI = 3.1415926;
PI = 8;
console.log(PI); // invalid assignment to const `PI'
当我们尝试去改变用const声明的常量时,浏览器就会报错。const有一个很好的应用场景,就是当我们引用第三方库的时声明的变量,用const来声明可以避免未来不小心重命名而导致出现bug:
const monent = require('moment')
Set 和 Map 数据结构
与 Array 增、删、改、查对比
let map = new Map();
let set = new Set();
let array = [];
// 增
map.set('t', 1);
set.add( { t : 1 } );
array.push( { t:1 } );
console.info( map, set, array ); // Map { 't' => 1 } Set { { t: 1 } } [ { t: 1 } ]
// 查
let map_exist = map.has( 't' );
let set_exist = set.has( {t:1} );
let array_exist = array.find(item => item.t)
console.info(map_exist, set_exist, array_exist); //true false { t: 1 }
// 改
map.set('t', 2);
set.forEach(item => item.t ? item.t = 2:'');
array.forEach(item => item.t ? item.t = 2:'');
console.info(map, set, array); // Map { 't' => 2 } Set { { t: 2 } } [ { t: 2 } ]
// 删
map.delete('t');
set.forEach(item => item.t ? set.delete(item):'');
let index = array.findIndex(item => item.t);
array.splice(index,1);
console.info(map, set, array); // Map {} Set {} []
class、extends、super
ES5中原型、构造函数,继承问题一直困扰我们。ES6引入了Class(类)的概念。新的class写法让对象原型的写法更加清晰、更像面向对象编程的语法,也更加通俗易懂。
class Human {
constructor(name) {
this.name = name;
}
sleep() {
console.log(this.name + " is sleeping");
}
}
let man = new Human("Davis");
man.sleep(); //Davis is sleeping
class Boy extends Human {
constructor(name, age) {
super()
this.name = name;
this.age = age;
}
info(){
console.log(this.name + 'is ' + this.age + 'years old');
}
}
let son = new Boy('Faker','8');
son.sleep(); // Faker is sleeping
son.info(); // Faker is 8 years old
上面代码首先用class定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。简单地说,constructor内定义的方法和属性是实例对象自己的,而constructor外定义的方法和属性则是所有实例对象可以共享的。
Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。上面定义了一个Boy类,该类通过extends关键字,继承了Human类的所有属性和方法。
super关键字,它指代父类的实例(即父类的this对象)。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
ES6的继承机制,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
arrow function 箭头函数
ES6中新增的箭头操作符 =>
简化了函数的书写。操作符左边为输入的参数,而右边则是进行的操作以及返回的值,可以说是 ES6 最最常用的一个新特性了,看下面的实例:
// ES5
let arr = [1, 3, 5, 7, 9];
arr.forEach (function(val, key) {
return console.log(val, key);
});
// ES6
let arr = [1, 3, 5, 7, 9];
arr.forEach (
(val, key) => console.log(val, key)
);
JavaScript语言的this对象指向是一个头疼的问题,在对象方法中使用this,必须非常小心。
class Human {
constructor(name) {
this.name = name;
}
sleep() {
setTimeout(function(){
console.log(this.name + " is sleeping");
}, 1000)
}
}
let man = new Human("Davis");
man.sleep(); // is sleeping
上面代码之所以不能输出 Davis,这是因为setTimeout中的this指向的是全局对象。为了让它能够正确的运行,传统的解决方法有两种:
// 第一种,将this传给self,再用self来指代this
sleep() {
var self = this;
setTimeout(function(){
console.log(self.name + " is sleeping");
}, 1000)
// 第二种,是用bind(this)
sleep() {
setTimeout(function(){
console.log(self.name + " is sleeping")
}.bind(this), 1000)
使用箭头函数,非常方便:
class Human {
constructor(name){
this.name = name;
}
sleep() {
setTimeout( () => {
console.log(this.name + "is sleeping");
}, 1000)
}
}
let man = new Human("Davis");
man.sleep(); // Davis is sleeping
当我们使用箭头函数时,函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,它的this是继承外面的,因此内部的this就是外层代码块的this。
template string 模板字符串
ES6中允许使用反引号 ` 来创建字符串,此种方法创建的字符串里面可以包含由美元符号加花括号包裹的变量${vraible}。
let num = Math.random();
console.log(` num is ${num}`); // num is xx
不再通过 \ 来做多行字符串拼接,模板字符串可以多行书写:
$("#main").html(`
<h1>今天天气很好!</h1>
<p>产生一个随机数${num}</p>
`);
模板字符串中所有的空格、新行、缩进,都会原样输出在生成的字符串中。
destructuring 解构
ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构。
用途一,交换变量的值,不再需要中间变量
let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
用途二,提取JSON数据
let jsonData = {
id: 1,
title: "OK",
data: [5, 6]
};
let {id, title, data:number} = jsonData;
console.log(id, title, number); // 1, "OK", [5, 6]
用途三,函数参数的定义
// 参数是一组有次序的值
function f([x, y, z]) {
...
}
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) {
...
}
f({z: 3, y: 2, x: 1});
default默认参数
default很简单,就是默认值。现在可以在定义函数的时候指定参数的默认值,而不用像以前那样通过逻辑或操作符来达到目的了。
// 传统指定默认参数
function say1(name) {
var name = name || 'Faker';
console.log( 'Hello ' + name );
}
// ES6默认参数
function say2(name='Davis') {
console.log(`Hello ${name}`);
}
say1(); // Hello Faker
say1('Tom'); // Hello tom
say2(); //Hello Davis
say2('Bob'); // Hello Bob
注意: say2(name='tom')这里的等号,指的是没有传这个参数,则设置默认值Davis,而不是给参数赋值。
rest参数
rest参数只包括那些没有给出名称的参数;
rest参数是Array的实例,可以直接应用sort, map, forEach, pop等方法;
rest参数之后不能再有其它参数(即,只能是最后一个参数);
函数的length属性,不包括rest参数;
function fn(x, y, ...rest){
console.log(rest)
}
fn(1, "cat", "dog", 2); //["dog", 2]
console.log(fn.length); //2
Proxy 代理
Proxy可以监听对象身上发生了什么事情,并在这些事情发生后执行一些相应的操作。让我们对一个对象有了很强的追踪能力,同时在数据绑定方面也很有用处。
//定义被监听的目标对象
let man = { name: 'Davis', age: 21 };
//定义处理程序
let handle = {
set(receiver, property, value) {
console.log(property, 'is changed to', value);
receiver[property] = value;
}
};
//创建代理以进行侦听
man = new Proxy(man, handle);
//做一些改动来触发代理
man.age = 22; //age is change to 22
man.name = "Faker"; // name is change to Faker
Promise
Promise对象状态
Promise/A+规范, 规定Promise对象是一个有限状态机。它三个状态:
- pending(执行中)
- Resolved(已完成)
- Rejected(已失败)
其中pending为初始状态,Resolved和rejected为结束状态(表示promise的生命周期已结束)。
let val = 1;
// 我们假设step1, step2, step3都是ajax调用后端或者是在Node.js上查询数据库的异步操作
// 每个步骤都有对应的失败和成功处理回调
// step1、step2、step3必须按顺序执行
function step1(resolve, reject) {
console.log('步骤一:执行');
if (val >= 1) {
resolve('Hello I am No.1');
} else if (val === 0) {
reject(val);
}
}
function step2(resolve, reject) {
console.log('步骤二:执行');
if (val === 1) {
resolve('Hello I am No.2');
} else if (val === 0) {
reject(val);
}
}
function step3(resolve, reject) {
console.log('步骤三:执行');
if (val === 1) {
resolve('Hello I am No.3');
} else if (val === 0) {
reject(val);
}
}
new Promise(step1).then(function(val){
console.info(val);
return new Promise(step2);
}).then(function(val){
console.info(val);
return new Promise(step3);
}).then(function(val){
console.info(val);
return val;
}).then(function(val){
console.info(val);
return val;
});
// 执行之后将会打印
步骤一:执行
Hello I am No.1
步骤二:执行
Hello I am No.2
步骤三:执行
Hello I am No.3
Hello I am No.3
常用关键点:
-
在Promise定义时,函数已经执行了;Promise构造函数只接受一个参数,即带有异步逻辑的函数。这个函数在 new Promise 时已经执行了。只不过在没有调用 then 之前不会 resolve 或 reject。
- 在then方法中通常传递两个参数,一个 resolve 函数,一个 reject 函数。reject就是出错的时候运行的函数。resolve 函数必须返回一个值才能把链式调用进行下去。
- resolve 返回一个新 Promise
返回一个新Promise之后再调用的then就是新Promise中的逻辑了。 - resolve 返回一个值
返回一个值会传递到下一个then的resolve方法参数中。
- resolve 返回一个新 Promise
Generator
Generator函数跟普通函数的写法有非常大的区别:
- function关键字与函数名之间有一个 *;
- 函数体内部使用yield语句,定义不同的内部状态;
function* f() {
yield 'a';
yield 'b';
yield 'c';
return 'ending';
}
let fn = f();
console.log(fn.next()); // { value: 'a', done: false }
console.log(fn.next()); // { value: 'b', done: false }
console.log(fn.next()); // { value: 'c', done: false }
console.log(fn.next()); // { value: 'ending', done: true }
第一次输出fn.next()返回一个简单的对象{value: "a", done: false},'a'就是f函数执行到第一个yield语句之后得到的值,false表示f函数还没有执行完,只是在这暂停。
第二次,返回的就是{value: "b", done: false},说明f函数运行到了第二个yield语句,返回的是该yield语句的返回值'b'。返回之后依然是暂停。
第三次,第四次同理,这样整个f函数就运行完毕了。
异步操作的同步化写法
Generator函数的暂停执行的效果,意味着可以把异步操作写在yield语句里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield语句下面,反正要等到调用next方法时再执行。所以,Generator函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
例子:假设我在测试服务器的某目录建了4个文件,分别是'test.html'、'a.html'、'b.html'、'c.html',后三个文件的文件内容跟文件名相同,现在我编辑'test.html'的代码,想要先ajax-get相对网址'a.html',然后再回调里ajax-get相对网址'b.html',然后在回调里ajax-get相对网址'c.html'。
function req(url) {
$.get(url, function(res){
it.next(res);
});
}
// 生成器函数
function* ajaxs() {
console.log(yield req('a.html'));
console.log(yield req('b.html'));
console.log(yield req('c.html'));
}
var it = ajaxs(); // 遍历器对象
it.next();
// a.html
// b.html
// c.html
强调:只有当yield后面跟的函数先执行完,无论执行体里面有多少异步回调,都要等所有回调先执行完,才会执行等号赋值,以及再后面的操作。这也是yield最大的特性。
export、import
export用于对外输出本模块(一个文件可以理解为一个模块)变量的接口;
import用于在一个模块中加载另一个含有export接口的模块。
导出一组对象
导出模块文件app.js:
class Human{
constructor(name) {
this.name = name;
}
sleep() {
console.log(this.name + " is sleeping");
}
}
function walk() {
console.log('i am walking');
}
function play() {
console.log('i am playing');
}
export { Human, walk }
模块导出了两个对象:Human类和walk函数,能被其他文件使用。而play函数没有导出,为此模块私有,不能被其他文件使用。
main.js导入app.js模块
import { Human, walk } from 'app.js';
Default导出
使用关键字default,可将对象标注为default对象导出。default关键字在每一个模块中只能使用一次。
... //类,函数等
export default App;
main.js导入app.js模块
import App from 'app.js';
结语
以上这些是ES6最常用的几个语法,基本上学会它们,就可以满足我们日常的使用!ES6的新语法特性让前端和后端的差异越来越小了,我们必须要了解这些新的前沿知识(虽然ES8已经到来,但我相信目前还是ES6的时代),才能更好运用于项目中。
个人博客:http://www.dpfblog.com/
项目源码:https://github.com/pengfeidai/lottery-es6
热门评论
这是我这两天看到的关于es6解释的最好的一篇文章!!
这是我这两天看到的关于es6解释的最好的一篇文章!!