阅读 17486

项目实用 | 看完阮一峰老师的ECMAScript 6、我总结了那么多✍

globalThis 顶层对象

JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。

var a = 2;
window.a // 2
复制代码

ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

var a = 2;
window.a // 2

let b = 3
window.b //undefined
复制代码
  • 浏览器里面,顶层对象是window,但 NodeWeb Worker 没有window
  • 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self
  • Node 里面,顶层对象是global,但其他环境都不支持。

ES2020 在语言标准的层面,引入globalThis作为顶层对象。也就是说,任何环境下,globalThis都是存在的,都可以从它拿到顶层对象,指向全局环境下的this

Destructuring

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

Array Destructuring

let [a, , b, ...c] = [1, 2, 3, 4, 5]
a   // 1
b   // 3
c   // [4,5]

//error
let [a] = 1  //不具备 Iterator 接口

let [a, b = 2, c = 3] = [1, , undefined]
a //1,
b //2,
c //3

let [x = 1, y = x] = [];     // x=1; y=1
let [x = y, y = 1] = [];     // ReferenceError: y is not defined

//交换位置
let x = 1, y = 2;
[y, x] = [x, y]
x // 2
y // 1
复制代码

Object Destructuring

Object的解构与Array有一个重要的不同。Array的元素是按次序排列的,变量的取值由它的位置决定;而Objectprop没有次序,变量必须与prop同名,才能取到正确的value

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

//解构对象方法
let { log, sin, cos } = Math;
const { log } = console;
复制代码

以上说明,对象的解构赋值是下面形式的简写

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

let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};
let { p: [x, { y }] } = obj;
p // p is not defined  // p是模式,不参与赋值
x // "Hello"
y // "World"
let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]
复制代码

Tip:对象的解构赋值可以取到继承的属性。

const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf(obj1, obj2);

const { foo } = obj1;
foo // "bar"
复制代码

设置默认值时,默认值生效的条件是,对象的属性值严格等于undefined

var { message: msg = 'Something went wrong', say } = { say() { console.log(`say`) } };
msg // Something went wrong
say // function say(){}
复制代码

Symbol

表示独一无二的值。它是 JavaScript 语言的第七种数据类型.前六种是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)

let sy = Symbol('a')
let sy1 = Symbol('a')
sy === sy1  // false
sy.toString() === sy1.toString() // true
// ES2019 提供 description
sy.description === sy1.description // true

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2 // true
复制代码

Symbol.for()Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("foo")30 次,每次都会返回同一个 Symbol 值,但是调用Symbol("foo")30 次,会返回 30 个不同的 Symbol 值。

Symbol.iterator

对象的Symbol.iterator属性,指向该对象的默认遍历器方法。这个着重记了一下

const myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable]  //  [1, 2, 3]   // ...的底层是for...of
复制代码

对象进行for...of循环时,会调用Symbol.iterator方法,返回该对象的默认遍历器,

class Collection {
  *[Symbol.iterator]() {
    let i = 0;
    while(this[i] !== undefined) {
      yield this[i];
      ++i;
    }
  }
}

let myCollection = new Collection();
myCollection[0] = 1;
myCollection[1] = 2;

for(let value of myCollection) {
  console.log(value);
}
// 1
// 2
复制代码

Symbol.toStringTag

对象的Symbol.toStringTag属性,指向一个方法。在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object][object Array]object后面的那个字符串。

({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"

class Collection {
  get [Symbol.toStringTag]() {
    return 'xxx';
  }
}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
复制代码

Set && Map

ES6 提供了新的数据结构 Set, Map

Set成员的值都是唯一的,没有重复的值,Set内的元素是强类型,会进行类型检查。

let set = new Set([1, true, '1', 'true', 1])
// Set(4) {1, true, "1", "true"}
复制代码

Set处理并集(Union)、交集(Intersect)和差集(Difference)

let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}

// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}
复制代码

Map类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

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

map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
复制代码

数据间的转换

Map To Array

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

Array To Map

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

Map To Object

function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}

const myMap = new Map()
  .set('yes', true)
  .set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
复制代码

Object To Map

let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));
复制代码

Map To JSON

function strMapToJson(strMap) {
  return JSON.stringify(strMapToObj(strMap));
}

let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'
复制代码

JSON To Map

function jsonToStrMap(jsonStr) {
  return objToStrMap(JSON.parse(jsonStr));
}

jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
复制代码

WeakSet && WeakMap 所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakSet && WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

Proxy

概述

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

Object.defineProperty是使用的数据劫持:直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。数据劫持最典型的应用 -----> 双向的数据绑定(一个常用的面试题),

  • Vue 2.x 利用 Object.defineProperty(),并且把内部解耦为 Observer, Dep, 并使用 Watcher 相连
  • Vue3.x 版本之后改用 Proxy 进行实现

ProxyObject.defineProperty的对比

  • Object.defineProperty
    • 只能监听对象(Object),不能监听数组的变化,无法触发push, pop, shift, unshift,splice, sort, reverse
    • 必须遍历对象的每个属性
    • 只能劫持当前对象属性,如果想深度劫持,必须深层遍历嵌套的对象
    "use strict"
    let obj = {};
    let value = 1
    Object.defineProperty(obj, 'listenA', {
      writable: true,  //可修改
      enumerable: true,  // 可枚举   for...in...   Object.keys()
      configurable: true,  // 可配置,可删除
      get: () => value,
      set: val => {
        console.log(`set obj.listenA .. ${val}`);
        value = val
      },
    });
    obj.listenA = 2 //set obj.listenA .. 2
    console.log(obj.listenA)  // 2
    复制代码
  • Proxy
    • 可以直接监听对象而非属性
    • 可以直接监听数组的变化
    // 代理整个对象
    let proxyObj = new Proxy({}, {
      get: (target, key, receiver) => {
        console.log(`getting ${key}!`);
        return target[key];
      },
      set: (target, key, value, receiver) => {
        console.log(target, key, value, receiver);
        return target[key] = value;
      },
    });
    proxyObj.val = 1;    // {} val 1 {}
    proxyObj.val;       // getting val!
      
    //代理数组
    let proxyArr = new Proxy([], {
      get: (target, key, receiver) => {
        console.log(`getting ${key}!`);
        return target[key];
      },
      set: (target, key, value, receiver) => {
        console.log(target, key, value, receiver);
        return (target[key] = value);
      },
    });
    proxyArr[0] = 1; //  {} val 1 {}
    console.log(proxyArr[0]); //  getting val!  // 1
    console.log(proxyArr); // [1]
    复制代码

Reflect

Reflect翻译过来是反射的意思,与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。有一下几个作用

  • Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
  • 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false
 // 老写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {  // 成功返回true
  // success
} else {
  // failure
}
复制代码
  • Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。
// 老写法
'assign' in Object // true

// 新写法
Reflect.has(Object, 'assign') // true
复制代码
  • Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
Proxy(target, {
  set: function(target, name, value, receiver) {
    var success = Reflect.set(target, name, value, receiver);
    if (success) {
      console.log('property ' + name + ' on ' + target + ' set to ' + value);
    }
    return success;
  }
});

复制代码

async function

ES2017 标准引入了 async 函数,使得异步操作变得更加方便,由于async函数返回的是Promise对象,可以作为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);
复制代码

返回 Promise 对象

async function f() {
  return 'hello world';
}

f().then(v => console.log(v))
// "hello world"
复制代码

async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到

async function f() {
  throw new Error('出错了');
}

f().then(
  v => console.log('resolve', v),
  e => console.log('reject', e)
)
//reject Error: 出错了
复制代码

Promise 对象的状态变化

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('http://localhost:8080/').then(console.log(123))
复制代码

await 命令

正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

async function f() {
  // 等同于
  // return 123;
  return await 123;
}

f().then(v => console.log(v))
// 123
复制代码

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。

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

如果希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。

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

f()
.then(v => console.log(v))
// hello world
复制代码

async 函数的实现原理

Generator 函数和自动执行器,包装在一个函数里

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}
复制代码

严格模式

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

严格模式主要有以下限制。

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protected、staticinterface

export && import

export && import来进行模块的导出导入。

// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};

// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};

import 'lodash';
import 'lodash';  //加载了两次lodash,但是只会执行一次。

export { foo, bar } from 'my_module';  //当前模块不能直接使用foo和bar
// 接口改名
export { foo as myFoo } from 'my_module';
// 整体输出
export * from 'my_module';
//默认接口
export { default } from 'foo';
export { default as es6 } from './someModule';
复制代码

结束语

以上是小编学习完阮一峰老师的ES6之后的心得,希望大家指正😜

文章推荐