JS知识点:ES6 中常见的知识点

2,183 阅读7分钟

前言

ES6(ESMAScript 6, ESMAScript2015)是指ESMA组织在2015年发布的ESMAScript 2015标准,由于相比ES5(2009年发布),ES6的改动非常大,故广义上,我们也将ES6统称所有ES5之后的新特性,包括ES7(ESMAScript2016),ES8(ESMAScript2017)...ES10(ESMAScript2019)等,这里专指ES6

1、var,letconst

我们知道在ES5中,用var关键字声明的变量存在变量提升,函数及变量的声明都将被提升到函数的最顶部,这就导致可以在使用之后在声明变量,这种不规范的行为很多时候都依靠编程习惯来约束,尤其对于新手来说存在不少隐患,于是letconst应运而生,二者的作用域都是当前代码块域,let声明的变量可以修改,const声明的是常量,声明后不可修改。

let foo = 10;
const bar = {key: 20};
foo = 20;
bar.key = 30;
bar = 30; // TypeError

这里解释一下bar.key = 30这句话改变的不是bar的值,只是修改了其中的属性,就好比古代的女人嫁鸡随鸡、从一而终,后来这只鸡变了,那也只是变成了一只“大鸡”,但鸡还是这只鸡……

1.1、 作用域的差别

JavaScript中,我们总能听到这样的话:在JavaScript中只有函数作用域,没有块级作用域。那么他们到底是什么呢?

以一个function声明的代码块是一个函数作用域,如下代码块中的scope 1scope 2就是同一个函数作用域,但他们是两个不同的代码块

function fn(){

    console.log(foo1); // undefined
    console.log(bar1); // undefined
    
    // scope 1
    var foo1 = 10;
    let foo2 = 20;
    if(true) {
        // scope 2
        var bar1 = 30;
        let bar2 = 40;
    }
    
    console.log(foo1); // 10
    console.log(foo2); // 20
    console.log(bar1); // 30
    console.log(bar2); // ReferenceError
}

我们可以看出用let声明的变量bar2if语句外是不能访问的(ReferenceError引用错误,代表引用了一个未声明的变量)

1.2、 变量提升

在上面的例子中第一句console.log(bar1);输出了undefined,并未报错,说明用var声明的变量在函数开始时就已经定义(即变量提升),但需要注意的是此时并未初始化。但采用letconst声明的变量则不存在变量提升,必须先声明后使用,否则就会报错:ReferenceError,同时我们把这种在声明之前不可使用这种特性成为 暂时性死区(Temproal Dead Zone, TDZ)。

console.log(foo); // ReferenceError
let foo = 10;

1.3、 重复声明

var foo = 10;
var foo = 20;

let bar = 20;
let bar = 20; // SyntaxError

当我们用var声明变量的时候,是可以重复声明的,后声明的会覆盖前面的声明,但当我们用letconst声明的时候则会报语法错误SyntaxError,注意,function声明的函数也和var有相似的表现:存在 变量提升重复声明

2、解构赋值

解构顾名思义按照一定的结构“解析”一个对象,通过这种方式我们可以从数组或对象中取值,本质上属于 模式匹配,这是ES6给我们提供的新语法,只要等号两边的模式相同即可解析相应的值。

// 解构一个对象,可以只解构我们需要的部分
const {foo, bar} = {foo: 1, bar: 2, other: 3};
console.log(foo, bar); // 1 2

// 解构一个数组
const [first, second] = [10, 20, 30];
console.log(first, second); // 10 20

// 忽略数组的某个值
const [first, , third] = [10, 20, 30];
console.log(first, third); // 10 30

// 解构一个不存在的键时,其值为 undefined
const {foo, bar} = {foo: 1}; // undefined

// 注意数组的 length
const [length] = [12];
console.log(length); // 12
// 用方括号可花括号解析的差别
const {length} = [12];
console.log(length); // 1

// 当属性中包含关键字或保留字时需要重命名
const {class: clazz} = {class: 'class-name'};

// 动态匹配,同样需要重命名
const key = 'thisKey';
const {[key]: value} = {thisKey: 23};
console.log(value); // 23

// 同样动态匹配也可用在对象声明中
const key = 'currentKey';
const obj = {[key]: 20};
console.log(obj); // {currentKey: 20}

// 用解构赋值交换两个变量,不需要声明第三个变量
let foo = 10;
let bar = 20;
console.log(foo, bar); // 10 20
[foo, bar] = [bar, foo];
console.log(foo, bar); // 20 10

3、 扩展运算符

在使用解构赋值的时候,可以使用扩展运算符...来将剩余值统一放入一个对象中,

// 用于对象
const {foo, ...rest} = {foo: 1, bar: 2, other: 3};
console.log(foo, rest); // 1 {bar: 2, other: 3}

// 用于数组
const [first, ...others] = [1, 2, 3];
console.log(first, others); // 1 [2, 3]

// 函数调用中传递参数
function callback(first, second){
    console.log(first, second);
}
let params = [1, 2];
callback(...params); // 输出 1 2

// 用于获取 arguments,这种特性主要用于箭头函数
const arrow = (...args) => {
    console.log(args);
};
arrow(1, 2, 3); // 输出数组 [1, 2, 3]

扩展运算符实际上是调用了Iterator接口,通常的Array,String,NodeList,Arguments,Set,Map都实现了这个接口,所以可通过扩展运算符获得arguments,也可用于函数调用时传递参数,但Object并未实现Iterator,所以当我们调用fn(...obj)时,会报错TypeError

4、默认值和默认参数

在我们使用解构赋值的时候,可以为不存在的值或值为undefined赋默认值

// 不存在指定映射
const {foo, bar = 20} = {foo: 10};
console.log(foo, bar); // 10 20

// 不存在指定值为 undefined
const {foo, bar = 20} = {foo: 10, bar: undefined};
console.log(foo, bar); // 10 20

只有为undefined时才会执行默认值赋值步骤

const {foo, bar = (function(){
    console.log('默认值');
    return 20;
})()} = {foo: 10};
// 默认值
console.log(foo, bar); // 10 20

function defaultVal(){
    console.log('默认值');
    return 20;
}
const {key1, key2 = defaultVal()} = {key1: 10, key2: null};
console.log(key1, key2); // 10 null
// 可见 key2 为 null,并未输出 “默认值”

在声明函数时,我们也可以为函数参数赋默认值,同样只有在传入参数为undefined时,才执行默认值赋值操作

function fn(foo, bar = 20){
    console.log(foo, bar);
}

fn(10); // 10 20

5、箭头函数

ES6给我们带来了新的函数语法箭头函数,在平常的书写中,极大的提高了我们的开发效率,但是箭头函数和普通函数却有着很大的差别

// 基本使用
const arrow = () => {};

// 有一个参数
const arrow = (arg) => {};
const arrow = arg => {}; // 括号可省略

// 有多个参数
const arrow = (arg1, arg2, arg3) => {}

// 函数体只有一句
const arrow = () => 1; // 花括号可省略
// 等价于
const arrow = () => {return 1;};

// 函数体有多句
const arrow = arg => {
    if(arg){
        return 1;
    } else {
        return 2;
    }
};

// 返回一个空对象
// 这实际上只有一句,但括号不能省略,否则会被解析成代码块
const arrow = () => ({});

5.1、没有自身this

箭头函数的this是和声明时的父作用域绑定的,不可通过其他方式(call,apply,bind)更改this指向,如:

'use strict';
function fn1(){
    console.log(this.foo);
    return function() {
        console.log(this.foo)
    }
}

const obj = {foo: 10};
const bar = fn1.call(obj); // 10
bar(); // TypeError(因为此时 this 指向 undefined)

const changedObj = {foo: 60};
bar.call(changedObj); // 60 (this 可以重新绑定)

如果是箭头函数:

'use strict';
function fn1(){
    console.log(this.foo);
    return () => {
        console.log(this.foo)
    }
}

const obj = {foo: 10};
const bar = fn1.call(obj); // 10
bar(); // 10 (此时 this 仍然指向 obj)
obj.foo = 20;
bar(); // 20

const changedObj = {foo: 60};
bar.call(changedObj); // 20

5.2、不可使用new

const foo = () => {};
const bar = function(){
}

new bar(); // bar {}
new foo(); // TypeError

5.3、没有arguments

箭头函数没有arguments,但可以使用上面的解构语法获得 参数列表,但得到的不是Arguments对象,而是一个包含所有参数的数组

const arrow = (...args) => {
    console.log(args);
}
arrow(1, 2, 3); // [1, 2, 3]

6、class语法

ES5中,只能通过Function.prototype来实现类的声明和继承,ES6提供了新的语法来声明一个类,使其更加贴合面向对象编程,

// ES5
function Student(){}

Student.staticMethod = function(){
    // ES5 的静态方法
}
Student.prototype.memberMethod = function(){
    // ES5 的实例方法
}

// ES6
class Person {
    constructor(name) {
        this.name = name;
        this.memberMethod = this.memberMethod.bind(this);
    }
    
    static staticMethod(){
        console.log('这是一个静态方法');
    }
    
    static memberMethod(){
        // 注意实例方法 ES6 并未绑定 this
        // React 中也建议了,在构造器中应手动绑定 this
        // 如上
        console.log('这是实例方法');
    }
}

7、Promise

(相比以上,这是大概是最复杂的,下一篇将专门将这一特性,后续还会介绍ES7async,await,他们都是依赖Promise,所以……很重要!)