踏歌

ES6/ES2015 小菜一碟

踏歌 · 2016-11-24翻译 · 1336阅读 原文链接

ES6/ES2015 小菜一碟

js-logo

最新的Javascrip迭代即将来临。截至2015年六月 es2015 ES6 /规格已获得批准,因此,你的浏览器将会支持大量的新特性和语法。

让我们从新版本中拆分一些小菜一碟的知识点.当我说小菜一碟时,这意味着那些知识不需要大量的研究就可以轻松上手。例如我不会在这里讲生成器和展开运算符,我认为(解释)这些需要一个更深入的文章。

我们会在这篇文章中讲到下面的这些新特性:

  • let

  • const

  • Template Strings

  • Classes

  • Arrow Functions

  • Promises

let

在目前的JS中,我们有函数作用域。这意味着如果变量在一个函数中通过var关键字创建,那么它和这个函数是绑定的。在这个函数外面,它是不可被访问的。它可以在创建该变量的函数内部,以及此函数内部创建的函数内部被访问。

在一些其他语言中也存在块级作用域的理念。在{}(或者称之为块)中的声明的任何东西,其作用域都只会和这个块绑定,所以我们可以有不依赖于函数的作用域。

像下面这样的for循环在JS中比比皆是:

for(var i = 0; i < 10; i++) {
  console.log(i);
}
console.log('After:', i); //10

你最初的想法大概会是这样的,var i=0创建了一个i变量,这个变量的作用域仅仅是在这个语句中。然而这并不正确,如果你在for循环后面打印console.log(i),你会发现你在这里竟然可以访问这个变量!这可能不是你期待的行为。

ES6中有一个let的新关键字,它可以让你创建一个只和创建它的语句块绑定的变量。

for(let i = 0; i < 15; i++) {
  console.log(i);
}
// i undefined
console.log('After:', i);

使用let关键字来代替var,并尝试在for循环后面打印console.log(i),你会发现现在返回了undefined。这个规则对于任何语句块都好使,也就是{}中的所有代码。这也包括内部的闭包。

function myFunction() {
    let a = 'Hey';
    return (function() {
        return a;
    })();
}

调用myFunction()会返回"Hey".又或者是像下面这样在if语句内部使用let关键字声明变量。

if(true) {
    let name = 'Ryan';
}
console.log(name);

在这种情况下,变量name仅仅和if语句的作用域绑定。

常量

在目前的JS版本中我们不能创建不能被改变的变量。我们有这样一个约定,一个变量是在所有引用的地方应该是一个不应该改变的值。那么这应该是一个常数变量。

ES6提供给我们一个新的const关键字,它允许我们创建一个只读的常数值。

const CONFIG = 'Configure'
CONFIG = 'New String' //Silent error
console.log(CONFIG); //Configure

在创建一个const值后,你不能修改它。如果你试图修改常量的值,这个操作会失败且不会提供任何错误提示。const声明的变量和let声明的变量的一样也是块级作用域。

例如:

const API_KEY = 'h879u3iu1789123';

function apiCall() {
    //Do some ajax call/whatever with API_KEY
    return data;
}

这是一个相当小的例子,但也用到了我们刚刚谈到的const关键字。

模板字符串 & 字符串插值

不喜欢字符串拼接?在ES6中我们现在可以使用模板字符串。模板字符串的语法非常直接,你可以直接使用 `` 字符,也就是我们所知道的反单引号,键入`Hey there`就创建了一个内容是“Hey there”的字符串。

你或许在想,“这很酷...但是 + 号部分又是怎么样的呢”。别急,我们有一个像${variable}这样的语法可以用来替换冗长的字符串拼接语法。鉴于此,假设我们有一个变量let name = "Ryan",我们想把它拼进一个字符串,它看起来可能像下面这样:

var name = "Ryan";
var string = "Hi my name is " + name + " and I live in Toronto";

通过Es6模板字符串语法,我们可以使用反单引号和${}表达式来拼接字符串和变量name

var name = "Ryan";
var string = `Hi my name is ${name} and I live in Toronto`;

当你有一个对象或者很多信息需要和字符串拼接在一起的时候,模板字符串可以让你事半功倍。

var data = {
name: "Ryan Christiani",
age: 29,
location: 'Toronto, ON'
}
var string = 'Hi my name is ' + data.name + ' and I am ' + data.age + ' and I live in ' + data.location;
console.log(string);
//Hi my name is Ryan Christiani and I am 29 and I live in Toronto, ON

使用模板字符患:

var data = {
name: "Ryan Christiani",
age: 29,
location: 'Toronto, ON'
}
var string = `Hi my name is ${data.name} and I am ${data.age} and I live in ${data.location}`;
console.log(string);
// Hi my name is Ryan Christiani and I am 29 and I live in Toronto, ON

Classes

在大多数编程语言中都有类的概念,类是用来创建面向对象结构的抽象的数据结构。

Before classes in ES6 there were a couple ways to create this structure. One way would be using a function as a constructor. 在ES6类的概念出现之前也有一些方法来创建这样的数据结构。一种途径是使用函数来作为构造器。

var Warrior = function() {
this.hp = 100;
this.str = 5;
this.attack = function(target) {
target.hp = target.hp -= this.str;
}   
};
var character = new Warrior();
character.attack(enemy);

这里我们使用了一个函数和this关键字将属性添加到Warrior上。然而,在这个方法内部this关键字默认指向的是window对象,为了让它指向我们的Warrior,在创建对象的时候我们需要用new关键字。这样就我们实例化了一个这个对象的新副本。

如果你想使用Warrior作为基本对象来创建更多的对象,我们需要使用prototype属性。

var Fighter = function() {
this.weapons = ['sword','shield'];  
};
Fighter.prototype = new Warrior();

var myFighter = new Fighter();

这里我们在Fighter上用了prototype属性来使它继承了Warrior对象的属性。所以Fighter可以使用Warrios.attack()方法以及其他属性,它还有自己的特性,例如weapons.

现在让我们来看下ES6中的新类型,一个已经介绍了的新关键字,class.

class Warrior {
constructor() {
this.hp = 100;
this.str = 5;
}
attack(target) {
target.hp = target.hp -= this.str;
}   
};
var character = new Warrior();
character.attack(enemy);

这个类中有一个constructor()的方法,它会在你实例化类的时候运行。在这个方法内部你可以运行你需要的任何形式的初始化代码。如果你希望class接受参数,他们将在constructor()中被赋值。

class Warrior {
constructor(name) {
this.name = name;
this.hp = 100;
this.str = 5;
}
}

当你实例化这个类的时候,你需要按需传入对应的值。

var myWarrior = new Warrior('Ryan');
console.log(myWarrior.name); //"Ryan";

如果要从另一个类中继承,我们可以使用extends关键字。

class Fighter extends Warrior {
constructor() {
this.weapons = ['sword','sheild'];
}
}

这和下面是等价的

``Fighter.prototype = new Warrior();``

类之私有属性

你可能会想,既然我们有了class,那有没有私有属性呢?Es6中有一个新的symbol的数据类型,我们可以像下面那样去创建一个symbol。

``var mySymbol = Symbol();``

注意上面缺少new关键字。这样可以创建一个供我们使用的symbol. 一个Symbol是一个唯一的标识符。鉴于此我们可以用它来在类中设置我们的私有属性。没有两个Symbol是一样的。

var privateProp = Symbol();
class Warrior {
constructor() {
this[privateProp] = ['some', 'data'];
}
}

现在这个属性可以理解为是私有的,如果你需要访问它,你可以用this[ privateprop ]选择器。

箭头函数

ES6提供了一个新的函数语法,箭头函数!语法如下:

() => {
//statements
}

我们不再需要function关键字,我们在{}前面使用=>来定义函数。这只对匿名函数有效,你不可以用它来创建一个带有名字的箭头函数。

如果你的函数只接受一个参数,那么你实际上可以省略参数所在的圆括号。

``x => { return x * 2 }``

对于简单的回调,这非常有用。如果你想的话,你也可以将简单语句外面的{}去掉。

``var add = (a,b) => a + b;``

注意上面缺少return语句,省略{}将会创建一个隐式的return。

这里你可以找到更多关于箭头函数的使用语法.我认为MDN可以更好地解释一些细微的差别。

我确实想复习this的词法作用域,然而当我(真正来)处理函数中的this关键字时,我感觉this的当前上下文有时候还是挺迷惑人的。

var teacher = {
name: "Ryan",
location: "Toronto, ON",
courses: ['Front End Bootcamp', 'Intro Web Development', 'Advanced Web Development'],
print: function(){
this.courses.forEach(function(course,index) {
console.log(`${this.name} teaches ${course}`)
});
}
}
teacher.print();
//teaches Front End Bootcamp
//teaches Intro Web Development
//teaches Advanced Web Development

在上面这个例子中,this关键字的作用域在teacher对象中,通过箭头函数我们可以改变他的上下文。值得注意的是forEach的回调函数改变了this的值,它指向的是window对象。使用箭头函数可以得到我们想要的结果。

var teacher = {
name: "Ryan",
location: "Toronto, ON",
courses: ['Front End Bootcamp', 'Intro Web Development', 'Advanced Web Development'],
print: function() {
this.courses.forEach((course,index) => {
console.log(`${this.name} teaches ${course}`)
});
}
}
teacher.print();
//Ryan teaches Front End Bootcamp
//Ryan teaches Intro Web Development
//Ryan teaches Advanced Web Development

现在在forEach回调中,this的值指向创建它的对象,也就是teacher对象。

让我们再看一个例子,这样我们就可以更好的理解它。

class Europe {
constructor() {
this.count = 1000;
}
finalCountdown() {
var countInterval = setInterval(function() {
this.count--;
console.log(this.count);
if(this.count === 0) {
clearInterval(countInterval);
}
});
}
}
var gob = new Europe();
gob.finalCountdown();

或许你有一个Europe的类,我们要为它添加一个自我介绍的方法或者其他的东西。这里唯一的问题是当你写setInterval(function(){...})的时候,this关键字指向了window(全局作用域)。因此上面的结果会打印NaN.

然而如果我们把SetInterval中的回调函数改成箭头函数, 那么这个箭头函数的作用域就指向了创建它的函数,也就是Europe类。

class Europe {
constructor() {
this.count = 1000;
}
finalCountdown() {
var countInterval = setInterval(() => {
this.count--;
console.log(this.count);
if(this.count === 0) {
clearInterval(countInterval);
}
});
}
}

现在会打印1000到0!

Promises

Es6中我们有了原生的Promises!一个Promises是某些动作的最终的成功或者失败。他们通常用在Ajax请求或者动画中,Promise的概念并不新鲜,它已经存在了有一段时间。在jQuery中,我们使用$.Deferred()对象来模拟Promise的行为。其他流行的库像Q也实现了Promises。

一个Promise有penging,fulfilledrejected三种状态。我们会关注fulfilledrejected这两个状态。

我们可以用new关键字和Promise构造器来创建一个Promise.构造器接受一个带resolvereject两个参数的回调函数。

var myPromise = new Promise(function(resolve,reject) {
//...   
});

这些将被用来确定我们的Promises是fulfilled(resolved) 还是rejected(reject)。resolvereject实际上是我们可以用来传递一些数据的函数。

var myPromise = new Promise(function(resolve,reject) {
//do a bunch of stuff
if(something is true) {
resolve('Everything worked');
}   
else {
reject('Something went wrong');
}

});

既然我们打算将我们的promise置为完成或者拒绝状态,我们就要在最终动作上做一些事情。输入then()方法,这是我们创建的Promise对象的一个方法,它接受两个回调函数,第一个回调函数在promise完成的时候被调用,第二个回调函数在promise拒绝的时候调用。

myPromise.then(
//resolved
function(message) {
//Will only be called if it is fulfilled
console.log(message)
}, 
//rejected
function(message) {
//Will only be called if it is rejected
console.log(message)
}
);

值得注意的是一旦设置了promises的状态,它就不能被改变。

假设在这样一个情况下,你需要等待多个promises的执行结果.all()方法可以用来解决这个问题,它会一直处于等待状态直到一定数量promises的状态变为解决。例如:

var promise1 = new Promise(...);
var promise2 = new Promise(...);

Promise.all([promise1,promise2]).then(data => {
data.forEach((item) => {
console.log(item);
});
});

.all()方法接受一个包含promises的数组,当数组中的promises全部变为解决状态时才会调用then()方法。如果数组的一个promises被拒绝了,那么.all()方法会拒绝数组中所有的promises.

就现在,在你的项目中使用ES6

Es6这么强大,你现在怎么可以不用?现在在项目中使用ES6最简单的方法就是用Babel和Traceur之类的工具来构建它。这些转码器可以将你写的ES6代码转制成ES5.这让我们编写符合未来语法的代码,并使它们可以运行在那些目前不支持所有新特性的浏览器上。

gulp-babel

一个简单的设置就可以将gulpgulp-babel.结合使用

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

gulp.task('js', function() {
gulp.src('*.esnext.js')
.pipe(babel({
presets: ['es2015']
}))
.pipe(concat('script.js'))
.pipe(gulp.dest('.'));
});

gulp.task('default', function() {
gulp.watch('*.esnext.js',['js']);
});

注意:截至2015十月底,gulp-babel需要插件才可以转码你的ES6代码。确保已经运行npm install --save-dev babel-preset-es2015安装了 ES6/ECMAScript 2015插件。

ScratchJS

想在浏览器里面玩ES6吗?那我推荐来自Rich Gilbank at Shopify的ScratchJS。这是一个可以让你在浏览器开发者工具中编写并执行Es6代码的chrome插件。

ES6 支持

Babel和其他转码器并不能转码即将到来的所有新特性,如果你需要确定一个新特性是否支持可以看下兼容性表.

相关文章