ECMAScript 6 入门 学习笔记(持续更新中)


本文的目的是让了解ES5的人能快速上手ES6开发。
原文链接: 阮一峰的ES6入门教程

let和const命令

let命令

let命令用来声明变量,但它的作用范围只在let命令所在的代码块内有效。

{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

下述代码写了两种不同的循环体,他们区别在于:

方式一声明全局变量i,每一次循环i的值都进行变化。

方式二每一次循环都重新声明一个新变量i, JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

for(var i = 0; i < 10; i++){
	...
}
for(let i = 0; i < 10; i++){
	...
}

特点

1.不存在变量提升
var命令允许现使用后声明,而let命名不允许这种愚蠢的行为。

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

2.暂时性死区
由于let命令不存在变量提升,因此如果我们现使用变量,后声明,从使用变量开始到声明的这段语句就叫做暂时性死区。

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
3.不允许重复声明
let不允许在相同作用域内,重复声明同一个变量。

// 报错
function func() {
  let a = 10;
  var a = 1;
}

// 报错
function func() {
  let a = 10;
  let a = 1;
}

4.块级作用域
let实际上为 JavaScript 新增了块级作用域。

下述代码想要在某种情况下让n为10,否则为默认的5。假如变量n是var则会导致变量提升到if前,无论什么情况都是输出10.

function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}

需要注意的是块级作用域必须要要有{}

// 第一种写法,报错
if (true) let x = 1;

// 第二种写法,不报错
if (true) {
  let x = 1;
}

5.顶层对象的属性
ES6 为了改变顶层对象的属性与全局变量挂钩,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性

var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined

const命令

const声明一个只读的常量。一旦声明,常量的值就不能改变。

const PI = 3.1415;
PI // 3.1415

PI = 3;
// TypeError: Assignment to constant variable.

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据来说(数值,字符串,布尔值)等同于常量,而对于复合类型的变量(对象、数组)则不一定。

比如对象指向的只是一个地址,const只是保证这个地址不改变,而地址中存储的对象是可以改变的。(有点类似于java里的final关键字,final声明的对象,也可以之后赋值改变)

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

变量的解构赋值

数组的解构赋值

ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值。

//例子1
let [a, b, c] = [1, 2, 3];
//a 1 
//b 2
//c 3

这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值,接下来的例子就是嵌套数组的写法。

//例子2
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

值得注意的是

(1)...相当于把head后面的全部赋值给tail

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

(2)可以不完全解构,不完全解构也属于解构成功。

let [x, y] = [1, 2, 3];
x // 1
y // 2

(3)想要解构的前提右侧必须是可遍历结构(包含Iterator 接口),比如Set也可以被解构。

// 报错
let [foo] = 1;
//正确
let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"

(4)可以为变量赋默认值,不过要切记ES6中使用的是严格相等于,只有当一个数组成员严格等于undefined,默认值才会生效。

let [foo = true] = [];
foo // true

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

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

对象的解构赋值

对象的解构赋值与数组类似,但区别在于不会根据顺序来解构,而是根据属性/键值来解构

let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

值得注意的是

(1)可以通过对象的解构赋值将对象的方法赋值给变量。

const { log } = console;
log('hello') // hello

(2)匹配模式与对象的关系
下述代码中,foo是模式,baz才是真正的变量。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined

在ES6中是使用模式:变量形式来进行解构的,而默认的相当于以下代码。

let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };

(3)多层嵌套
再多层嵌套中,如果想取到值就不光需要模式匹配,同时也需要多个模式(比如最后去取line时的locstart

const node = {
  loc: {
    start: {
      line: 1,
      column: 5
    }
  }
};

let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc  // Object {start: Object}
start // Object {line: 1, column: 5}

(4)如果要将一个已经声明的变量用于解构赋值,必须非常小心
JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。

// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error
//正确的写法
// 正确的写法
let x;
({x} = {x: 1});

函数参数的解构赋值

其实这也是一个解构赋值,因为传入的是一个数组,数组参数会在传入时被结构出xy

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

add([1, 2]); // 3

函数参数同样允许有默认值,函数move的参数是一个对象,通过对这个对象进行解构,得到变量xy的值。如果解构失败,xy等于默认值。

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

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

用途

(1)交换变量的值

let x=1;
let y=2;
[x,y] = [y,x];

(2)从函数返回多个值
函数只能返回一个值,所以要么返回数组或对象,但通过解构就可以直接把想要的变量拿出来。

// 返回一个数组

function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象

function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();

(3)函数参数的定义
简单来说,如果传入的数据是按照形参顺序的,直接就扔进去个数组就完事了。如果传入的数据不是按照形参顺序传入,也可以传进去个对象,然后根据key去对应形参。
总之,这样就使得传入的数据要求降低了。

// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});

(4)提取 JSON 数据
我学的时候就觉得可以提取JSON数据,这个果然是最重要的一个应用。我们可以从JSON对象中去读取我们想要的参数,当然深入就是可以直接模式匹配,毕竟JSON里有数组和对象需要再解析。

let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};

let { id, status, data: number } = jsonData;

console.log(id, status, number);
// 42, "OK", [867, 5309]

(5)遍历 Map 结构
这个我感觉也很有用,任何带有了 Iterator 接口的对象,都可以用for...of循环遍历。我们可以轻松拿到keyvalue

const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
  console.log(key + " is " + value);
}
// first is hello
// second is world

单纯获取键值和键名就可以用下述方法来获取。

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}

(6)输入指定模块
这个超级常用,比如一个库里我们只想拿几个函数/类之类的就可以使用。

const { SourceMapConsumer, SourceNode } = require("source-map");

函数的扩展

函数参数的默认值

基本定义

当参数是基本类型时。

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

当参数是对象时,就要记得不光要赋默认值,同时也要与对象的解构相结合。

下面的例子就没有为参数赋默认值,就会导致foo只有传入对象才可以,否则就会报错。

//错误形式
function foo({x, y = 5}) {
  console.log(x, y);
}
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined
//正确形式
function foo({x, y = 5}=={}) {
  console.log(x, y);
}
foo()// undefined 5

下面再来一个例子来介绍下如何正确的与解构赋值结合。
第一种形式是把函数的参数默认值为空对象然后在对象解构时x与y有默认值,而第二种形式时把函数的参数默认值变为x为0,y为0的对象。

// 写法一
function m1({x = 0, y = 0} = {}) {
  return [x, y];
}

// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

它们在不传入参数时,没有任何区别,但当传入的对象不包含xy属性时就会体现出区别。因此我们建议使用第一种方式这样不会出现对象解构失败而出现undefined情况。

// 函数没有参数的情况
m1() // [0, 0]
m2() // [0, 0]

// x 和 y 都有值的情况
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]

// x 有值,y 无值的情况
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]

// x 和 y 都无值的情况
m1({}) // [0, 0];
m2({}) // [undefined, undefined]

m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]

参数的默认值位置

我们要切记参数的默认值应该是函数的尾参数,否则无法省略。
可以看到,除非我们传入undefined否则都会报错,(,1)也不行。

// 例一
function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。

如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。

(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

参数的作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。

下面的例子中参数是单独的作用域,和函数内部的不是同一作用域,而f()没有传入参数,因此会去外部找x,因此最后y就是1了。

let x = 1;

function f(y = x) {
  let x = 2;
  console.log(y);
}

f() // 1

再来一个复杂的例子:
上面代码中,函数foo的参数形成一个单独作用域。这个作用域里面,首先声明了变量x,然后声明了变量yy的默认值是一个匿名函数。这个匿名函数内部的变量x,指向同一个作用域的第一个参数x。函数foo内部又声明了一个内部变量x该变量与第一个参数x由于不是同一个作用域,所以不是同一个变量,因此执行y后,内部变量x和外部全局变量x的值都没变。

var x = 1;
function foo(x, y = function() { x = 2; }) {
  var x = 3;
  y();
  console.log(x);
}

foo() // 3
x // 1

如果将var x = 3var去除,函数foo的内部变量x就指向第一个参数x,与匿名函数内部的x是一致的,所以最后输出的就是2,而外层的全局变量x依然不受影响。

var x = 1;
function foo(x, y = function() { x = 2; }) {
  x = 3;
  y();
  console.log(x);
}

foo() // 2
x // 1

简单应用

值得注意的是:参数mustBeProvided的默认值等于throwIfMissing函数的运行结果,这表明参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。

function throwIfMissing() {
  throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}

foo()
// Error: Missing parameter

rest参数

ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

function add(...values) {
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

rest参数来代替arguments的好处是rest就是一个数组。可以直接使用数组的方法,而arguments是对象,还需要转换成数组。

// arguments变量的写法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();

箭头函数

基础定义

ES6允许使用箭头来定义函数,形式如下。

var f = v => v;

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

当函数没有参数时使用()来表示,多个参数时用(arg1,arg2)来表示,如果代码语句超过一句,需要用{},并且使用return语句返回。

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

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

let foo = () => { a: 1 };
foo() // undefined

应用

1.与变量结构相结合
传入一个person对象,可以进行解构。

const full = ({ first, last }) => first + ' ' + last;

// 等同于
function full(person) {
  return person.first + ' ' + person.last;
}

2.简化回调函数
也可以叫做简化匿名函数,可以直接用箭头函数来代替匿名函数。

// 正常函数写法
[1,2,3].map(function (x) {
  return x * x;
});

// 箭头函数写法
[1,2,3].map(x => x * x);

另一个是在更改排序的内部函数,要从小到大排序。

// 正常函数写法
var result = values.sort(function (a, b) {
  return a - b;
});

// 箭头函数写法
var result = values.sort((a, b) => a - b);

注意

1.函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
2.不可以当作构造函数
3.没有arguments对象,可以用rest参数代替。

其实第一个注意点是个好事,可以将this绑定。DOM的回调函数封装在handler对象中,添加的方法使用箭头函数,可以使this绑定至handler对象,否则的话this就会指向最外层,就会报错,因为外层没有dosomething方法。

var handler = {
  id: '123456',

  init: function() {
    document.addEventListener('click',
      event => this.doSomething(event.type), false);
  },

  doSomething: function(type) {
    console.log('Handling ' + type  + ' for ' + this.id);
  }
};

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

// ES6
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

// ES5
function foo() {
  var _this = this;

  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}

可以看到上述代码将ES6转成ES5之后其实是在外边定义了一个对象,来指向外层的this,这就和ES5中为了让内部函数的this绑定在外部的对象使用了相同的方法。

var o = {
  f1: function() {
    console.log(this);
    var that = this;
    var f2 = function() {
      console.log(that);
    }();
  }
}

o.f1()
// Object
// Object

4.不可以在定义对象的方法/属性里使用箭头函数,因为它没有this

globalThis.s = 21;

const obj = {
  s: 42,
  m: () => console.log(this.s)
};

obj.m() // 21

上面例子中,obj.m()使用箭头函数定义。JavaScript 引擎的处理方法是,先在全局空间生成这个箭头函数,然后赋值给obj.m,这导致箭头函数内部的this指向全局对象,所以obj.m()输出的是全局空间的21,而不是对象内部的42。上面的代码实际上等同于下面的代码。核心原因在于对象不具有单独的作用域。

globalThis.s = 21;
globalThis.m = () => console.log(this.s);

const obj = {
  s: 42,
  m: globalThis.m
};

obj.m() // 21

假如是普通的函数中使用this的话,就会指向使用它的对象,那么就会是正确的值而非全局空间了。
5.不可以在需要动态的时候使用箭头函数

var button = document.getElementById('press');
button.addEventListener('click', () => {
  this.classList.toggle('on');
});

上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。

数组的扩展

扩展运算符

...,它的作用是将一个数组转化为用逗号分割的参数序列,相当于rest的逆运算。

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

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

其应用如下:
1.替代apply函数,直接展开数组。

例如Math.max方法需要传入一组参数,而数组没有自带取最大值的方法,就可以使用扩展运算符。

// ES5 的写法
Math.max.apply(null, [14, 3, 77])

// ES6 的写法
Math.max(...[14, 3, 77])

// 等同于
Math.max(14, 3, 77);

2.数组的复制

如果想将数组a中的元素复制到数组b,在es5需要将数组a展开,这里可以借助扩展运算符。

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' ]

3.生成数组

与解构赋值相结合

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

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

const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错

const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错

4.任何实现了Iterator 接口的对象都可以变成数组

let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

let arr = [...map.keys()]; // [1, 2, 3]

其他函数

Array.from

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 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.from函数,允许传入第二参数,用来对每个元素进行处理,将处理后的值放入返回的数组。

Array.from(arrayLike, x => x * x);

Array.of

该方法可以将一组数变成数组。

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

该方法得目的是为了规避掉Array方法的不同含义。因为参数个数的不同,会导致Array()的行为有差异。

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

find和findIndex

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

[1,2,3].find((x) => x >= 2);	
//2

findIndex是找到第一个满足的元素的位置,否则返回-1。

[1,2,3].findIndex(function(x){
       return x>=2;
    });
//1

findfindIndex都允许传入第二个函数以绑定this

function f(v){
  return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person);    // 26

数组实例的 entries(),keys() 和 values()

这是ES6新提供的遍历数组的新方法,它们都返回一个遍历器对象(《Iterator》),可以用for…of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

   for(i of[1,2,3,4,5].keys()){
    console.log(i);
   }
    for(v of[1,2,3,4,5].values()){
        console.log(v);
    }
    for([key,value] of [1,2,3,4,5].entries()){
        console.log(key+","+value);
    }

数组实例的includes()

可以看到和indexof的区别在于可以判断NaN了。因为indexof方法判断的是===就会导致NaN无法满足条件。

[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true
[NaN].indexOf(NaN)        // -1

第二个参数代表从哪里开始寻找。

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

对象的扩展

属性的简洁表达式

ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。

function f(x, y) {
  return {x, y};
}

// 等同于

function f(x, y) {
  return {x: x, y: y};
}

f(1, 2) // Object {x: 1, y: 2}

方法也可以简写

const o = {
  method() {
    return "Hello!";
  }
};

// 等同于

const o = {
  method: function() {
    return "Hello!";
  }
};

但切记,不要在构造函数中简写,否则会报错。

我们常见的例子就是getset函数。

const cart = {
  _wheels: 4,

  get wheels () {
    return this._wheels;
  },

  set wheels (value) {
    if (value < this._wheels) {
      throw new Error('数值太小了!');
    }
    this._wheels = value;
  }
}

属性的可枚举性和遍历

对象的每一个属性都有一个描述对象(Descriptor),用来控制属性的行为。

let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
//  {
//    value: 123,
//    writable: true,
//    enumerable: true,
//    configurable: true
//  }

其中的enumerable是可枚举性,通常在遍历中,会根据该属性判断是否会规避该属性,在for...in语句中,就会因为length属性的可枚举性为false,而不遍历。

而在正常使用中,我们更多的关注对象本身,而不关注其继承的对象,因此尽量使用Object.keys(obj)而不是for...in语句。

for…in循环:只遍历对象自身的和继承的可枚举的属性。
Object.keys():返回对象自身的所有可枚举的属性的键名。

super关键词

ES6 新增了另一个类似this的关键字super,指向当前对象的原型对象。但只能在对象方法的简略法中使用。

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"

错误实例:

// 报错
const obj = {
  foo: super.foo
}

// 报错
const obj = {
  foo: () => super.foo
}

// 报错
const obj = {
  foo: function () {
    return super.foo
  }
}

对象的扩展运算符

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

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

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

链判断运算符

ES6中提出了链判断运算符,以防止出现以下情况,如果其中一个为null的话就会报错,因此常规会有很麻烦的判断。

// 错误的写法
const  firstName = message.body.user.firstName;
// 正确的写法
const firstName = (message
  && message.body
  && message.body.user
  && message.body.user.firstName) || 'default';

而在ES6中简化了写法,使用?.

const firstName = message?.body?.user?.firstName || 'default';

该运算符具有中断特性,只要其为nullundefined就会返回undefined
链判断运算符有三种用法。

  • obj?.prop // 对象属性
  • obj?.[expr] // 同上
  • func?.(…args) // 函数或对象方法的调用

例子如下:

a?.b
// 等同于
a == null ? undefined : a.b

a?.[x]
// 等同于
a == null ? undefined : a[x]

a?.b()
// 等同于
a == null ? undefined : a.b()

a?.()
// 等同于
a == null ? undefined : a()

Null运算符

我们会选择在对象的属性为nullundefined的时候为其赋初始值,通常情况如下:

const headerText = response.settings.headerText || 'Hello, world!';
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;

可由于JS的自动转换,当值为0或者false都会使用初始值,因此我们这里引进新的Null判断符??。它的行为类似||,但是只有运算符左侧的值为null或undefined时,才会返回右侧的值。

该运算符可以和?.相结合,并赋值。

const animationDuration = response.settings?.animationDuration ?? 300;

上述代码代表当response.settingsnullundefined时,就会返回300。

Set和Map数据结构

Set

基础

可以通过两种方式创建Set,普通创建和用数组(或者具有Iterable接口的其他数据结构)创建。

const s = new Set();
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]

值得一提的是Set中判断是否相等的机制类似于完全相等(===),区别是,这里认为NaN等于自身。

常用方法

s.add(1).add(2).add(2);
// 注意2被加入了两次

s.size // 2

s.has(1) // true
s.has(2) // true
s.has(3) // false

s.delete(2);
s.has(2) // false

遍历方式:
常规的就是keys(),values(),entries()三个方法,而Set没有value,所以和key是相同的值。

let set = new Set(['red', 'green', 'blue']);

for (let item of set.keys()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.values()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.entries()) {
  console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]

扩展运算符(…)内部使用for...of循环,所以也可以用于 Set 结构。我们可以利用Set和扩展运算符去掉数组中重复的元素。

let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)];
// [3, 5, 2]

Map

常用方法

  • size属性
  • set(key,value)
  • get(key)
  • has(key)
  • delete(key)
  • clear()

遍历与Set相似,顺序都是插入顺序。

数据结构互换

1.map转数组,使用扩展运算符

const myMap = new Map()
  .set(true, 7)
  .set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]

2.数组转map,直接传入

new Map([
  [true, 7],
  [{foo: 3}, ['abc']]
])
// Map {
//   true => 7,
//   Object {foo: 3} => ['abc']
// }

Iterator和for…of循环

基础

Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。

Iterator的实质是通过调用next函数。每次next函数都会返回一个对象,包括valuedone。当done为true时,就会停止遍历。

Iterator 的遍历过程是这样的。
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

Iterator过程可以通过如下代码来理解,it是一个可以遍历数组的遍历器,当调用其next函数就可以进行遍历。

var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++], done: false} :
        {value: undefined, done: true};
    }
  };
}

ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。

const obj = {
  [Symbol.iterator] : function () {
    return {
      next: function () {
        return {
          value: 1,
          done: true
        };
      }
    };
  }
};

ES6中实现iterator接口的有:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

对象之所以不实现该接口的缘故是不知道遍历顺序,因此可以我们自己定义。下面就是个例子,我们使用了RangeIterator来实现输出范围内数字的功能。range函数生成了一个对象,该对象具有Symbol.Iterator属性,也就是本身对象,因为在构造函数中定义了next函数。在for…of过程中就会通过Symbol.Iterator属性,找到迭代器,并调用其next函数。

class RangeIterator {
  constructor(start, stop) {
    this.value = start;
    this.stop = stop;
  }

  [Symbol.iterator]() { return this; }

  next() {
    var value = this.value;
    if (value < this.stop) {
      this.value++;
      return {done: false, value: value};
    }
    return {done: true, value: undefined};
  }
}

function range(start, stop) {
  return new RangeIterator(start, stop);
}

for (var value of range(0, 3)) {
  console.log(value); // 0, 1, 2
}

上面代码首先在构造函数的原型链上部署Symbol.iterator方法,调用该方法会返回遍历器对象iterator,调用该对象的next方法,在返回一个值的同时,自动将内部指针移到下一个实例。有点类似于链表。

function Obj(value) {
  this.value = value;
  this.next = null;
}

Obj.prototype[Symbol.iterator] = function() {
  var iterator = { next: next };

  var current = this;

  function next() {
    if (current) {
      var value = current.value;
      current = current.next;
      return { done: false, value: value };
    } else {
      return { done: true };
    }
  }
  return iterator;
}

var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);

one.next = two;
two.next = three;

for (var i of one){
  console.log(i); // 1, 2, 3
}

遍历器对象同时具有return函数和throw函数,return()方法的使用场合是,如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return()方法。throw()方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。

调用return函数的时机如下:情况一输出文件的第一行以后,就会执行return()方法,关闭这个文件;情况二会在执行return()方法关闭文件之后,再抛出错误。

function readLinesSync(file) {
  return {
    [Symbol.iterator]() {
      return {
        next() {
          return { done: false };
        },
        return() {
          file.close();
          return { done: true };
        }
      };
    },
  };
// 情况一
for (let line of readLinesSync(fileName)) {
  console.log(line);
  break;
}

// 情况二
for (let line of readLinesSync(fileName)) {
  console.log(line);
  throw new Error();
}

实例

最好在工作中实际遇到这种问题关注下,看看自己实现的Iterator都注意了哪些东西。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值