ES6的这些新知识你记住了没?

658 阅读12分钟

前言

本人是个新手,刚入门不久,最近学习了ES6的新语法,有点个人感想,就写出来和大家分享一下。有哪里不对的希望各位大佬能及时指出。哪里与您观念不和的地方还请评论发出您的见解,让大家共同参考一下。共同学习共同进步!

本文参考了阮一峰老师的《ECMAScript 6入门》

1. let 和 const

作为ES6语法里很好用,也很有新颖的当属这两个命令了。 let 和 const都实现了块级作用域的概念。怎么去理解块级作用域这个概念呢?笔者认为,块级作用域和函数作用域类似,都会统治一片作用域。不同之处在于let声明的块级作用域内部再声明不影响外部。而函数的却会影响。代码如下。

function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    function f() { console.log('I am inside!'); } 
  }
  f(); //理论上块级作用域,应该输出I am outside!实际上输出了I am inside!
}());

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

有一个需要注意的地方。ES6 的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。

// 不报错
if (true) {
  function f() {}
}

// 报错
if (true)
  function g() {}

接下来我们比较一下var,let和const。

首先var是很熟悉的命令了。var可以提升变量。var可以声明全局变量和局部变量。

let功能类似于var,不过它比var多了一个块级作用域。刚才我们已经简单的介绍了一下块级作用域了。现在直接来对比一下var和let的区别,更直观的反映一下它们的不同之处。

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

由以上代码不难看出,var把a定义到了全局里面,而let不一样。因此我们可以在{}外查询到a而查不到b。

在使用let时经常会带来一些负面影响。比如TDZ(暂时性死区)。let命令在块级作用域中,即使不存在变量提升,它也会影响当前块级作用域,即绑定在了当前作用域。在作用域中引用外部的变量将会报错。

 var a=10;
  {
      console.log(a);  //ReferenceError: a is not defined
      let a=10;
  }

const声明一个只读的常量。一旦声明,常量的值就不能改变。用法类似let。

const保证常量的值不被修改只是常量在栈区的值不变,如果这个值是一个基本数据类型,const就能够保障常量的值不变;但是如果是引用类型的数据,栈区保存的其实是对应常量的地址。地址无法改变,但是对应地址的堆区内容却可以改变。代码如下。

 const m = 10;
 m = 12;
 console.log(m); 
 // TypeError: Assignment to constant variable.
 
 
 const n = [1,2,3];
 n.push(4);
 console.log(n); //[1, 2, 3, 4]

2. 解构赋值

在ES6中,允许了按照一定的模式,从数组和对象中取值,对变量赋值。这被称为解构。但是有个要求是等号两边的模式要相同,这样左边的变量就会被赋予对应的值。如果解构不成功,变量的值就等于undefined。代码如下。

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

let [x, ,y] = [1,2,3];
console.log(x);  //  1
console.log(y);  //  3

let [one, ...all] = [1,2,3,4];
console.log(one); //  1
console.log(all); //  [2,3,4]

let [a,b, ...c] = [1];
console.log(a);  //  1
console.log(b);  //  undefined
console.log(c);  //  []

但是要注意,以下几种类型的会报错(严格地说,不是可遍历的结构,详情可参见《Iterator》一章)。代码如下。

let [a] = 1;
let [a] = false;
let [a] = NaN;
let [a] = undefined;
let [a] = null;
let [a] = {};

对象的解构赋值与数组类似。举几个例子,代码如下。

let {a,b} = {a:"Hello",b:"World"};
console.log(a);  //  "Hello"
console.log(b);  //  "World"

//与数组一样,解构也可以用于嵌套的对象。
let obj = {
    P:[
    'Hello',{y:'World'}
    ]
}

let {p:[x,{y}]} = obj;
console.log(x);  //  'Hello'
console.log(y);  //  'World'
//注意,这时p是一个模式,不是变量,因此不会被赋值。

对象是一个无序集合,而数组是有序的。所以在解构赋值的时候变量必须与属性同名才可以取到我们想要的结果。代码如下。

let {h,w} = {h:'Hello',w:'World'}
console.log = (h);  //  'Hello'
console.log = (w);  //  'World'

let {m,n} = { m:"123" }
console.log(m);  //  "123"
console.log(n);  //  undefined

其实关于解构赋值,最常用的还是在函数中。

function f({x = 0, y = 0} = {}) {
  return [x, y];
}

f({x: 1, y: 2});  // [1, 2]
f({x: 1});        // [1, 0]
f({});            // [0, 0]
f();              // [0, 0]
//函数f的参数是一个对象,通过对这个对象进行解构,得到变量x和y的值。如果解构失败,x和y等于默认值。

function f({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

f({x: 1, y: 2});  // [1, 2]
f({x: 1});        // [1, undefined]
f({});            // [undefined, undefined]
f();              // [0, 0]
//此时,函数f的参数指定默认值,而不是为变量x和y指定默认值,所以会得到与前一种写法不同的结果。

//还有一种undefined会触发函数参数的默认值。
[1, undefined, 3].map((x = 'yes') => x);     // [ 1, 'yes', 3 ]

3. 模版字符串

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。若要将变量嵌入模版字符串中,需要将变量名写在${}内。举个小例子吧。

let obj = {
    name : "掘金",
    level : 100,
    sex : "man"
}
console.log(`我在一个名字为${obj.name}的网站上发现了一个${obj.level}级的大神,可惜他是个${obj.sex`});

//上面这段话换成以前的写法看起来会繁琐很多。如下。

console.log("我在一个名字为"+obj.name+"的网站上发现了一个"+obj.level+"级的大神,可惜他是个"+obj.sex)

大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。模板字符串之中还能调用函数。代码如下:

let x = 1;
let y = 2;

`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"

`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"

let obj = {x: 1, y: 2};
`${obj.x + obj.y}`
// "3"

function f() {
  return "welcome";
}

`I ${f()} you to read this article`
//  I Welcome you to read this article.

4. 函数

4.1 箭头函数

ES6 允许使用“箭头”(=>)定义函数。这是个很实用的技能。会让我们的代码变得简洁很多。代码如下。

var f = function (x) {
  return x;
};
// 等于
var f =(x)=>{return x};
// 当只有一个或不需要参数时,且只有一个非return语句时,可以省略成如下。
var f = (x) => console.log("hello")
//如果是return语句,则不能省略,会报错。我们可以将{}和return一起省略。
var f = x => x;
//如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

//如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了
let g = () => void NoNeedReturn();

箭头函数有几个使用注意点。

(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

5. 数组

5.1 数组的扩展运算符

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

扩展运算符等同于复制了一个数组。为什么要说等同于,是因为数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。代码如下:

let arr = [1,2,3,4,5];
let ars = [...arr];
console.log(ars);    // [1,2,3,4,5]

// 修改 ars 会造成 arr 不会随着修改。
ars[0] = 6;
console.log(ars);    // [6,2,3,4,5]
console.log(arr);    // [1,2,3,4,5]

// 修改 arr 也不会造成 ars 修改。
arr[2] = 7;
console.log(ars);    // [6,2,3,4,5]
console.log(arr);    // [1,2,7,4,5]

扩展运算符还有个用处是合并数组。

let arr1 = ["a","b"];
let arr2 = ["c"];
let arr3 = ["d","e","f"];
let arrs = [...arr1,...arr2,...arr3];
console.log(arrs);     //["a","b","c","d","e","f"]

扩展运算符还有个妙用,它可以把伪数组变成真数组。代码如下:

<ul>
    <li>test 1</li>
    <li>test 2</li>
    <li>test 3</li>
    <li>test 4</li>
    <li>test 5</li>
    <li>test 6</li>
    <li>test 7</li>
    <li>test 8</li>
    <li>test 9</li>
</ul>
<script>
    let lis = document.getElementsByTagName("lis");
    let arr = [...lis];
    console.log(Array.isArray(arr))     //true
</script>

扩展运算符也常用于函数调用。

function push(array, ...items) {
  array.push(...items);
}

function add(x, y) {
  return x + y;
}

const numbers = [4, 38];
add(...numbers) // 42

扩展运算符与正常的函数参数可以结合使用,非常灵活。

function f(m, n, x, y, z) { }
const arrs = [0, 1];
f(-1, ...arrs, 2, ...[3]);

5.2 Array.from()

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

<ul>
    <li>test 1</li>
    <li>test 2</li>
    <li>test 3</li>
    <li>test 4</li>
    <li>test 5</li>
    <li>test 6</li>
    <li>test 7</li>
    <li>test 8</li>
    <li>test 9</li>
</ul>
<script>
    let lis = document.getElementsByTagName("lis");
    let arr = Array.from(lis);
    console.log(Array.isArray(arr))     //true
</script>
//  此处用法极其类似前面的扩展运算符。

众所周知,函数内部有个类似数组的arguments对象。我们可以用Array.from()将它们转为真正的数组。如下:

function f(){
    let args = Array.from(arguments);
    console.log(Array.isArray(args));    // true
}
f();   

5.3 Array.of

Array.of是将一组值转换为数组。与 Array.from 功能相似,我们可以理解成是用来创建数组。 主要目的是弥补构造器 Array()的不足。

Array()           // []
Array(3)          // [, , ,]
Array(3, 5, 8)    // [3, 5, 8]

Array.of()           // []
Array.of(undefined)  // [undefined]
Array.of(1)          // [1]
Array.of(1, 2)       // [1, 2] 
Array.of('a','b')       // ['a','b'] 

5.4 神奇好用的Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

Set 本身是一个构造函数,用来生成 Set 数据结构。 Set可以用于数组的除重。

//将一个数组排序并除重。
let arr = [4,2,4,3,5,2,3,1];
let newArr = [];
function f(array){
    array = array.sort();
    for(let i=0; i<arr.length; i++){
        if(newArr.includes(arr[i])){
        }else{
            newArr.push(arr[i]);
        }
    }
    return newArr;
}
f(arr);
console.log(newArr);

// 用 Set 之后代码量一下子少了很多。
let arr = [4,2,4,3,5,2,3,1];
console.log(new Set(arr.sort())  );

5.5 特别的map

map在数组中的作用是逐一处理原数组元素,返回一个新数组。其中需要注意的是map的参数是回调函数,回调函数中的参数和forEach是一样的,回调函数中要有return。代码如下:

let arr = [1,2,3];
let ars = arr.map((item,index,arr)=>{
    return item += 10;
})
console.log(ars);    //    [11,12,13]

还可以利用map去操作对象数组。

let arr = [
    {name:"掘金1",score:59},
    {name:"掘金2",score:82},
    {name:"掘金3",score:16},
    {name:"掘金4",score:23},
    {name:"掘金5",score:64},
    {name:"掘金6",score:76},
    {name:"掘金7",score:98}
]
let rs = arr.map((item,index,arr)=>{
    return item.score -= 5;
})
console.log(rs);  //  [ 54, 77, 11, 18, 59, 71, 93 ]

map常用在我们不想创建新数组,只想修改原数组时。需要对数组进行加工时也要用到。

6. class

6.1 class基础知识

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

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

    function NBA(name,age,score){
        this.name = name;
        this.age = age;
        this.score = age;
    }
    NBA.prototype.say = function(){
        console.log(`我是${this.name},今年${this.age}岁了,场均得分${this.score}`)
    }
    let p1 = new NBA("Curry","30","25.5");
    let p2 = new NBA("Durant","30","29");
    let p3 = new NBA("James","34","34");
    console.log(p1.say());  //我是Curry,今年30岁了,场均得分25.5
    console.log(p2.say());  //我是Durant,今年30岁了,场均得分29
    console.log(p3.say());  //我是James,今年34岁了,场均得分34


//用class改写了一下上面的代码。
 class NBA{
        constructor(name,age,score){
            this.name = name;
            this.age = age;
            this.score = score;
        }
        say(){
            console.log(`我是${this.name},今年${this.age}岁了,场均得分${this.score}`)
        }
    }
    let p1 = new NBA("Curry","30","25.5");
    let p2 = new NBA("Durant","30","29");
    let p3 = new NBA("James","34","34");
    console.log(p1.say());  //我是Curry,今年30岁了,场均得分25.5
    console.log(p2.say());  //我是Durant,今年30岁了,场均得分29
    console.log(p3.say());  //我是James,今年34岁了,场均得分34

6.2 class继承

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

但是需要注意的是在子类中的构造器 constructor 中,必须要显式调用父类的 super 方法,如果不调用,则 this 不可用。如下:

//在ES5的继承如下:
    function NBA(name,age,score){
        this.name = name;
        this.age = age;
        this.score = age;
    }
    NBA.prototype.say = function(){
        console.log(`我是${this.name},今年${this.age}岁了,场均得分${this.score}`)
    }
    function MVP(name,age,score,year){
        NBA.call(this,name,age,score);
        this.year = year;
    }
    for(let p in NBA.prototype){
        MVP.prototype[p] = NBA.prototype[p];
    }
    MVP.prototype.showMVP = function(){
        console.log(`我是${this.name},场均得分${this.score},我是${this.year}年的MVP`)
    }
    let mvp1 = new MVP("Curry","30","25.5","2015");
    let mvp2 = new MVP("Durant","30","29","2017");
    let mvp3 = new MVP("James","34","34","2016");
    mvp1.showMVP();  //我是Curry,场均得分30,我是2015年的MVP
    mvp2.showMVP();  //我是Durant,场均得分30,我是2017年的MVP
    mvp3.showMVP();  //我是James,场均得分34,我是2016年的MVP

//使用ES6中的extends来实现继承:
     class NBA{
        constructor(name,age,score){
            this.name = name;
            this.age = age;
            this.score = score;
        }
        say(){
            console.log(`我是${this.name},今年${this.age}岁了,场均得分${this.score}`)
        }
    }
    class MVP extends NBA{
        constructor(name,age,score,year){
            super(name,age,score);
            this.year = year;
        }
        showMVP(){
            console.log(`我是${this.name},场均得分${this.score},我是${this.year}年的MVP`)
        }
    }
    let mvp1 = new MVP("Curry","30","25.5","2015");
    let mvp2 = new MVP("Durant","30","29","2017");
    let mvp3 = new MVP("James","34","34","2016");
    mvp1.showMVP();  //我是Curry,场均得分30,我是2015年的MVP
    mvp2.showMVP();  //我是Durant,场均得分30,我是2017年的MVP
    mvp3.showMVP();  //我是James,场均得分34,我是2016年的MVP

super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。

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

最后送大家一个好玩的东西。

将下面的代码在控制台中运行。

const m_style = document.createElement('style');
const m_style_text = '*{background-color:rgba(255,0,0,.2)}* *{background-color:rgba(0,255,0,.2)}* * *{background-color:rgba(0,0,255,.2)}* * * *{background-color:rgba(255,0,255,.2)}* * * * *{background-color:rgba(0,255,255,.2)}* * * * * *{background-color:rgba(255,255,0,.2)}';
m_style.appendChild(document.createTextNode(m_style_text));
document.getElementsByTagName('head')[0].appendChild(m_style)

最后,还希望各路大佬能留下宝贵的意见。笔者是个新人还请多多指教,哪里不对尽管批评。非常感谢您的阅读。