语法糖、操作符、关键字、特性
语法糖、操作符、关键字、特性
语法糖(Syntactic sugar)是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。
—— Syntactic sugar (opens new window)
各种前端和编程教程中会提到 JS 的基本语法,
然而在各种源码和片段中,不认识的奇怪代码可能会造成困惑。
比如 ??
是干什么的?甚至可能连如何搜索也无从下手。
所以提前集中了解散落在文档各处的概念,对于后续学习代码会有所帮助。
大部分语法糖基本上都有简单的等价形式,
因为它们的设计目的本来就是为了简化写法。
实际上, 几乎所有的现代语言都有这些语法
—— 为什么会有程序员不喜欢 Python? (opens new window)
同时,要记得这些只是语法层面的实用技巧,
而如何更“优雅”地编写复杂的软件,那是更大话题了。
是否能写出好的代码在于人,而不在于语言。如果你的心中没有清晰简单的思维模型,你用任何语言表述出来都是一堆乱麻。
—— 如何掌握所有的程序语言 (opens new window)
# 分类
- ES6+ 基本
- 遍历器:
for...of
- 模板字符串:
` `
、${}
- 箭头函数:
() => {}
- 默认参数:
(a = 1) => {}
- 类:
class
、constructor
- 解构赋值:
[ ] = [ ]
、{ } = { }
、...
- 异步函数:
async
、await
- 生成器(Generator):
function*
、yield
- 遍历器:
- 技巧
- 三元运算符:
a ? b : c
- 短路运算(布尔运算):
&&
、||
- 布尔化的快速写法(布尔运算):
!!
- 取整的快速写法(位运算):
~~
- 三元运算符:
- 陷阱
- 连续赋值:
a = b = c
- 连续比较:
a < b < c
- 连续赋值:
- TypeScript
- 类型:
type
、interface
- 泛型:
<T1, T2, ...>
- 可选参数:
?
- 可选链:
?
- 非空断言:
!
- 空值合并:
??
- 函数重载:(函数声明和函数实体)
- 类:
public
、private
、protected
、static
- 装饰器(Decorator):
@something
- 类型:
- JS 不支持的/其他的
- 元组(Tuple):Python 的例子
- 模式匹配:Haskell 的例子
- 管道(Pipeline):F# 的例子
- 运算符重载:C++ 的例子
# 示例
注:更多示例参考我在 Learn By Doing (opens new window) 中的代码
ES6+ 基本
- 遍历器:
for...of
for (const iterator of object) { console.log(iterator); }
1
2
3 - 模板字符串:
` `
、${}
const str = `hello ${name}`;
1 - 箭头函数:
() => {}
const fn = (a, b) => a + b;
1 - 默认参数:
(a = 1) => {}
const fn = (a = 1) => a;
1 - 类:
class
、constructor
class MyClass { constructor() {} method() {} }
1
2
3
4 - 解构赋值:
[ ] = [ ]
、{ } = { }
、...
const [a, b, , c] = arr; const { d, ...e } = obj; const fn = (p, ...ps) => {}; [x, y] = [y, x];
1
2
3
4 - 异步函数:
async
、await
async function fn() { await delay(100); console.log(100); }
1
2
3
4 - 生成器(Generator):
function*
、yield
function* gen() { const input = yield null; console.log(input); }
1
2
3
4
- 遍历器:
技巧
- 三元运算符:
a ? b : c
const msg = name === 'admin' ? 'name is invalid'; : 'name is valid'
1
2
3
4 - 短路运算(布尔运算):
&&
、||
const result = getAlias() || getNickName(); result && process();
1
2 - 布尔化的快速写法(布尔运算):
!!
const input = 'John'; const isInputNotEmpty = !!input;
1
2 - 取整的快速写法(位运算):
~~
// prettier-ignore ~~ 2.7 === 2; // prettier-ignore ~~ -2.7 === -2;
1
2
3
4
- 三元运算符:
陷阱
- 连续赋值
// prettier-ignore const a = b = c;
1
2 - 连续比较
a < b < c; // prettier-ignore a === b === c;
1
2
3
- 连续赋值
TypeScript
类型:
type
、interface
type ItemId = string; interface Item { id: ItemId; } const item: Item = { id: 'qjne' };
1
2
3
4
5泛型:
<T1, T2, ...>
interface Pair<T, U> { item1: T; item2: U; } let pairToArr = (p: Pair<string, number>) => { return [p.item1, p.item2]; };
1
2
3
4
5
6
7可选参数:
?
const fn = (a?) => a;
1可选链:
?
const x = foo?.bar?.baz();
1非空断言:
!
const x = foo!.bar!;
1空值合并:
??
const x = foo ?? bar();
1函数重载:(函数声明和函数实体)
function simpleAdd(a: number): (b: number) => number; function simpleAdd(a: number, b: number): number; function simpleAdd(a, b?) { if (b === undefined) return (b) => a + b; return a + b; }
1
2
3
4
5
6类:
public
、private
、protected
、static
class Greet { public prop1; private prop2; protected prop3; constructor(public a1, private a2, protected a3) {} static p4; } new Greet().prop1; Great.p4;
1
2
3
4
5
6
7
8
9装饰器(Decorator):
@something
class Greeter { constructor(private greeting: string) {} @validate greet(@required name: string) { return 'Hello ' + name + ', ' + this.greeting; } }
1
2
3
4
5
6
7
8
JS 不支持的/其他的
元组(Tuple):Python 返回多个值 ↓
def times_ten(a, b): a = a * 10 b = b * 10 return a, b new_a, new_b = times_ten(5, 6)
1
2
3
4
5
6模式匹配:Haskell 多个函数体 ↓
sum :: [Int] -> Int sum [] = 0 sum (x:xs) = x + sum xs
1
2
3管道(Pipeline):F# 用
|>
链接多个函数 ↓let sumOfSquare n = [1..n] |> List.map square |> List.sum
1
2
3
4运算符重载:C++ 定义运算符行为 ↓
class complex { int a; void operator--() { a = --a } } complex obj; obj++;
1
2
3
4
5
6
7
8
9
# 详情
# ES6+ 基本
for...of
除了普通地支持对象和数组,
还支持迭代器,如生成器函数。
# async 和生成器
async function 用来定义一个返回 AsyncFunction 对象的异步函数。异步函数是指通过事件循环异步执行的函数,它会通过一个隐式的 Promise 返回其结果。如果你在代码中使用了异步函数,就会发现它的语法和结构会更像是标准的同步函数。
async 的一个主要作用是用同步的风格写异步代码。
- function* (opens new window)
- Generator.prototype.next() (opens new window)
- Generator 函数的语法 (opens new window)
function* 这种声明方式(function 关键字后跟一个星号)会定义一个生成器函数 (generator function),它返回一个 Generator 对象。
生成器对象是由一个 generator function 返回的,并且它符合可迭代协议和迭代器协议。
生成器的一个主要作用是方便地生成延迟计算的函数。
- 异步迭代器(iterators)与生成器(generators) (opens new window)
- Async Generator Functions in JavaScript (opens new window)
函数、async、生成器,总共有以下几种形式,
注意:生成器不支持箭头函数的写法。
function() {}
() => {}
async function() {}
async () => {}
function*() {}
async function*() {}
2
3
4
5
6
# 技巧
# 三元运算符
三元(条件运算符 (opens new window))实际上是语言标准语法中的一部分
const msg =
name === 'admin' ? 'name is invalid' : 'name is valid';
2
等效于以下代码:
let msg;
if (name === 'admin') {
msg = 'name is invalid';
} else {
msg = 'name is valid';
}
2
3
4
5
6
# 短路运算
JavaScript: What is short-circuit evaluation? (opens new window)
短路计算 (opens new window) 实际上也是语言标准中的一部分,
由于逻辑表达式的运算顺序是从左到右,可以利用规则进行"短路"计算,
后续的表达式将不会执行。
const result = getAlias() || getNickName() || getUserName();
result && console.log(result);
2
3
等效于以下代码:
let result = getAlias();
if (!result) result = getNickName();
if (!result) result = getUserName();
if (result) console.log(result);
2
3
4
5
# 布尔化的快速写法
实际上是进行了两次 逻辑非 (opens new window) 运算,
true === true;
!true === false;
!!true === true;
2
3
再加上 JS 有 隐式类型转换 (opens new window) 的语言特性,
于是就能得到布尔值(节省了几个字符)。
!!value === Boolean(value);
# 取整的快速写法
实际上是进行了两次按位非运算,
位运算 (opens new window) 会先将数字从浮点数转换为整数,
所以能够实现取整的效果。
需要注意的是:从效果上,结果是趋向于向 0 取整。
~1 === -2;
~-2 === 1;
~~2.7 === 2;
~~-2.7 === -2;
Math.floor(2.7) === 2;
Math.ceil(-2.7) === 2;
2
3
4
5
6
7
8
# 陷阱
# 连续赋值
JS 中的等号是 赋值运算符 (opens new window),
而变量声明需要根据 var
、let
、const
关键字进行,
如果未显式地声明,则变量会成为 隐式全局变量 (opens new window) 或报错。
// prettier-ignore
let a = b = c;
2
等价于:
let a;
b = c;
a = b;
2
3
b
会成为全局变量。
更安全的写法是提前声明好所需变量:
let a, b;
a = b = c;
2
或拆分成多个语句:
let b = c;
let a = b;
2
另一方面,连续赋值的写法由于可能会造成理解偏差,
会被格式化工具加上括号。
let a = (b = c);
# 连续比较
在 Python 中,支持 连续比较 (opens new window),如:
x > y > z
等价于 x > y and y > z
JS 中没有这样的特性,
每个操作符和两侧表达式运算后,表达式的结果参与剩余运算。
最终结果可能是反直觉的,和直观的字面意思完全不同。
而同 优先级 (opens new window) 表达式的计算顺序根据 关联性 (opens new window),一般是左到右,赋值是右到左。
所以在 JS 中,3 > 2 > 1
就是 (3 > 2) > 1
,等于 false
逐步解析:
3 > 2 > 1;
true > 1;
Number(true) > 1;
1 > 1;
false;
2
3
4
5
对于优先级,更好的做法是借助格式化工具添加 括号 (opens new window),
或根据业务逻辑手动添加。
// prettier-ignore
a === b === c;
(a === b) === c;
2
3
4
或使用和编写 TypeScript,防止 JS 中的隐式类型转换行为。
let a: number;
let b: string;
let c: string;
a > b > c;
// * 将报错
// Operator '>' cannot be applied to types 'number' and 'string'.
// Operator '>' cannot be applied to types 'boolean' and 'string'.
2
3
4
5
6
7
8
9
# TypeScript
# TS 基本
TS 部分,按作用可大致分为类型系统相关,和业务代码语法糖。
类型、类、泛型等基本 TS 概念,在各种文档中直接能查到。
可选参数 (opens new window)(函数参数上的 ?
)用于支持类型系统。
可选链 ?
、非空断言 !
、空值合并 ??
,在 TS 3.7 更新 (opens new window) 以后支持,
主要作用在于简化业务代码语法,属于语法糖,
实际上它们也出现在 JS 目前的草案中。
# 可选链
确保字段为空时提前中断而不会报错。
const x = foo?.bar?.baz();
等价于:
const x =
foo === null || foo === undefined
? undefined
: foo.bar === null || foo.bar === undefined
? undefined
: foo.bar.baz();
2
3
4
5
6
# 非空断言
非空断言操作符 (opens new window) 需要开启 strictNullChecks
编译选项 (opens new window)
以下例子中,如果 entity
为空,则在 validOrThrow
就抛出错误。
如果能够执行到 entity.name
一行,说明 entity
一定不为空。
const validOrThrow = (entity) => {
if (!entity) throw 'your value is empty';
};
const process = (entity?: Entity) => {
validOrThrow(entity);
const name = entity.name;
console.log(name);
};
2
3
4
5
6
7
8
9
由于 TS 只是静态类型检查,不会进行逻辑检测,
entity
是可选参数,TS 会提示 Object is possibly 'undefined'.
所以在类似这样的边界情况下,需要告诉 TS 检查器 entity
肯定不为空,
也就是修改成 entity!.name
,其中 !
就是非空断言操作符。
const name = entity.name;
const name = entity!.name;
Cleaner TypeScript With the Non-Null Assertion Operator (opens new window)
在 React 中有个更常见的使用场景:
const Comp = () => {
const ref = React.useRef<HTMLDivElement>(null);
React.useLayoutEffect(() => {
const h = ref.current!.offsetHeight;
// ...
});
return <div ref={ref}></div>;
};
2
3
4
5
6
7
8
9
10
ref.current
的状态在渲染后才会确定,和初始值不一样,
使得 ref.current
的类型(HTMLDivElement | null
)可能为空。
实际情况是,在 useLayoutEffect
中,
ref.current
肯定有确定的类型 HTMLDivElement
,
我们可以使用非空断言解决这个问题,
解决类型识别报错,同时重新拥有 API 智能提示。
# 空值合并
在值为空(undefined
或 null
)时提供默认值,
??
类似 ||
,但是不会处理数字 0、空字符串等隐式假值。
const x = foo ?? bar();
等价于:
const x = foo !== null && foo !== undefined ? foo : bar();
# 泛型
- Utility Types - TypeScript (opens new window)
- 泛型 - FAQ - 深入理解 TypeScript (opens new window)
- Infer - Tips - 深入理解 TypeScript (opens new window)
对于泛型的大致理解:泛型是用于处理类型的“函数”。
函数,对于不同的输入,运算出得不同的结果。
泛型,对于不同类型,运算出得相应的另一种类型。
一个简单的代码片段,通过泛型和推断,以下代码拥有正确的类型识别:
type MapEveryToPromise<T extends object> = {
[K in keyof T]: T[K] extends infer P ? Promise<P> : never;
};
const obj1 = {
key1: 1,
key2: 'hello',
};
const obj2: MapEveryToPromise<typeof obj1> = {
key1: Promise.resolve(1),
key2: Promise.resolve('hello'),
};
2
3
4
5
6
7
8
9
10
11
12
13
泛型常见于各种工具库的源码中(如 Redux、Ramda),
部分工具函数支持用户传入任意类型,得到的结果需要有正确的类型,
那么工具函数对应的类型声明就需要使用泛型完成。
# 重载
TypeScript 中的重载(Overload),是函数声明的重载,
和面向对象中的重载有所差异。
TS 中的重载是指多个同名的类型声明,具体判断还是要手动实现,
手动在唯一的函数本体中进行传参的判断。
TS 中的重载:
function simpleAdd(a: number): (b: number) => number;
function simpleAdd(a: number, b: number): number;
function simpleAdd(a, b?) {
if (b === undefined) return (b) => a + b;
return a + b;
}
2
3
4
5
6
而如 Java、C# 中的重载,是直接写多个同名函数本体。
Java 中的重载:
class Dog {
public void bark() {
System.out.println('woof')
}
public void bark(int num) {
for (int i = 0; i < num; i++)
System.out.println('woof')
}
}
2
3
4
5
6
7
8
9
TS 重载的作用主要用于类型识别上。
比如在各种工具库的源码中(如 Redux、Ramda),
对于不同的传参情况,会得到不同的对应类型提示,
这就需要借助重载。
# 装饰器
- 装饰器 - ECMAScript 6 入门 (opens new window)
- Decorators - TypeScript-Handbook (opens new window)
- Decorators - TypeScript (opens new window)
装饰器的编写和使用可以类比高阶函数,
都是对一个实体的包装、“装饰”,
差别在于:
- 装饰器只能用于类、类的方法和属性
- 高阶函数适用于函数
一些有用的装饰器(顾名思义):
- @enumerable
- @configurable
- @readonly
- @required
- @autobind
另外还有类似 lodash-decorators (opens new window) 这样的装饰器工具集合。
对于类来说,装饰器可以有效提升业务代码的可读性和信噪比:
class Tool {
@log
@Memoize
load(file, encode) {
//
}
}
2
3
4
5
6
7
如果改成高阶函数的形式,代码就显得非常别扭:
class Tool {
load = log(
Memoize(function load(file, encode) {
//
}),
);
}
2
3
4
5
6
7
然而对于非面向对象的开发模式来说(比如函数式和组合编程),
不存在类或 this 的使用,高阶函数也是正常的选择。
const load = (file, encode) => {};
const sysLoad = log(Memoize(load));
2
3
# 其他
# 元组
Python 中的函数返回,用逗号隔开就表示返回了多个值:
return a, b
而 JS 中的逗号是一个 运算符 (opens new window),
整行代码作为表达式的结果是最后一个逗号右边的值。
所以上面的代码在 JS 中相当于:
a;
return b;
2
不过 JS 中有 解构赋值 (opens new window) 的概念。
我们可以退而求其次,返回数组就好了,数组也是有序值列,
多个值封装成一个数组,返回的就是一个东西了。
const times_ten = (a, b) => [a * 10, b * 10];
const [x, y] = times_ten(1, 2);
2
实际上返回数组的设计在 React Hooks 中大量出现,如:
const [state, setstate] = useState(initialState);
而 TS 中 元组类型 (opens new window) 的概念也是类似于数组。
# 模式匹配
模式匹配的概念和重载有关联,
JS 在语法层面不支持模式匹配,
需要的话,可以按照不同的设计模式手动实现。
# 管道运算
Unix 和一些编程语言中有管道的语法和概念,
实际上 JS 中目前也有 管道操作符 (opens new window) 的草案:
const double = (n) => n * 2;
const increment = (n) => n + 1;
// 没有用管道操作符
double(increment(double(5))); // 22
// 用上管道操作符之后
5 |> double |> increment |> double; // 22
2
3
4
5
6
7
8
可以借助 @babel/plugin-proposal-pipeline-operator (opens new window) 提前使用。
或者在不引入新语法的前提下,使用诸如 Ramda 或 Lodash 提供的管道函数。
// prettier-ignore
const fn = R.pipe(
double,
increment,
double,
)
fn(5); // => 22
2
3
4
5
6
7
// prettier-ignore
const fn = _.flow([
double,
increment,
double,
])
fn(5); // => 22
2
3
4
5
6
7
# 运算符重载
JS 不支持运算符重载
JavaScript: Can (a==1 && a==2 && a==3) ever evaluate to true? (opens new window)
利用 toString
、valueOf
和隐式类型转换,实现的所谓“运算符重载”。
属于奇技淫巧和冷知识,在业务中不应提倡使用。