- TypeScript 3.3 更新梳理
- Object.assign vs Object Spread in Node.js
- New in Chrome 72
- New JavaScript features in ES2019
- iOS 12.2 Beta's 支持 Web Share API
- Yarn's Future - v2 and beyond
TypeScript 3.3 更新梳理
首先需要声明的是,TypeScript 3.3 是个小版本 release,不包含 breaking changes,so 升级的话应该比较轻松。那么 3.3 中有什么新东西呢?我们一起来探索一下。
联合类型(union types)的优化
先梳理一下
我们先梳理下之前联合类型是怎样的。假设我们有联合类型 A | B,则可以访问 A 或 B 共有的属性或方法(也就是 A 或 B 的交集)。
interface A {
aProp: string;
commonProp: string;
}
interface B {
bProp: number;
commonProp: number
}
type Union = A | B;
declare let x: Union;
x.aProp; // error - 'B' doesn't have the property 'aProp'
x.bProp; // error - 'A' doesn't have the property 'bProp'
x.commonProp; // okay! Both 'A' and 'B' have a property named `commonProp`.
这一点还是符合直觉的,我们只能访问联合类型的公共部分,这一点颇有些像多态性。
而当我们不是想访问属性,而是想处理调用方法的返回类型时,当然,不论是什么类型,我们的标识符参数都是一样的,那么也一样 work:
type CallableA = (x: boolean) => string;
type CallableB = (x: boolean) => number;
type CallableUnion = CallableA | CallableB;
declare let f: CallableUnion;
let x = f(true); // Okay! Returns a 'string | number'.
更新点
但是问题来了,有时候这些约束还是会有些严格了,我们来看下面的例子:
type Fruit = "apple" | "orange";
type Color = "red" | "orange";
type FruitEater = (fruit: Fruit) => number; // eats and ranks the fruit
type ColorConsumer = (color: Color) => string; // consumes and describes the colors
declare let f: FruitEater | ColorConsumer;
// Cannot invoke an expression whose type lacks a call signature.
// Type 'FruitEater | ColorConsumer' has no compatible call signatures.ts(2349)
f("orange");
上面的例子就相对复杂了些,除了联合类型还用到了 declare 声明关键字和 type 别名关键字。我们调用了函数 f,而函数 f 的声明表示,它是 FruitEater 和 ColorConsumer 的联合类型,其中,FruitEater 和 ColorConsumer 分别是两种自定义的函数类型的别名,而它们的函数参数也都是联合类型。
这个代码很傻瓜但却报了错,FruitEater 和 ColorConsumer 都应该能够接收 "orange" 参数,它既可以返回一个数字,也可以返回一个字符串。
上述的问题在 TypeScript 3.3 中被干掉了:
type Fruit = "apple" | "orange";
type Color = "red" | "orange";
type FruitEater = (fruit: Fruit) => number; // eats and ranks the fruit
type ColorConsumer = (color: Color) => string; // consumes and describes the colors
declare let f: FruitEater | ColorConsumer;
f("orange"); // It works! Returns a 'number | string'.
f("apple"); // error - Argument of type '"apple"' is not assignable to parameter of type '"orange"'.
f("red"); // error - Argument of type '"red"' is not assignable to parameter of type '"orange"'.
上面就很清晰了,"orange" 符合联合类型的要求,所以 f 应该可以接收并正确返回,我们只需要保证返回的值是数字或者字符串就够了。
在 TypeScript 3.3 中,这些交叉的类型会创建新的签名。举个例子,FruitEater 和 ColorConsumer 的参数 fruit 和 color 被交叉成了一个新的参数类型:Fruit & Color,如替换掉别名,Fruit & Color 实际上就是 ("apple" | "orange") & ("red" | "orange"),我们继续演进,其实就是 ("apple" & "red") | ("apple" & "orange") | ("orange" & "red") | ("orange" & "orange")。而我们取交集运算后,最后得到的结果就是 "orange" & "orange",也就是 "orange"。
我们整理一下,用数学中集合那一块的运算重新演算一遍:
Fruit & Color
= ("apple" | "orange") & ("red" | "orange")
= ("apple" & "red") | ("apple" & "orange") | ("orange" & "red") | ("orange" & "orange")
= "orange" & "orange"
= "orange"
日了狗了,我不就写写 JS 吗,怎么算起了数学呢?万恶的 MS...
复合项目的增量 watch
TypeScript 3.0 引入了一个用于构建过程的被称为「复合项目」的新功能。 其目的有二:
- 确保用户可以将大型项目拆分为更小的部分,从而快速构建
- 保留项目结构,不影响现有的 TypeScript 体验
在3.3版本之前,在使用 --build、--watch 构建复合项目时,实际上并没有使用 watch 增量文件的基础结构。 如果一个项目中有了更新,将会强制完全重新构建该项目,而不是检查项目中有哪些文件受到影响。
在TypeScript 3.3中, --build 模式的 --watch 标志也可以利用增量文件机制进行监视了。这意味着在 --build --watch 模式下构建速度能将更快。测试下来的构建时间比原来缩短了 50% 到 75%。
接下来的发展
我们可以在 roadmap 里看到接下来的发展路线。TypeScript 3.4 中我们就可以用上「只读 数组和元组」和 const 上下文了,后续我们再看吧。
源地址:https://blogs.msdn.microsoft....
Object.assign vs Object Spread in Node.js
今天来比较下 Object.assign 和 Object Spread 的差异。
因为我们在项目中配了 Babel,所以这两个也是常用的。其中,2018 年的时候,[Ojbect Rest/Spread Proposal] 提案就已经到了 stage 4,也就是说不久就会进入 ECMAScript 规范。当然,从 Node.js 8 的 LTS 版本开始,这一提案的内容就已经有了,我们可以直接进入 Node 环境使用它:
$ node -v
v8.9.4
$ node
> const obj = { foo: 1, bar: 1 };
undefined
> ({ ...obj, baz: 1 });
{ foo: 1, bar: 1, baz: 1 }
Object spread operator,即对象展开运算符 { ...obj }
和 Object.assign()
的功能是很相似的,那我们应该用哪个,它们有什么区别呢?
Object spread 概况
对象展开运算符本质上是创建一个新的由现有对象的 own property 组成的普通对象。什么是 own property?一部分是 Object.getOwnPropertyNames()
返回的属性。那 Object.getOwnPropertyNames()
返回的是什么属性?它返回的是对象中所有的、包括不可枚举属性的、排除 Symbol 在外的的属性。另一部分就是 Symbol 类型的属性,也就是 Object.getOwnPropertySymbols()
返回的属性。
因此,{ ...obj }
返回的就是和 obj
所包含的属性和值相同的对象。
const obj = { foo: 'bar' };
const clone = { ...obj }; // `{ foo: 'bar' }`
obj.foo = 'baz';
clone.foo; // 'bar'
和 Object.assign()
一样,对象展开运算符同样不复制继承来的属性和类本身的信息(因为它们挂在原型上),且会复制 ES6 的 Symbol 类型:
class BaseClass {
foo() { return 1; }
}
class MyClass extends BaseClass {
bar() { return 2; }
}
const obj = new MyClass();
obj.baz = function() { return 3; };
obj[Symbol.for('test')] = 4;
// Does _not_ copy any properties from `MyClass` or `BaseClass`
const clone = { ...obj };
console.log(clone); // { baz: [Function], [Symbol(test)]: 4 }
console.log(clone.constructor.name); // Object
console.log(clone instanceof MyClass); // false
这就很清楚了,Object spread 把继承关系一概丢掉,类相关信息一概丢掉,只留着普通对象的属性,可不可枚举都有,Symbol 也有。
我们还可以利用对象展开运算符来混合属性,当然这里就很简单了,受顺序影响会产生覆盖行为:
const obj = { a: 'a', b: 'b', c: 'c' };
{ a: 1, b: null, c: void 0, ...obj }; // { a: 'a', b: 'b', c: 'c' }
{ a: 1, b: null, ...obj, c: void 0 }; // { a: 'a', b: 'b', c: undefined }
{ a: 1, ...obj, b: null, c: void 0 }; // { a: 'a', b: null, c: undefined }
{ ...obj, a: 1, b: null, c: void 0 }; // { a: 1, b: null, c: undefined }
差异对比
上述的例子里,这两者其实没什么区别。事实上这俩操作可以写个等式:
{ ...obj } === Object.assign({}, obj) // 这个等式是开玩笑的!
因此有:
const obj = { a: 'a', b: 'b', c: 'c' };
Object.assign({ a: 1, b: null, c: void 0 }, obj); // { a: 'a', b: 'b', c: 'c' }
Object.assign({ a: 1, b: null }, obj, { c: void 0 }); // { a: 'a', b: 'b', c: undefined }
Object.assign({ a: 1 }, obj, { b: null, c: void 0 }); // { a: 'a', b: null, c: undefined }
Object.assign({}, obj, { a: 1, b: null, c: void 0 }); // { a: 1, b: null, c: undefined }
那么问题来了,既然没啥区别,我干嘛要用一个而不是另一个?
行为
差异之一也是大家一般都知道的点是,对象展开运算符总是会返回一个 POJO(Plain Old Java Object) 对象,而 Object.assign()
如果将原对象作为第一个参数传入,原对象会被修改:
class MyClass {
set val(v) {
console.log('Setter called', v);
return v;
}
}
const obj = new MyClass();
Object.assign(obj, { val: 42 }); // Prints "Setter called 42"
换句话讲,Object.assign()
会修改第一个参数的对象,并且会触发 ES6 setter。因此,如果我们更希望使用 Immutable 技术,自然要配置一下用上 object spread。这也是为什么,很多时候我们用 Object.assign()
的方式就是第一个参数置为空对象:
const copyOne = Object.assign({}, obj);
性能
总的来说,如果我们向 Object.assign()
的第一个参数传入 {}
,那么对象展开运算符更快,否则基本差不多。
我们来使用 benchmark.js 对其进行评估:
const Benchmark = require('benchmark');
const suite = new Benchmark.Suite;
const obj = { foo: 1, bar: 2 };
suite.
add('Object spread', function() {
({ baz: 3, ...obj });
}).
add('Object.assign()', function() {
Object.assign({ baz: 3 }, obj);
}).
on('cycle', function(event) {
console.log(String(event.target));
}).
on('complete', function() {
console.log('Fastest is ' + this.filter('fastest').map('name'));
}).
run({ 'async': true });
这种场景下,而这差别不大:
但如果向 Object.assign()
的第一个参数传入 {}
,对象展开运算符就总是更快了:
suite.
add('Object spread', function() {
({ baz: 3, ...obj });
}).
add('Object.assign()', function() {
Object.assign({}, obj, { baz: 3 });
})
得到结果为:
总结
- 行为上,
Object.assign()
可以修改第一个参数传入的对象,触发 ES6 setter - 性能上,当
Object.assign()
的第一个参数传入{}
,{ ...obj }
更快
New in Chrome 72
在 Chrome 72 中新增支持如下:
- 创建类的共有域变得更加简洁
- 新的 User Activation API 可以帮助我们知道一个页面是否被「激活」
- Intl.format() API 可以帮助我们更简单的定位列表
当然,第一条我们曾经提到过,而第二第三条我没有了解过。
Public class fields
现在我们不再需要在 constructor 中声明,只需要直接在类定义中声明类的共有域即可。主要的使用就是通过下划线来表明其域的身份,然后可以通过 getter、setter 来操作。
class Counter {
_value = 0;
get value() {
return this._value;
}
increment() {
this._value++;
}
}
const counter = new Counter();
console.log(counter.value);
// → 0
counter.increment();
console.log(counter.value);
// → 1
对私有域的支持以及在开发中了。当然如果 Babel 动作更快的话,就又可以先用上了。
User Activation API
我们可能都遇到过页面加载好就自动播放音乐的场景,然后被吓一跳,赶紧找个静音键或者把 tab 关掉。这就是为什么一些 API 需要通过用户的某种操作触发后才起作用。不过不同浏览器对这个触发的操作的处理并不相同。
Chrome 72 引入了 User Activation v2,它对所有这类 API 进行了简化。v2 基于新的标准,为了能够使所有浏览器标准化。
新的 userActivation 属性挂在了 navigator 和 MessageEvent 上:hasBeenActive
和 isActive
。
- hasBeenActive 表明关联的窗口是否在其周期内见到了 user activation
- isActive 表明关联的窗口是否已经触发了 user activation
这一点没了解过,略有些不理解。看下这个图吧,图为用户触发 User activation API 之前和之后的变化。
Localizing lists of things with Intl.format
用我们想用的语言初始化,然后调用 format 方法,它就会使用正确的词语和语法。例如下面的例子是将列表中的单词拼接起来,并且按照指定的语言(fr,指法语),补充了相应语言的「and 与」及「or 或」的单词。这部分操作都会交给 JS 引擎而不会牺牲性能。如下就是将一些动物名词用 or 来拼接:
const opts = {type: 'disjunction'};
const lf = new Intl.ListFormat('fr', opts);
lf.format(['chien', 'chat', 'oiseau']);
// → 'chien, chat ou oiseau' 意思是「狗、猫或鸟」
lf.format(['chien', 'chat', 'oiseau', 'lapin']);
// → 'chien, chat, oiseau ou lapin' 意思是「狗、猫、鸟或兔子」
源地址:https://developers.google.com...
New JavaScript features in ES2019
Chrome 73 默认支持这些 ES2019 特性,因此今天来盘一部分新特性。
Array#{flat,flatMap}:
Array.prototype.flat,一开始提案为 Array.prototype.flatten,其作用就是递归的将数组展平到指定深度,默认深度为 1。这个方法感觉像过去被问过的题。
// Flatten one level:
const array = [1, [2, [3]]];
array.flat();
// → [1, 2, [3]]
// Flatten recursively until the array contains no more nested arrays:
array.flat(Infinity);
// → [1, 2, 3]
提案还包括 Array.prototype.flatMap,它会把数组再展成新的数组。
[2, 3, 4].flatMap((x) => [x, x * 2]);
// → [2, 4, 3, 6, 4, 8]
源地址:https://developers.google.com...
Object.fromEntries:
Object.fromEntries 是针对 Object.entries 的补充,基本可以理解成 Object.fromEntries(Object.entries(object)) ≈ object。
const object = { x: 42, y: 50 };
const entries = Object.entries(object);
// [['x', 42], ['y', 50]]
const result = Object.fromEntries(entries); // { x: 42, y: 50 }
源地址:https://github.com/tc39/propo...
String#{trimStart,trimEnd}:
除了已有的 String.prototype.trim(),V8 现在实现了 String.prototype.trimStart() 和 String.prototype.trimEnd()。过去这俩功能有非标准的 trimLeft() 和 trimRight()。
const string = ' hello world ';
string.trimStart();
// → 'hello world '
string.trimEnd();
// → ' hello world'
string.trim();
// → 'hello world'
源地址:https://v8.dev/blog/v8-releas...
Symbol#description:
Symbol.prototype.description 提案在 stage 4,所以也是 ES2019 中的内容。
当我们通过工厂函数 Symbol() 创建了一个 symbol 的时候,我们可以选择传入一个字符串作为说明:
const sym = Symbol('The description');
过去我们想要使用这个说明的方法是通过 String:
assert.equal(String(sym), 'Symbol(The description)');
现在提案引入了 Symbol.prototype.description,我们可以通过原型链上的属性来访问:
assert.equal(sym.description, 'The description');
源地址:http://2ality.com/2019/01/sym...
try {} catch {} // optional binding:
这一条说的是 try-catch 可以不强制使用参数了,也就是 err。
try {
doSomethingThatMightThrow();
} catch { // no binding!
handleException();
}
stable Array#sort:
过去 V8 对于超过 10 个元素的数组使用的是不稳定的快排,现在使用的是稳定的 TimSort 算法。
源地址:https://twitter.com/mathias/s...
iOS 12.2 Beta's 支持 Web Share API
iOS 12.2 beta 版在 Safari 和其他 web 视图里引入了 Web Share API。这说明什么呢?我们可以通过在页面上创建一个按钮,而这个按钮可以触发原生系统拉起「share sheet」。
具体是什么样的呢?我们可以看下官方 Demo,页面如下:
可以看下这个 Demo 的源码,核心部分就是下面这句:
try {
await navigator.share({
title,
text,
url
});
} catch (error) {
logError('Error sharing: ' + error);
return;
}
所以这里我们可以做些能力检测,再决定是否调用:
if (navigator.share === undefined) {
logError('Error: You need to use a browser that supports this draft proposal.');
}
其效果就是拉起「share sheet」,放张图就知道了(当然这里并没有升系统版本,但唤起的 share sheet 就是图中所示):
补充:Web Share API 文档见这里。
Yarn's Future - v2 and beyond
今天看下 Maël Nison 关于 Yarn 未来的展望。嗯,大佬终究是大佬,发量果然还可以。
Yarn 将成为一个开发优先的工具
这话我们已经说了一段时间了,但是现在我们已经准备好开始了。包管理器不是应该在生产服务器上运行的工具。在那里运行的代码越多,出现问题的可能性就越高,最终会导致生产系统崩溃。Yarn 的开发优先意味着我们将使您能够做到像是克隆存储库的状态。这包括重点使用 Plug'n'Play。
Yarn 将使用 TypeScript 重写
Yarn 已经完全被 Flow 类型覆盖,而且运作的非常不错。我们希望让第三方贡献者尽可能轻松地进行 shim 并帮助我们维护这个非常棒的工具,所以我们将代码切换为 TypeScript 实现。我们希望这有助于使代码库比您已经贡献的项目更熟悉。
Yarn 会成为一个 API,内部组件可以被拆分成模块化实体
这是个很大的目标。目前在使用 Yarn 时,我们唯一的选择是命令行界面。我们没有提供允许您利用我们实现的复杂逻辑的 primitives -— 无论是解析器、链接器还是访问配置。Yarn 将首先是 API,然后是 CLI。
Yarn 会支持不同的安装目标而不仅是 Node
包管理是一个总是被不断推翻重来的难题 - 即使 Yarn 过去也是这样做的。我们认为原因是所有包管理器都需要以稍微不同的方式布置已安装的包,以便宿主读取它们。遗憾的是,这很难实现,最终重写包管理器并丢弃已有的用户体验和功能反而变得更容易些。从 Berry 开始,我们明确了一个目标,即可以切换管道的每个组件以适应不同的安装目标。在某种程度上,Yarn 现在将不仅是包管理器,还要成为包管理器平台。如果您仍想使用 Yarn 来对 PHP、Python、Ruby 软件包进行管理,开一个 issue,我们一起来做!
需要的时候,整体上的兼容性将会被保留
这种 semver-major changes 在设计上是不向后兼容的,但我们会确保将它们保持在可接受的水平,特别是核心命令(yarn install
、yarn add
、yarn remove
、yarn run
)将保持相同的行为。先前 yarn.lock将被静默迁移。一个重要的警告:我们的安装现在默认使用 Plug'n'Play。在过去的几个月里,我们已经证明这种方法是合理的,可以解决所有潜在的实施问题,最后与项目维护人员讨论,以确定是否有任何我们可以做的事情来帮助他们确保一切准备就绪。现在是时候冒险了。