【前端基础】 ES6 常用特性

239 阅读11分钟

let 和 const

关于 let 和 const 我们需要知道的一些语法如下:

变量提升

变量可以在声明之前使用,值为 undefined,这种现象叫做变量提升。

var 命令存在变量提升

letconst 不存在,其声明的变量一定要再声明后使用,否则报错。

暂时性死区

ES6 明确规定,如果区块内存在 let 或 const 命令,则这个区块对这些命令声明的变量从一开始就形成了封闭的作用域,只要在声明之前使用这些变量就会报错。

所以在代码块内,使用let 变量在声明之前,该变量都是不可用的,这种情况叫做 暂时性死区

letconst 存在暂时性死区

const 声明的变量值是否可以改动

const 实际上保证的是变量指向的内存地址不能改动,所以对于简单数据类型而言,值就保存在变量指向的内存地址中,因此等同于常量,但是对于引用数据类型来说,变量指向的内存地址保存的只是一个指针,不是具体的值,const 只能保证这个指针是固定的,但是指针指向的数据结构是不是可变的就不是 const 能控制的了,所以我们在声明一个对象为 const 时需要非常小心。

如果我们想冻结一个对象呢,可以使用 Object.freeze 方法:

const foo = Object.freeze({});

并且对象自身的属性也应该冻结,可以使用下面的彻底冻结对象的方法:

let constantize = (obj) => {
    Object.freeze(obj);
    Object.keys(obj).forEach((key, i) => {
        if (typeof obj[key] === 'object') {
            constantize(obj[key]);
        }
    })
}

变量的解构赋值

ES6 引入的解构赋值功能大大的减少了代码量,但是解构赋值仅能对具有迭代器 (Iterator) 的数据使用

Iterator 迭代器

Iterator 作为一种统一的接口机制供我们调用处理不同的数据结构,让可以用相同的方式来遍历集合,而不用去考虑集合的内部实现,若数据的形式发生改变,只要数据内部还存在 System.iterator 属性,那遍历的代码就可以不用做修改直接使用。 同时,其服务于 for...of 循环, for...of 又有效地避免了以往对数据集仅能通过 forEach 、 for..in 遍历时遇到的一部分问题。

数组的解构赋值

let [a, b, c] = [1, 4, 6]; // a:1  b:4  c:6
let [foo, [[bar], baz]] = [1, [[2], 3]];    // foo:1    bar:2   baz:3

let [head, ...tail] = [1, 2, 3, 4]; // head:1   tail:[2,3,4]

// 如果解构不成功,变量的值为 undefined
let [x, y, ...z] = ['a'];   // x:a  y:undefined     z:[]

// 如果等号的右边不是可遍历的解构(不具有Iterator接口),那么将会报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = {};
let [foo] = null;
// 以上语句都会报错

解构赋值的默认值

let [foo = true]  = []; // foo:true
// 这句话表示,若 foo 为空则默认为 true

let [x, y = 'b'] = ['a'];  // x:'a'  y:'b'
let [x, y = 'b'] = ['a', 'undefined'];  // x:'a'  y:'b'

ES6 内部使用严格相等(===)来判断一个位置是否有值,所以如果一个数组成员不严格等于 undefined,默认值不会生效。

let [x = 1] = [null]; // x:null

let [x = 1] = [undefined]; x:1

上面的代码,如果一个数组的成员是 null,默认值不会生效,因为 null 不严格等于 undefined。

如果默认值是一个表达式,那么这个表达式是惰性求值的,意思是只有在用到的时候才会求值。

function fn(){
	return 'xxx';
}
let [x = fn()] = [1];

比如说上面的代码,因为 x 可以取到非 undefined 得值,所以 fn 函数不会执行。

并且默认值可以使用解构赋值的其他变量,但是该变量必须已经声明。

let [x = y, y = 1] = []; // 报错
let [x = 1, y = x] = []; // x:1	y:1 不报错

对象的解构赋值

对象的解构与数组有一个重要的不同:

数组的元素是按次序排列的,变量的取值是由它的位置决定的,但是对象的属性没有次序,变量必须与属性同名才能取到正确的值

let { bar, foo, baz } = {
    foo: 'aaa',
    bar: 'bbb'
}   
// bar:'bbb'  foo:'aaa'   baz:undefined

如果变量名和属性名不一致,那就必须写成下面这样

对象的解构赋值的内部机制是先找到同名属性,然后再赋值给对于的变量,所以真正被定义赋值的是后者,不是前者,所以我们新定义的那个变量要写在冒号的右边

let { kkk: hello, foo } = {
    foo: 'aaa',
    bar: 'bbb',
    kkk: 'ccc'
}   // hello:'ccc'  foo:'aaa' 

同样的,对象的赋值解构也可以指定默认值,生效的条件也同样是对象的属性值严格等于 undefined。

字符串 数值 布尔值的解构赋值

字符串也可以解构赋值,因为此时字符串被转换成了一个类似于数组的对象。

const [a, b, c] = 'hello'; // a:'h'   b:'e'   c:'c'

解构赋值时,如果等号的右边是数值类型或者布尔值类型,则会先转换成对象

使用场景

  1. 交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
  1. 从函数返回多个值
function example () {
    return [1, 2, 3];
}
let [a, b, c] = example();
  1. 函数参数的定义
function f ([x, y, z]) { };
f([1, 2, 3]);
  1. 提取 JSON 数据
let jsonData = {
    name: 'echo',
    age: '19',
    status: 'ok'
}
let { name, status: type, age } = jsonData;
// name:echo    type:'ok'   age:'19'
  1. 输入模块的指定方法

我们在加载模块的时候,往往只需要引入特定的几个方法,这种时候赋值解构就很好用。

const { sourceMap, sourceNode } = require('source-map');

字符串扩展方法

我们知道在 ES5 及之前只有 indexOf 方法可以确定一个字符串是否包含在另一个字符串内,ES6 这里有提供了 3 中新的方法

  1. includes() - 返回布尔值,表示是否找到了参数字符串
  2. startsWith() - 返回布尔值,表示参数字符串是否是源字符串的头部
  3. endsWith() - 返回布尔值,表示参数字符串是否是源字符串的尾部

以上 3 个方法都支持第二个参数,表示开始搜索的位置。

还有其他的扩展方法如下:

  • repeat() - 返回一个新字符串,表示将原字符串重复 n 次,参数如果是小数将会被取整,负数或者 infinity 将会报错 'x'.repeat(3) // xxx
  • padStart() - 用于头部补全,如果省略第二个参数,则会用空格补全,如果原字符串的长度大于或者等于指定的最小长度,返回原字符串
  • padEnd() - 用于尾部补全,如果省略第二个参数,则会用空格补全,如果原字符串的长度大于或者等于指定的最小长度,返回原字符串
'x'.padStart(5, 'ab');  // ababx
'x'.padEnd(5, 'ab');  // xabab
'xxx'.padStart(2, 'ab'); // xxx

另外还提供了 模板字符串的方法,用得比较多,所以这里略过。

数值的扩展方法

Number.isFinite() 和 Number.isNaN()

Number.isFinite() 用于检查一个数值是否是有限的

Number.isFinite(1.8) // true
Number.isFinite(NaN) // false

Number.isNaN() 用于检查一个值是否为 NaN

首先我们知道 NaN === NaN 这个等式会返回 false

Number.isNaN(NaN) // true
Number.isNaN(15) // false

Number.parseInt() 和 Number.parseFloat()

ES6 将 parseInt() 和 parseFloat() 移植到了 Number 对象上,行为完全保持不变

Number.isInteger()

Number.isInteger() 用来判断一个值是否为整数,由于 JavaScript 中整数和浮点数的储存方法是一样的,所以 3 和 3.0 是同一个值。

安全整数和 Number.isSafeInteger()

我们知道 JavaScript 能够准确表示的整数范围在 -2^53 到 2^53 之间(不含两个端点),超过这个范围就语法精确表示了。

ES6 中引入了这两个最大值和最小值作为常量,Number.isSafeInteger() 这个方法则是能够用来判断一个整数是否落在这两个数的范围内。

在 Math 对象上的扩展

这里写得很详细~~

看这里:👉

函数的扩展

函数的默认参数值

ES6 允许为函数的参数设置默认值,直接卸载参数定义的后面即可

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

同时参数默认值也可以和解构赋值结合使用

function log ({ x, y = 'hello' }) {
    console.log(x, y);
}
log({}); // undefined, 'hello'
log({ x: 1 }); // 1,'hello'
log(); // 报错

箭头函数

ES6 允许使用箭头定义函数,这个使用广泛了... 就不花篇幅介绍了。

另外,箭头函数可以绑定 this 对象,大大减少了使用 call / apply / bind 之类的显式绑定 this 对象的写法。

绑定 this

函数绑定运算符是并排的双冒号( :: ),双冒号左边是一个对象,右边是一个函数,该运算符会自动将左边的对象作为 this 对象绑定到右边的函数上

foo::bar;
// 等价于
bar.bind(foo);

尾调用优化

尾调用是函数式编程的一个重要概念,其概念非常简单,即 某个函数的最后一步是调用另一个函数,例如:

function f (x) {
    return g(x);
}

像上面这样,函数的最后一步是调用函数 g,这样的就叫做尾调用。

下面这样的都不属于尾调用:

function f (x) {
    let y = g(x);
    return y;
}

function f (x) {
    return g(x) + 1;
}

function f (x) {
    g(x);
    // 这种写法等价于
    g(x);
    return undefined;
}

尾递归

函数自身调用自身称为递归,如果尾调用自身就叫做尾递归

递归非常的耗费内存,因为需要保存成千上百个调用帧,很容易发生栈溢出,但是对于尾递归来说,由于只存在一个调用帧,所以不会发生栈溢出。

这里有个非常经典的例子就是斐波那契数列,我们先来看看非尾递归的 Fibonacci 数列实现如下:

function Fibonacci (n) {
    if (n <= 1) {
        return 1;
    }
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}
// Fibonacci(10)  =>  89
// Fibonacci(100)  => 堆栈溢出

尾递归优化后的 Fibonacci 数列实现如下

function Fibonacci (n, ac1 = 1, ac2 = 1) {
    if (n <= 1) {
        return ac2;
    }
    return Fibonacci(n - 1, ac2, ac1 + ac2);
}

数组的扩展

扩展运算符

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

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

Array.from() 和 Array.of()

Array.from() 方法用于将两类对象转为真正的数组,类似数组的对象和可遍历的对象(比如 Set 和 Map)。

例子如下:

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
}

// es5的写法
var arr1 = [].slice.call(arrayLike);  // ['a', 'b', 'c']

// es6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

Array.of() 方法用于将一组值转换为数组:

Array.of(3, 11, 8);  // [3,11,8]
Array.of(3);  // [3]
Array.of();  // []

Array.of() 方法主要是用来弥补 Array() 的不足,因为 Array() 的参数个数不同会导致 Array() 的行为有差异。

Array(); // []
Array(3); // [, , ,]
Array(2, 11, 8); // [2,11,8]

上面的代码中,Array 方法没有参数,有 1 个参数或多个参数时,返回的结果都不一样,只有当参数个数不少于 2 个的时候,Array 才回返回由参数组成的新数组,参数个数只有 1 时,实际上是指定数组的长度。

数组实例的 find() / findIndex() / fill()

数组的一些遍历方法之前写过,find() / findIndex()里面都有,指路~

fill() - 该方法使用指定值填充一个数组

['a', 'b', 'c'].fill(7); // [7,7,7]
new Array(3).fill(7); // [7,7,7]

// 如上,fill 方法用于空数组的初始化时非常方便,数组中已有的元素会被全部抹去。
// fill 方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
['a', 'b', 'c'].fill(7, 1, 2); // ['a',7,'c']

数组实例的 entries() / keys() / values()

ES6 提供了 3 个新方法:entries() / keys() / values() 用于遍历数组,它们均返回一个遍历器对象,可以用 for...of 循环遍历.

唯一的区别在于:

keys() 是对键名的遍历

values() 是对键值的遍历

entries() 是对键值对的遍历

for (let [index, ele] of ['a', 'b'].entries()) {
    console.log(index, ele);
    // 0 'a'
    // 1 'b'
}

for (let ele of ['a', 'b'].values()) {
    console.log(ele);
    // 'a'
    // 'b'
}

for (let index of ['a', 'b'].keys()) {
    console.log(index);
    // 0
    // 1
}

数组实例的 includes()

includes() 方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的 includes() 方法类似,该方法的第二个参数表示搜索的起始位置,默认为 0。如果第二个参数为负数,则表示倒数的位置。

[1, 3, 5].includes(3);  // true
[1, 3, NaN].includes(NaN);  // true


[1, 3, 7].includes(3, 3);  // false
[1, 3, 7].includes(7, -1);  // true

对象的扩展

首先 ES6 允许直接写入变量和函数作为对象的属性和方法,使书写更加的简洁,即在对象中只写属性名不写属性值,这时的属性值等于属性名所代表的的变量。

let foo = 'hello';

let obj = {
    foo,
    kk: 'kkk'
};
// obj => {foo:'hello',kk: 'kkk'}

Object.is()

ES5 比较两个值是否相等只有两个运算符:

  • == 相等运算符:缺点是会自动转换数据类型
  • === 严格相等运算符: 缺点是 NaN 不等于自身,以及 +0 等于 -0

但是我们有时候需要在所有环境中,只要两个值是一样的,它们就应该相等。

所以 ES6 提出了 同值相等 算法来解决这个问题,Object.is() 就是这个新方法,它用来比较两个值是否严格相等,与严格相等运算符 === 行为基本一致。不同之处只有两个:

  • +0 不等于 -0
  • NaN 等于自身

Object.assign()

Object.assign() 用于将源对象的所有可枚举属性复制到目标对象,其方法的第一个参数时目标对象,后面的参数都是源对象(如果目标对象和源对象有同名属性,则后面的属性会覆盖前面的)

let target = {
    a: 1
}
let source1 = {
    b: 2,
    c: 4
}
let source2 = {
    b: 3
}
Object.assign(target, source1, source2);
// {a: 1, b: 3, c: 4}

需要注意的是,如果其他类型的值(数值,字符串,布尔)不在首参数也不会报错,但是除了字符串会以数组的形式复制到目标对象,其他值都不会产生效果。

另外需要注意的是,Object.assign() 实行的是浅拷贝。所以如果源对象某个属性值是对象,那么目标对象复制得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上。

Object.assign() 方法的用途有很多:

  • 给对象添加属性
class Point {
    constructor(x, y) {
        Object.assign(this, { x, y });
    }
}
// 上面的方法通过 assign 方法将 x 属性和 y 属性添加到了 Point 类的对象实例中。
  • 给对象添加方法
Object.assign(SomeClass.prototype, {
    someMethod (arg1) {
    }
});
  • 克隆对象 / 合并多个对象
function clone (origin) {
    return Object.assign({}, origin);
}

Set

Set 是 ES6 提供的新的数据结构,它类似于数组,但是成员的值都是唯一的,没有重复的值。Set 它本身是一个构造函数,用来生产 Set 数据结构,Set 的实例化时可以接受一个数组或者具有 iterable 接口的其他数据结构作为参数。

let s = new Set();

Set 的属性和方法

  • Set.prototype.constructor —— 构造函数,默认就是 Set 函数
  • Set.prototype.size —— 返回 Set 实例的成员总数
  • add(value) —— 添加某个值,返回 Set 结构本身
  • delete(value) —— 删除某一个值,返回一个布尔值表示是否删除成功
  • has(value) —— 返回一个布尔值,表示是否删除成功
  • clear() —— 清楚 Set 中的所有成员,没有返回值

并且 Array.from 可以将 Set 结构转为数组

Set 的遍历操作

Set 结构的实例为我们提供了 4 个遍历方法:

  • keys() —— 遍历返回键名
  • values() —— 遍历返回键值
  • entries() —— 返回键值对遍历
  • forEach() —— 对每个成员使用回调函数

但是呢,由于 Set 对象没有键名,只有键值(或者说键名和键值是同一个值),所以 keys 和 values 方法的行为完全一致。

Promise

Iterator(迭代器)

Generator

Async

Class

Class 的继承

Module