重读 ES6 标准入门(第3版)

3,499 阅读10分钟

前言

仅将自己的理解做整理、归类并结合实际遇到的问题做记录,更推荐阅读 ECMAScript 6 入门

扩展篇

数组的扩展

  • 解构赋值

“ES6 中允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)”

作用:可以快速取得数组或对象当中的元素或属性,而无需使用arr[x]或者obj[key]等传统方式进行赋值

let [a,b,c] = [1,2,3];
  • 扩展运算符

“扩展运算符是三个点,它如同rest参数的逆运算,将一个数组转为用逗号分隔的参数序列”

作用:把数组或类数组对象展开成一系列用逗号隔开的值

console.log(...[1,2,3])
//1 2 3
console.log([1,2,3].join())
//1,2,3
console.log(typeof(...[1,2,3]))
//Uncaught SyntaxError: Unexpected token ...
console.log(typeof([1,2,3].join()))
//string
* 为什么typeof(...[1,2,3])报错Uncaught SyntaxError: Unexpected token ...???

常用

let [head,...tail] = [1,2,3,4]; //head:1,tail:[2,3,4]
let colors = ["red","green","blue"];
let [...cloneColors] = colors;
console.log(cloneColors)  //["red", "green", "blue"]

1.将数组转为函数的参数,替代 apply 方法

Math.max.apply(null,[14,3,77])//ES5
Math.max(...[14,3,77])//ES6

2.合并数组

var arr1=['a','b']
var arr2=['c','d']
console.log(arr1.concat(arr2))//ES5
console.log([...arr1,...arr2])//ES6
  • Array.from()

“Array.from方法用于将两类对象转成真正的数组”

  • Array.of()

“Array.of方法用于将一组值转换为数组”

常用

console.log(Array.of(3,11,8))//[3, 11, 8]
  • copyWithin()

复制替换数据

Array.prototype.copyWithin(target,start=0,end=this.length)

target:必选,从该位置开始替换 start:替换内容的起点 end:到该位置前停止读取

console.log([1,2,3,4,5].copyWithin(0,3,4))//[4, 2, 3, 4, 5]
console.log(['a','b','c'].copyWithin(0,1,3))//["b", "c", "c"]
  • etc. find()、fill()、entries()、keys()、values()、includes、数组的空位(非重点关注)

对象的扩展

  • 属性简洁表示、属性名表达式、方法的name属性、Object.is()、Object.assign():'合并对象-浅复制'、属性可枚举性
  • 遍历对象的属性

1.for...in

2.Object.keys()

var obj = { 0: 'a', 1: 'b', 2: 'c' };
console.log(Object.keys(obj)); // console: ['0', '1', '2']
  • 对象的解构赋值

对象的属性没有次序,变量必须与属性同名,才能取到正确的值

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

字符串的扩展

  • 字符串遍历 for...of
console.log('foo'.split(''))//["f", "o", "o"]//ES5
for(let codePoint of 'foo'){
    console.log(codePoint)//'f',"o","o"//ES6
}
  • 返回字符串给定位置的字符at()
console.log('lallallal'.indexOf('a'))//1 Number//ES5
console.log('lallallal'.charAt('a'))//1 String//ES6
  • includes():是否找到参数字符串、startsWith()/endswith():参数字符串是否在源字符串头/尾部...

函数的扩展

  • 函数参数的默认值
function log(x,y='world'){
    console.log(x,y)
}
log('moximoxi')//moximoxi world
  • rest参数(...变量名)
function test(...values){
    console.log(values)
    for(var val of values){
        console.log(val)
    }//for...of可以不仅可以遍历字符串,还可以遍历数组 ES6
}
test(1,2,3)//[1, 2, 3]
  • name属性
name属性:function foo(){} foo.name//"foo"
  • 箭头函数 →→→

函数名=参=>返回值

  • 尾调用、尾递归

某个函数的最后一步是调用另一个函数

正则的扩展

数值的扩展

承诺篇

Promise

from MDN:

var promise1 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve('foo');
  }, 300);
});

promise1.then(function(value) {
  console.log(value);
  // expected output: "foo"
});

console.log(promise1);
// expected output: [object Promise]

Generator

from CSDN:

function* helloGenerator() {
   console.log("this is generator");
 }
var h = helloGenerator();
h.next();
function* helloGenerator() {
       yield "hello";
       yield "generator";
       return;
   }
   var h = helloGenerator();
   console.log(h.next());//{ value: 'hello', done: false }
   console.log(h.next());//{ value: 'generator', done: false }
   console.log(h.next());//{ value: 'undefined', done: true }

yield实际就是暂缓执行的标示,每执行一次next(),相当于指针移动到下一个yield位置。

总结一下,Generator函数是ES6提供的一种异步编程解决方案。通过yield标识位和next()方法调用,实现函数的分段执行。

async

迭代器=>Generator=>async

1.迭代器:不暴露对象的内部表示的情况下,能够遍历整个元素

遍历 ~ Traverse 访问一个集合(广义)的每个元素

迭代 ~ Iterate 反复调用同一个过程最终达成目的(迭代是循环的一种方式),这个过程如果是一个函数,那就是递归,如果是一个循环体,那就是狭义上的迭代。递归和迭代之间的关系、转换、优化等等又是另一个故事了。

function makeIterator (arr) {
    let nextIndex = 0;
    //返回一个迭代器方法
    return {
        next: () => {
            if(nextIndex < arr.length){
                return {value: arr[nextIndex++], done: false}
            }else{
                return {done: true}
            }
        }
    }
}
const it = makeIterator([1,2,3]);
console.log('1:', it.next());
console.log('2:', it.next());
console.log('3:', it.next());
console.log('end:', it.next());

2.Generator函数执行后会返回一个迭代器

3.async函数是Generator的语法糖

async 函数返回一个Promise对象,可以使用 then 方法添加回调函数

实际遇到

(function(){
    let pro=new Promise((resolve,reject)=>{
        resolve(true)
    })
    console.log(pro)//promise对象
})()

(async function(){
    let pro=await new Promise((resolve,reject)=>{
        resolve(true)
    })
    console.log(pro)//true 
//(async函数的返回值是一个promise对象;waite命令后面是一个Promise对象,如果不是,会被转成一个立即resolve的Promise对象)
})()

数据结构篇

Set、Map

  • Set

Set 是成员值唯一的数据结构,类似于数组 Array

所以,有一种重要且好用的去重方法:

var set1 = Array.from(new Set([1,1,2,2,33,'aa','aa','bb'
]))
console.log(set1)//[1, 2, 33, "aa", "bb"]

或:

var set1 = [...new Set([1,1,2,2,33,'aa','aa','bb'
])]
console.log(set1)//[1, 2, 33, "aa", "bb"]

常用

Set 实例的方法分为两大类:操作方法(用于操作数据)、遍历方法(用于遍历成员)

分类 方法
操作方法 Set.prototype.add(value):添加某个值,返回 Set 结构本身

Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。

Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。

Set.prototype.clear():清除所有成员,没有返回值。
遍历方法 Set.prototype.keys():返回键名的遍历器

Set.prototype.values():返回键值的遍历器

Set.prototype.entries():返回键值对的遍历器

Set.prototype.forEach():使用回调函数遍历每个成员
  • Map

Map 是可以用非字符串当作键的键值对数据结构,类似于对象 Object

Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。

常用

Map 实例的方法分为两大类:操作方法(用于操作数据)、遍历方法(用于遍历成员)

分类 方法
操作方法 Map.prototype.set(key, value):设置

Map.prototype.get(key):获取

Map.prototype.has(key):判断存在

Map.prototype.delete(key):删除

Map.prototype.clear():清除
遍历方法 Map.prototype.keys():返回键名的遍历器。

Map.prototype.values():返回键值的遍历器。

Map.prototype.entries():返回所有成员的遍历器。

Map.prototype.forEach():遍历 Map 的所有成员。

Proxy、Reflect

  • Proxy

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

Proxy就是对象的拦截器

典例:set()方法:用validator拦截person.age的创建;

let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // 对于满足条件的 age 属性以及其他属性,直接保存
    obj[prop] = value;
  }
};

let person = new Proxy({}, validator);

person.age = 100;
person.age // 100
person.age = 'young' // The age is not an integer
person.age = 300 // The age seems invalid
  • Reflect

将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。

为了修改某些Object方法的返回结果,让其变得更合理。

例:用defineProperty,不抛出错误,返回false:

// 老写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

重点食用:使用 Proxy 实现观察者模式——结合Vue的双向绑定原理

观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行。

const person = observable({
  name: '张三',
  age: 20
});

function print() {
  console.log(`${person.name}, ${person.age}`)
}

observe(print);
person.name = '李四';
// 输出
// 李四, 20

上面代码中,数据对象person是观察目标,函数print是观察者。一旦数据对象发生变化,print就会自动执行。

下面,使用 Proxy 写一个观察者模式的最简单实现,即实现observable和observe这两个函数。 思路是observable函数返回一个原始对象的 Proxy 代理,拦截赋值操作,触发充当观察者的各个函数。

const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});

function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  queuedObservers.forEach(observer => observer());
  return result;
}

上面代码中,先定义了一个Set集合,所有观察者函数都放进这个集合。 然后,observable函数返回原始对象的代理,拦截赋值操作。拦截函数set之中,会自动执行所有观察者。

lterator和for...of循环

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

lterator有点难理解了 传送门 结合迭代器、生成器理解

默认调用 Iterator 接口(即Symbol.iterator方法)的场合:

(1)解构赋值

对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。

let set = new Set().add('a').add('b').add('c');

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

let [first, ...rest] = set;
// first='a'; rest=['b','c'];

(2)扩展运算符

扩展运算符(...)也会调用默认的 Iterator 接口。

// 例一
var str = 'hello';
[...str] //  ['h','e','l','l','o']

// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']

上面代码的扩展运算符内部就调用 Iterator 接口。

实际上,这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。

let arr = [...iterable];

(3)yield*

yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。

let generator = function* () {
  yield 1;
  yield* [2,3,4];
  yield 5;
};

var iterator = generator();

iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }

(4)其他场合

由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。

for...of
Array.from()
Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
Promise.all()
Promise.race()

模块化篇

Class

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

∴ JS实现继承还是通过原型链的方式!

ES5:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);

ES6:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

Modlule

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。 ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

// circle.js

export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}
// main.js

import { area, circumference } from './circle';

console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));

编程风格的变化

(1) let 取代 var

(2) 在let和const之间,建议优先使用const,尤其是在全局环境,不应该设置变量,只应设置常量。

(3) 静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。

// bad
const a = "foobar";
const b = 'foo' + a + 'bar';

// acceptable
const c = `foobar`;

// good
const a = 'foobar';
const b = `foo${a}bar`;

(4) 善用解构赋值⭐

(5) 对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法。

// bad
const a = {};
a.x = 3;

// if reshape unavoidable
const a = {};
Object.assign(a, { x: 3 });

// good
const a = { x: null };
a.x = 3;

(6) 使用扩展运算符(...)拷贝数组。⭐

// bad
const len = items.length;
const itemsCopy = [];
let i;

for (i = 0; i < len; i++) {
  itemsCopy[i] = items[i];
}

// good
const itemsCopy = [...items];

(7)立即执行函数可以写成箭头函数的形式

(() => {
  console.log('Welcome to the Internet.');
})();

(8)善用Map⭐

(9)Module 语法是 JavaScript 模块的标准写法,坚持使用这种写法。使用import取代require。

(10)ESLint