2019-05-26 | 学习笔记 | UNLOCK | 更新时间:2022-10-8 9:14

ES6的各知识点概念的学习笔记

2011年,ECMAScript 5.1版发布后,就开始制定6.0版了。因此,ES6 这个词的原意,就是指 JavaScript 语言的下一个版本。ES6 的第一个版本在2015年6月发布了,正式名称就是《ECMAScript 2015标准》(简称 ES2015)

ECMAScript6 的定义

ECMAScript6(简称 ES6) 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。很多时候提到的 ES6 ,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”

ES6 的第一个版本在2015年6月发布了,正式名称就是《ECMAScript 2015标准》(简称 ES2015).2016年6月,小幅修订的《ECMAScript 2016标准》(简称 ES2016)如期发布,

声明变量的命令

ES6 新增加 let 和 const 俩个声明变量的新命令,和 var 不同的是,两者声明的变量都不存在声明提前的现象。因此带来了暂时性死区的现象(既在声明前使用 let 和 const 声明的变量会导致报错),同时两者不允许重复声明同一个变量。两者不同的是,let 声明一个变量,const 则声明一个指针不变的变量。对于简单类型(数值、字符串、布尔值),既值不变,对于复杂类型,则保证指向的内存地址不变,数据结构不能保证不变,如要保证数据不变需要使用冻结方法 Object.freeze(

console.log(str)  //str is not defined
let str = '字符串'

const arr = []
arr.push('22')
console.log(arr)  // ["22"]

const arr2 = Object.freeze([11])
arr2.push('22')     //object is not extensible
console.log(arr2)  // [11]

const num = 2
num = 3  //Assignment to constant variable

块级作用域和函数声明

新的声明方式带来了块级作用域的出现,ES6 允许块级作用域的任意嵌套,同时块级作用域使得获得广泛应用的立即执行函数表达式(IIFE)不在必要;
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明(但是由于厂商为了兼容以前的旧代码,浏览器没有遵守ES5的之前的规定,还是支持在块级作用域之中声明函数);
ES6 则允许函数在块级作用域中声明(但是 ES6 在附录 B里面规定,浏览器的实现可以不遵守这个规定,有自己的行为方式):

  • 允许在块级作用域内声明函数。
  • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
  • 同时,函数声明还会提升到所在的块级作用域的头部

所以考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句

// 块级作用域内部,优先使用函数表达式
{
  let a = 'secret';
  let f = function () {
    return a;
  };
}

注意

  • ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域
  • ES5 只有两种声明变量的方法:var命令和function命令。ES6
    除了添加let和const命令,另外两种声明变量的方法:import命令和class命令。所以,ES6 一共有 6 种声明变量的方法。

变量的结构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构,从数组中提取值,按照对应位置,对变量赋值本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。如果解构不成功,变量的值就等于undefined

以数组的形式

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

//不完全解构 
let [a,b,c] = [1,2]; // a = 1,b = 2,c = undefinef
let [a,b] = [1,2,3]; // a = 1,b = 2

//默认值: 只有当特定数组成员严格等于undefined,默认值才会生效
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
let [x, y = 'b',z = 'c'] = ['a',null] // x='a', y=null,z='c'

以对象的形式

let { x, y, z } = { y: 'a', z: 'b' } // x=undefined y='a' z='b'

// 变量和属性名不一致; 对象属性匹配的是模式,后面的才是匹配数据的变量
let { foo: x } = { foo: 'a', bar: 'b' } // x='a'

//默认值
let {x, y = 5} = {x: 1}; // x=1 y=5

// JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误,需要使用圆括号进行包裹
let x;
{x} = {x: 1}; // Unexpected token =
({x} = {x: 1}); // x=1

以字符串,数字或布尔值的形式

let [a, b, c] = 'her'; //a='h' b='e' c='r'

let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true

数组的扩展运算

扩展运算符

扩展运算符(spread)是三个点(…)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。扩展运算符后面还可以放置表达式,只要是数组

console.log(...[1, 2, 3]) // 1 2 3
console.log(...(5 > 0 ? ['a'] : [])) // a

//  取代 applay
Math.max.apply(null, [14, 3, 77]) // 获取数组中最大值
Math.max(...[14, 3, 77]) // 效果同上

var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2); // 将数组合并
arr1.push(...arr2); //效果同上,arr1会改变 和cancat()比较,避免了内存浪费,当然如果还需要使用arr1就另说

注意

  • 只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错

数组的扩展

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

let arrayLike = {
    '0': 'a',
    '1': 'b',
    length: 2
}
let arr2 = Array.from(arrayLike); // ['a', 'b']

Array.of 方法用于将一组值,转换为数组。

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

Array.fill() 方法用于填充数组

['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

对象的扩展运算

ES6 允许直接写入变量和函数,作为对象的属性和方法。变量名为属性名, 变量的值为属性值。函数则通过return 一个对象的方法

const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}

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

对象的扩展

Object.is() 用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

Object.assign() 用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target):如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性,如果参数不是对象,则会先转成对象,然后返回

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

//拷贝对象
const obj1 = {a: {b: 1}};
const obj2 = Object.assign({}, obj1);
obj1.a.b = 2;
obj2.a.b // 2
//Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用

prototype

JavaScript 语言的对象继承是通过原型链实现的。ES6 提供了更多原型对象的操作方法,__proto__属性(前后各两个下划线),用来读取或设置当前对象的prototype对象。目前,所有浏览器(包括 IE11)都部署了这个属性。标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的

Object.setPrototypeOf() 作用与__proto__相同,用来设置一个对象的prototype对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法

// 格式
Object.setPrototypeOf(object, prototype)

let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);
proto.y = 20; // 设置原型属性
obj.x // 10
obj.y // 20

Object.getPrototypeOf() 该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象

//格式
Object.getPrototypeOf(object);

function Rectangle() {
  // ...
}
const rec = new Rectangle();
Object.getPrototypeOf(rec) === Rectangle.prototype
// true

Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype
// false

独一无二的Symbol

在ES6之前 Javascr 有五种基本原始数据类型:string,number,boolean,Null,undefined,和 复杂数据类型 object。现在ES6 新增加一种原始数据类型:Symbol,它表示独一无二的值。Symbol 值通过Symbol函数生成。且不能与其他类型的值进行混合运算

let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false

// Symbol 值作为对象属性名时,不能用点运算符
let a=Symbol()
let b={
    [a]:'c',
  a:'d'
}
b.a // 'd'
b[a] // 'c'
b['a'] // 'd'

注意

  • Symbol 作为属性名,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回.但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol 属性名

Symbol的扩展运用

Symbol.for() 接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值.和Symbol() 不同的是,你调用Symbol()多少次,它就创建多少个不同的Symbol,而Symbol.for()则没有才创建,否则返回

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true

Symbol.keyFor() 方法返回一个已登记的 Symbol 类型值的key

let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

//Symbol()写法没有登记机制,所以每次调用都会返回一个不同的值
let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

Set和 Map数据结构

Set 数据结构

Set 数据结构类似数组,但是它的成员的值都是唯一,没有重复的值,Set 本身就是一个构造函数,用于生存Set 数据结构

const s = new Set();
[2, 3, 5, 5, 2, 2].forEach(x => s.add(x));
s // [2,3,5]

// Set 由内部算法进行判断成员值是否相等,类似“===”(全等),区别在于:在Set中 NaN等于NaN;“===”(全等)则否认他们相等

[...new Set(array)] // 可用于去除数组的重复
[...new Set('ababbc')].join('') // 可用于去除字符串的重复

Set 结构的实例有以下属性。

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。

Set 实例的操作方法(用于操作数据)

  • add(value):添加某个值,返回 Set 结构本身。
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值。

Set 实例的遍历方法(用于遍历成员。)。

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

Map 数据结构

Map 类似于对象,也是键值对的集合,但是的范围不限于字符串,各种类型的值(包括对象)都可以当作键。Map提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,也许 Map 比 Object 更合适

let map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);

map.size // 2
map.has('name') // true
map.get('name') // "张三"

const o = {p: 'Hello World'};
map.set(o, 'content') // 以对象o作为健名
map.get(o) // "content"
map.has(o) // true
map.delete(o) // true
map.has(o) // false

//只有对同一个对象的引用,Map 结构才将其视为同一个键
map.set(['a'], 555);
map.get(['a']) // undefined

// Map 转数组
[...map]

Map 数据结构的操作方法 (用于操作数据)

  • set() 设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键
  • get() 读取key对应的键值,如果找不到key,返回undefined
  • has() 返回一个布尔值,表示某个键是否在当前 Map 对象之中
  • delete() 删除某个键,返回true。如果删除失败,返回false
  • clear() 清除所有成员,没有返回值

Map 数据结构的遍历方法(用于遍历成员。)

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。
  • forEach():遍历 Map 的所有成员。

数据结构的遍历循环

Javascript中,数据集合在新增了 Set 和 Map 后,目前有四种,Array, Object,Set,和 Map.而JS 可以通过遍历机制对数据集合进行遍历操作(即依次处理该数据结构的所有成员)。

let Arr = [1,2,3]

for(let i=0;i<Arr.length;i++){ //for 循环
    console.log(Arr[i])
}

for(let i in Arr){
    console.log(Arr[i])
}

for(let i of Arr){
    console.log(i)
}
//如果是数组,三者目前效果都是 1 2 3 。当然forEach will 和do/will 也可以用于遍历

异步编程相关Promise

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象,Promise对象存在三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作可以改变状态,且改变后无法修改

const promise = new Promise((resolve,reject)=>{
  console.log('1')
  if(true){ // 可以通过条件判断执行不同的状态或只执行成功
    resolve('成功')
  }else{
    reject('失败')
  }
})

promise.then(function(value) {
  console.log(value) // 成功后的回调
}, function(error) {
  console.log(error) // 失败后的回调
});

console.log('2')

// 图片加载异步执行函数
function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image();
    image.src = url;
    image.onload = function() {
      console.log(url + new Date().getTime())
      resolve(image);
    };
    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };
  });
}

例子的执行顺序为:1 ,2 ,成功。因为异步回调只有在所有同步任务执行完才会执行。resolve 状态必须是要有。reject 状态可选

Promise 的数据处理

promise
.then((resolve)=>{ 
    // 执行状态成功的回调
},(reject)=>{
    // 执行状态失败的回调(包括代码出错)
})
.catch((err)=>{ // 和then 第二个参数对比,建议使用 catch 捕获
    // 执行状态失败的回调(包括代码出错)
})
.finally(() => {
    // 无论怎样都会执行的回调
});

异步编程相关Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,Generator 有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式. Generator 函数和普通函数最大区别在于它是分段执行

function* generator(){
    console.log('1')
    yield 'hello'
    yield 'word'
    return 'end'
}
let gen=generator()
gen.next() // { value: 'hello', done: false }
gen.next() // { value: 'word', done: false }
gen.next() // { value: 'end', done: true }
gen.next() // { value: 'end', done: true }

Generator 函数分段执行,yield表达式是切割分段的标记,通过next() 调用,除此外 for...of... 可以遍历Generator 函数且不需要next()

Generator 数据操作

Generator.prototype.throw()

在函数体外抛出错误,然后在 Generator 函数体内捕获

var g = function* () {
  try {
    console.log(1)
    yield 'one';
    console.log(2)
    yield 'two';
    return 'three'
  } catch (e) {
    console.log('内部捕获', e);
  }
};

var i = g();
i.next();

try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕获', e);
}

Generator.prototype.return()

返回给定的值,并且终结遍历 Generator 函数

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next()        // { value: undefined, done: true }

异步编程相关 async

async 函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句

async function timeout(ms) {
  await new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello world', 50);// 50毫秒后,打印hello word

// 函数声明式
async function foo() {}
// 函数表达式
const foo = async function () {};
// 箭头函数
const foo = async () => {};
// 对象的方法
let obj = { async foo() {} };

async 的数据处理

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误,任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行

async function f() {
  await Promise.reject('出错了');
  await Promise.resolve('hello world'); // 不会执行
}

async function f() {
  try {
    await Promise.reject('出错了');
  } catch(e) {
    ···
  }
  return await Promise.resolve('hello world');
}

Class 的基本语法

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

class Bar {
  hello() {
    console.log('hello word');
  }
  string(name){
    this.name=name
  }
}

// 表达式的形式
const Bar = class Me {
  getClassName() { // 只能在类内部使用Me名称。外界new 需要使用 Bar
    return Me.name; 
  }
};

var b = new Bar();
b.hello() // "hello word"

constructor 类的默认方法

通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加

class Point { }

// 等同于
class Point {
  constructor() {}
}

get 和 set :拦截和存储

的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为

class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter'; // 在读取 prop 属性的时候,返回
  }
  set prop(value) {  // 在设置 prop 属性的时候,打印
    console.log('setter: '+value);
  }
}

let inst = new MyClass();
inst.prop = 123;  // setter: 123
inst.prop  // 'getter'

注意:

  • 类必须使用new调用,否则会报错

不被继承的静态方法

所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

class Foo {
  static classMethod() { // 静态方法
    return 'hello';
  }
}
// 类可以直接调用
Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod() 
// TypeError: foo.classMethod is not a function

// 如果静态方法包含this关键字,这个this指的是类,而不是实例
class Foo {
  static bar() {
    this.baz();
  }
  static baz() {
    console.log('hello');
  }
  baz() {
    console.log('world');
  }
}
Foo.bar() // hello

模块体系的加载

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入.export命令除了输出变量,还可以输出函数或类(class),import的加载方式都是编译时加载,所以不可以有算术表达式

//整体加载
import * as React from 'react'
// 用星号(*)指定一个对象,所有输出值都加载在这个对象上面

//静态加载
import { stat, exists, readFile } from 'fs';
// 上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载

//匿名加载
import customName from './export-default';
// 上面的代码会加载导入文件中的默认输出 即 export default ,且可以随意为默认输出指定名称

//兼容加载
import load, { each, forEach } from 'lodash';
// 如果想同时加载默认输出和其他接口,可以写成如上形式

export default 命令:为模块指定默认输出

export default function () {
  console.log('foo');
}

// 写法一
export function m(){
  console.log('1')
}

// 写法二
var m = function(){
  console.log('2')
}
export {m};

// 写法三
export {n as m};

浏览器的加载体系

浏览器脚本的默认语言是 JavaScript,默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。所以就有异步加载的需求

// 异步加载
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

当浏览器遇到标签存在 deferasync 属性,脚本就会异步加载(不会等它下载和执行,而是直接执行后面的脚本)。defer 是“渲染完再执行”,async 是“下载完就执行”。因此 async 不能保证加载的顺序

ES6模块的加载

// ES6 模块的加载属于异步执行
<script type="module" src="./foo.js"></script>

与君共勉

ES6