ES6常用但被忽略的方法(终弹-最新提案)

4,859 阅读10分钟

写在开头

  • ES6常用但被忽略的方法 系列文章,整理作者认为一些日常开发可能会用到的一些方法、使用技巧和一些应用场景,细节深入请查看相关内容连接,欢迎补充交流。

相关文章

最新提案

do表达式

  • do表达式 提案 之前,块级作用域是一个语句,将多个操作封装在一起,没有返回值。do表达式使得块级作用域可以变为表达式,也就是说可以返回值,返回内部最后执行的表达式的值。
// x是最后 t * t + 1 的值
let x = do {
  let t = f();
  t * t + 1;
};

// 不同情况执行不同函数
let x = do {
  if (foo()) { f() }
  else if (bar()) { g() }
  else { h() }
};

throw表达式

  • JavaScript 语法规定throw是一个命令,用来抛出错误,不能用于表达式之中。
// 报错
console.log(throw new Error());
  • 现在有个 提案throw可以直接用于表达式中。
// 参数的默认值
function save(filename = throw new TypeError("Argument required")) {
}

// 箭头函数的返回值
lint(ast, {
  with: () => throw new Error("avoid using 'with' statements.")
});

// 条件表达式
let typeof x === number ? x : throw new Error("Is NaN");

// 逻辑表达式
let val = value || throw new Error("Invalid value");
  • 语法上,throw表达式里面的throw不再是一个命令,而是一个运算符。为了避免与throw命令混淆,规定throw出现在行首,一律解释为throw语句,而不是throw表达式。

函数的部分执行

  • 多参数的函数有时需要绑定其中的一个或多个参数,然后返回一个新函数。
function add(x, y) { return x + y; }
function add7(x) { return x + 7; }

// bind 方法
const add7 = add.bind(null, 7);

// 箭头函数
const add7 = x => add(x, 7);
  • 现在有一个 提案,使得绑定参数并返回一个新函数更加容易。这叫做函数的部分执行(partial application)。?是单个参数的占位符,...是多个参数的占位符。
f(x, ?)
f(x, ...)
f(?, x)
f(..., x)
f(?, x, ?)
f(..., x, ...)
  • ?...只能出现在函数的调用之中,并且会返回一个新函数。
const g = f(?, 1, ...);
// 等同于
const g = (x, ...y) => f(x, 1, ...y);
  • 函数的部分执行,也可以用于对象的方法。
let obj = {
  f(x, y) { return x + y; },
};

const g = obj.f(?, 3);
g(1) // 4
注意点
  1. 函数的部分执行是基于原函数的。如果原函数发生变化,部分执行生成的新函数也会立即反映这种变化。
let f = (x, y) => x + y;
const g = f(?, 3);
g(1); // 4

// 替换函数 f
f = (x, y) => x * y;
g(1); // 3
  1. 如果预先提供的那个值是一个表达式,那么这个表达式并不会在定义时求值,而是在每次调用时求值。
let a = 3;
const f = (x, y) => x + y;
const g = f(?, a);
g(1); // 4

// 改变 a 的值
a = 10;
g(1); // 11
  1. 如果新函数的参数多于占位符的数量,那么多余的参数将被忽略。
const f = (x, ...y) => [x, ...y];
const g = f(?, 1);
g(2, 3, 4); // [2, 1]
  1. ...只会被采集一次,如果函数的部分执行使用了多个...,那么每个...的值都将相同。
const f = (...x) => x;
const g = f(..., 9, ...);
g(1, 2, 3); // [1, 2, 3, 9, 1, 2, 3]

管道运算符

  • 提案JavaScript 的管道是一个运算符,写作|>。它的左边是一个表达式,右边是一个函数。管道运算符把左边表达式的值,传入右边的函数进行求值。
x |> f
// 等同于
f(x)
  • 管道运算符只能传递一个值,这意味着它右边的函数必须是一个单参数函数。如果是多参数函数,就必须进行柯里化,改成单参数的版本。
function double (x) { return x + x; }
function add (x, y) { return x + y; }

let person = { score: 25 };
person.score
  |> double
  |> (_ => add(7, _))
// 57
  • 管道运算符对于await函数也适用。
x |> await f
// 等同于
await f(x)

const userAge = userId |> await fetchUserById |> getAgeFromUser;
// 等同于
const userAge = getAgeFromUser(await fetchUserById(userId));

数值分隔符

  • 欧美语言中,较长的数值允许每三位添加一个分隔符(通常是一个逗号),增加数值的可读性。比如,1000可以写作1,000
  • 现在有一个提案,允许 JavaScript 的数值使用下划线(_)作为分隔符。
let budget = 1_000_000_000_000;
budget === 10 ** 12 // true
  • JavaScript 的数值分隔符没有指定间隔的位数,小数和科学计数法也可以使用数值分隔符。
123_00 === 12_300 // true

12345_00 === 123_4500 // true
12345_00 === 1_234_500 // true

// 小数
0.000_001
// 科学计数法
1e10_000
  • 注意点。
    1. 不能在数值的最前面(leading)或最后面(trailing)。
    2. 不能两个或两个以上的分隔符连在一起。
    3. 小数点的前后不能有分隔符。
    4. 科学计数法里面,表示指数的eE前后不能有分隔符。
    // 全部报错
    3_.141
    3._141
    1_e12
    1e_12
    123__456
    _1464301
    1464301_
    
  • 除了十进制,其他进制的数值也可以使用分隔符。
// 二进制
0b1010_0001_1000_0101
// 十六进制
0xA0_B0_C0
  • 分隔符不能紧跟着进制的前缀0b0B0o0O0x0X
// 报错
0_b111111000
0b_111111000
  • Number()parseInt()parseFloat()三个将字符串转成数值的函数,不支持数值分隔符。主要原因是提案的设计者认为,数值分隔符主要是为了编码时书写数值的方便,而不是为了处理外部输入的数据。
Number('123_456') // NaN
parseInt('123_456') // 123

Math.signbit()

  • Math.sign()用来判断一个值的正负,但是如果参数是-0,它会返回-0。实际编程中,判断一个值是+0还是-0非常麻烦,因为它们是相等的。
Math.sign(-0) // -0
+0 === -0 // true
  • 有一个提案,引入了Math.signbit()方法判断一个数是否设置了符号位。
Math.signbit(2) //false
Math.signbit(-2) //true
Math.signbit(0) //false
Math.signbit(-0) //true
  • 该方法的算法如下。
    1. 如果参数是NaN,返回false
    2. 如果参数是-0,返回true
    3. 如果参数是负值,返回true
    4. 其他情况返回false

双冒号运算符

  • 箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(callapplybind),箭头函数并不适用于所有场合,所以现在有一个 提案,提出了“函数绑定”(function bind)运算符,用来取代callapplybind调用。函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。
foo::bar;
// 等同于
bar.bind(foo);

foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);
  • 如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。
var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;

let log = ::console.log;
// 等同于
var log = console.log.bind(console);
  • 如果双冒号运算符的运算结果,还是一个对象,就可以采用链式写法。
import { map, takeWhile, forEach } from "iterlib";

getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));

Realm API

  • Realm API 提案 提供沙箱功能(sandbox),允许隔离代码,防止那些被隔离的代码拿到全局对象。提供一个Realm()构造函数,用来生成一个 Realm 对象。该对象的global属性指向一个新的顶层对象,这个顶层对象跟原始的顶层对象类似。Realm 顶层对象与原始顶层对象是两个对象。
const globalOne = window;
const globalTwo = new Realm().global;
let a1 = globalOne.evaluate('[1,2,3]');
let a2 = globalTwo.evaluate('[1,2,3]');
a1.prototype === a2.prototype; // false
a1 instanceof globalTwo.Array; // false
a2 instanceof globalOne.Array; // false
  • Realm 沙箱里面只能运行 ECMAScript 语法提供的 API,不能运行宿主环境提供的 API 例如console不是语法标准,是宿主环境提供的。
globalTwo.evaluate('console.log(1)')
// throw an error: console is undefined

// 解决方法
globalTwo.console = globalOne.console;
  • Realm()构造函数可以接受一个参数对象,该参数对象的intrinsics属性可以指定 Realm 沙箱继承原始顶层对象的方法。
const r1 = new Realm();
r1.global === this;
r1.global.JSON === JSON; // false

const r2 = new Realm({ intrinsics: 'inherit' });
r2.global === this; // false
r2.global.JSON === JSON; // true

#!命令

  • Unix 的命令行脚本都支持#!命令,又称为 ShebangHashbang。这个命令放在脚本的第一行,用来指定脚本的执行器。
// Bash 脚本的第一行。
#!/bin/sh

// Python 脚本的第一行。
#!/usr/bin/env python
  • 现在有一个 提案,为 JavaScript 脚本引入了#!命令,写在脚本文件或者模块文件的第一行。
// 写在脚本文件第一行
#!/usr/bin/env node
'use strict';
console.log(1);

// 写在模块文件第一行
#!/usr/bin/env node
export {};
console.log(1);
  • 有了这一行以后,Unix 命令行就可以直接执行脚本。
# 以前执行脚本的方式
$ node hello.js

# hashbang 的方式
$ ./hello.js
  • 对于 JavaScript 引擎来说,会把#!理解成注释,忽略掉这一行。

import.meta

  • 开发者使用一个模块时,有时需要知道模板本身的一些信息(比如模块的路径)。现在有一个 提案,为 import 命令添加了一个元属性import.meta,返回当前模块的元信息。
  • import.meta只能在模块内部使用,如果在模块外部使用会报错。
  • 这个属性返回一个对象,该对象的各种属性就是当前运行的脚本的元信息。具体包含哪些属性,标准没有规定,由各个运行环境自行决定。一般来说,import.meta至少会有下面两个属性。
  1. import.meta.url
  • import.meta.url返回当前模块的 URL 路径。举例来说,当前模块主文件的路径是https://foo.com/main.jsimport.meta.url就返回这个路径。如果模块里面还有一个数据文件data.txt,那么就可以用下面的代码,获取这个数据文件的路径。
new URL('data.txt', import.meta.url)
  • Node.js 环境中,import.meta.url返回的总是本地路径,即是file:URL协议的字符串,比如file:///home/user/foo.js
  1. import.meta.scriptElement
  • import.meta.scriptElement是浏览器特有的元属性,返回加载模块的那个<script>元素,相当于document.currentScript属性。
// HTML 代码为
// <script type="module" src="my-module.js" data-foo="abc"></script>

// my-module.js 内部执行下面的代码
import.meta.scriptElement.dataset.foo
// "abc"

总结

  • 在最新的提案中,有写特性在我看来还是非常有用的。例如
    1. throw表达式,我们之前不能在块级作用域中使用,提案通过后我们可以直接在表达式中去做一些异常的处理。
    2. 管道运算符(|>),我们在连续调用的时候可以使用管道运算符直接写在一行,逻辑也更加的清楚。
    3. 双冒号运算符(::),之前我们绑定this要使用callapplybind或者使用箭头函数,但是有些情况箭头函数又会存在限制,所以双冒号运算符可以简介写法还可以处理箭头函数之外的特殊情况。
    4. 其他的特新都有各自的用处,可能我们在开发时有些情况没有遇到,存在即合理。
  • ES6ES2016后统称)的特性越来越多,我们需要不断去熟悉新的特新有哪些,哪些是我们在开发中会运用到的,可以提高我们的开发效率,每次看完都会有不一样的收获,大家也可以隔一段时间去回顾一下,加深印象。
  • ES6常用但被忽略的方法系列文章到这儿就完结了,撒花🎉🎉🎉,感谢大家的支持。