浏览器和 JavaScript 的一些新特性
CookieStore API
目前, 浏览器可用的存储方式有 cookie
/sessionStorage
/localStorage
/IndexedDB
, 后三者都暴露了十分友好的 API 供开发者访问, 只有 cookie 例外. 回想一下, 平时我们都是怎么操作 cookie 的. 比如想要获取某个 cookie, 因为 document.cookie
返回所有 cookie 联合的字符串, 所以必须要手动解析才能获取到某个 cookie 的值.

如果想要添加一个 cookie, 那么需要像下面一样将 cookie 转换成字符串然后赋值给 document.cookie
.

是不是觉得很奇怪, 明明 document.cookie
是所有 cookie 的集合, 但是添加一个 cookie 却是直接给 document.cookie
赋值. 还有另一个奇怪的点, 有些时候添加一个 cookie 根本不生效, 但是浏览器并不会给出任何的错误信息, 所以只能在 setCookie
之后通过 getCookie
判断是否生效.
setCookie('name', 'value', 1);
if (getCookie('name')) {
console.log('success to setCookie');
} else {
console.log('fail to setCookie');
}
然而, 删除一个 cookie 更奇怪, 我们无法直接删除, 而是让 cookie 过期.

基于以上的问题推出了新的 CookieStore API.
首先, 在 window 上添加了一个名为 cookieStore
的对象, 然后 cookieStore
对象上挂载 get/set/delete
三个方法分别对应单个 cookie 的 获取/添加/删除
操作. 需要注意的是, get/set/delete
返回的都是 Promise
, 所以需要处理异常.
/** 获取一个 cookie */
try {
// 根据 name 获取
const cookie = await window.cookieStore.get('name');
// 获取根据条件获取
const cookie = await window.cookieStore.get({
value: 'xxx',
});
if (cookie === null) {
console.log('name is a emtpy cookie');
} else {
console.log(cookie);
/**
* cookie 包含以下字段
* { domain, expires, name, path, sameSite, secure, value }
* 如果某些字段未设置则为 null
*/
}
} catch (error) {
// do with error
}
/** 添加一个 cookie, 修改一个 cookie 跟之前一样通过覆盖实现 */
try {
/**
* 可配置的字段如下
* { domain, expires, name, path, sameSite, secure, value }
*/
await window.cookieStore.set({
name: 'name',
value: 'value',
expires: Date.now() + 1000 * 60 * 60 * 24, // 一天后过期
});
} catch (error) {
// do with error
}
/** 删除一个 cookie */
try {
await window.cookieStore.delete('name');
} catch (error) {
// do with error
}
同时, cookieStore 还提供了 getAll
的方法, 用于获取 cookie 列表.
try {
// 根据 name 获取, 因为 cookie 可以存在同名的 cookie
const cookieList = await window.cookieStore.getAll('name');
// 或者根据条件获取
const cookieList = await window.cookieStore.getAll({
value: 'xxx',
});
// 如果没有条件, 则返回所有 cookie
const cookieList = await window.cookieStore.getAll();
// ...
} catch (error) {
// do with error
}
以前想要监听 cookie 变化, 只能通过定时器定时检查 cookie, 而 cookieStore
直接提供了监听 cookie 变化的能力.
cookieStore.addEventlistener('change', (event) => {
const {
changed, // 发生变化的 cookie 数组
deleted, // 删除的 cookie 数组
} = event;
// ...
});
兼容性及参考
Media Session API
如果想要控制页面上的 audio
/video
, 只能通过浏览器自带控制组件或者由开发者自己实现控制组件, 而且当页面处于无法点击状态时(比如切换到其他 Tab 或最小化浏览器窗口), 那么将无法实现控制 audio
/video
.
Media Session API
可以暴露页面 audio
/video
的控制, 实现系统媒体中心控制页面的 audio
/video
, 包括正在播放媒体的基本信息(标题/作者/封面)以及操作(播放/暂停/快进/快退/下一个媒体/上一个媒体).
上面的例子实现了一个基本的 MediaSession
. 基本信息通过全局对象 MediaMetadata
实例化, 其中 artwork
可以设置多个值, 浏览器根据出现的场景自动选择最优尺寸, 然后赋值给 navigator.mediaSession.metadata
实现设置. 媒体的控制通过 navigator.mediaSession.setActionHandler
方法设置, play
/pause
/seekbackward
/seekforward
/previoustrack
/nexttrack
分别对应 播放
/暂停
/快退
/快进
/上一个媒体
/下一个媒体
操作. 当媒体播放后, 浏览器会将基本信息和操作与系统映射, 并在系统提供对应的操作菜单.
比如在 Windows10 下, 音量控制附近会出现媒体控制面板.

在 Android 系统下, 状态栏将会出现媒体控制面板.

某些浏览器头部也会出现媒体控制面板.

兼容性及参考
Shape Detection API
现在随处可见二维码, 但是在网页上识别二维码不是一件容易的事, 要么上传到后端解析, 要么使用复杂的 JS 库. 新的 BarcodeDetector
特性提供了友好的 API, 能够脱离后端在本地识别二维码.
使用 BarcodeDetector
首先需要实例化, 然后将图片数据传给实例的 detect
方法, detect
方法是个异步操作, 所以返回值是一个 Promise
. 同时, 一张图片可能包含多个二维码, 所以识别结果是个数组.
BarcodeDetector
不仅能够识别二维码, 还支持各种格式的条形码, 支持的格式包括 aztec
/ code_128
/ code_39
/ code_93
/ codabar
/ data_matrix
/ ean_13
/ ean_8
/ itf
/ pdf417
/ qr_code
/ upc_a
/ upc_e
. BarcodeDetector
默认识别所有格式的条形码, 如果你只想识别其中的某几种格式, 可以在实例化的时候指定:
const detector = new BarcodeDetector({
formats: ['qr_code', 'codabar'], // 只识别图片中的 qr_code 和 codebar
});
BarcodeDetector
属于 Shape Detection API
的一部分, 除此之外, Shape Detection API
还有 TextDetector
和 FaceDetector
, 分别对应文本识别和人脸识别, 以下是一个文本识别的例子:
TextDetector
目前尚未稳定, 所以不一定能够识别画布上的文字, 上传的图片识别结果也可能不准确.
兼容性及参考
- Can I use 传送门
- Accelerated Shape Detection in Images
- The Shape Detection API: a picture is worth a thousand words, faces, and barcodes
Top-level await
以前 await
关键字只允许在 async function
的内部使用, top-level await
可以让我们直接在 async function
外使用 await
关键字.
// module-a.js
(async function() {
const { default: axios } = await import('axios');
const response = await axios.request('https://registry.npm.taobao.org/react');
console.log(response.data.name); // react
})();
使用 top-level await
上面的脚本可以直接移除 async function
.
// module-a.js
const { default: axios } = await import('axios');
const response = await axios.request('https://registry.npm.taobao.org/react');
console.log(response.data.name); // react
如果一个模块使用了 top-level await
, 那么引用这个模块的其他模块将会等待这个模块 resolve
.
// a.js
console.log(1);
const { default: axios } = await import('axios');
const response = await axios.request('https://registry.npm.taobao.org/react');
console.log(2);
export default response.data.name;
// b.js
import name from './a.js';
console.log(name); // react
上面的代码, 输出顺序是 1 2 react
, 也就是说, b 模块将会等待 a 模块 resolve
才会继续执行. 同理, 当 a 模块 reject
, b 模块也会无法正常工作. 要想 b 模块正常工作, 那么需要对 a 模块添加错误处理.
// a.js
let name = 'default name';
try {
const { default: axios } = await import('axios');
const response = await axios.request('https://registry.npm.taobao.org/react');
} catch (error) {
// do with error
}
export default name;
// b.js
import name from './a.js';
console.log(name); // 没有发生错误输出 react, 发生错误输出 default name
top-level await
非常适合某些场景.
条件引入模块
我们知道 static import
是无法实现根据条件引入, 比如下面的代码是不合法的.
if (process.env.NODE_ENV === 'production') {
import a from 'a.js';
} else {
import a from 'a_development.js';
}
通过 top-level await
配合 dynamic import
可以模拟条件静态引入.
let a;
if (process.env.NODE_ENV === 'production') {
a = await import('a.js');
} else {
a = await import('a_development.js');
}
依赖回退
当我们引入一个静态模块时, 如果模块加载失败, 那么引用这个模块的其他模块都无法正常工作.
import $ from 'https://cdn.example.com/jquery.js';
// 如果 https://cdn.example.com/jquery.js 加载失败, 那么下面的代码都无法正常工作
// do with $
通过 top-level await
配合 dynamic import
可以实现依赖的回退操作.
// jquery_wrapper.js
let $;
try {
$ = await import('https://cdn_a.example_a.com/jquery.js');
} catch (error) {
$ = await import('https://cdn_b.example_b.com/jquery.js');
}
export default $;
// example.js
import $ from 'path_to/jquery_wrapper.js';
// do with $
资源初始化
以前, 当一个资源需要异步操作才能初始化时, 通常会有以下写法.
// ws.js
let ws;
async function getWs() {
if (!ws) {
const url = await getUrl();
ws = new Websocket(url);
}
return ws;
}
export default {
sendMessage: async (message) => {
const ws = await getWs();
return ws.sendMessage(message);
},
};
注意, 上面的代码只是实例, 实际应用中还需要大量的错误处理
上面的代码中, 同步的 sendMessage
方法因为异步的 getWs
方法被迫变成异步方法, 使用 top-level await
可以避免这种问题.
const url = await getUrl();
const ws = new Websocket(url);
export default ws;
兼容性及参考
BigInt
在 JavaScript 中, 整数的范围是 [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]
, 也就是负 2 的 53 次方减 1 到 2 的 53 次方减 1, 如果超出这个范围将不能保证其精度, 比如:

如果想要精确地表示超出安全整数, 通常做法是转换成字符串, 然后实现各种字符串运算的方法(相信不少人都做过数字字符串相加的一道算法题, 比如 '123' + '789' = '912'
).
BigInt
可以表示任意大的整数, 即使超出 SAFE_INTEGER
的范围依然能够保证精度. 声明一个 BigInt
可以在 number 后面添加 n
或者通过 BigInt
方法:
const a = 123n;
const b = BigInt(123);
const c = BigInt('123');
上面的 a
/ b
/ c
都表示 123n
. BigInt
支持 +
/ -
/ *
/ /
/ **
/ %
运算符, 但是不能与其他类型混合运算:
const a = 1n + 2n; // 3n
const b = 2n - 1n; // 1n
const c = 3n * 3n; // 9n
const d = 4n / 2n; // 2n
const e = 2n ** 2n; // 4n
const f = 5n % 3n; // 2n
/** 除数不能为 0n */
const g = 2n / 0n; // Uncaught RangeError: Division by zero
/** 不能与其他类型运算 */
const h = 2n + 1; // Uncaught TypeError: Cannot mix BigInt and other types
/** 运算结果将会忽略小数 */
const i = 3n / 2n; // 1n
const k = -3n / 2n; // -1n
BigInt
可以与 number 进行转换, 如果 BigInt
超过 SAFE_INTER
的范围, 那么转换后的 number 将会丢失精度:
const a = BigInt(Number.MAX_SAFE_INTEGER);
const b = a + a;
const c = Number(b); // c 不能保证准确
BigInt
同样支持比较操作, 并且能够与 number 进行比较:
/** 与 number 不严格相等 */
1n == 1; // true
1n === 1; // false
2n > 1; // true
2 > 1n; // true
2n >= 2; // true
BigInt
也有一些局限性, 首先不支持调用 Math
对象上的方法, 其次不支持 JSON.stringify
, 如果想要序列化, 可以实现 BigInt
的 toJSON
方法:
BigInt.prototype.toJSON = function() {
// BigInt toString 不会带上 n, 例如 2n.toString() === '2'
return this.toString();
};
JSON.stringify({ value: 2n }); // { "value": "2" }
需要注意的是, BigInt
和 Symbol
一样是个普通方法, 不是一个构造方法, 所以无法通过 new
实例化:
const a = new BigInt(1); // Uncaught TypeError: BigInt is not a constructor
JavaScript 一共有 7 种数据类型
undefined
/null
/boolean
/number
/string
/symbol
/object
,BigInt
是第 8 种数据类型,typeof 1n
的值是bigint
, 而且属于基本类型.
兼容性及参考
数字分隔符
通常情况下, 如果一个数字特别大, 我们会在显示界面添加分隔符显得更有可读性:

现在通常是千位分隔符, 应该是源于英语中的 thousond/million/billion/trillion/…, 每增加 3 位都有一个专有名词(https://en.wikipedia.org/wiki/Namesoflarge_numbers). 对于我来说更偏向于万位分隔符, 可能是小学数学课养成的习惯.
但是在 JavaScript 层面, 无论一个数多大我们都只能连写, 比如 1032467823482.32134324
, 需要认真数的情况下才能得知准确的值.
现在, Numeric separators
特性允许在数字字面量之间插入 _
分隔符, 使数字字面量更具可读性.
const a = 123_456;
const b = 123.345_789;
_
分隔符可以出现在整数部分, 也可以出现在小数部分. 除了十进制, 分隔符同样可以出现在其他进制:
const a = 0b101_101; // 二进制
const b = 0o765_432; // 八进制
const c = 0xfed_dba_987; // 十六进制
上面的代码是每隔 3 位添加分隔符, 其实分隔符是可以随意添加的, 但是需要注意的是, 分隔符只能在数字之间
添加, 不能在数字开头/结尾/进制标志/小数点/科学计数法符号两边添加, 也不能连续两个分隔符, 下面分隔符的位置都是错误
的:
_123; // 开头, 这其实是一个合法的变量名
123_; // 结尾
/** 进制标志 */
0_b101; // 进制中间
0x_fd9; // 进制后面
/** 科学计数法 */
1.23_e14; // 科学计数法前面
1.23e_14; // 科学计数法后面
123__456; // 连续两个分隔符
数字分隔符只是为了提高代码可读性, 并没有实际意义, 带有分隔符的数字在转换成字符串的时候不会带上分隔符. 同样地, 带有 _
字符串也不能正确地转换成数字:
(123_456.123_456).toString(); // 123456.123456
Number('123_456.123_456'); // NaN
Number.parseInt('123_456', 10); // 123
Number.parseFloat('123_456.123_456', 10); // 123
此外, 数字分隔符同样适用于上面提到的 BigInt
.
兼容性及参考
CSS 颜色方法新的语法
CSS 中提供了 4 个颜色方法
, 分别是 rgb
/ rgba
/ hsl
/ hsla
. 以前每个方法的参数都需要用逗号
分隔, 现在 rgb
/ hsl
新的语法可以省略参数中的逗号
而直接使用空格
分隔.
color: rgb(1, 2, 3);
/* 等同于 */
color: rgb(1 2 3);
color: hsl(1, 2%, 3%);
/* 等同于 */
color: hsl(1 2% 3%);
省略逗号的同时, rgb
/ hsl
都支持第 4 个参数, 表示透明度, 从而替换 rgba
和 hsla
.
color: rgba(1, 2, 3, 0.4);
/* 等同于 */
color: rgb(1 2 3 / 0.4);
color: hsla(1, 2%, 3%, 0.4);
/* 等同于 */
color: hsl(1 2% 3% / 0.4);
其中, /
两侧的空格可有可无.
兼容性及参考
aspect-ratio
如果想要实现一个指定比例的矩形, 通常使用 padding
, 利用其百分比根据父元素宽度计算的特点, 但是真正的内容往往需要放在一个额外的子元素, 并且需要设置绝对定位:
<!-- 4 / 1 的矩形 -->
<style>
.container {
padding-bottom: 25%;
background-color: pink;
position: relative;
}
.content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
<div class="container">
<div class="content">内容内容内容</div>
</div>
aspect-ratio
属性可以让我们直接设置元素的宽高比. 上面的例子使用 aspect-ratio
可以写成这样:
<style>
.content {
background-color: pink;
aspect-ratio: 4 / 1;
}
</style>
<div class="content">内容内容内容</div>
兼容性及参考
gap
在 grid
布局中, 可以用 grid-gap
属性来设置行与行和列与列之间的间隙, 现在可以直接使用 gap
属性替代 grid-gap
, 而且 gap
属性增加了对 flex
和 column-count
的支持.
兼容性及参考
CSS math functions
在 CSS 中可以使用 calc
方法进行数学计算, 现在新增了三个新的方法 min
/ max
/ clamp
.
min
方法接受一个或多个值, 返回其中最小值, 比如 width: min(1vw, 4rem, 80px);
, 如果 viewport
的宽度等于 800px
, 则 1vw === 8px
, 4rem === 64px
, 所以结果是 width: 1vw;
.
max
方法接受一个或多个值, 返回其中最大值, 上面例子中, 结果是 width: 80px;
.
clamp
方法接受 3 个值 clamp(MIN, VAL, MAX)
, 从左到右分别是最小值/首选值/最大值, 如果首选值小于最小值则返回最小值, 如果大于最大值则返回最大值, 如果首选值介于最小值和最大值之间则返回首选值, 具体逻辑可以这样用 JS 表示:
function clamp(min, val, max) {
// 小于最小值的话返回最小值
if (val < min) {
return min;
}
// 大于最大值的话返回最大值
if (val > max) {
return max;
}
// 介于最小值和最大值, 返回首选值
return val;
}
也就是说, clamp
限定了 VAL
的取值范围, 比如 width: clamp(1rem, 10vw, 2rem);
在 viewport
的宽度等于 800px
的情况下, 结果是 width: 2rem;
, 因为 (10vw = 80px) > (2rem = 32px)
.
有趣的是, min
/ max
/ clamp
还可以与其他方法嵌套, 比如 clamp(1rem, calc(10vw - 5px), min(2rem, 20vw))
, 所以 clamp
可以写成 max(MIN, min(VAL, MAX))
或 min(MAX, max(VAL, MIN))
.