yinguangyao / blog

关于 JavaScript 前端开发、工作经验的一点点总结。
262 stars 12 forks source link

面向未来的 ECMAScript 标准 #60

Open yinguangyao opened 3 years ago

yinguangyao commented 3 years ago

前言

随着 ES6 的发布,JavaScript 语法也越来越趋于成熟,新的提案也在不断地提出。在第一篇文章中,也有提到过 ECMA 提案的四个阶段,处于 stage3 的都需要我们持续关注,以后很可能就会被纳入新标准中。 那么就来一起看看 ECMA 中的新特性以及一些有趣的新提案吧。

Dynamic Import

如果你写过 node,会发现和原生的 import/export 不一样的地方就是支持就近加载。 你可以在你用到的时候再去加载这个模块,而不用全部放到顶部加载。 以下面这个 node 模块为例子,最后依次打印出来的是 mainnoop

// noop.js
console.log('noop');
module.exports = function() {}
// main.js
console.log('main')
const noop = require('./noop')

如果换成 import/export,那么打印结果则相反。比如下面依次打印的是 noopmain

// noop.js
console.log('noop');
export default function() {}
// main.js
console.log('main')
import noop from './noop'

而在前端开发中,为了优化用户体验,往往需要用到懒加载。如果只想在用户进入某个页面的时候再去加载这个页面的资源,那么就可以配合路由去动态加载资源。 其实我们自己也完全可以通过 Promise 来封装这样一个 api,核心在于动态生成 script 标签,在 script 中导入需要懒加载的模块,将其挂载到 window 上面。

function importModule(url) {
    return new Promise((resolve, reject) => {
        const script = document.createElement("script");
        script.type = "module";
        script.textContent = `import * as m from "${url}"; window.tempModule = m;`;
    })
}

当 script 的 onload 事件触发之时,就把 tempModule 给 resolve 出去,同时删除 window 上面的 tempModule

function importModule(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    const tempGlobal = "__tempModuleLoadingVariable" + Math.random().toString(32).substring(2);
    script.type = "module";
    script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;

    script.onload = () => {
      resolve(window[tempGlobal]);
      delete window[tempGlobal];
      script.remove();
    };

    script.onerror = () => {
      reject(new Error("Failed to load module script with URL " + url));
      delete window[tempGlobal];
      script.remove();
    };

    document.documentElement.appendChild(script);
  });
}

因此,自然而然的,一个动态 import 的提案就被提了出来,这个提案目前已经走到了 stage4 阶段。而上面的 importModule 则是官方推荐的在不支持动态 import 的浏览器环境中的一种实现。 通过动态 import 允许我们按需加载 JavaScript 模块,而不会在最开始的时候就将全部模块加载。

const router = new Router({
    routes: [{
        path: '/home',
        name: 'Home',
        component: () =>
            import('./pages/Home.vue')
    }]
})

动态 import 返回了一个 Promise 对象,这也意味着可以在 then 中等模块加载成功后去做一些操作。

<nav>
  <a href="books.html" data-entry-module="books">Books</a>
  <a href="movies.html" data-entry-module="movies">Movies</a>
  <a href="video-games.html" data-entry-module="video-games">Video Games</a>
</nav>

<main>Content will load here!</main>

<script>
  const main = document.querySelector("main");
  for (const link of document.querySelectorAll("nav > a")) {
    link.addEventListener("click", e => {
      e.preventDefault();

      import(`./section-modules/${link.dataset.entryModule}.js`)
        .then(module => {
          module.loadPageInto(main);
        })
        .catch(err => {
          main.textContent = err.message;
        });
    });
  }
</script>

Top-level await

前面讲了动态 import,但是如果想在动态引入某个模块之后再导出当前模块的数据,那么该怎么办呢? 如果在模块中我依赖了某个需要异步获取的数据之后再导出数据怎么办? 传统的做法如下:

// module.js
let data = null;
(async () => {
})()
export default 

globalThis

如果你有看过 Underscore 之类的库的源码,经常能看到会有这么一个操作。

var root = typeof self == 'object' && self.self === self && self || typeof global == 'object' && global.global === global && global || this;

这么一长串的操作,它到底是在做什么呢?其实它只是在取全局对象。 事实上,在不同的 JavaScript 环境中拿到全局对象是需要不同的语句的。 在 Web 中,可以通过 window、self 或者 frames 取到全局对象,但是在 Web Workers 中只有 self 可以。 在 Node.js 中,它们都无法获取,必须使用 global。 在松散模式下,可以在函数中返回 this 来获取全局对象,但是在严格模式下 this 会返回 undefined 。 因此,一个叫做 globalThis 的提案就这么诞生了,从此不必在去不同环境中判断全局对象。

if (typeof globalThis.setTimeout !== 'function') {
  // no setTimeout in this environment!
}

globalThis 现在还处于 stage3 阶段。

Private instance methods and accessors

在 JavaScript 的类中一直都没有私有属性的概念,以往的做法是用下划线来指定私有属性,但依然能够被访问到,这个下划线也只起到了说明的作用。

class Counter {
_count = 0;
increment() {
this._count++;
}
decrement() {
this._count--;
}
}
const counter = new Counter();
counter._counter; // 0

或者干脆放弃使用 class,转而利用闭包来实现私有属性,但是看起来总是没有那么简洁。

const Counter = function() {
let _count = 0;
function Counter() {
}
Counter.prototype.increment = function() {
return _count++;
}
Counter.prototype.decrement = function() {
return _count--;
}
return Counter
}()
const counter = new Counter();
counter._count; // undefined

其实,早在 TypeScript 中就已经实现了 Class 中的私有属性。只需要使用 private 关键字,就能够将属性设置为私有。这和很多语言保持了一致。

class Counter {
private count = 0;
increment() {
this.count++;
}
decrement() {
this.count--;
}
}

后来,一个争议非常大的提案被提了出来。虽然已经走到了 stage3,但依然有很多开发者对之嗤之以鼻。该提案允许你使用 # 当做私有变量的前缀,以此来声明一个私有变量。该私有属性只能在当前类中被访问到,无法在外部被访问到。

class Counter {
#_count = 0;
increment() {
this.#_count++;
}
decrement() {
this.#_count--;
}
}
const counter = new Counter();
counter.#count; // error

# 还允许将静态属性设置为私有属性,也只能在当前类中被访问到。

class Person {
static #instance = null;
static getInstance() {
if (Person.#instance) {
return Person.#instance;
}
return new Person()
}
}
Person.#instance // error

由于这个提案的一些问题,以 hax 为首的广大中国开发者,在 GitHub 上也开启了对这个私有属性的讨论。 感兴趣的可以去围观一下:https://github.com/hax/js-class-fields-chinese-discussion

Optional Chaining

根据统计 stackoverflow 上面的前端相关问题,有相当一部分是 JS 报错导致的。 如果是你是个前端老司机,那么看到这个报错一眼就能知道问题是什么。

image_1dqru9mv2r3uhob1r80bvp1t1h9.png-16kB

在 JavaScript 中,深层取值是有很大风险的,因为你无法保证前面的返回是个对象。 为了解决深层取值的问题,经验丰富的前端老司机们也折腾出来了一堆骚操作。比如使用短路符号来避免 JS 报错。

const city = country && 
    country.province && 
    country.province.city

后来大家一看,为了取个值搞的那么麻烦,如果有五层六层,岂不是要了命了?于是又有聪明的人想出了使用 reducer 来循环访问的形式。这就是 lodash 中的 get 方法。

const city = _.get(country, 'province.city')

在前面讲解 Proxy 的时候,我带着大家手把手用 Proxy 实现了一个深层取值的 get 方法,那也是一个很不错的方法。

const city = country.province.city();

当然了,今天我们的主角就是大名鼎鼎的 Optional Chaining ?.。 Optional Chaining 也叫可选链、链判断运算符,主要是解决深层取值的问题。目前虽然只在 stage1 阶段,但 TypeScript 已经支持。

const city = country?.province?.city
// or
const city = country?.['province']?.['city']

可选链还能运用在函数上面。

person.say?.()

但需要注意的是,如果 person.say 不是 null 也不是 undefined,但同时也不是函数,那么依然会报错。

bigInt

Pipeline operator draft

如果你有使用过 lodash/underscore 之类的函数库,那么一定会对链式调用比较熟悉吧。这种链式调用在一定程度上提高了代码整体的简洁性。

_.chain([1, 2, 3])
.sort()
.last()
.isNumber()
.value();

最近非常火热的 rxjs 中也有类似概念,基于 rxjs 编程,就像是在组装管道一样。

from([2, 3]).pipe(
  scan((acc, item) => acc += item, 10))
  .subscribe(v => console.log(v))

管道操作符 |> (目前处于 stage 1 阶段)允许使用一种易读的方式去对函数进行链式调用。 本质上来说,管道操作符是单参数函数调用的语法糖,它允许你像这样执行一个调用:

let url = '%21' |> decodeURI;
// 等效于
let url = decodeURI('%21');

如果链式调用多个函数的时候,使用管道操作符可以改善代码可读性。之所以叫管道操作符,就是因为数据可以在多个函数中像在管道中一样流动。

const double = (n) => n * 2;
const sqrt = (n) => Math.sqrt(n);

// 没有用管道操作符
double(sqrt(double(5))); // 22

// 用上管道操作符之后
5 |> double |> sqrt |> double; // 22

WeakRefs

Promise.any

未命名.png-64.2kB