karen

JavaScript ES6功能概述(也称为ECMAScript 6和ES2015 +)

原文链接: adrianmejia.com

JavaScript在去年新增了12个特性可以使用。

JavaScript 历史link

这个新版本语言被称作ECMAScript6,也可以被称为ES6或ES2015+。

自从1995年JavaScript诞生以来,它一直在缓慢发展。 每隔几年就会有新的版本。 ECMAScript诞生于1997年,旨在指导JavaScript的发展。 它已经发布了ES3,ES5,ES6等版本。

如你所见,ES3、ES5和ES6之间存在10年或6年的差距。 不像每年进行少量的更改那样 ,ES6一次就进行了大规模更改。

浏览器支持link

现如今很多浏览器环境都支持ES6。

来源:https://kangax.github.io/compat-table/es6/

Chrome,MS Edge,Firefox,Safari,Node和许多其他环境已经内置支持JavaScript ES6的大部分功能。 因此,在本教程中你学习的所有知识都可以马上开始应用。

让我们开始学习ECMAScript 6吧!

ES6 特性link

你可以在浏览器控制台上测试下边所有的代码段!

不要只相信我说的话,而是要去测试每个ES5和ES6示例,让我们深入了解吧💪

变量的块级作用域link

因为ES6,我们从var到开始使用let / const声明变量。

var有什么问题吗?

var 的问题是变量泄漏到其他代码块中,例如for循环或if语句。

ES5
var x = 'outer';
function test(inner) {  
  if (inner) {    
    var x = 'inner';// scope whole function    
        return x;
  }  
      return x;// gets redefined because line 4 declaration is hoisted
    }
    test(false);// undefined 😱
    test(true);// inner

test(false) 按理说会返回 outer, 但是不是的, 它返回了undefined

为什么呢?

因为即使 if块没有执行, 第4行的表达式 var x 提升了

变量 提升

  • var 是函数作用域。即使在声明之前,它在整个函数中也可用。

  • 声明被提升,您可在它被声明之前使用一个变量。

  • 初始化不会提升, 如果您使用var,则始终在顶部声明变量。

  • 应用了提升规则后,我们更好得了解发生了什么:


ES5 var x = 'outer'; function test(inner) {
var x;// HOISTED DECLARATION
if (inner) {
x = 'inner';// INITIALIZATION NOT HOISTED
return x; }
return x; }


ECMAScript 2015 找到了解决办法:

ES6
let x = 'outer';
function test(inner) {  
  if (inner) {    
      let x = 'inner';
    return x;
  }  
      return x;// gets result from line 1 as expected
}
test(false);// outer
test(true);// inner

var更改为let使代码按预期工作。 如果未调用if块,变量x将不会在代码块外被提升。

Let 提升 和 “暂时性死区”

  • 在ES6中, let会把变量提升到块的顶部(而不是像ES5这样的函数顶部)。

  • 不过,在变量声明之前在代码块中引用会导致ReferenceError错误。

  • let是块级作用域。不能在它被声明之前引用。

  • “暂时性死区” 是从代码块开始到变量声明为止的区域。

IIFE(立即执行函数表达式)

在解释IIFE之前先看一个例子:

ES5
{  
  var private = 1;
}
console.log(private);// 1

可以看到, private 变量泄漏。你需要使用 IIFE (立即执行函数表达式)来包裹它:

ES5
(function(){  
  var private2 = 1;})();
console.log(private2);// Uncaught ReferenceError

观察 jQuery/lodash 或其他开源项目,你将会发现他们使用了IIFE来避免污染全局环境,只在全局定义_, $jQuery等。

ES6写法简化了很多, 我们可以只用代码块和let,不再需要使用IIFE:

ES6
{  
  let private3 = 1;
}
console.log(private3);// Uncaught ReferenceError

Const常量

如果你想要一个变量保持不变,可以使用const常量。

总之:使用letconst而不是var

  • 对所有引用使用const;避免使用 var

  • 如果需要重新指定引用,使用let 代替 const

模板字符串link

当我们有了模板字符串,就不用再做一些嵌套操作:

ES5
var first = 'Adrian';
var last = 'Mejia';
console.log('Your name is ' + first + ' ' + last + '.');

现在可以使用反引号和字符串插值${}

const first = 'Adrian';
const last = 'Mejia';
console.log(Your name is ${first} ${last}.);

多行字符串link

我们再也不用像这样添加+\n将字符串连接起来了:

var template = '<li *ngFor="let todo of todos" [ngClass]="{completed: todo.isDone}" >\n' +
'  <div class="view">\n' +
'    <input class="toggle" type="checkbox" [checked]="todo.isDone">\n' +
'    <label></label>\n' +
'    <button class="destroy"></button>\n' +
'  </div>\n' +
'  <input class="edit" value="">\n' +
'</li>';
console.log(template);


在ES6中,我们可以同样使用反引号来解决这个问题:

const template = `<li *ngFor="let todo of todos" [ngClass]="{completed: todo.isDone}" >
  <div class="view">
    <input class="toggle" type="checkbox" [checked]="todo.isDone">
    <label></label>
    <button class="destroy"></button>
  </div>
  <input class="edit" value="">
</li>`;
console.log(template);

这两段代码将具有完全相同的结果。

解构赋值link

ES6的解构非常实用和简洁。如下例所示:

从数组中获取元素

ES5
var array = [1, 2, 3, 4];
var first = array[0];
var third = array[2];
console.log(first, third);// 1 3

等同于:

ES6
const array = [1, 2, 3, 4];
const [first, ,third] = array;
console.log(first, third);// 1 3

交换值

ES5
var a = 1;
var b = 2;
var tmp = a;
a = b;
b = tmp;
console.log(a, b);// 2 1

等同于

ES6
let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a, b);// 2 1

解构多个返回值

ES5
function margin() {  
  var left=1, right=2, top=3, bottom=4;
  return { 
      left: left, right: right, top: top, bottom: bottom };
}
var data = margin();
var left = data.left;
var bottom = data.bottom;
console.log(left, bottom);// 1 4

在第3行, 你也可以返回一个这样的数组 (用时省去了一些代码):

return [left, right, top, bottom];

但是调用者需要考虑返回数据的顺序。

var left = data[0];
var bottom = data[3];

在ES6中, 调用者只需选取他们需要的数据即可(第6行):

ES6
function margin() {  
  const left=1, right=2, top=3, bottom=4;
  return { left, right, top, bottom };
}
const { left, bottom } = margin();
console.log(left, bottom);// 1 4

注意: 在第3行, 展示了一些ES6的其他特性。我们可以把 { left: left }简化成{ left }。跟ES5相比是不是很简洁,有没有很酷?

函数参数的解构赋值

ES5
var user = {
  firstName: 'Adrian', lastName: 'Mejia'
};
function getFullName(user) {  
  var firstName = user.firstName;
  var lastName = user.lastName;
  return firstName + ' ' + lastName;
}
console.log(getFullName(user));// Adrian Mejia

等同于(但更简洁):

ES6
const user = {
  firstName: 'Adrian', lastName: 'Mejia'
};
function getFullName({ 
  firstName, lastName 
}) {  return ${firstName} ${lastName};}
console.log(getFullName(user));// Adrian Mejia

深匹配

ES5
function settings() {  
  return { display: { color: 'red' }, keyboard: { layout: 'querty'} };}
var tmp = settings();
var displayColor = tmp.display.color;var keyboardLayout = tmp.keyboard.layout;
console.log(displayColor, keyboardLayout);// red querty

等同于(不过更简洁):

ES6
function settings() {  
  return { display: { color: 'red' }, keyboard: { layout: 'querty'} };
}
const { 
  display: { color: displayColor }, keyboard: { layout: keyboardLayout }
} = settings();
console.log(displayColor, keyboardLayout);// red querty

这也称作对象的解构。

可以看到,解构很实用并且可以促进良好的编码风格。

最佳实践:

  • 使用数组解构来获取元素或交换变量。 它可以避免创建临时引用。

  • 不要对多个返回值使用数组解构,而应使用对象解构。

类与对象link

有了ECMAScript 6,我们从“构造函数” 🔨 过渡到了 “类” 🍸。

在JavaScript中,每个对象都有一个原型对象。 所有JavaScript对象都从其原型继承方法和属性。

在ES5中,为了实现面向对象编程(OOP),我们使用构造函数创建对象,如下所示:

ES5
var Animal = (function () {  
  function MyConstructor(name) {    this.name = name;
}  
MyConstructor.prototype.speak = function speak() {    console.log(this.name + ' makes a noise.');
};
return MyConstructor;})();
var animal = new Animal('animal');
animal.speak();// animal makes a noise.

在ES6中,我们有一些语法糖。 我们可以用更少的模板代码和classconstructor这样的关键字来做同样的事情。 另外,speak()constructor.prototype.speak = function()方法相比更简洁:

ES6
class Animal {  
  constructor(name) {    this.name = name;}  
    speak() {    console.log(this.name + ' makes a noise.');
 }
}
const animal = new Animal('animal');
animal.speak();// animal makes a noise.

正如我们所看到的,两种方式(ES5 / 6)产生的结果相同,使用的方法也一样。

最佳实践:

  • 始终使用class语法,并避免直接操作原型。 为什么? 因为它使代码更简洁,更易于理解。

  • 避免使用空的构造函数。 如果未指定,则类具有默认的构造函数。

继承link

在 之前Animal 类的基础上。假设我们想要扩展它并定义一个 Lion

在ES5中,它更多地与原型继承有关。

ES5
var Lion = (function () {  
  function MyConstructor(name){    
      Animal.call(this, name);
}  
// prototypal inheritance  
MyConstructor.prototype = Object.create(Animal.prototype);
MyConstructor.prototype.constructor = Animal;
MyConstructor.prototype.speak = function speak() {    
  Animal.prototype.speak.call(this);
  console.log(this.name + ' roars 🦁');
};
return MyConstructor;})();
var lion = new Lion('Simba');lion.speak();// Simba makes a noise.
// Simba roars.

请注意以下细节:

  • 第3行中,我们用参数显式调用Animal构造函数。

  • 在7-8行,我们将Lion原型指派给Animal的原型。

  • 第11行,我们从父类Animal中调用 speak方法。

在ES6中,我们有新的关键词 extendssuper superman shield.

class Lion extends Animal {  
  speak() {    super.speak();
  console.log(this.name + ' roars 🦁');
}}
const lion = new Lion('Simba');
lion.speak();// Simba makes a noise.
// Simba roars.

与ES5相比,ES6代码看起来更清晰易读,并且功能完全相同。

最佳实践:

  • extends内置方法实现继承。

原生Promises对象link

从回调地狱👹到promises 🙏

ES5
function printAfterTimeout(string, timeout, done){  
  setTimeout(function(){    
      done(string);
}, timeout);}
printAfterTimeout('Hello ', 2e3, function(result){  
  console.log(result);// nested callback  
  printAfterTimeout(result + 'Reader', 2e3, function(result){    
    console.log(result);
 });
});

有一个函数需要接收done执行完成后的回调。 我们必须一个接一个地调用它两次。 这就是为什么我们在回调中调用第二次printAfterTimeout的原因。

如果需要执行第3次或第4次回调,很快就会变得混乱。 我们来看下promises是怎么工作的:

ES6
function printAfterTimeout(string, timeout){  
  return new Promise((resolve, reject) => {    
      setTimeout(function(){      resolve(string);
}, timeout);
});}
printAfterTimeout('Hello ', 2e3).then((result) => {  
  console.log(result);
  return printAfterTimeout(result + 'Reader', 2e3);
}).then((result) => {  
  console.log(result);
});

如你所见,有了promise,我们能在函数完成后使用“ then”进行一些操作。 不再需要嵌套函数。

箭头函数link

ES6并未移除函数表达式,但添加了一个新的函数,即箭头函数。

在ES5中,this存在一些问题:

ES5
var _this = this;// need to hold a reference
$('.btn').click(function(event){  
  _this.sendData();// reference outer this
});
$('.input').on('change',function(event){  
  this.sendData();// reference outer this
}.bind(this));// bind to outer this

您需要在函数内部引用临时的this或使用bind绑定。 在ES6中,可以使用箭头函数!

ES6
// this will reference the outer one
$('.btn').click((event) =>  this.sendData());
// implicit returns
const ids = [291, 288, 984];
const messages = ids.map(value => `ID is ${value}`);


For…of循环link

forforEach 再到 for...of

ES5
// for
var array = ['a', 'b', 'c', 'd'];
for (var i = 0;i < array.length;i++) {  
  var element = array[i];
  console.log(element);}// forEach
array.forEach(function (element) {  console.log(element);});

ES6 的for…of同样适用于循环迭代。

ES6
// for ...of
const array = ['a', 'b', 'c', 'd'];
for (const element of array) {    
 console.log(element);
}

默认参数link

从检查变量是否被定义到给默认参数赋值,你之前写过类似这样的代码吗?

ES5
function point(x, y, isFlag){
  x = x || 0;
  y = y || -1;
  isFlag = isFlag || true;
  console.log(x,y, isFlag);
}
point(0, 0) // 0 -1 true 😱
point(0, 0, false) // 0 -1 true 😱😱
point(1) // 1 -1 true
point() // 0 -1 true

可能有过,检查变量是否赋值,不然则分配默认值是一种常见的模式。 但是,请注意一些问题:

  • 第8行,我们传入 0, 0返回0, -1

  • 第9行,我们传入 false 但是返回了true

如果你传入布尔值作为默认参数,或者将该值设置为0,则此它不能正常起作用。 你知道为什么吗? 我会在ES6示例之后告诉你。

使用ES6,你可以用更少的代码做得更好!

ES6
function point(x = 0, y = -1, isFlag = true){
  console.log(x,y, isFlag);
}
point(0, 0) // 0 0 true
point(0, 0, false) // 0 0 false
point(1) // 1 -1 true
point() // 0 -1 true

注意第5行和第6行,我们得到了预期的结果。 ES5示例则无效。 我们必须首先检查是否等于“ undefined”,因为falsenullundefined0都是假值。 我们可以避开这些数字:

ES5
function point(x, y, isFlag){
  x = x || 0;
  y = typeof(y) === 'undefined' ? -1 : y;
  isFlag = typeof(isFlag) === 'undefined' ? true : isFlag;
  console.log(x,y, isFlag);
}
point(0, 0) // 0 0 true
point(0, 0, false) // 0 0 false
point(1) // 1 -1 true
point() // 0 -1 true

现在,当我们检查是否为undefined后,获得了预期的结果。

剩余参数link

从参数到剩余参数和扩展运算符。

在ES5中,获取任意数量的参数很麻烦:

ES5
function printf(format) {
  var params = [].slice.call(arguments, 1);
  console.log('params: ', params);
  console.log('format: ', format);
}

我们可以用rest操作符...做到一样的事 。

ES6
function printf(format, ...params) {
  console.log('params: ', params);
  console.log('format: ', format);
}

扩展运算符link

apply() 到扩展运算符。 我们同样用 ... 来解决:

提醒:我们使用apply()将数组转换为一列参数。 例如,Math.max()作用于一列参数,但是如果我们有一个数组,我们可以使用apply使它生效。

如前所述,我们可以使用apply将数组作为参数列表传递:

ES5
Math.max.apply(Math, [2,100,1,6,43]) // 100

在ES6中,可以使用扩展运算符:

ES6
Math.max(...[2,100,1,6,43]) // 100

同样,从concat数组到使用扩展运算符:

ES5
var array1 = [2,100,1,6,43];
var array2 = ['a', 'b', 'c', 'd'];
var array3 = [false, true, null, undefined];
console.log(array1.concat(array2, array3));

在ES6中,可以使用扩展运算符展开数组并合成新数组:

ES6
const array1 = [2,100,1,6,43];
const array2 = ['a', 'b', 'c', 'd'];
const array3 = [false, true, null, undefined];
console.log([...array1, ...array2, ...array3]);

总结:link

JavaScript经历了许多更改。 本文介绍了每个JavaScript开发人员都应该了解的大多数核心功能。 此外,我们还介绍了一些让你的代码更加简洁,更易理解的最佳实践。

如果你认为还有一些没有提到的必知功能,请在下边留言,我将更新这篇文章。