Appearance
最新提案
do 表达式
块级作用域是一个语句,将多个操作封装在一起,没有返回值
js
{
let t = f()
t = t * t + 1
}块级作用域外,无法获得 t 的值
新提案 [1],块级作用域前加 do,使得块级作用域变为表达式,返回内部最后执行的表达式的值
js
let x = do {
let t = f()
t * t + 1
}变量 x 值为 t * t + 1;
do 表达式:封装的是什么,就会返回什么
js
// 等同于 <表达式>
do { <表达式>; }
// 等同于 <语句>
do { <语句> }do 表达式好处:封装多个语句,让程序更加模块化
js
let x = do {
if (foo()) {
f()
} else if (bar()) {
g()
} else {
h()
}
}do 块级作用域提供了单独的作用域,内部操作可以与全局作用域隔绝
do 表达式在 JSX 语法中非常好用
js
return (
<nav>
<Home />
{do {
if (loggedIn) {
;<LogoutButton />
} else {
;<LoginButton />
}
}}
</nav>
)throw 表达式
JavaScript 语法规定:throw 是一个命令,用来抛出错误,不能用于表达式之中
js
// 报错
console.log(throw new Error())提案[2]允许 throw 用于表达式
js
// 参数的默认值
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')
}
}注意:避免与 throw 命令混淆,规定 throw 出现在行首,一律解释为 throw 语句,而不是 throw 表达式
函数部分执行
语法
多参数的函数有时需要绑定其中的一个或多个参数,然后返回一个新函数(柯里化)
js
function add(x, y) {
return x + y
}
function add7(x) {
return x + 7
}将参数一个参数绑定为 7,从 add 得到 add7
js
// bind 方法
const add7 = add.bind(null, 7)
// 箭头函数
const add7 = (x) => add(x, 7)两种写法都有些冗余。
其中,bind 方法的局限更加明显,它必须提供 this,并且只能从前到后一个个绑定参数,无法只绑定非头部的参数
提案[3],函数的部分执行(partial application):让绑定参数并返回更加容易
js
const add = (x, y) => x + y;
const addOne = add(1, ?);
const maxGreaterThanZero = Math.max(0, ...);?是单个参数的占位符,...是多个参数的占位符
js
f(x, ?)
f(x, ...)
f(?, x)
f(..., x)
f(?, x, ?)
f(..., x, ...)?和...只能出现在函数的调用之中,并且会返回一个新函数
js
const g = f(?, 1, ...);
// 等同于
const g = (x, ...y) => f(x, 1, ...y);函数的部分执行,也可以用于对象的方法
js
let obj = {
f(x, y) {
return x + y
}
}
const g = obj.f(?, 3)
g(1) // 4注意点
(1) 函数的部分执行是基于原函数的。原函数变化,部分执行生成的新函数也会立即反映这种变化
js
let f = (x, y) => x + y
const g = f(?, 3)
g(1) // 4
// 替换函数 f
f = (x, y) => x * y
g(1) // 3(2) 提供的值是一个表达式,那么这个表达式并不会在定义时求值,而是在每次调用时求值
js
let a = 3
const f = (x, y) => x + y
const g = f(?, a)
g(1) // 4
// 改变 a 的值
a = 10
g(1) // 11(3) 如果新函数的参数多于占位符的数量,那么多余的参数将被忽略
js
const f = (x, ...y) => [x, ...y]
const g = f(?, 1)
g(2, 3, 4) // [2, 1]写成下面这样,多余的参数就没问题
js
const f = (x, ...y) => [x, ...y];
const g = f(?, 1, ...);
g(2, 3, 4); // [2, 1, 3, 4];(4) ...只会被采集一次,如果函数的部分执行使用了多个...,那么每个...的值都将相同
js
const f = (...x) => x;
const g = f(..., 9, ...);
g(1, 2, 3); // [1, 2, 3, 9, 1, 2, 3]管道运算符
Unix 操作系统有一个管道机制(pipeline),把前一个操作的值传给后一个操作。
JavaScript 的管道是一个运算符,写作|>[5]。符号左边是一个表达式,右边是一个函数。管道运算符把左边表达式的值,传入右边的函数进行求值。
js
x |> f
// 等同于
f(x)管道运算符好处:把嵌套的函数,写成从左到右的链式表达式
js
function doubleSay(str) {
return str + ', ' + str
}
function capitalize(str) {
return str[0].toUpperCase() + str.substring(1)
}
function exclaim(str) {
return str + '!'
}嵌套执行,传统的写法和管道的写法
js
// 传统的写法
exclaim(capitalize(doubleSay('hello')))
// "Hello, hello!"
// 管道的写法
'hello' |> doubleSay |> capitalize |> exclaim
// "Hello, hello!"管道运算符只能传递一个值。多参数函数,就必须进行柯里化,改成单参数的版本
js
function double(x) {
return x + x
}
function add(x, y) {
return x + y
}
let person = { score: 25 }
person.score |> double |> ((_) => add(7, _))
// 57管道运算符对于 await 函数也适用
js
x |> await f
// 等同于
await f(x)
const userAge = userId |> await fetchUserById |> getAgeFromUser;
// 等同于
const userAge = getAgeFromUser(await fetchUserById(userId));Math.signbit()
Math.sign()用来判断一个值的正负
问题:无法区分 -0 和 +0
IEEE 754 规定第一位是符号位,0 表示正数,1 表示负数。+0 是符号位为 0 时的零值,-0 是符号位为 1 时的零值
js
;+0 === -0 // true问题:判断一个值是+0 还是-0 非常麻烦,因为它们是相等
Math.signbit():方法判断一个数的符号位是否设置。提案[5]
js
Math.signbit(2) //false
Math.signbit(-2) //true
Math.signbit(0) //false
Math.signbit(-0) //true规则
- 如果参数是
NaN,返回false - 如果参数是
-0,返回true - 如果参数是负值,返回
true - 其他情况返回
false
双冒号运算符
箭头函数可以绑定 this 对象,大大减少了显式绑定 this 对象的写法(call、apply、bind),箭头函数并不适用于所有场合。
提案[6],“函数绑定”(function bind)运算符,用来取代 call、apply、bind 调用
函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。
运算符会自动将左边的对象,作为上下文环境(即 this 对象),绑定到右边的函数上面。
js
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)
}双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面
js
var method = obj::obj.foo
// 等同于
var method = ::obj.foo
let log = ::console.log
// 等同于
var log = console.log.bind(console)双冒号运算符的运算结果,还是一个对象,就可以采用链式写法
js
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),允许隔离代码,防止那些被隔离的代码拿到全局对象
以前,经常使用<iframe>作为沙箱
js
const globalOne = window
let iframe = document.createElement('iframe')
document.body.appendChild(iframe)
const globalTwo = iframe.contentWindow<iframe>的全局对象是独立的(iframe.contentWindow)。Realm API 可以取代这个功能
js
const globalOne = window
const globalTwo = new Realm().globalRealm()构造函数:生成 Realm 对象。
该对象的 global 属性指向一个新的顶层对象,这个顶层对象跟原始的顶层对象类似
js
const globalOne = window
const globalTwo = new Realm().global
globalOne.evaluate('1 + 2') // 3
globalTwo.evaluate('1 + 2') // 3可以通过顶层对象的 evaluate()方法,可以运行代码
Realm 顶层对象与原始顶层对象是两个对象
js
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 // falseRealm 沙箱里面只能运行 ECMAScript 语法提供的 API,不能运行宿主环境提供的 API
js
globalTwo.evaluate('console.log(1)')
// throw an error: console is undefined原因:Realm 沙箱里面没有 console 对象,导致报错。因为 console 不是语法标准,是宿主环境提供的
解决:手动赋值
js
globalTwo.console = globalOne.console构造函数:Realm(Object)
参数说明:
Object:参数对象的 intrinsics 属性,表示继承原始顶层对象的方法
正常情况,沙箱的 JSON 方法不同于原始的 JSON 对象。通过 intrinsics 属性,会继承原始对象方法
js
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自己定义 Realm 的子类,用来定制自己的沙箱
js
class FakeWindow extends Realm {
init() {
super.init();
let global = this.global;
global.document = new FakeDocument(...);
global.alert = new Proxy(fakeAlert, { ... });
// ...
}
}#!命令
Unix 的命令行脚本都支持#!命令,又称为 Shebang 或 Hashbang。命令放在脚本的第一行,用来指定脚本的执行器
Bash 脚本的第一行
bash
#!/bin/shPython 脚本的第一行
python
#!/usr/bin/env python提案[8],为 JavaScript 脚本引入了#!命令,写在脚本文件或者模块文件的第一行
js
// 写在脚本文件第一行
#!/usr/bin/env node
'use strict';
console.log(1);
// 写在模块文件第一行
#!/usr/bin/env node
export {};
console.log(1);引入 #! 命令后,Unix 命令行就可以直接执行脚本
js
# 以前执行脚本的方式
$ node hello.js
# hashbang 的方式
$ ./hello.jsJavaScript 引擎来说,会把#!理解成注释,忽略掉这一行
import.meta
有时需要知道模板本身的一些信息(比如模块的路径),提案[9],import 命令添加了一个元属性 import.meta,返回当前模块的元信息
注意:import.meta 只能在模块内部使用,如果在模块外部使用会报错
import.meta.url
import.meta.url:当前模块的 URL 路径
js
// 当前文件 https://foo.com/main.js
new URL('data.txt', import.meta.url) // href:https://foo.com/data.txtURL 构造函数[10]:返回新创建的 URL,不是有效 URL,会抛出错误
注意:Node.js 环境中,import.meta.url 返回的总是本地路径,即是 file:URL 协议的字符串,比如 file:///home/user/foo.js
import.meta.scriptElement
import.meta.scriptElement 是浏览器特有的元属性,返回加载模块的<script>元素,相当于 document.currentScript 属性
js
// HTML 代码为
// <script type="module" src="my-module.js" data-foo="abc"></script>
// my-module.js 内部执行下面的代码
import.meta.scriptElement.dataset.foo
// "abc"JSON 模块
import 命令目前只能用于加载 ES 模块。提案[11],允许加载 JSON 模块
fetch()加载 JSON 模块
js
const response = await fetch('./config.json')
const json = await response.json()import 命令能够直接加载 JSON 模块
js
import configData from './config.json' assert { type: 'json' }
console.log(configData.appName)结尾的 assert {type: "json"}不可缺少。这种写法叫导入断言,告诉 JavaScript 引擎,现在加载的是 JSON 模块
Q: 为什么不通过.json 后缀名判断呢?
A: 因为浏览器的传统是不通过后缀名判断文件类型,标准委员会希望遵循这种做法,这样也可以避免一些安全问题
导入断言是 JavaScript 导入其他格式模块的标准写法,JSON 模块将是第一个使用这种语法导入的模块。以后,还会支持导入 CSS 模块、HTML 模块等等
动态加载模块的 import()函数也支持加载 JSON 模块。
js
import('./config.json', { assert: { type: 'json' } })脚本加载 JSON 模块以后,还可以再用 export 命令输出
js
export { config } from './config.json' assert { type: 'json' }