# do 表达式
本质上,块级作用域是一个语句,将多个操作封装在一起,没有返回值。有一个提案使得块级作用域可以变为表达式,也就是说可以返回值,办法就是在块级作用域之前加上do,使它变为do表达式,然后就会返回内部最后执行的表达式的值。
let x = do {
let t = f();
t * t + 1;
}; //变量x会得到整个块级作用域的返回值(t * t + 1)
2
3
4
do表达式的好处是可以封装多个语句,让程序更加模块化,就像乐高积木那样一块块拼装起来。
let x = do { //根据函数foo的执行结果调用不同的函数,将返回结果赋给变量x
if (foo()) { f() }
else if (bar()) { g() }
else { h() }
};
2
3
4
5
do表达式在JSX语法中非常好用,如果不用do表达式,就只能用三元判断运算符(?😃。那样的话,一旦判断逻辑复杂,代码就会变得很不易读。
# import.meta
开发者使用一个模块时,有时需要知道模板本身的一些信息(比如模块路径)。有一个提案为import 命令添加了一个元属性import.meta,返回当前模块的元信息。
import.meta只能在模块内部使用,如果在模块外部使用会报错。这个属性返回一个对象,该对象的各种属性就是当前运行的脚本的元信息。具体包含哪些属性,标准没有规定,由各个运行环境自行决定。一般来说,import.meta至少会有下面两个属性。
import.meta.url
import.meta.url返回当前模块的URL路径。举例来说,当前模块主文件的路径是https://foo.com/main.js,import.meta.url就返回这个路径。如果模块里面还有一个数据文件data.txt,那么就可以用下面的代码,获取这个数据文件的路径。
new URL('data.txt', import.meta.url)
//注意,Node.js环境中,import.meta.url返回的总是本地路径,即是file:URL协议的字符串,比如file:///home/user/foo.js。
2
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"
2
3
4
5
# throw 表达式
JavaScript语法规定throw是一个命令,用来抛出错误,不能用于表达式之中。现在有一个提案,允许throw用于表达式。语法上,throw表达式里面的throw不再是一个命令,而是一个运算符。为了避免与throw命令混淆,规定throw出现在行首,一律解释为throw语句,而不是throw表达式。
// 参数的默认值
function save(filename = throw new TypeError("Argument required")) {
}
// 箭头函数的返回值
lint(ast, {
with: () => throw new Error("avoid using 'with' statements.")
});
// 条件表达式
function getEncoder(encoding) {
const encoder = encoding === "utf8" ?
new UTF8Encoder() :
encoding === "utf16le" ?
new UTF16Encoder(false) :
encoding === "utf16be" ?
new UTF16Encoder(true) :
throw new Error("Unsupported encoding");
}
// 逻辑表达式
class Product {
get id() {
return this._id;
}
set id(value) {
this._id = value || throw new Error("Invalid value");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 函数的部分执行
多参数的函数有时需要绑定其中的一个或多个参数,然后返回一个新函数。现在有一个提案,使得绑定参数并返回一个新函数更加容易。这叫做函数的部分执行。
const add = (x, y) => x + y;
const addOne = add(1, ?);
const maxGreaterThanZero = Math.max(0, ...);
2
3
根据新提案,?是单个参数的占位符,...是多个参数的占位符。以下的形式都属于函数的部分执行。
f(x, ?)
f(x, ...)
f(?, x)
f(..., x)
f(?, x, ?)
f(..., x, ...)
2
3
4
5
6
?和...只能出现在函数的调用之中,并且会返回一个新函数。
const g = f(?, 1, ...);
// 等同于
const g = (x, ...y) => f(x, 1, ...y);
2
3
函数的部分执行,也可以用于对象的方法。
let obj = {
f(x, y) { return x + y; },
};
const g = obj.f(?, 3);
g(1) // 4
2
3
4
5
函数的部分执行有一些特别注意的地方
- 函数的部分执行基于原函数。如果原函数发生变化,部分执行生成的新函数也会立即反映这种变化。
- 如果预先提供的值是一个表达式,那么这个表达式并不会在定义时求值,而是在每次调用时求值。
- 如果新函数的参数多于占位符的数量,那么多余的参数将被忽略。
- ...只会被采集一次,如果函数的部分执行使用了多个...,那么每个...的值都将相同。
# 管道运算符
JavaScript的管道是一个运算符,写作|>。它的左边是一个表达式,右边是一个函数。管道运算符把左边表达式的值传入右边的函数进行求值。
x |> f
// 等同于
f(x)
2
3
管道运算符最大的好处,就是可以把嵌套的函数写成从左到右的链式表达式。
// 传统的写法
exclaim(capitalize(doubleSay('hello'))) // "Hello, hello!"
// 管道的写法
'hello'
|> doubleSay
|> capitalize
|> exclaim // "Hello, hello!"
2
3
4
5
6
7
管道运算符只能传递一个值,这意味着它右边的函数必须是一个单参数函数。如果是多参数函数,就必须进行柯里化,改成单参数的版本。
function double (x) { return x + x; }
function add (x, y) { return x + y; }
let person = { score: 25 };
person.score
|> double
|> (_ => add(7, _)) // 57
//add函数需要两个参数。但管道运算符只能传入一个值,因此需要事先提供另一个参数,并将其改成单参数的箭头函数_ => add(7, _)
2
3
4
5
6
7
管道运算符对于await函数也适用。
x |> await f
// 等同于
await f(x)
const userAge = userId |> await fetchUserById |> getAgeFromUser;
// 等同于
const userAge = getAgeFromUser(await fetchUserById(userId));
2
3
4
5
6
# 数值分隔符
现在有一个提案,允许 JavaScript 的数值使用下划线(_)作为分隔符。数值分隔符没有指定间隔的位数,也就是说,可以每三位添加一个分隔符,也可以每一位、每两位、每四位添加一个。小数和科学计数法也可以使用数值分隔符。除了十进制,其他进制的数值也可以使用分隔符,注意,分隔符不能紧跟着进制的前缀0b、0B、0o、0O、0x、0X。
数值分隔符有几个使用注意点。
- 不能在数值的最前面(leading)或最后面(trailing)。
- 不能两个或两个以上的分隔符连在一起。
- 小数点的前后不能有分隔符。
- 科学计数法里面,表示指数的e或E前后不能有分隔符。
下面三个将字符串转成数值的函数,不支持数值分隔符。主要原因是提案的设计者认为,数值分隔符主要是为了编码时书写数值的方便,而不是为了处理外部输入的数据。
- Number()
- parseInt()
- parseFloat()
# Math.signbit()
目前有一个提案,引入了Math.signbit()方法判断一个数的符号位是否设置了。
该方法的算法如下:
- 如果参数是NaN,返回false
- 如果参数是-0,返回true
- 如果参数是负值,返回true
- 其他情况返回false
Math.signbit(2) //false
Math.signbit(-2) //true
Math.signbit(0) //false
Math.signbit(-0) //true 该方法正确返回了-0的符号位是设置了的
2
3
4
# 双冒号运算符(函数绑定运算符)
箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(call、apply、bind)。但箭头函数并不适用于所有场合,所以有一个提案,提出了函数绑定运算符,用来取代call、apply、bind调用。
函数绑定运算符是并排的两个冒号(:😃,双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象作为上下文环境(即this对象),绑定到右边的函数上面。
foo::bar;
// 等同于
bar.bind(foo);
foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);
const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
return obj::hasOwnProperty(key);
}
2
3
4
5
6
7
8
9
10
如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。
var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;
let log = ::console.log;
// 等同于
var log = console.log.bind(console);
2
3
4
5
6
如果双冒号运算符的运算结果还是一个对象,就可以采用链式写法。
import { map, takeWhile, forEach } from "iterlib";
getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));
2
3
4
5
# Realm API
Realm API 提供沙箱功能,允许隔离代码,防止那些被隔离的代码拿到全局对象。提供一个Realm()构造函数,用来生成一个Realm 对象,该对象的global属性指向一个新的顶层对象,这个顶层对象跟原始的顶层对象类似。Realm()构造函数可接受一个参数对象,该参数对象的intrinsics属性可指定Realm沙箱继承原始顶层对象的方法。用户可以自己定义Realm的子类,用来定制自己的沙箱。
# #!命令
有一个提案,为JavaScript脚本引入了#!命令,写在脚本文件或者模块文件的第一行。有了这一行以后,Unix命令行就可以直接执行脚本。对JavaScript引擎来说,会把#!理解成注释,忽略掉这一行。