ECMAScript 6

2017-12-05 鲁鲁槟 收藏

一、展开运算符

JavaScript是ECMAScript的实现和扩展,ES6标准的制定也为JavaScript加入了许多新特性。本文主要记录展开运算符。

展开运算符(spread operator)允许一个表达式在某处展开。展开运算符在多个参数(用于函数调用)或多个元素(用于数组字面量)或者多个变量(用于解构赋值)的地方可以使用。

展开运算符不能用在对象当中,因为目前展开运算符只能在可遍历对象(iterables)可用。iterables的实现是依靠[Symbol.iterator]函数,而目前只有Array,Set,String内置[Symbol.iterator]方法,而Object尚未内置该方法,因此无法使用展开运算符。不过ES7草案当中已经加入了对象展开运算符特性。

1.1、函数调用中使用展开运算符

在以前我们会使用apply方法来将一个数组展开成多个参数:

function test(a, b, c) { }
var args = [0, 1, 2];
test.apply(null, args);

如上,我们把args数组当作实参传递给了a,b,c,这边正是利用了Function.prototype.apply的特性。

不过有了ES6,我们就可以更加简洁地来传递数组参数:

function test(a,b,c) { }
var args = [0,1,2];
test(...args);

我们使用…展开运算符就可以把args直接传递给test()函数。

1.2、数组字面量中使用展开运算符

在ES6的世界中,我们可以直接加一个数组直接合并到另外一个数组当中:

var arr1=['a','b','c'];
var arr2=[...arr1,'d','e']; //['a','b','c','d','e']

展开运算符也可以用在push函数中,可以不用再用apply()函数来合并两个数组:

var arr1=['a','b','c'];
var arr2=['d','e'];
arr1.push(...arr2); //['a','b','c','d','e']

1.3、用于解构赋值

解构赋值也是ES6中的一个特性,而这个展开运算符可以用于部分情景:

let [arg1,arg2,...arg3] = [1, 2, 3, 4];
arg1 //1
arg2 //2
arg3 //['3','4']

展开运算符在解构赋值中的作用跟之前的作用看上去是相反的,将多个数组项组合成了一个新数组。

不过要注意,解构赋值中展开运算符只能用在最后:

let [arg1,...arg2,arg3] = [1, 2, 3, 4]; //报错

1.4、类数组对象变成数组

展开运算符可以将一个类数组对象变成一个真正的数组对象:

var list=document.getElementsByTagName('div');
var arr=[...list];

list是类数组对象,而我们通过使用展开运算符使之变成了数组。

1.5、ES7草案中的对象展开运算符

ES7中的对象展开运算符符可以让我们更快捷地操作对象:

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

如上,我们可以将一个对象当中的对象的一部分取出来成为一个新对象赋值给展开运算符的参数。

同时,我们也可以像数组插入那样将一个对象插入另外一个对象当中:

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

另外还要很多用处,比如可以合并两个对象:

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

二、let和块级作用域

ES6中加入了let,也让JavaScript拥有了块级作用域。

2.1、块级作用域的JavaScript

在ES5及其之前的版本里,作用域只有全局作用域和函数作用域两种,而不像其他许多语言一样还拥有块级作用域。没有块级作用域的JavaScript在使用的过程中出现了许多意想不到的具体问题,比如下面这段代码的demo:

var arr = [];
for (var i = 0; i < 10; i++) {
  arr[i] = function () {
    console.log(i);
  };
}
arr[3]();

如果是学过C++或者其他有块级作用域的人,可能类比觉得上述代码会输出3,不过事实上该代码输出的是10。

因为ES5及之前是没有块级作用域的,i所处的仍是全局作用域而不是块级作用域。因此,循环过程中数组arr的每个数组项所引用的函数中的变量i都是引用全局作用域中的i,因此arr3中i为for循环结束时的i的值10。

对于该问题,有很多方法解决。比如可以将for循环体的代码放入一个立即执行函数中,相当于创建一个新的作用域,将i当做实参传入里及执行函数,本质上是创造了一个模拟的块级作用域,当然也可以认为为内部的函数创建一个闭包(闭包的本质和作用域链息息相关)。

2.2、let的出现

现在我们再写之前的那段代码是,有了更加简洁的方法,使用ES6的let。

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

let的出现使得JavaScript终于拥有了块级作用域。因为ES6要考虑之前版本的兼容,所以是通过声明let来使用块级作用域。

当我们在一个代码块中使用let来声明变量,通过let声明的变量只在当前块作用域中有效。

{
  let a = 1;
  var b = 2;
}
console.log(a); //ReferenceError
console.log(b); //2

如上,let的声明方式让let所在的块成为块级作用域,同时let声明的变量无法在全局作用域中访问到,但是var变量依旧可以在全局作用域访问到。

2.3、无变量提升

let中不存在变量提升的现象。变量在使用之前必须被声明。

因此,这个定义也导致了暂时性死区的现象。

var c = 'test';
if (true) {
  c = 'new'; //ReferenceError
  let c;
  console.log(c);
}

如上在块级块级作用域中重新声明全局作用域中的c时,这时,编译器会屏蔽全局作用域中的c,在该块级作用域中只能使用新声明的c。但由于块级作用域中let声明的变量无作用域提升现象,因此无法在声明c之前使用c(包括赋值c),出现暂时性死区的现象。

2.4、块级作用域

let的出现让JavaScript可以充分利用块级作用域的特性。我们可以在不同的块级作用域中使用同名变量。

if (true) {
    let a = 1;
    if(true){
        let a = 2;
        console.log(a); //2
    }
    console.log(a); //1
}

由于块级作用域出现,我们可以实现上述变量隔离的效果。

2.5、总结

let在不影响var使用的情况下,开创了JavaScript的块级作用域,未来想必let也会大量取代var的使用。

三、箭头函数的this

this真是个谜。箭头函数加上this更是迷雾重重。

网上部分资料对于this在箭头函数中解释是:箭头函数的this是由它定义时所在对象决定的。对于这个解释,可能是表述关系 ,我也不能说它错,对此也不多解读,不过我认为这个解释不能让我们真正理解箭头函数this。

箭头函数中是没有不会创造独立的上下文的,它的上下文来自enclosing context,即来自其所在上下文的this作为自己的this。

下面通过例子来解读下这个定义:

var person = {
  name: "test",
  shout: () => console.log(this.name)
}
person.shout();

如果按照this是由定义时所在对象决定的解释,那么上述代码应该输出为test,然而事实上我们测试得到没有输出。

事实上,箭头函数的this是其所在上下文的this决定的。我们结合上一个例子来解析一下,这里的箭头函数() => console.log(this.name)的上下文是全局上下文也就是window,而这里的name: "test"实际上是person.name,因此person.shout()调用shout函数时,this指向全局上下文,而此时全局上下文中没有定义name,因此不会有任何输出。

当我们将代码作如下修改:

var person = {
  name: "test",
  shout(){
    const shout = () => console.log(this.name);
    return shout;
  }
}
person.shout()();

此时,() => console.log(this.name)中的this是由shout()的上下文的this决定的,当我们调用person.shout()时,shout()的上下文是person对象,因此箭头函数() => console.log(this.name)中的上下文将在person.shout()执行时就确定为person对象。因此,person.shout()()虽然执行时的上下文是window,但是箭头函数的this却是指向person对象。

为了加深理解,我们再来看另外一个典型的例子:

function Person(){
  this.age = 0;
  setInterval(() => {
    this.age++;
  }, 1000);
}
var p = new Person();

如果定时器内部的函数是普通函数,那么函数的上下文是全局window,但是改成箭头函数之后,我们根据前面的定义,() => { this.age++; }是this是由所在的上下文决定的,因此当我们执行var p = new Person()时,Person函数内部的上下文的this便指向p,因此箭头函数内部的上下文也必定是p,因此this.age++可以正常执行。

总而言之,箭头函数的this是由箭头函数定义时候所处的上下文决定的。

四、数组实例的 find() 和 findIndex()

4.1、find

数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。

[1, 4, -5, 10].find((n) => n < 0)  
// -5
[1, 5, 10, 15].find(function(value, index, arr) {  
return value > 9;  
}) // 10

上面代码中,find方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。

4.2、findIndex

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。

[1, 5, 10, 15].findIndex(function(value, index, arr) {  
return value > 9;  
}) // 2

这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。

4.3、indexOf

另外,这两个方法都可以发现NaN,弥补了数组的IndexOf方法的不足。

[NaN].indexOf(NaN)  
// -1  
[NaN].findIndex(y => Object.is(NaN, y))  
// 0

上面代码中,indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法做到。

Object.is()方法判断两个值是否是相同的值。

五、数组实例的 map() 和 filter()

5.1、map

map方法的作用不难理解,“映射”嘛,也就是原数组被“映射”成对应新数组。下面这个例子是数值项求平方:

var data = [1, 2, 3, 4];

var arrayOfSquares = data.map(function (item) {
  return item * item;
});

console.log(arrayOfSquares); // 1, 4, 9, 16

callback需要有return值,如果没有,就像下面这样:

var data = [1, 2, 3, 4];

var arrayOfSquares = data.map(function() {});

console.log(arrayOfSquares); // [undefined, undefined, undefined, undefined]

结果可以看到,数组所有项都被映射成了undefined。

//在实际使用的时候,我们可以利用map方法方便获得对象数组中的特定属性值们。例如下面这个例子(之后的兼容demo也是该例子):

var users = [
  {name: "张含韵", "email": "zhang@email.com"},
  {name: "江一燕",   "email": "jiang@email.com"},
  {name: "李小璐",  "email": "li@email.com"}
];

var emails = users.map(function (user) { return user.email; });

console.log(emails.join(", ")); // zhang@email.com, jiang@email.com, li@email.com

5.2、filter

filter为“过滤”、“筛选”之意。指数组filter后,返回过滤后的新数组。用法跟map极为相似:

array.filter(callback,[ thisObject]);

filter 的 callback 函数需要返回布尔值 true 或 false。 如果为 true 则表示,恭喜你,通过啦!如果为 false, 只能高歌“我只能无情地将你抛弃……”。

可能会疑问,一定要是 Boolean 值吗?我们可以简单测试下嘛,如下:

var data = [0, 1, 2, 3];
var arrayFilter = data.filter(function(item) {
    return item;
});
console.log(arrayFilter); // [1, 2, 3]

由此可见,返回值只要是弱等于== true/false就可以了,而非非得返回 === true/false.

5.3、map 和 filter 混合使用

var emailsZhang = users
  // 获得邮件
  .map(function (user) { return user.email; })
  // 筛选出zhang开头的邮件
  .filter(function(email) {  return /^zhang/.test(email); });

console.log(emailsZhang.join(", ")); // zhang@email.com
阅读 1005