ES6789?笔记

[Array]Change array by copy(ES2023)

组数老头疼的问题就是 sort, splice, reverse 会改变原来的对象,这个 proposal (opens in a new tab) 提出了

  • toSorted
  • toReversed
  • toSpliced
  • with (opens in a new tab)array.with(index, value) 将一个数组的某下标的值改成新的 value

这几个方法来 copy 数组

Stage 4,corejs 也有对应的 polyfill 了。

Node 尚未支持(v18)

Top-Level await(ES2022)

tc39 (opens in a new tab)

可以在一个 module 的顶层就使用 await 了,解决什么问题可以详细看 issue,内容比较详细

BTW:Nodejs 也支持(>= 14.8,并且声明模块是 esm 的情况 type: module 文件后缀 .js)

深入了解

阅读了字节内 haoli(ulivz) 深度剖析 TLA(Top-level await) 的文章后大受震撼。(公开版 (opens in a new tab)

  • tla-fuzzer (opens in a new tab) 测试各种打包器对 TLA 预期的正确性
  • Webpack Runtime 是如何处理 TLA 的,结合源码分析,非常复杂

摘录一下总结部分的内容:

  1. TLA 诞生的目的是为了尝试解决 ES Module 的异步初始化能力
    1. 简单来说,当一个模块导出的是一个异步初始化之后的变量,可能在初始化完成之前就已经被消费了,会存在异常
  2. node v14.8.0 支持。如果在 UI 代码中使用(浏览器),需要借助 Bundler 打包,除非直接使用 es module 形式,一般来说需要打包成 iife
  3. 大多数 bundler 都可以在 target format 为 esm 的时候成功编译 TLA,但只有 webpack 能支持编译到 iife,并且 webpack 是唯一一个能够正确模拟 TLA 语义的
    1. webpack 5.83.0 以下需要手动开启 experiments.topLevelAwait (opens in a new tab) 配置
  4. 对于使用的建议:虽然 webpack 能够将 TLB 打包成 iife,但是产物中仍然包含 async await 的语法(具体原因 (opens in a new tab)),导致了只能在 iOS11 / Chrome 55 以上的环境运行,出于稳定性考虑,C 端项目中不该使用 TLA,B 端项目其实也尽量不使用吧
  5. TLA 和 async function 一样有传染性,使得 Dependent 也被处理为 Async Module 了,但这对开发者是无感的

hasOwn(ES2022)

tc39 proposal (opens in a new tab)

Object.prototype.hasOwnProperty 更好用的检查对象是否有某个属性的方法。

polyfill 代码 (opens in a new tab)

let object = { foo: false };
Object.hasOwn(object, "foo"); // true
 
let object2 = Object.create({ foo: true });
Object.hasOwn(object2, "foo"); // false
 
let object3 = Object.create(null);
Object.hasOwn(object3, "foo"); // false

通常我们会用到 obj.hasOwnProperty,但是有些时候,这个方法甚至都没法用,因为这个方法是在 Object 的原型上的,比如:

  1. 对象根本就没有原型:__proto__ (被赋值)是 null,或者 Object.create(null)
  2. hasOwnProperty 已经被注册了

.at()(ES2022 (opens in a new tab))

built-in indexables,数组、字符串

const cart = ["🍎", "🍌", "🍍"];
 
// first element
cart.at(0); // '🍎'
 
// last element
cart.at(-1); // '🍍'
 
// out of bounds
cart.at(-100); // undefined
cart.at(100); // undefined

Object.fromEntries(ES 2019)

MDN (opens in a new tab)

Object.entries 的逆操作

有的时候通过 entries/values 重组的对象可以直接通过这个方法再组装回去

Object.fromEntries([["eee", "ee"]]);
// { eee: 'ee' }

From Map

const m = new Map([
  ["23", 123],
  ["eew", 333],
]);
 
Object.fromEntries(m);
// { '23': 123, eew: 333 }

数值分隔符 Numeric Separators(ES 2021)

还挺好,用起来吧

let fee = 123_00; // $123 (12300 cents, apparently)
let fee = 12_300; // $12,300 (woah, that fee!)
let amount = 12345_00; // 12,345 (1234500 cents, apparently)
let amount = 123_4500; // 123.45 (4-fixed financial)
let amount = 1_234_500; // 1,234,500

logical assignment operators(ES 2021)

Stage 4 了 (opens in a new tab)

// "Or Or Equals" (or, the Mallet operator :wink:)
a ||= b;
a || (a = b);
 
// "And And Equals"
a &&= b;
a && (a = b);
 
// "QQ Equals" QQ 应该是 question question
a ??= b;
a ?? (a = b);

why

经常会有判断某个变量是否有值,如果没有就给他赋值

function foo(a) {
  if (!a) {
    a = "foo";
  }
  // or 利用 || 的断路机制
  // a || (a = "foo")
}

所以 a ||= "foo" 就是先判断是否 a 值,没有就给 a 赋值,??= 同理

&&= 就有点不太一样了,是当 a 是 truthy 的时候,重新给 a 赋值。

语义

注意逻辑赋值操作符其实是两个过程:

  1. 先取值判断 truthy(get)
  2. 根据上个过程决定是否需要赋值(set)

所以要注意不一定每次都会调用 setter

最后注意的是,这个短语法还是有别于算术操作符(比如 +=)的,有 issue (opens in a new tab) 讨论(懒得细看了)

String 标签模版

在函数名后紧跟着模版字符串(标签其实就是函数)

alert`yes ok`;
// 等价于
alert(["yes ok"]);

当模版字符串中有变量 ${} 的时候,处理起来就不一样了,会将变量所在的占位符作为 split 的位置将字符串分隔成数组作为函数的第一个参数,变量依次取值作为剩余参数传入,形如

// const tag = (stringVals, ...args) => {}
tag = (stringVals, ...args) => {
  console.log(stringVals);
  console.log(args);
};
// let a = 123;
// tag`yes ok${a}`
// ["yes ok", ""]
// [123]

还原字符串

// 还原模版字符串 with 变量
const pass2 = (stringArr, ...args) => {
  // console.log(stringArr, args);
  const res = [];
  for (let i = 0; i < args.length; ++i) {
    res.push(`${stringArr[i]}${args[i]}`);
  }
  res.push(stringArr[stringArr.length - 1]);
  return res.join("");
};
 
// .... 一行超人 没必要
const pass = (stringArr, ...args) =>
  [
    ...args.map((v, i) => `${stringArr[i]}${v}`), // 可以对 v 做一些操作 比如 safe parse
    stringArr[stringArr.length - 1],
  ].join("");

Set 集合

集合类型的常规方法就不多说了

看一下他的迭代器 Symbol.iterator 指向的是 Set.prototype.values

Set.prototype[Symbol.iterator] === Set.prototype.values;

也有 keysentries,和 forEach 方法,也可以对其使用 ... 展开

集合操作

没有提供官方的 API,但是利用上面的特性去实现也很简单

并集(Union)

const union = new Set([...setA, ...setB]);

交集(Intersect)

const intersect = new Set([...setA].filter((v) => setB.has(v)));

差集(Difference)

A - B: A 中有的,B 中没有

const differenceAB = new Set([...setA].filter((v) => !setB.has(v)));

Promise.allSettled

Promise.all用法一样,都是接受一组 promise,最大的区别是 all 只有全部的 promise 都是 resolved 了才会返回,allSettled 会等待所有 promise 的状态从 pending 变化了才会返回,能获得所有的结果(相当于是 all 做了一个 .catch 的处理)

返回的结果会有statusvalue来表示,rejected 情况下会是reason

const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);
 
const allSettledPromise = Promise.allSettled([resolved, rejected]);
 
allSettledPromise.then(function (results) {
  console.log(results);
});
// [
//    { status: 'fulfilled', value: 42 },
//    { status: 'rejected', reason: -1 }
// ]

Proxy

要明白 Vue3 的响应式原理当然先要学这个 Proxy 和 Reflect 咯

阮一峰的描述:

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

就是在编程语言上的一个代理的作用,对谁做代理呢?当然是 Object,可以对一些对象的操作做拦截,进行改写和过滤(Charles)。

let obj = {
  name: "dio",
};
 
obj.proxy = new Proxy(obj, {
  get(target, propKey, receiver) {
    console.log(target, propKey, receiver);
    console.log(`getting ${propKey}`);
    return target[propKey];
  },
  set(target, propKey, value, receiver) {
    console.log(target, propKey, receiver);
    if (propKey in target) {
      console.log(`setting ${propKey}`);
      target[propKey] = value;
      return true;
    }
    return false;
  },
});
 
const p = obj.proxy;
 
console.log(p.name);
p.name = 123;
console.log(p);
console.log(obj);

可以看出,其实 proxy 做的事情是对.运算符做了重载(overload),令人怀念的 c++ 重载运算符啊。

proxy可以重新定义 handle 来代理原来的各种方法

可拦截的操作

摘自:阮一峰,一共 13 种。

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

handler 拦截方法应用

get

让数组访问不越界

function myArray(...elements) {
  const handler = {
    get(target, propKey, receiver) {
      let index = Number(propKey);
      if (index < 0) {
        // 实现 arr[-1] 获取最后一个 这里仅修改 propKey 的值 让 Reflect 去 get
        propKey = String(target.length + index);
      }
      if (Number(propKey) < 0) {
        propKey = "0";
      }
      return Reflect.get(target, propKey, receiver);
    },
  };
  const target = [];
  target.push(...elements);
  return new Proxy(target, handler);
}
 
let arr = myArray(1, 2, 3, 4, 5, 6);
console.log(arr);
console.log(arr[0]);
console.log(arr[-124]);

get方法可继承(让 proxy 成为 prototype)

利用get拦截,实现一个生成各种 DOM 节点的通用函数dom(来自阮一峰)

看看 receiver 是什么

它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。所以对 proxy 操作还是对其本身对象进行操作

const px = new Proxy(obj, {
  get(target, propKey, receiver) {
    if (propKey === "_receiver") {
      return receiver;
    }
    if (propKey === "_target") {
      return target;
    }
  },
});
console.log(px._receiver === px); // true
console.log(px._target === obj); // true

set

阻止私有变量被修改

const handler = {
  get(target, key) {
    invariant(key, "get");
    return target[key];
  },
  set(target, key, value) {
    invariant(key, "set");
    target[key] = value;
    return true;
  },
};
function invariant(key, action) {
  if (key[0] === "_") {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop;
// Error: Invalid attempt to get private "_prop" property
proxy._prop = "c";

注意,如果目标对象自身的某个属性,不可写(writeable)且不可配置(configurable),那么set方法将不起作用。

注意,严格模式下,set代理如果没有返回true,就会报错。

apply

apply方法拦截函数的调用、callapply操作

var handler = {
  apply(target, ctx, args) {
    // ctx 就是 上下文
    return Reflect.apply(...arguments);
  },
};

has

拦截hasProperty的操作,比如用in的时候。

const hasHandler = {
  has(target, key) {
    if (key[0] === "_") {
      return false;
    }
    return key in target;
  },
};
 
const obb = {
  _private: "oops",
  name: "yes",
};
 
const pobb = new Proxy(obb, { ...hasHandler });
console.log("name" in pobb); // true
console.log("_private" in pobb); // false
console.log("pp" in pobb); // false
for (let k in pobb) {
  console.log(k); // _private name
  // for 循环的 in 不起作用
}

注意,has方法拦截的是HasProperty操作,而不是HasOwnProperty操作,即has方法不判断一个属性是对象自身的属性,还是继承的属性。

对 for 循环的 in 不起作用

construct

construct方法用于拦截new命令

const newHandler = {
  construct(target, args, newTarget) {
    return new target(...args);
  },
};

construct方法返回的必须是一个对象,否则会报错

ownKeys

拦截对象自身属性的读取操作

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
  • for...in
const ownKey = {
  ownKeys(target) {
    return Reflect.ownKeys(target).filter((key) => key[0] !== "_");
  },
};
 
const obb = {
  _private: "oops",
  name: "yes",
};
 
const pobb = new Proxy(obb, { ...hasHandler, ...ownKey });

注意,使用Object.keys()方法时,有三类属性会被ownKeys()方法自动过滤,不会返回。

  • 目标对象上不存在的属性
  • 属性名为 Symbol 值
  • 不可遍历(enumerable)的属性

Proxy 的静态方法

Proxt.revocable()

返回一个可取消的 Proxy 实例。返回一个对象属性 proxy 是代理,revoke 是取消代理的函数开关

const { proxy: rep, revoke } = Proxy.revocable(obj, hasHandler);
console.log(rep.name);
revoke();
// console.log(rep.name);  // 这里报错了 如下
const maybeCustom = value[customInspectSymbol];
                             ^
TypeError: Cannot read property 'Symbol(nodejs.util.inspect.custom)' of null

可见实际上也是用 Symbol 实现

this 问题

代理之后,target 内部的 this 指向代理!

const target = {
  m: function () {
    console.log(this === proxy);
  },
};
const handler = {};
 
const proxy = new Proxy(target, handler);
 
target.m(); // false
proxy.m(); // true

polyfill

core-js 是不支持 Proxy 的(Readme (opens in a new tab) 最后写了),并推荐使用了 proxy-polyfill (opens in a new tab) 作为 polyfill**(但也是只提供了核心功能)**

Reflect

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API

增加这个 API 的几个目的

  1. Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
  2. 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false意味着之后有些方法直接用 Reflect 来操作会更好。
  3. Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为
  4. Reflect对象的方法与**Proxy对象的方法一一对应**,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。在 Proxy 中也不需要考虑原来的行为是如何实现了,直接交给Reflect

静态方法

Reflect对象一共有 13 个静态方法。

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • Reflect.set(target, name, value, receiver)
  • Reflect.defineProperty(target, name, desc)
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)

怎么感觉也是一层代理呢?

关于 receiver 可以以 set 为例,会将 this 绑定给 receiver

let ssee = {
  _name: "init",
  get name() {
    return this._name;
  },
  set name(value) {
    console.log("set name");
    return (this._name = value);
  },
};
 
let rec = {
  _name: "eee",
  get name() {
    return this._name;
  },
};
// 有 set 的情况执行
let res = Reflect.set(ssee, "name", 0);
console.log(res);
console.log(ssee.name);
// 给 receiver 的情况下 上下文的 this 会帮绑定给 receiver
res = Reflect.set(ssee, "name", "kekekeke", rec);
console.log(res);
console.log(ssee.name); // 0
console.log(rec.name); // kekekeke

Reflect.defineProperty(target, propertyKey, attributes)

Reflect.defineProperty方法基本等同于Object.defineProperty,用来为对象定义属性。未来,后者会被逐渐废除,请从现在开始就使用Reflect.defineProperty代替它。

Reflect.getOwnPropertyDescriptor(target, propertyKey)

Reflect.getOwnPropertyDescriptor基本等同于Object.getOwnPropertyDescriptor,用于得到指定属性的描述对象,将来会替代掉后者。

Reflect.isExtensible (target)

如果参数不是对象,Object.isExtensible会返回false,因为非对象本来就是不可扩展的,而Reflect.isExtensible会报错。

Object.isExtensible(1); // false
Reflect.isExtensible(1); // 报错

观察者模式例子

// 观察者模式
const queuedObservers = new Set();
 
const observe = (fn) => queuedObservers.add(fn);
const observable = (obj) => new Proxy(obj, { set }); // 对 obj 做观察 set 直接用的下面那个
 
function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  // set 之后 通知所有的 观察者 执行
  console.log("notify");
  queuedObservers.forEach((observer) => observer(value));
  return result;
}
 
observe((value) => {
  console.log("ob1:", value);
});
 
const proxyOfObj = observable(obj);
proxyOfObj.foo = "123123";
// notify
// ob1: 123123

数组 Array.from() Array.of()

Array.from()

将可迭代对象(有 Symbol.iterator )或者类数组的对象(arguments)转换为数组类型

可以接受第二个参数,一个 function,类似 map,对元素进行转换后放入数组

Array.from()的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种 Unicode 字符,可以避免 JavaScript 将大于\uFFFF的 Unicode 字符,算作两个字符的 bug。

那么很多时候就很方便了,比如逆序字符串

const reverseStr = (value) => Array.from(value).reverse().join("");

Array.of()

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

Array.of基本上可以用来替代Array()new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一:返回的都是有值数组,而不会是 Array(n) 返回一个长度为 n 的空数组。

数组 flat() flatMap()

第一次见到这两个函数是在一次面试题中。。。

函数顾名思义:将数组拉平(如果是多维数组 nested)

flat()

[1, [2, 2, 3] 2, 3].flat()
// 1, 2, 2, 3, 2, 3

默认拉 1 层,可以给层数,不管多少层都 flat 的时候可以flat(Infinity)

flatMap()

flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。

返回新的数组,不改变原数组,只能展开一层

数组 find() findIndex()

都接受一元谓词,还可以接受第二个参数的,一个对象,bind 给第一个方法的

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

find()

返回第一个满足条件的成员,如果都无,返回undefined

findIndex()

返回第一个满足条件的下标,都无则返回 -1

数组 some() every()

some()

有一些(some)满足条件即可

接受一个 callback predicate,只要有一个元素中满足 predicate 为 true 的,整个就返回 true,否则为 false

会遍历整个数组,不会断路

every()

每一个(every)满足条件即返回 true 否则 false

是可以断路的,立即返回 false

装饰器

装饰器 for

回忆一下 Python 装饰器,其实也是利用闭包的语法糖,对原始函数进行增强功能,甚至是修改(感觉只有在 JS 中可以),有了这个语法糖,写起来方便,读起来清晰

用法

类装饰器

@ + functionName基本都是这个语法

@decorator
class A {}
 
// 等同于
class A {}
A = decorator(A) || A;

装饰器函数是一个高阶函数,接收一个 target,

function testable(target) {
  target.isTestabel = true;
}

如果需要多个参数,可以再闭包一层

function testable(isTestable) {
  return function (target) {
    target.isTestable = isTestable;
  };
}
 
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable; // true
 
@testable(false)
class MyClass {}
MyClass.isTestable; // false

注意,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数。

在看一些来自阮一峰的例子

// mixins.js
export function mixins(...list) {
  return function (target) {
    Object.assign(target.prototype, ...list);
  };
}
 
// main.js
import { mixins } from "./mixins";
 
const Foo = {
  foo() {
    console.log("foo");
  },
};
 
@mixins(Foo)
class MyClass {}
 
let obj = new MyClass();
obj.foo(); // 'foo'

在 React 和 Redux 一起用的时候(还没学 Redux。。。)

class MyReactComponent extends React.Component {}
 
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent); // 调用

可以写成

@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}

for class's method

因为是用在类方法的,装饰器可以接受另外两个参数:name 和 descriptor,对应的是属性 name 和 Object.getOwnPropertyDescriptor(obj, name)

class Math {
  @log
  add(a, b) {
    return a + b;
  }
}
function log(target, name, descriptor) {
  var oldValue = descriptor.value;
  // 对 descriptor 进行修改 返回一个新的函数 操作一波
  descriptor.value = function () {
    console.log(`Calling ${name} with`, arguments);
    return oldValue.apply(this, arguments);
  };
 
  return descriptor;
}
 
const math = new Math();
 
// passed parameters should get logged now
math.add(2, 4);

多个装饰器

洋葱模型

function dec(id) {
  console.log("evaluated", id);
  return (target, property, descriptor) => console.log("executed", id);
}
 
class Example {
  @dec(1)
  @dec(2)
  method() {}
}
// evaluated 1
// evaluated 2
// executed 2
// executed 1

其实可以这么看

@dec(1)
@dec(2)
method(){}
// 等价于
dec(1)(
  dec(2)(method)
)()

why 没有函数的装饰器

python 是有的。。

装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。

var readOnly = require("some-decorator");
 
@readOnly
function foo() {
}
// 实际是
var readOnly;  // 此时是 undefined 下面的函数定义就会报错
 
@readOnly
function foo() {
}
 
readOnly = require("some-decorator");

总之,由于存在函数提升,使得装饰器不能用于函数。类是不会提升的,所以就没有这方面的问题。

另一方面,如果一定要装饰函数,可以采用高阶函数的形式直接执行,写一个 wrapper。

core-decorators.js

一个第三方库,提供了一些类装饰器,详见github (opens in a new tab)

一些应用

mixin

class

让 JS 写 OOP 的时候也像其他语言(C++,Java,Python)那么流畅(看起来像那么回事)

class 其实是 ES6 为了实现类的一个语法糖

class Tank {
  constructor(name, year) {
    this.name = name;
    this.year = year;
  }
  toString() {
    return `Tank: ${this.name}-${this.year}`;
  }
}
 
const KingTiger = new Tank("tiger", 1940);
console.log(Tank); // Function
console.log(Tank.prototype.constructor === Tank); // true
console.log(KingTiger);

其实就是写了一个构造函数的 function,只不过用 class 语法糖将一些实现细节给屏蔽了,我们能标准的、顺畅的开发了。

所以我们需要明白的是 class 做了哪些转换规则:

构造函数中的toString()toValue()等方法也都放到prototype去了,是不可枚举的

console.log(Object.keys(Tank.prototype)); // []
console.log(Object.getOwnPropertyNames(Tank.prototype)); // [ 'constructor', 'toString', 'toValue', 'fire' ]

构造函数

回忆一下 python/c++ 的__init__(self, ),Class::Class()

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

没有写的话会自动加上这个

也是通过new关键字来创建实例,调用这个constructor

关于__proto__是指向prototype的,但是不是标准的用法,可以查阅 MDN

__proto__ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。

getter & setter

与 ES5 一样,在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

class Tank {
  constructor(name, year) {
    this.name = name;
    this.year = year;
  }
  get amount() {
    return this._amount ? this._amount : (this.amount = 0);
  }
  set amount(value) {
    this._amount = this._amount ? this._amount + value : value;
    return this._amount;
  }
}
const KingTiger = new Tank("tiger", 1940);
console.log(KingTiger.amount); // 0
KingTiger.amount = 100;
console.log(KingTiger.amount); // 100

写的时候踩坑了,如果上面的this._amount没有下划线,那就会无限循环调用setget,然后就爆 stack 了

类的名字Tank.name和普通对象构造函数一样用法

立即执行的类

let person = new (class {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(this.name);
  }
})("张三");
 
person.sayName(); // "张三

值为类的变量

const ttt = class Tank {
  constructor(name, year) {
    this.name = name + Tank.name;
    this.year = year;
    this.bomb = 100;
    ...

可以将类赋值给一个对象,但是类的名称还是Tank所以class只是声明了一个类,他后面的就是名字ttt.name => Tank

注意点

1.严格模式

在类和 ES 模块中默认就是use strict

考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。

2.不存在变量提升 hoist

{
  let Foo = class {};
  // 如果 class 提升了 这里 Bar 就无法继承 Foo 了
  class Bar extends Foo {}
}

3.类的 name

4.Generator 方法

在类方法前面加*就可以了

* fire() {
  console.log('boommmmm!');
  for (let i = 0; i < this.bomb; ++i) {
    yield 'booommmm! ' + i;
  }
}
// 这个对象属性 指向实例的默认迭代器 详见下面的 Symbol
* [Symbol.iterator]() {
  for (let i = 0; i < this.amount; ++i) {
    yield `gen no.${i+1} tank`;
  }
}

5.this 的指向

类方法的 this 都是指向实例的,但是如果单独拿出来类方法const {fire} = Tank就要注意this了,在严格模式下就是undefined

一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。

constructor() {
  // bind 一下
  this.printName = this.printName.bind(this);
}

使用箭头函数

class Obj {
  constructor() {
    // 保存的是调用时的上下文
    this.getThis = () => this;
  }
}
 
const myObj = new Obj();
myObj.getThis() === myObj; // true

还有一种解决方法是使用Proxy,获取方法的时候,自动绑定this

TODO 这个之后在学!

静态方法

和其他的 oop 一样的类静态方法

注意:静态类方法的this指向的是这个类!

class Foo {
  static bar() {
    this.baz();
  }
  static baz() {
    console.log("hello");
  }
  baz() {
    console.log("world");
  }
}
 
Foo.bar(); // hello

可以继承父类的静态方法

静态方法也是可以从super对象上调用的

静态属性

class ModernTank extends ttt {
  static modern = true;
  tag = "TAG";
  constructor(name, year) {
    super(name, year);
    this.modified = 1000;
  }
  static get isModern() {
    // 这里的 this 是 ModernTank 我们调用他的静态成员
    return this.modern;
  }
}
const mtk = new ModernTank("M4A1", 2020);
console.log(ModernTank.isModern); // true

实例属性写在顶级

不仅可以在constructor中写,也可以在类的顶级写,例子见上

公有私有属性

现在只有解决方案和提案:阮一峰 (opens in a new tab)有写到

new.target

new关键字的属性!返回new命令作用于的那个构造函数,new 的目标,一般用在构造函数里面

function R() {
  if (typeof new.target === "undefined") {
    throw new Error("用 new 来构造!");
  }
  this.e = 123;
  this.name = R.name;
}
R(); // 报错咯

可以防止直接调用构造函数

继承的时候,会返回子类

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    // ...
  }
}
 
class Square extends Rectangle {
  constructor(length, width) {
    super(length, width);
  }
}
new Square(3, 3);

继承

关键字extends,子类的构造函数必须用super()来初始化父类,因为实际上是先构造父类,再将子类的东西添加上去

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this

不写构造函数的时候会自动给

class ColorPoint extends Point {}
 
// 等同于
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}

super

作为方法:只能在构造函数中使用,用来初始化父类

作为对象:在类方法中指向父类原型(也就是包含父类的全部属性),在静态方法中指向父类

由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。

class A {
  constructor() {
    this.x = 1;
  }
}
 
class B extends A {
  constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined
    console.log(this.x); // 3
    // console.log(super) // 会报错
  }
  go(x) {
    super.x = x;
  }
}
 
let b = new B();
b.go(1000);
console.log(b); // x: 1000

extends

可以继承原始类型

可以实现 mixin 模式 (opens in a new tab)

padStart() padEnd()

字符串新增的方法!前后填充到指定长度,默认用空格

"xxx".padStart(5, "a"); // -> aaxxx
"xxx".padStart(5, "abc"); // -> abxxx
"xxx".padStart(5, "0"); // -> 00xxx
"xxx".padStart(10, "pad"); // -> padpadpxxx

相当于是将填充字符串 pad 的前 n - str.length 位加到 str 前,当然 pad 如果长度不足 n ,会进行循环重复

Symbol

用来制作一个唯一的标识(symbol)

内容很多,介绍常用的,其他的看阮一峰 (opens in a new tab)

以及看这篇 (opens in a new tab)讲 metaprogramming,非常好的深入 Symbol 的文章,以及一些常用的静态变量(Symbol.species、Symbol.toPrimitive...)

结合参考阮一峰 ES6 的描述:对象的属性都是字符串嘛,然后由于模块各种导入,谁知道某个人会在啥地方引入,为这个对象增加一个属性,万一冲突了,就 gg 了,这样 ES6 引入一个 Symbol,能保证每个属性都是独一无二的,根本上能防止属性名的冲突。

注意:他是一个新的原始类型!

复习一下其他的:。。。。自己默念一下哈

如何使用

获得一个 Symbol

Symbol不能用new来构造,只是一个原始类型的值,不是对象,所以这个函数返回的实例也不是对象。。添加属性也没用

let y = Symbol("hhh"); // 可以给一个字符串描述一下这个标识
console.log(typeof y); // symbol
y.a = "a";
console.log(y); // Symbol(hhh)

如果用一个对象去描述

const obj = {
  toString() {
    return "ohla ohla ohla";
  },
};
 
let sm = Symbol(obj);
console.log(sm); // Symbol(ohla ohla ohla)

会调用他的toString()方法,对象默认 to string 是[Object Object]

不管是什么情况下得到的Symbol,都不会有相等的,都是独一无二的(我们)

无法进行运算

console.log(y + "123");
// 直接报错了,但是可以显示的变成字符串
console.log(y.toString() + "123"); // Symbol(hhh)123

可以转为布尔值

console.log(Boolean(y)); // true
console.log(!y); // false

获取描述

Symbol.prototype.description

ES2019 提供的实例属性

console.log(y.description); // hhh

作为属性名的 Symbol

const PROP_ABC = Symbol("abc");
const PROP_BBC = Symbol("bbc");
let a = {
  [PROP_BBC]() {
    if (this[PROP_ABC]) {
      console.log(this[PROP_ABC]);
    }
    console.log("bbc");
  },
};
 
a[PROP_ABC] = "abc for u";
a[PROP_BBC]();

就是写起来不能用.来取属性了

用作常量的定义(例子来自阮一峰),常量没有值的话给Symbol

const log = {};
 
log.levels = {
  DEBUG: Symbol("debug"),
  INFO: Symbol("info"),
  WARN: Symbol("warn"),
};
console.log(log.levels.DEBUG, "debug message");
console.log(log.levels.INFO, "info message");
const COLOR_RED = Symbol(); // 虽然这其实也可以用字符串。。。
const COLOR_GREEN = Symbol();
 
function getComplement(color) {
  switch (color) {
    case COLOR_RED:
      return COLOR_GREEN;
    case COLOR_GREEN:
      return COLOR_RED;
    default:
      throw new Error("Undefined color");
  }
}

常量使用 Symbol 值最大的好处,就是其他任何值都不可能有相同的值了

魔法数字/字符串的值也不重要了!

通常我们会在项目中看到一些定义

type = getType();
 
if (type === "node") {
  // 魔法字符串哦
  // ...
}

我们一般会写成

const TYPES = {
  node: "node",
  path: "path",
};
 
if (type === TYPES.node) {
  // ...
}

可是发现没有,对应的字符串都没有用到,直接用Symbol也不是很舒服吗

const TYPES = {
  node: Symbol("node"),
  path: Symbol("path"),
};
 
if (type === TYPES.node) {
  // ...
}

所以,我们其实在用 vuex 的时候,需要定义commit名字常量的时候也可以用Symbol了!

下次记得一定用!

属性名遍历

Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。

但也不是私有属性,可以通过Object.getOwnPropertySymbols(obj)方法得到一个对象的所有Symbol属性数组

console.log(Object.getOwnPropertySymbols(a)); // [ Symbol(bbc), Symbol(abc) ]

所以遍历上面的结果就可以啦

模块的单例模式

如果要实现模块属性的单例模式,我们通常会将属性挂在全局的global或者window上,但是这样其他人就可以直接修改属性值

让这个属性值是一个 Symbol,再挂到全局对象上去

Symbol.for() & Symbol.keyFor()

有时,我们希望重新使用同一个Symbol值,构造一个相同的Symbol

Symbol.for(string)返回一个Symbol,如果已经有了那个描述的标识就返回那个Symbol,如果没有,就新建一个,就是个单例模式啊。

而直接调用Symbol()就是创建一个独一无二的

let s1 = Symbol.for("foo");
let s2 = Symbol.for("foo");
// 如果给 s1 手贱添加属性的话 就永远是 false... why?
s1 === s2; // true

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

Symbol.for()Symbol()这两种写法,都会生成新的 Symbol。

它们的区别是,前者会被登记在全局环境中供搜索(再次Symbol.for()的时候),后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。

let x = Symbol.for("eee");
let z = Symbol.for("eee");
let eee = Symbol("eee");
console.log(x === z); // true
console.log(Symbol.keyFor(eee)); // undefined 没有登记

注意,Symbol.for()为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行。是 cross-realm 的,在不同的框架或者 service worker 中创建的 Symbol 都是同一个 realm 的!

function foo() {
  return Symbol.for("bar");
}
 
const x = foo();
const y = Symbol.for("bar");
console.log(x === y); // true

Symbol 内置的 Symbol 值

除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。

Symbol内置的属性,用来扩展对象的能力,简单了解几个,其他的见阮一峰 (opens in a new tab)

Symbol.hasInstance

对象的Symbol.hasInstance属性,指向一个内部方法,使用instanceof运算符的时候判断是否为该对象的实例

class MyArray {
  [Symbol.hasInstance](obj) {
    console.log(obj);
    return Array.isArray(obj);
  }
}
 
const ma = new MyArray();
console.log([1, 2, 3] instanceof ma);
// [ 1, 2, 3 ]
// true

会将运算符左边的值作为函数的参数,函数返回的值其实会被Boolean转换一下

class Odd {
  static [Symbol.hasInstance](obj) {
    return Number(obj) & 1;
  }
}
 
// 等同于
const Odd = {
  [Symbol.hasInstance](obj) {
    return Number(obj) & 1;
  },
};
console.log(1 instanceof Odd); // true
console.log(2 instanceof Odd); // false

Symbol.isConcatSpreadable

可以控制一个Array对象是否可以在Array.prototype.concat()被展开

const ar = [1, 4, 3];
console.log(ar[Symbol.isConcatSpreadable]); // undefined
ar[Symbol.isConcatSpreadable] = false;
console.log([555, 444].concat(ar));
// [ 555, 444, [ 1, 4, 3, [Symbol(Symbol.isConcatSpreadable)]: false ] ]

可以看到是不能展开的,而且多了一个属性,而且我们可以展开类似数组的对象,默认是不能在 concat 的时候展开的

const ou = {
  0: 123,
  1: 312,
  length: 2, // 要有 length
};
ou[Symbol.isConcatSpreadable] = true;
console.log([555, 444].concat(ou));
// [ 555, 444, 123, 312 ]

Symbol.iterator

对象的属性,指向该对象的默认遍历方法,返回迭代器,详细看迭代器的部分

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

Map

核心:比 object 更好的映射、遍历 key 等

简介

映射,类似字典,会记住键值对插入的顺序

底层其实维护的是两个数组,分别存放 key 和 value,遍历 Map 的时候也就是for...of这两个数组

key 如果是NaN的话,不会出现NaN !== NaN的情况,同时-0+0在最新标准中是相等的

问题来了,对象也似乎拥有字典的作用,之前我们也都一直用对象来作为字典,新的 Map 有啥不一样呢?

MapObject
Accidental Keys不包含任何的默认 key,每个键值对都需要显式的加入原本就是一个构造函数嘛,会有一些内置的 key,甚至会和你定义的 key 造成冲突
key 的类型可以是任意的!是能是字符串 or Symbol
Key 的顺序有序的无序(es6 之后是只有 string 类型的 key 在遍历时有序的)
size可以访问.size属性看大小手动的看吧(Object.keys 可枚举属性)
Iterationiterable只能遍历 key
性能处理键值对肯定推荐,处理是做优化的无优化

用法

注意别像object那样给键值对,因为这个 Map 必然是一个对象啊。。

const m = new Map();
// don't do this below
m["a"] = 123;
m.has("a"); // false
m.delete("a");
console.log(m); // Map(0) { a: 123 }  won't delete

正确用法:

m.set(2141, "dsfaa");
const o = { a: 123 };
m.set(o, { b: 312 });
console.log(m);
console.log(m.has({ a: 123 })); // false
console.log(m.has(o)); // true
m.delete(o);

注意如果是引用类型作为键,那么他寻找的就是那个指针(引用)

方法 & 属性

构造函数

const kv1 = [
  [123, "sdf"],
  [223, "erw"],
];
 
const kv2 = [
  [123, "rrrr"],
  ["22222", "5555"],
];
 
const mp = new Map(kv1);
const mp2 = new Map([...kv1, ...kv2]);
const mp3 = new Map([...kv1, ...kv2, [{ ae: 33 }, 666]]);
console.log(...mp2); //	可以解构的... => 键值对数组 在 console.log 能解构因为他本身就接受无限参数 ...args
const arr = [...mp2]; // -> [ [ 123, 'rrrr' ], [ 223, 'erw' ], [ '22222', '5555' ] ]

注意:解构数组 后面的重复元素(键值对)会在 Map 中 merge 前面的键值对

属性

  • size:返回键值对的数量

方法

  • clear()

  • delete(key)

  • entries():返回一个可迭代对象,包含键值对数组[key, value],且有序(插入顺序)

    for (const [k, v] of m.entries()) {
      console.log(k + "+" + v);
    }
  • forEach(callback, [thisArg])

    m.forEach((value, key, map) => {
      if (typeof key !== "object" && typeof value !== "object") {
        map.set(key + value, 0);
      }
    });

    别这样写代码,是个死循环。。。 map 无限扩容

  • get(key)

  • has(key)

  • keys()

  • values()

  • set(key, value)

使用场景 v.s object

  • key 一开始不确定,只有在运行时可得到(虽然 object 也有 [key] 的动态取值)
  • 需要将其他的原始值存为 key 的时候,object 一律存为 string
  • 存在对单个元素做操作的时候,用 object
  • 更直接的迭代for [key, valye] of map

更多优势可以看 Stop Using Objects as Hash Maps in JavaScript (opens in a new tab)(墙外)

WeakRef(ES 2021)

弱引用 MDN (opens in a new tab)

强引用:变量对一个对象的指向,没有强引用之后,对象会被 GC 释放内存(reclaim the memory)

Correct use of WeakRef takes careful thought, and it's best avoided if possible

能够避免对象被 GC,但是用之前,请三思,最好别用

用法

new WeakRef(targetObj) 构造函数

deref() 返回实例创建时的 target 或者是 undefined(已经被 GC 了),但是并不是去释放空间哦,只是拿到 obj

具体代码看 MDN 的例子或者 tc39 (opens in a new tab),感觉实战中不会用到

注意点

  • 对于同一个 target 生成多个 WeakRef 之间是相互关联的,其中一个 deref 了 target 之后,其他的 deref 只会拿到 undefined
  • You cannot change the target of a WeakRef, it will always only ever be the original target object or undefined when that target has been reclaimed. 不能改变 target

FinalizationRegistry

Finalizater (opens in a new tab) 又是一个不被推荐使用的,配合 WeakRef 可以一起用

FinalizationRegistry 对象可以在一个对象被垃圾回收的时候调用一个 callback

// 定义 callback
const registry = new FinalizationRegistry((heldValue) => {
  // ....
});
// 注册一个对象
registry.register(theObject, "some value");

WeakMap

弱 Map ,其中 key 必须object ,值可以是任意的

核心:防止内存泄漏

和 Map 的比较

还记得 Map 的底层其实维护的是两个数组吗,所以每次 set 和 get 的时候都会去便利数组,也就是 O(n) 的效率,这其实和 py 的字典相比,差太多了(人家用哈希表 O(1))

第二个问题是 Map 会造成内存泄漏,因为维护的两个数组会一直持有元素的引用,不会被 gc

weak

持有“弱”引用的 key,在没有其他引用时候可以自动的垃圾回收(key 和 value)

所以在映射一些 key 只有在被引用的情况下的时候会非常好用,不引用的时候(置为 null)自动释放内存了

由于 weak,WeakMap的 key 是不可枚举的,也获取不到,如果要获取一组 key 还是用Map

方法

  • get(key)

  • has(key)

  • delete(key)

  • set(key, value)

用 WeakMap 来保存类的私有属性

来自 14 年的一篇 (opens in a new tab)

很有趣的用法

// module People.js
const privates = new WeakMap(); // 闭包一个 weakmap 来存放所有实例的私有数据
 
function Person(name, age, money = 0, longevity = 80) {
  this.name = name;
  this.age = age;
  const me = {
    money,
    longevity,
  };
  privates.set(this, me); // 在这里将 this 作为 key 私有数据 me 作为值
  // this 上是访问不到私有数据的
}
// 定义一些操作私有数据的方法
Person.prototype.getMoney = function () {
  const me = privates.get(this);
  return me.money;
};
Person.prototype.die = function () {
  const me = privates.get(this);
  me.longevity = 0;
};
Person.prototype.buy = function (expense = 0) {
  const me = privates.get(this);
  if (me.money < expense) {
    return false;
  }
  return (me.money -= expense);
};
 
module.exports = Person;
// 调用
const Person = require("./weak_map");
const p = new Person("JoJo", 18, 1500);
 
console.log(p); // Person { name: 'JoJo', age: 18 }
console.log(p.money); // undefined 不在 this 上
console.log(p.getMoney()); // 1500
console.log(p.buy(4444)); // false

TODO https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Keyed_collections (opens in a new tab)

?? (nullish coalescing operator ES 2020)

||可以做一个对比,用法相似

let a = undefined ?? 123;

看名字,nullish 是指 null或者undefined,如果??左边的值为 nullish,则返回右边的值,否则直接返回左边的值

|| 最大的区别是,||是判断左边的是否是falsy

如果能快点加入标准,在很多场景下会比||好用的多,比如参数可以传 0 的时候。

配合下面的可选链

const a = obj?.eee?.fff ?? "default"; // 如果没有取到值 当然也可以用 ||

注意,不能和||以及&&链式使用,要加个括号

(null || undefined) ?? "foo"; // returns "foo"

可选链( ES2020 )

optional chaining (opens in a new tab)

我们之前要访问一个对象的属性的属性时,我们会先判读有没有第一个属性

if (obj.p && obj.p.pp && obj.p.pp.ppp) {
  console.log(obj.p.pp.ppp.pppp);
}

或者我们会这样写:

const res = obj.p
  ? obj.p.pp
    ? obj.p.pp.ppp
      ? obj.p.pp.pppp
      : undefined
    : undefined
  : undefined;

看到这样的代码就吐了....

用 optional chaining 语法之后,我们可以这样

const res = obj.p?.pp?.ppp?.pppp; // obj?.p?....也是可以的
if (!res) {...}

语法

obj.val?.prop;
obj.val?.[expr];
obj.arr?.[index];
obj.func?.(args);

可以少些很多判断了对不对!

如果某个属性是一个函数,也可以这样写,就只是?.()有点奇怪

const obj = {
  a: 123,
  b: {
    c: "www",
  },
  d: {
    dd: {
      ddd: "dddd",
      dddd: () => {
        console.log(123123);
        return 123;
      },
    },
  },
};
 
const bb = obj.d?.dd?.dddd?.();

注意

  • 如果在链中的一个属性不存在或者是 null就会直接停止,返回undefined,如果最终的取值是 null,那么返回的值还是null,当然我们要确保目标对象本身是存在的哈。
  • Optional chaining not valid on the left-hand side of an assignment:不能用它来作为左值来赋值!
  • null v 和 undefined:当我们把foo && foo.bar替换为foo?.bar的时候,注意当foo的值为null的时候,第一个情况返回的是null,第二个返回的就是undefined

比较中的优先级问题:

if (foo && foo.bar === baz) {
  /* ... */
}
// 等价于
if (foo && foo.bar === baz) {
  /* ... */
}

foonull的时候,表达式直接返回 false 了。

当我们改为

if (foo?.bar === baz) {
  /* ... */
}

bazundefined的时候,此时的foonull的时候,整个表达式是true!因为根据上面的规则,可选链返回的也是undefined了!这样就出了 bug!

===改为!==的时候,更大的问题就来了:

if (foo && foo.bar !== baz) {
  /* ... */
}
if (foo?.bar !== baz) {
  /* ... */
}

注意比较两者,如果 foo 是null,第二个可选链的情况就永远是true

如何使用

但是目前还只是语法上可以这样写,运行需要 babel 的支持

npm install --save-dev @babel/plugin-syntax-optional-chaining

babel.config.json or .babelrc中使用插件

{
  "plugins": ["@babel/plugin-syntax-optional-chaining"]
}

或者直接在 CLI 使用

babel --plugins @babel/plugin-syntax-optional-chaining script.js

当然用 node 的 api 也可以

require("@babel/core").transform("code", {
  plugins: ["@babel/plugin-syntax-optional-chaining"],
});

async/await

async

函数修饰符,让这个函数成为一个异步函数

返回值是一个 Promise 对象(因为是个异步函数了)

是 promise 的语法糖(可以改写所有的 Promise 写成 async/await)

原理

直接用 Promise.resolve() 将函数体包住了,函数中的return的内容需要用.then()去接收处理。

详细可看阮一峰 (opens in a new tab):将 Generator 函数和自动执行器,包装在一个函数里。

配合await使用能够将多个异步 Promise 写成同步的形式(只是代码写起来是): Promise 都是用.then()去处理的,多个异步要顺序执行的时候,会出现.then()的疯狂嵌套,用await能够使得这些代码写起来是串行的。

这样就让代码可读性提高了,而且让代码不那么 nested ,变的 flat 了

await

只能用在async函数中

  • 等待一个 Promise 对象或者是其他值
  • 等待 Promise 达到 fulfilled 的状态,将 resolve 的值赋值给左边
  • 如果是 rejected 状态 则将 Promise 的异常抛出
  • 如果不是 promise 则返回那个值
  • 以上操作都会阻塞 async 函数,因为要 wait

例子

只要看到是返回 promise 的语句,都能在前面加上await

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(ms);
    }, ms);
  });
}
 
async function aPrint(value, ms) {
  await timeout(ms);
  console.log(value);
  return ms;
}
 
const res = aPrint(1000, 2000).then((value) => console.log(`timeout ${value}`));
console.log(res); // 这是个 Promise 会先打印

踩坑

来看看上面的console.log为什么会先打印,可见async不是同步方法!一旦遇到await之后就会将后面的所有语句放到resolve的回调!(写起来是给左值)

也可以理解为

async function aPrint(value, ms) {
  timeout(ms).then((v) => {
    console.log(value);
  });
}

所以await的实质还是放到微任务队列去做了后续的操作。。。

Tips

  • async函数中别忘了用await

  • 不需要使用await的时候

    async function asyncFunc() {
      const writer = openFile("someFile.txt");
      writer.write("hello");
      writer.write("world");
      await writer.close(); // wait for file to close
      //return writer.close();
    }
  • await顺序执行,会阻塞,Promise.all()让操作并行,如果在不考虑执行顺序的情况,可以加快速度

async function foo() {
  const [result1, result2] = await Promise.all([asyncFunc1(), asyncFunc2()]);
}
  • 回调的时候使用异步函数

    const arr = ["123", "sdaf", "dfaf", "dasf"];
    // 假设发送到服务器做逆序的处理 是异步操作 有没有 await 也不影响
    const reverseAsync = async (value) => value.split("").reverse().join("");
    const reverse = (value) => value.split("").reverse().join("");
    // 非异步函数
    console.log(arr.map(reverse));

    如果用 async 函数作为回调,注意最初描述的 async 返回的都是 promise

    console.log(arr.map(reverseAsync));
    // 是一个 Promise 的数组..

    怎么办呢?都是 Promise 的数组。。想到了什么? Promise.all

    // 用一个async函数包起来
    async function reverseArr(arr) {
      return await Promise.all(arr.map(reverseAsync));
    }
     
    reverseArr(arr).then(console.log);

    概括一下这种情况:数组的每个元素都要经过异步处理,结果要放回数组

    接着可以优化一下代码,反正返回值都是 Promise 嘛,await 都可以不用了,直接将 Promise.all返回给.then(),一行就搞定了,就是可读性不高

    const reverseArrayAsync = async (arr) => Promise.all(arr.map(reverseAsync));
  • 异步函数中的forEach遍历改成for ... of会更好

异步迭代器

为什么要迭代器?本质上也是一种遍历数据的方式,让每次迭代得到的数据被“造”出来,而不是预先都造好放在内存里。但有些时候我们需要一些异步操作来得到结果,基础的便利迭代器next方法得到的都是同步的结果,所以在 ES2018 引入了异步迭代器接口。

Symbol.asyncIterator

一个对象的迭代器接口是用Symbol.iterator来实现,那么异步的迭代器接口就是用Symbol.asyncIterator来实现,其中的next方法返回的是一个用 Promise 包装起来的结果。

所以我们可以在每个迭代器的next结果通过.then注册回调来处理 value,同时在回调中可以返回迭代器的下一个next,来链式调用!

asyncIterator
  .next()
  .then((iterResult1) => {
    console.log(iterResult1); // { value: 'a', done: false }
    return asyncIterator.next();
  })
  .then((iterResult2) => {
    console.log(iterResult2); // { value: 'b', done: false }
    return asyncIterator.next();
  })
  .then((iterResult3) => {
    console.log(iterResult3); // { value: undefined, done: true }
  });

也可以用await来写成同步语句,但注意要放在async函数中

async function f() {
  const asyncIterable = createAsyncIterable(["a", "b"]);
  const asyncIterator = asyncIterable[Symbol.asyncIterator]();
  console.log(await asyncIterator.next());
  // { value: 'a', done: false }
  console.log(await asyncIterator.next());
  // { value: 'b', done: false }
  console.log(await asyncIterator.next());
  // { value: undefined, done: true }
}

同样也可以放在一个数组中,最后用Promise.all来处理

接着我们可以看一下for await ... of,专门引入来处理异步迭代器的

for await ... of

for ... of最大的不同就是,它是对Symbol.asyncIterator来迭代的,并且需要在async函数中。

同时,迭代获得的值也是value

const asyncIter = {
  // 这个 asyncIterator 只能给 for await of 这个 异步迭代接口来用
  [Symbol.asyncIterator]: () => {
    const items = [11, 12, 13, 14, 15];
    return {
      next() {
        return Promise.resolve({
          done: 0 === items.length,
          value: items.shift(),
        });
      },
    };
  },
  [Symbol.iterator]() {
    const N = 5;
    let id = 0;
    return {
      next() {
        return {
          done: id === N,
          value: id++,
        };
      },
    };
  },
};
// for await of
(async function () {
  for await (const s of asyncIter) {
    console.log(s);
  }
})(); // 11 12 13 14 15

在迭代器被 reject 的错误会被当作是 unhandled error 被抛出,所以在for的外层最好加一个try catch

同样也是可以迭代非异步迭代器的!

P.S.那么我们什么时候会用到它呢?不知道。。。遇到再看看吧

箭头函数

const foo = (a) => console.log(a);

(形参列表)=>箭头 {函数体}

  • 将调用时的上下文作为函数体的调用上下文
  • 箭头函数不会生成自己的this arguments super new.target关键字
  • 更适合作为匿名函数
  • 函数体只有一行的话可以省略花括号,作为 return 的值

更短的函数表达式

举几个例子看吧

let uesa = ["aefjf", "aefjsdf", "aefjf1q", "aefjf1qq34"];
console.log(uesa.map(({ length: len }) => len));
console.log(uesa.map(({ length }) => length));

支持解构,默认值,括号包裹函数体(parenthesize),如果写在一行的时候需要返回对象,用()包裹起来

// 摘自MDN
// Parenthesize the body of a function to return an object literal expression:
params => ({foo: bar})
 
// Rest parameters and default parameters are supported
(param1, param2, ...rest) => { statements }
(param1 = defaultValue1, param2,, paramN = defaultValueN) => {
statements }
 
// Destructuring within the parameter list is also supported
var f = ([a, b] = [1, 2], {x: c} = {x: a + b}) => a + b + c;
f(); // 6

箭头函数的 this

会找上下文中闭包的的 this,遵循 look up

通过 call,apply 调用

  • 只能传递参数

  • 第一个参数作为this 会被忽略!

    • 看个例子

      let a = {
        x: 0,
       
        add: function (y) {
          let f = (v) => v + this.x;
          return f(y);
        },
       
        addCall: function (y) {
          let f = (v) => v + this.x;
          const b = {
            x: 123123,
          };
          // 希望f在b上下文中调用
          return f.call(b, y);
        },
      };
      console.log(a.add(10)); // 10
      console.log(a.addCall(10)); // 10
    • 可见箭头函数在用 call 调用的时候传入的第一个参数不影响 this,this 是在调用的时候直接找上下文

解析顺序

箭头=>不是一个操作符!有特殊的解析规则

let callback;
 
callback = callback || function() {}; // ok
 
callback = callback || () => {};
// SyntaxError: invalid arrow-function arguments
 
callback = callback || (() => {});    // ok

箭头函数不能乱用哦

构造函数不能是 arrow

学 oop 的时候就发现了,问题很大,直接报错了

对象的方法不能

对象调用方法的时候,如果是箭头函数,方法中的 this 是调用时所在的上下文,而不是该对象

let ew = {
  name: "eeee",
  f: () => console.log(this.name),
};
ew.f(); // undefined

如果实在不想用function那就用 ES6 的新定义方法

let ew = {
  name: "eeee",
  f() {
    console.log(this.name);
  },
};
ew.f(); // eeee

事件的 callback

<input id="btn" type="button" value="click" />
<script>
  let btn = document.getElementById("btn");
  // btn.addEventListener('click', () => {
  //     console.log(this === window)
  //     this.value = 'xxxx'
  // })
  btn.addEventListener("click", function () {
    console.log(this === window);
    this.value = "xxxx";
  });
</script>

注释的部分使用了箭头函数写这个 callback,事件监听的回调都是异步操作的回调,放在 task queue 中的,等到事件触发的时候才放到主线程,此时的调用上下文是全局!所以在箭头函数中的 this 是找到了 window

Use of prototype property

Arrow functions do not have a prototype property.

var Foo = () => {};
console.log(Foo.prototype); // undefined

解构...(deconstructing)

用作函数参数的剩余部分

function sum(...theArgs) {
  return theArgs.reduce((previous, current) => {
    return previous + current;
  });
}

函数形参的最后一个可以用...来前缀修饰(?),来囊括剩下的所有参数,注意类型不是数组,但是打印出来是数组,那就是个 ArrayLike 的对象,能用 reduce 函数我想是因为底层实现调用了 Array.prototype.reduce 的 apply/call 吧

好的刚刚犯二了,用typeof去检查类型,清一色object,改用~~instanceof~~Array.isArray()去判断,是 Array,没错,是的,长记性啊。

普通数的arguments是个 ArrayLike 的对象。

解构赋值

看例子吧

let a, b, rest;
[a, b] = [10, 20];
[a, b, ...rest] = [10, 20, 30, 40, 50];
 
({ a, b } = { a: 10, b: 20 });
 
({ a, b, ...rest } = { a: 10, b: 20, c: 30, d: 40 });

默认值

let a, b;
 
[a = 5, b = 7] = [1];
console.log(a); // 1
console.log(b); // 7

交换变量,哦哟和 python 有点像了

let ei = 12;
let we = 13; // 有一个小坑 这里分号一定要加 不然解析报错
[ei, we] = [we, ei];
const [ae, ...es] = [1, 2, 3, 4, 5];

对象解构,直接获取属性和值,import 的时候也很好用,也可以给默认值

const o = { p: 42, q: true };
const { p, q } = o;
 
console.log(p); // 42
console.log(q); // true
// const {p: foo, q: bar} = o;

解构对象的覆盖顺序,后面的对象有重复属性会覆盖前者

const ob = { a: 123, b: 223 };
const oc = { a: 333, d: 3123 };
e = { ...ob, ...oc };
// { a: 333, b: 223, d: 3123 }
e = { ...ob, ...oc };
// { a: 333, b: 223, d: 3123 }
e = { ...oc, ...ob };
// { a: 123, d: 3123, b: 223 }

函数参数解构的默认值

function f({ a = 123, b, c = "ccc" } = { b: 33 }) {}

在迭代里面巧用解构

arr.forEach({name, age} => {...})

可以给结构出的原始属性赋给新的变量:

arr.forEach({name: n, age: a} => {...})

此时原来的 name 和 age 都不能用了

函数参数的默认值

不知道是不是 ES6 新增啊。。

function add(a, b = 1) {
  return a + b;
}
  • 本身如果不给 function 传递参数,那么调用的时候就是 undefined

  • 如果给默认值参数赋值 undefined,默认值还是会起作用的

  • 传递其他假值(falsy)的时候上一条不生效

    • 看看什么是 falsy 吧,就是 false 值的字面量
    • false 0 -0 0n '' null undefined NaN
  • 默认值在调用的时候创建,不会发生类似闭包的情况

    • 所以默认值完全可以是一个定义过的函数

    • 看个例子

      let count = 0;
      const ff = (value = counter()) => console.log(value);
      const counter = () => count++;
      ff();
      ff();
      ff();
      ff(); // 0 1 2 3
  • 默认值左边的参数都可以被调用

    const hello = (name, greet = "hello " + name) => greet;
    console.log(hello("jjjjsss"));
    console.log(hello("jjjjsss", "jjjjjj"));
    // hello jjjjsss
    // jjjjjj
  • 可以用解构赋值

迭代器 & for ... of

迭代器

是一种接口,为不同的数据结构提供统一的访问机制,任何类型只要部署了Iterator接口,就可以完成遍历

ES6 提供了for...of来遍历迭代器,其实和 python 的 for 循环是一样的,C++ 也有的,内部是一个指针遍历

具备迭代器的数据结构:

  • Array
  • arguments
  • Set
  • Map
  • String
  • TypedArray
  • NodeList

其实说到迭代器,会想到生成器(python),会用next函数来消费下一个元素

在 js 中也是一样,next函数返回一个人对象包含value:anydone:Boolean

我们可以通过Symbol.iterator来得到对象的迭代器

let arr = ["a", "d", "e"];
 
let iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: 'e', done: false }
console.log(iterator.next()); // { value: undefined, done: false }

迭代器能干嘛?可以自定义迭代的顺序

for ... of

遍历的是对象的迭代器

for (let v of { a: 123, b: 223 }) {
}

这样是会报错的,Object不是可迭代对象

我们需要构造一个自定义的迭代器

const fyi = {
  to: "JoJo",
  info: ["olaolaola", "hhhhhh", "yeyeyeyeye", "mudamudamuda"],
  [Symbol.iterator]() {
    // 在这里定义一个
    let index = 0;
    const that = this; // 当然下面的 next 也可以写成 箭头函数
    return {
      next() {
        return { value: that.info[index++], done: index > that.info.length };
      },
    };
  },
};
for (let v of fyi) {
  console.log(v);
}
// olaolaola
// hhhhhh
// yeyeyeyeye
// mudamudamuda

直到next()返回了{value: undefined, done: true}才停止

试着自己实现了一个不能breakfor..of

function forOf(iterable, fn) {
  if (!iterable[Symbol.iterator] || !iterable[Symbol.iterator]()) {
    throw new Error("not iterable!");
  }
  const iterator = iterable[Symbol.iterator]();
  let iterVal = iterator.next();
  while (!iterVal.done) {
    fn(iterVal.value);
    iterVal = iterator.next();
  }
}
 
forOf([1, 2, 3], (v) => {
  console.log(v);
});

当然了。。babel 不是这样做的!是直接将那一部分都转为 for 循环,直接可以 break

生成器

Generator,很简单。。就是 python

在 js 中是一个异步编程的解决方案!但是现在有async/await语法糖了

语法

function* gen(doneMsg) {
  yield 123;
  yield 333;
  let x = yield 233;
  console.log(doneMsg + x);
}
let iter = gen("yes");
// 返回一个迭代器对象
iter.next();
iter.next();
iter.next();
iter.next("!!!"); // 作为最后一个 yield 的返回值
// 1
// 2
// 3
// yes!!!

每次调用next()就会返回下一个yield,哎呀和 python 一样,但是可以 yield* iterable

let generator = function* () {
  yield 1;
  yield* [2, 3, 4];
  yield 5;
};
 
let 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 }

解决回调地狱

// 解决异步编程 回调地狱
function userData(id) {
  setTimeout(() => {
    let user = id + "-JoJo";
    iterator.next(user); // 执行下一步!
  }, 1000);
}
 
function schoolData(user) {
  setTimeout(() => {
    let school = user + "-Dio";
    iterator.next(school);
  }, 1000);
}
 
function feeData(school) {
  setTimeout(() => {
    let fee = school + "fee: " + 400000;
    iterator.next(fee);
  }, 1000);
}
// 这些数据都是有先后依赖关系的
// userData()
// schoolData()
// feeData()
 
// 业务逻辑处理在这里 让异步写的和同步一样
function* getUserTuitionFee(id) {
  const user = yield userData(id);
  console.log(user);
  const school = yield schoolData(user);
  console.log(school);
  const fee = yield feeData(school);
  console.log(fee);
}
 
const iterator = getUserTuitionFee(1);
iterator.next();
// 分别间隔 1s 输出
// 1-JoJo
// 1-JoJo-Dio
// 1-JoJo-Diofee: 400000

部署迭代器接口

生成器也是用next方法发现吗!所以可以用它包装迭代器

function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i = 0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}
 
let myObj = { foo: 3, bar: 7 };
 
for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}
function* genN(n) {
  for (let i = 0; i < n; ++i) {
    console.log("gen " + i);
    yield { id: i };
  }
}
 
for (let { id } of genN(10)) {
  console.log(id);
}