炼金中...

幸运兔脚

开辟未来!颠覆过去的新特性,进化吧ES5!迈向新的时代-ES6降临!
1.let & const命令let在ES6中,新增了let命令,这个命令的用法与var类似。在js中var作为定...
扫描右侧二维码阅读全文
20
2018/12

开辟未来!颠覆过去的新特性,进化吧ES5!迈向新的时代-ES6降临!

1.let & const命令

let

在ES6中,新增了let命令,这个命令的用法与var类似。
在js中var作为定义变量的关键字,并不是完美的,而let的出现弥补了var的不足之处。

作用域

在ES5中只有全局作用域和函数作用域,没有块级作用域。
这会导致几个不太合理的问题(至少在我看来不太合理)
列个比较典型的作用域问题:同名变量覆盖,这个问题解释起来比较麻烦,直接看例子吧。

var tmp = 10;
function func() {
    console.log(tmp);
    if (false) {
        var tmp = 20;
    }
    return tmp;
}
console.log(func());
--------------------------------
output:
undefined
undefined

为啥输出结果为undefined呢,因为在ES5中只有全局作用域的关系,所有的变量都会出现变量提升的现象,即实际上运行的代码会变成这样:

var tmp = 10;
function func() {
    var tmp;  //在这里,tmp被重新赋值为undefined。
    console.log(tmp);
    if (false) {
        tmp = 20;
    }
    return tmp;
}
console.log(func());

let的出现,主要就是弥补了这个作用域的问题。

块级作用域

let为 JavaScript 新增了块级作用域。
简单讲,就是在{}内,let声明的变量是块内唯一的。
就这样。

不存在变量提升

变量提升,从名称中可以猜个大概:变量上升了。
先看个简单的栗子:

console.log(foo);
var foo = 2;

outpit:
undefined

变量foo在声明前被使用了,但是没有报错,而是输出了undefined,这是因为变量foo发生了提升现象:即脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出undefined
而通过let定义的变量则不会出现变量提升现象:

console.log(foo);
let foo = 2;

outpit:
报错ReferenceError

假设在定义前使用了变量,就会报错。

PS.js引擎的工作方式是①先解析代码,获取所有被声明的变量;②然后在运行。也就是专业来说是分为预处理和执行两个阶段。

暂时性死区

在前面块级作用域中,已经讲过let声明的变量是块内唯一的,那么如果在块外出现了同名变量会怎么样?
在ES6中,明确规定了,如果区块中存在let命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
用例子说话:

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

简单讲,就是块级作用域会造成类似C/C++中的变量遮蔽情况,在块内使用块内定义的变量,而块外就使用块外的变量,井水不犯河水。

不允许重复声明

let不允许在相同作用域内,重复声明同一个变量。
这个也和块级作用域有关系,就和前面讲的 "let声明的变量是块内唯一的" 一样,他不可以重复声明。

const

从使用规则上来说,constlet基本相同,唯一的区别是:const声明一个只读的常量。一旦声明,常量的值就不能改变。
PS.const本质上固定的是地址,所以在定义const对象时需要注意,他只能保证变量指向的对象不被修改,而对象的属性和方法是可以被修改的。

2.模板字符串

通常,使用拼接字符串变量时一般都是使用+来实现的,但是通过模板字符串,就可以省去+的使用。

// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

模板字符串(template string)是增强版的字符串,用反引号(`)标识。就如同上面例子所示,它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

3.对函数的扩展

参数默认值

ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法在函数内部进行默认值的赋值。

function log(x, y) {
  y = y || 'World';
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World

这种方法并不直观,也比较麻烦,而在ES6中允许为函数的参数设置默认值,即直接写在参数定义的后面。

function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello

除了简洁,ES6的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。
需要注意的是,参数变量是默认声明的,所以不能用let或const再次声明。

function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

rest 参数

ES6引入rest参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
这个东西就像C++中的...运算符,作用是接收函数实参中,多余的值。
比如:

function func(a, ...values) {
  console.log(a, values);
}
func(1, 2, 3, 4, 5);
-------------------------------
output:
1 [ 2, 3, 4, 5 ]

可以看到变量values接收了剩下的2, 3, 4, 5并把他们组合成了应该数组。

如果要使用arguments就会变得麻烦多了,首先arguments对象不是数组,而是一个类似数组的对象,他存储了所有输入的实参。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组。

// arguments变量的写法
function func(a) {
  console.log(a, Array.prototype.slice.call(arguments).sort());
}
func(1, 2, 3, 4, 5);
----------------------------------------------------------------
output:
1 [ 1, 2, 3, 4, 5 ]

可以看到arguments输出的数组中也含有a的值,这就证实了arguments对象存储了所有输入的实参,如果需要使用多参数的话,使用他就会变得很不便利。

name 属性

函数的name属性,返回该函数的函数名。
这个属性早就被浏览器广泛支持,但是直到 ES6,才将其写入了标准。
ES5与ES6中name属性的对比:

var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"

对于匿名函数,如果将一个匿名函数赋值给一个变量,ES5的name属性,会返回空字符串,而 ES6 的name属性会返回实际的函数名。

const bar = function baz() {};

// ES5
bar.name // "baz"

// ES6
bar.name // "baz"

如果将一个具名函数赋值给一个变量,则ES5和ES6的name属性都返回这个具名函数原本的名字。

箭头函数

基本用法

ES6 允许使用“箭头”(=>)定义函数。

var f = v => v;

// 等同于
var f = function (v) {
  return v;
};

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

var f = () => 5;
// 等同于
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

var sum = (num1, num2) => { return num1 + num2; }

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。

// 报错
let getTempItem = id => { id: id, name: "Temp" };

// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });

注意点

  1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
  2. 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

总的来说在箭头函数中对于this的使用要非常注意,因为一般情况下this对象的指向是可变的,而在箭头函数中,它是固定的。如果是普通函数,执行时this应该指向全局对象。但是,箭头函数导致this总是指向函数定义生效时所在的对象。

function foo() {
    console.log(this);
}

let foo2 = () => console.log(this);

foo();
foo2();
--------------------------------------------
output:
Object [global]
{}

可以看到,函数foo中的this指向了全局对象global,而箭头函数foo2中的this则指向了一个空对象。

4.扩展运算符(与解构)

数组的情况

扩展运算符(spread)是三个点(...)。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。

console.log(...[1, 2, 3])
// 1 2 3

console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]

有一点需要注意的是,如果把扩展运算符放在括号中,除非是函数调用,否则就会报错。

(...[1,2])
// Uncaught SyntaxError: Unexpected number

console.log(...[1,2])
// output:1 2

简单应用

  • 复制数组
    在js中,数组是复合的数据类型,如果直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。在ES5中,通常采用变通方式(克隆)来进行数组的复制:
const a1 = [1, 2];
const a2 = a1.concat();

a2[0] = 2;
a1 // [1, 2]

而通过扩展运算符就可以让这个过程变得更加简便。

const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;
  • 合并数组
    扩展运算符提供了数组合并的新写法。
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

// ES5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

需要注意的是,这种方式所作的拷贝是浅拷贝,如果内部还有对象的话,那么修改了原数组的成员,会同步反映到新数组。

  • 与解构赋值结合
    扩展运算符可以与解构赋值结合起来,用于生成数组。

下面是一些例子:

const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]

const [first, ...rest] = [];
first // undefined
rest  // []

const [first, ...rest] = ["foo"];
first  // "foo"
rest   // []

如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

  • 字符串
    扩展运算符还可以将字符串转为真正的数组。
[...'hello']
// [ "h", "e", "l", "l", "o" ]

对象的情况

解构赋值

对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }

上面代码中,变量z是解构赋值所在的对象。它获取等号右边的所有尚未读取的键(ab),将它们连同值一起拷贝过来。

由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefined或null,就会报错,因为它们无法转为对象。

let { x, y, ...z } = null; // 运行时错误
let { x, y, ...z } = undefined; // 运行时错误

和数组时一样,解构赋值必须是最后一个参数,否则也会报错。

let { ...x, y, z } = obj; // 句法错误
let { x, ...y, ...z } = obj; // 句法错误

PS.注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。

let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2

上面代码中,x是解构赋值所在的对象,拷贝了对象obja属性。a属性引用了一个对象,修改这个对象的值,会影响到解构赋值对它的引用。

扩展运算符

对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。

let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }

由于数组是特殊的对象,所以对象的扩展运算符也可以用于数组。

let foo = { ...['a', 'b', 'c'] };
foo
// {0: "a", 1: "b", 2: "c"}

和数组一样,扩展运算符也可以用于合并两个对象。

let a = { a:1, b:2 };
let b = { c:1, d:2 };
let ab = { ...a, ...b };
//{ a:1, b:2, c:1, d:2 }

5.类(class)

ES6提供了更接近传统语言的写法,引入了Class(类)这个概念。新的class写法让对象原型的写法更加清晰、更像面向对象编程的语法,也更加通俗易懂。

关键字class

基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

//ES5的写法
function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);
-------------------------------------------------------
//ES6的写法
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5的构造函数Point,对应ES6的Point类的构造方法。
Point类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。

  • constructor 方法
    constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。通过调用这个方法,可以返回实例对象(即this)。

注意点

  • 不存在提升
    和ES5中,类存在变量提升的情况不同,class不存在变量提升的情况。
  • name 属性
    ES6的类只是ES5的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性。通过name属性,可以获得class关键字后面的类名。

关键字extends

Class可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y);// 调用父类的constructor(x, y)
    this.color = color;
  }
}

上面代码中,constructor方法之中,出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。

class Point { /* ... */ }

class ColorPoint extends Point {
  constructor() {
  }
}

let cp = new ColorPoint(); // ReferenceError

上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。

ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面。ES6的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this

关键字super

super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
第一种情况,super作为函数调用时,代表父类的构造函数。ES6要求,子类的构造函数必须执行一次super函数。

class A {}

class B extends A {
  constructor() {
    super();
  }
}

上面代码中,子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。

第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();

上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()
需要注意的是,因为super指向原型对象,所有定义在父类实例上的方法或属性是无法通过super调用的。

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
b.m // undefined

上面代码中,p是父类A实例的属性,super.p就引用不到它。

6.异步回调(Promise)

特点

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

个人理解

Promise是ES6对于异步与回调的一个改进语法,通过promisethen的组合,用同步的语法实现异步功能。
举个例子:

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100).then((value) => {
  console.log(value);
});

console.log("1");

output:
1
done

上面代码中,timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(ms参数)以后,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数。
上面这个例子虽然看上去有点脱裤子放屁的感觉,但是我认为他把promisethen的性质很好的表现出来了。首先,从这个例子中可以看出,promise在新建后会立即执行,而then则是他的回调函数,在当前脚本所有的同步任务执行完之后,才会执行。

7.async 函数

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

基础语法

async函数返回一个Promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}

getStockPriceByName('goog').then(function (result) {
  console.log(result);
});

个人的简单理解

console.log(1)
async function func() {
    await console.log(2.0);
    await console.log(2.1);
    await console.log(3);

}
func()
console.log(4)
setTimeout(()=>{console.log(5);},200);
console.log(6)
--------------------------------------------
output:
1
4
6
2.0
2.1
3
5

在函数执行后,首先执行了日志1打印,之后运行函数func,进入后遇到了第一个await,如果await后面跟的是函数的话这时会进行函数的执行,否则func就会返回执行函数体外的的语句,在主线程空闲时,再会过来获取await后面的结果,所以在例子中,先输出了 1,4,6 之后才输出了2.0。
async中,所有的await都遵循上面的原则。
PS.这个机制貌似和js的事件轮询机制相关,具体我也了解的不是很透彻,还需要继续深入学习一下。

学习资料:
ECMAScript 6 入门

Last modification:December 21st, 2018 at 09:17 am
If you think my article is useful to you, please feel free to appreciate

Leave a Comment