ES6,你不得不学!

在没有学习 ES6 以前,学习 React,真的是一件很是痛苦的事情。即便以前你对 ES5 有着很好的基础,包括闭包、函数、原型链和继承,可是 React 中已经广泛使用 ES6 的语法,包括 modules、class、箭头函数等,还有 JSX 语法。因此,在学习 React 以前必定要先学习 ES6。javascript

关于 ES6 你必需要知道的一个教程,ECMAScript 6入门。这本书对于 ES6 的讲解很是详细,一步一步跟着来,绝对会对 ES6 的语法都了解到。html

学习 ES6,还要知道一个 ES6 的语法编译器,Babel。ES6 出来好久了,并非全部浏览器都支持,Babel 就能够把 ES6 代码转换成 ES5,让全部浏览器都支持你写的代码。Babel 内嵌了对 JSX 的支持,学习 React 必备。在线实验是一个 Babel 的在线编译器,能够用来练习 ES6 语法,并实时观测转换成 ES5 的代码效果。前端

准备工做作完了,接下来开始今天的主题,你不得不学的 ES6!java

箭头函数

讲真,自从出了箭头函数以后,不再用担忧 this 问题了,并且就简化代码这一方面来讲,箭头函数可谓是装逼神器。node

箭头函数有几点须要注意,若是 return 值就只有一行表达式,能够省去 return,默认表示该行是返回值,不然须要加一个大括号和 return。若是参数只有一个,也能够省去括号,两个则须要加上括号。好比下面的例子:python

var f = v => v*2;
// 等价于
var f = function(v){
  return v*2;
}

// 判断偶数
var isEven = n => n % 2 == 0;

// 须要加 return
var = (a, b) => {
  if(a >= b)
    return a;
  return b;
}

还有 this 的问题,我以为这篇文章说的很是好。普通函数的 this 是可变的,咱们把函数归为两种状态,一种是定义时,一种是执行时,若是仔细研究会发现,函数中的 this 始终是指向函数执行时所在的对象。好比全局函数执行时,this 执行 window,对象的方法执行时,this 执行该对象,这就是函数 this 的可变。而箭头函数中的 this 是固定的,看下面的例子:react

function obj(){
  setTimeout(()=>console.log(this.id), 20);
}
var id = 1;
obj.call({id: 2}); // 2

执行的结果是 2 而不是全局的 1,表示 setTimeout 函数执行的时候,this 指向的不是 window,这和普通函数是有区别的。es6

实际上,箭头函数并无 this 对象,将箭头函数转成 ES5 会发现:面试

// ES6
function obj() {
  setTimeout(()=>console.log(this.id), 20);
}

// ES5
function foo() {
  var _this = this;
  setTimeout(function () {
    console.log(_this.id);
  }, 20);
}

经过 call aply 等方法是没法绑定 箭头函数中的 this:正则表达式

var f = () => this.x;
var x = 1;
f.call({x: 2}); // 1

对 this 的一个总结就是 在对象的方法中直接使用箭头函数,会指向 window,其余箭头函数 this 会指向上一层的 this,箭头函数并无存储 this:

var obj = {
  id: 1,
  foo: ()=>{
    return this.id;
  }
}
var id = 2;
obj.foo(); // 2

除了 this 以外,箭头函数的 arguments 也是不存在,不能使用 new 来构造,也不能使用 yield 命令。

class

盼星星盼月亮,终于盼来了 JS 的继承。可是 ES6 中的继承和已经很完善的 ES5 中流行的继承库,到底有多少差别?

先来看一个例子:

//定义类
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  // 注意函数构造的方式
  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
var p1 = new Point(5, 5);
p1.toString(); //"(5, 5)"

typeof Point // function
p1.constructor == Point //true

直接使用 class 关键字,constructor 做为构造方法,函数能够直接 toString(){} 的方式。

可是,class 的本质仍然是函数,是构造函数的另一种写法。既然 class 的本质是函数,那么必不可少的一些 proto,prototype 方法也是存在的。

关于 class 的继承

经过关键字 extends 能够实现 class 的继承,

class Square extends Point{
  constructor(x){
    super(x, x);
  }
  toString(){
    return super.toString() + 'Square!';
  }
}
var s1 = new Square(4);
s1.toString(); //"(4, 4)Square!"
s1 instanceof Point // true
s1 instanceof Square // true

既然说到了继承,对 es5 中继承了解到小伙伴,确定会疑惑关于 class 中的 proto 和 prototype 是一个什么样的关系。

子类的 proto 指向父类,子类的 prototype 的 proto 指向父类的 prototype,这和 ES5 并无区别。

Square.__proto__ === Point
// true
Square.prototype.__proto__ === Point.prototype
// true

super 关键字

在 Java 等语言中,是有 super 继承父类函数,JS 中更加灵活,能够用做父类的构造函数,又能够用做对象。

子类的 constructor 必需要调用 super 方法,且只能在 constructor 方法中调用,其余地方调用会报错。

class A {
  constructor(a){
    this.x = a;
  }
}
A.prototype.y = 2;
class B extends A{
  constructor(a){
    super();
  }
  getY(){
    super() // 报错
    return super.y
  }
}

原生构造函数的继承

对于一些原生的构造函数,好比 Array,Error,Object,String 等,在 ES5 是没法经过 Object.create 方法实现原生函数的内部属性,原生函数内部的 this 没法绑定,内部属性得到不了。原生构造函数的继承

ES6 的 class 能够解决这个问题。

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1

arr.length = 0;
arr[0] // undefined

extends 关键字不只能够用来继承类,还能用来继承原生的构造函数,在原生函数的基础上,自定义本身的函数。

静态方法

ES6 支持 static 关键字,该关键字定义的方法,不会被实例继承,但能够被子类继承:

class A{
  static add(x, y){
    return x + y;
 }
}
A.add(1, 2);
var a = new A();
a.add()// error
class B extends A{}
B.add(2, 2)// 4

Module

ES6 以前,JS 一直没有 modules 体系,解决外部包的问题经过 CommonJS 和 AMD 模块加载方案,一个用于服务器,一个用于浏览器。ES6 提出的 modules (import/export)方案彻底能够取代 CommonJS 和 AMD 成为浏览器和服务器通用的模块解决方案。

关于模块,就只有两个命令,import 用于导入其余模块,export 用于输出模块。

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

// main.js
import {firstName, lastName, year} from './profile';
console.log(firstName, lastName) // Michael Jackson

import 加载的模块能够只加载用到的,可是必须使用同名的原则,能够用 as 来解决名字问题,一样,as 也能够解决 export 问题:

//main.js
import { lastName as surname } from './profile';
console.log(surname); // Jackson

//profile.js
export {firstName as name}

export 能够输出的内容不少,包括变量、函数、类,貌似均可以输出,还能够借助 export default 来加载默认输出。

//default.js
function add(a, b){
  return a + b;
}
export default add;
// 实际上
export {add as default};

// main.js
import add from './default'
//实际上 add 名字能够随便起
import {default as add} from './default'

模块加载的实质

这部分 ES6模块加载的实质 彻底只能参考了,由于对模块加载用的很少,没有一点经验,可是看到做者提到了拷贝和引用,感受逼格很高的样子。

ES6模块加载的机制,与CommonJS模块彻底不一样。CommonJS模块输出的是一个值的拷贝,而ES6模块输出的是值的引用。

好比一个 CommonJS 加载的例子:

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

这个值会被 mod 缓存,而取不到原始的值。

ES6 中不同,它只是生成一个引用,当真正须要的时候,才会到模块里去取值,

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

循环加载

循环加载也比较有意思,常常能看到 nodejs 中出现加载同一个模块,而循环加载却不常见,nodejs 使用 CommonJS 模块机制,CommonJS 的循环加载采用的是加载多少,输出多少,就像是咱们平时打了断点同样,会跳到另一个文件,执行完在跳回来。

//a.js
exports.done = '1';
var a = require('./b.js');
console.log('half a=%s', a);
exports.done = '3';
console.log('done a');

//b.js
exports.done = '2';
var b = require('./a.js');
console.log('half b=%s', b);
exports.done = '4';
console.log('done b');

//main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('all done! a=%s,b=%s',a,b)

node main.js 的结果:

half a=2
done a
half b=3
done b
all done! a=3,b=4

这就是 CommonJS 所谓的循环加载。

而 ES6 采用的加载模式也不同,由于使用动态引用,必需要开发者保证能 import 到值:

// a.js以下
import {bar} from './b.js';
console.log('a.js');
console.log(bar);
export let foo = 'foo';

// b.js
import {foo} from './a.js';
console.log('b.js');
console.log(foo);
export let bar = 'bar';

结果:

$ babel-node a.js
b.js
undefined
a.js
bar

循环加载稍有不慎,就会 underfined。

字符串模版

ES6 在字符串上面但是下了很多功夫,先是解决了字符 unicode 的 bug,增长了一些处理多字节字符串 codePointAt 函数,还多了字符串的遍历接口 for...of,这个遍历借口有点仿造 python 的感受。只要有迭代器功能的对象,均可以用 for...of 来遍历。

ES6 添加了一些有意思的函数,好比 repeat(),前几天比较火的文章‘五道经典的前端面试题’,就有提到一个在字符串上实现原生的重复方法,这里的 repeat 能够直接解决。

关于字符串上的新内容,很是有帮助的仍是模版字符串。以前在 js 中跨行的字符串实现起来很别扭,而 python 能够用三个反引号来实现。

ES6 中的模版字符串使用须要注意如下内容:

// ` 能够跨行
var html = `
  <ul>
    <li>first</li>
    <li>second</li>
  </ul>`

//${} 调用变量和函数
var name = 'window';
var str = `my name is ${name};`;
// my name is window;

var add = (a, b)=> a+b;
var str = `2 + 3 = ${add(2,3)}`;
// "2 + 3 = 5"

用过 ejs 、swig 或 hbs 等模版,它们能够嵌入 js 代码,ES6 的模版字符串也能够。使用 <%...%> 放置 JavaScript 代码,使用 <%= ... %> 输出 JavaScript 表达式。

var template = `
  <ul>
    <% data.forEach(function(item){ %>
      <li><%= item %></li>
    <% }) %>
  </ul>
`

下面就能够写正则表达式替换掉自定义字符并执行函数:

function compile(str){
  var evalExpr = /<%=(.+?)%>/g;
  var expr = /<%([\s\S]+?)%>/g;
  str = str.replace(evalExpr, '`); \n  join( $1 ); \n  join(`')
    .replace(expr, '`); \n $1 \n  join(`');
  str = 'join(`' + str + '`);';
  var script = `
    (function parse(data){
      var output = "";

      function join(html){
        output += html;
      }

      ${ str }

      return output;
    })
  `
  return script;
}
var strParse = eval(compile(template));
// 使用
var html = strParse(['shanghai', 'beijing', 'nanjing']);
//  <ul>    
//    <li>shanghai</li>
//    <li>beijing</li>
//    <li>nanjing</li>
//  </ul>

经过两次使用字符串模版,并使用 eval 函数,一个 ES6 简易模版就这样完成了。

一些其余核心功能

let const

ES5 经过 var 来申明变量,ES6 新添 let 和 const,且做用域是 块级做用域

let 使用和 var 很是相似,let 不存在变量提高,也不容许重复申明,let 的声明只能在它所在的代码块有效,好比 for 循环,很是适合使用 let:

for(let i = 0; i < data.length; i++){
  console.log(data[i]);
}
console.log(i); // error

若是用 var 来申明 i,最后不会报错。以前学闭包的时候,有一个利用闭包解决循环的问题,用 let 能够解决:

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

const 就是申明常量用的,一旦申明即被锁定,后面没法更改。

const PI = 3.14;
PI = 3; //error

let 和 const 都是块级做用域,块级做用域能够任意嵌套,且 {} 内定义的变量,外层做用域是没法得到的,且内外层的做用域能够同名。

function fn() {
  let n = 1;
  if (true) {
    let n = 2;
  }
  console.log(n); // 1
}

解构赋值

解构赋值真的很好用,可是我每次都忘记使用。ES6 解构赋值基本语法 var [a, b, c] = [1, 2, 3];,从数组中取值,并按照前后次序来赋值。若是解构赋值不成功,就会返回 underfined,解构赋值也容许指定默认值:

var [a, b] = [1];
b // undefined

// 指定默认值
var [a, b = 2] = [1];
b // 2

除了数组,对象也能够解构赋值,可是数组是有顺序的,而对象没有顺序,若是想要成功赋值,必须与对象属性同名,才能成功赋值,不然返回 underfined:

var {a, b} = {a: 1, b: 2};
a // 1
b // 2

var {a, c} = {a: 1, b: 2};
c // undefined

字符串的解构赋值比较有意思,既能够把字符串看成能够迭代的数组,又能够看成对象,好比:

var [a1,a2,a3,a4,a5] = 'hello';
a2 // e

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

函数参数的解构赋值,看一个 forEach 的例子:

var data = [[1, 2], [3, 4]];
data.forEach(([a, b]) => console.log(a+b));
// 3
// 7

Promise 解决回掉

一直以来,回掉问题都是一件使人头疼的事,调试的时候感受代码跳来跳去,玩着玩着就晕了。ES6 提供 Promise 对象(函数),专门用来处理回掉。

var promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 异步操做成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

resolve 和 reject 是两个异步操做调用函数,当异步操做完成时,调用 resolve,error 则调用 reject,这两个函数的功能就是把参数传递给回掉函数。then 函数用来处理成功或失败状态。

function loadImageAsync(url) {
  var p = new Promise(function(resolve, reject) {
    var image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(url);
    };

    image.src = url;
  });
  p.then(function(image){
    document.body.appendChild(image);
  }, function(url){
    throw new Error('Could not load '+ url);
  })
}

loadImageAsync('http://yuren.space/images/bg.gif');

上面是一个用 Promise 实现的异步加载图片的函数。

for of 与 ...

Python 中有 for in 运算符,ES6 就搞了个 for...of。当使用 for...of 循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。一种数据结构只要部署了 Iterator 接口,咱们就称这种数据结构是可遍历的,对象、数组、字符串都是可遍历的。

var str = 'hello';
for(let i of str){
  console.log(i);
}
// 'h' 'e' 'l' 'l' 'o'

...也很是好用,能够直接把可遍历对象直接转换成数组:

var str = 'hello';
[...str] //["h", "e", "l", "l", "o"]

let arr = ['b', 'c'];
['a', ...arr, 'd'] 
// ['a', 'b', 'c', 'd']

有了 ... 以后,方便对非数组可遍历的对象进行转换,好比 arguments 和 querySelectAll 的结果:

[...arguments] // Array

var selects = document.querySelectAll('a');
[...selects] // Array

set 集合和 Map 结构

ES6 新增 Set 集合对象,其实像其余语言早都支持了,不过,吃瓜群众,不觉明厉,之后,再遇到数组去重算法题,就能够:

[...(new Set([1, 2, 2, 3]))];
//[1, 2, 3]

Set 方法分为操做和遍历,操做方法有 add-添加成员, delete-删除成员, has-拥有判断返回布尔值, clear-清空集合。

遍历操做有 keys(),values(),entries(),forEach(),...,for of,map 和 filter 函数也能够用于 Set,不过要进行巧妙操做,先转换成数组,在进行操做:

let set = new Set([1,2,3]);
set = new Set([...set].map(a => a*2));
// Set {2, 4, 6}

Map 用来解决对象只接受字符串做为键名,Map 相似于对象,也是键值对集合,可是“键”的范围不限于字符串,各类类型的值(包括对象)均可以看成键。

Map 能够经过 [set、 get、 has、 delete] 方法来操做:

var m = new Map();
var arr = [1, 2];
m.set(arr, 'array');
m.get(arr); // 'array'

m.has(arr) // true
m.delete(arr) // true
m.has(arr) // false

参数默认

参数默认这个功能使用起来仍是比较方便的,之前参数都是经过 || 来实现默认,如今可使用默认参数。不过这个功能在 Python 等语言中已是支持的。

// 之前写代码
var sayHello = function(name){
  var name = name || 'world';
  console.log('hello ' + name);
}

//参数默认
var sayHello = function(name = 'world'){
  console.log('hello ' + name);
}

sayHello() // 'hello world'
sayHello('ES6') // 'hello ES6'

对于不定参数,之前都是对 arguments 对象处理,且 arguments 对象仍是个伪数组,如今方便了:

var add = function(...arr){
  console.log(arr.constructor.name) // Array
  return arr.reduce((a, b) => a+b, 0);
}
add(1,2,3) // 6

总结

总之,对于 ES6 的学习仍是要活用,当我看了一遍 ECMAScript 6入门时候,感受知识点仍是不少,有点乱。当接触了 react 以后,发现不少语法都很是的熟悉,因而就从头温习了 ES6,并整理了这篇文章。可能,你还不知道,这篇文章,大部分都是参考阮一峰老师的。共勉!

参考

ECMAScript 6入门
30分钟掌握ES6/ES2015核心内容(上)
30分钟掌握ES6/ES2015核心内容(下)
ES6 学习笔记
ES6新特性概览
ES6js.com 官网

欢迎来我博客交流。

相关文章
相关标签/搜索