DanLevy.net

测验:解构的乐趣

你是解构大师吗?

还是你的 破坏交响曲?

这套测验会检验你对 JavaScript 解构的掌握程度:从“基础”对象语法到嵌套解构以及默认值。还有 TypeScript 与内联类型的加分题!

直接进入热身——证明你的解构功力吧! 👇

这段代码会输出什么?

const person = {
name: 'Dan Levy',
location: 'Cape Town',
};
const { name, age } = person;
console.log(`Name: ${name}, Age: ${age}`);

age 属性在 person 上不存在,所以 age 将是 undefined。绝对不会是 Infinity 😅

结果是:

Name: Dan Levy, Age: undefined

这段代码会做什么?

const person = [ 'Dan Levy', 'Cape Town' ];
const [ name, origin, age ] = person;
console.log(`Name: ${name}, Age: ${age}`);

age 变量不存在于 tuple 数组中,因此它的值为 undefined

这将导致:

Name: Dan Levy, Age: undefined

来点嵌套解构吧?

'use strict';
const person = {
name: { first: 'Dan' },
address: { city: 'Denver' },
};
const {
name: { first },
address: { city },
birth: { place },
} = person;
console.log(
`First: ${first}, City: ${place}`,
);

birth: { place } 属性在 person 上不存在,因此会抛出错误。提供嵌套属性的默认值是一种解决方案。

访问嵌套属性时要小心——错误往往不易发现,而且不同浏览器和平台的错误信息各不相同,调试会更具挑战性。

在现代 Chrome 中:TypeError: Cannot read properties of undefined (reading 'place')

在 Node 中也是 TypeError,因为 JavaScript 在读取 place 之前就尝试从 undefined 解构 place

具体的错误文字会因浏览器和运行时而异。

现在有了一些默认值,这会怎么做?

'use strict';
const person = {
name: { first: 'Dan' },
address: { city: 'Denver' },
};
const {
name: { first = 'Unknown' },
birth: { place = 'Unknown' },
} = person;
console.log(
`Hi ${first} from ${place}`,
);

birth 属性在 person 上不存在,所以整个对象仍然需要一个默认值,而不仅仅是嵌套属性。基本上缺少一个 = {} 的默认值。

这样写的意思是“如果 person.birthundefined,则 placeUnknown”。但 person.birthundefined,于是尝试对 undefined 进行解构,导致错误。

In modern Chrome: `TypeError: Cannot read properties of undefined (reading 'place')`
In Node, this is also a `TypeError` because JavaScript tries to destructure `place` from `undefined`.
Exact wording varies between browsers and runtimes.

这段代码会做什么?

const person = {
name: { first: 'Dan' },
address: { city: 'Denver' },
};
const {
name: { first = 'Unknown' },
birth: { place = 'Unknown' } = {},
} = person;
console.log(
`Hi ${first} from ${place}`,
);

person 上不存在 birth 属性,所以会回退到一个空对象 = {}。这使得可以使用默认值。

现在把它当作函数参数,会发生什么?

'use strict';
function displayUser({
name = "Unknown",
age = -1,
} = { place: "Unknown" }) {
console.log(`Hi ${name} from ${place}`);
}
displayUser({ name: "Dan" });

这个函数会提取 nameage 属性,必要时使用默认值。这里默认对象里的 place 键只是噪音,displayUser() 内并未使用它。

严格模式并不会改变这一点:读取未声明的 place 绑定会抛出 ReferenceError

undefined 值是如何处理的?

'use strict';
function displayPlace({
name = "N/A",
place = "N/A",
age = -1,
} = { place: "Unknown" }) {
console.log(`${place}`);
}
displayPlace({ name: "Dan" });
displayPlace({ name: "Dan", place: undefined });
displayPlace({ name: "Dan", place: "Joburg" });

函数 displayPlace 只会在没有传入对象时使用默认对象。因此,唯一能得到 { place: "Unknown" } 默认值的方式是调用 displayPlace() 而不传参。

另一个值得注意的行为是,如果为 place 传入 undefined,默认值也会被使用,这有点类似于 JSON.stringify 的行为(会忽略 undefined,但会识别 null)。

结果是:

displayPlace() // Unknown
displayPlace({ name: "Dan" }) // N/A
displayPlace({ name: "Dan", place: undefined }) // N/A

和前一个类似……null 是怎么处理的?_

function displayPlace({
name = "N/A",
place = "N/A",
age = -1,
} = { place: "Unknown" }) {
console.log(`${place}`);
}
displayPlace({ name: "Dan", place: null });
displayPlace({ name: "Dan", place: undefined });

在这种情况下,第一次调用时 place 属性被设为 null,第二次调用时为 undefined。只有当整个对象缺失 undefined 时,place 的默认值才会生效。null 会原样传递为 null

TypeScript 在前方

现在在 TypeScript 中…… 这会怎么做?

'use strict';
function displayPlace(
{
name = 'N/A',
place = 'N/A',
}: {
name: string;
place: string;
age: number;
},
) {
console.log(`${place}`);
}
displayPlace({ name: 'Dan', place: null });

TypeScript 报告错误,因为 place 的类型被标记为 string,但调用时传入了 null。调用还省略了必需的 age 属性。

如果忽略类型错误,运行代码会在控制台打印 null

我们来试试重命名/赋值吧……

'use strict';
function displayPlace({
name = 'N/A',
place: location = 'N/A',
}: {
name: string;
place: string;
age?: number;
}) {
console.log(`${location}`);
}
displayPlace({ name: 'Dan', place: 'Denver' });

这将在控制台打印 Denver。函数签名中将 place 属性重命名为 location。在适配第三方数据结构时,这是一种常见模式(在解构时重命名属性)。

找出类型错误:

function greet({
name: {first = "N/A", last = "N/A"},
birth: {place = "N/A"} = {},
age = -1,
}: {
name: {first?: string, last?: string};
birth: {place?: string};
age: number;
}) {
console.log(`Hi ${first} ${last} from ${place}`);
}
greet({ name: {first: 'Dan'} });

错误出现在 greet 函数签名中。传入的对象缺少 agebirth 属性,因此在类型定义里它们应当是可选的。

即使 birth 属性使用了默认值进行解构,类型定义仍要求它必须存在。要在 TypeScript 中将属性标记为可选,需要使用 ? 运算符。

注意 birth?: { place?: string } 并不等同于 birth: { place?: string } | undefined

现在使用 赋值(注意 flp 变量)

'use strict';
function greet(
{
name: {first: f = "N/A", last: l = "N/A"},
birth: {place: p = "N/A"} = {},
age = -1,
}: {
name: {first?: string, last?: string};
birth?: {place?: string};
age?: number;
}
) {
console.log(`Hi ${f} ${l} from ${place}`);
// What will 👆 do?
}
greet({
name: {first: 'Dan', last: 'Levy'},
birth: {place: 'Cape Town'},
});

又一次错误!你开始猜测了,是吧?

读取带有默认值、赋值和类型的多层解构真是困难!

一旦 place 被重新赋值给 p 变量,它就在 console.log 语句的作用域中不再定义。

console.log(`Hi ${f} ${l} from ${place}`); // ❌
// to:
console.log(`Hi ${f} ${l} from ${p}`); // ✅