【利用AI刷面试题】AI:十道不常见的TypeScript面试题

在这里插入图片描述

文章目录

  • 前言😏
  • 以下是关于 TypeScript 的一些偏僻的面试题😝
    • 1. 泛型约束中的 "extends" 关键字有哪些用法和含义?
    • 2. 什么是交叉类型(Intersection Types)?如何在 TypeScript 中定义和使用它们?
    • 3. 请解释 "keyof" 关键字的含义和用法。
    • 4. TypeScript 中的 "never" 类型有什么用途?请给出一个示例。
    • 5. 如何定义一个函数类型的 "this" 参数,以及在函数中使用它?
    • 6. 如果有一个类型是由两个类型 "A" 和 "B" 组成的,并且 "A" 和 "B" 中都有相同的属性名,那么 TypeScript 会怎么处理?
    • 7. 如果有一个变量的类型是 "unknown",那么我们在使用这个变量时需要注意什么?
    • 8. TypeScript 中有哪些用于描述`函数形参`、`返回值`和`异常的装饰器`?
    • 9. `TypeScript` 中在类型声明中使用 "`import`" 语句,这有什么作用?
    • 10. 如果两个模块的接口名称相同,但结构不同,那么如何在 TypeScript 中合并这两个接口?

前言😏

笔者曾经被问道:TypeScript 中 type 和 interface 区别

我们看看AI的答案:

在 TypeScript 中,typeinterface 都可以用来定义类型。它们的主要区别在于:type 可以定义任何类型,而 interface 只能定义对象类型。

具体来说,type 可以定义原始类型、联合类型、元组类型和函数类型等,而 interface 只能定义对象类型、类类型和函数类型。

下面是一些具体的区别:

  1. type 可以定义原始类型、联合类型、元组类型和函数类型,而 interface 只能定义对象类型、类类型和函数类型
type MyType = boolean | number; // 原始类型和联合类型
interface MyInterface {
  x: number;
  y: number;
} // 对象类型

type Point = [number, number]; // 元组类型
interface Shape {
  color: string;
  width: number;
  height: number;
} // 对象类型

type HelloFunction = (name: string) => string; // 函数类型
interface GoodbyeFunction {
  (name: string): string;
} // 函数类型
  1. type 支持 extends 和交叉类型,而 interface 只支持 extends。
type Base = { x: number };
type Derived = Base & { y: number };
type Merged = Base | { z: number };

interface Base {
  x: number;
}
interface Derived extends Base {
  y: number;
}
  1. interface 支持定义合并,即多个同名接口的成员会合并为同一个接口,而 type 不支持。
interface Foo {
  x: number;
}
interface Foo {
  y: string;
}

const obj: Foo = { x: 1, y: "hello" };

总之,typeinterface 都是用于定义类型的关键字,它们的主要区别在于 type 可以定义任何类型,而 interface 只能定义对象类型。

在选择使用哪种关键字时,应根据需要来确定使用范围和功能,以提高代码的可读性和可维护性。

😊以上就是AI的回答啦~

那么,对于没有深入研究且在实际开发中没有使用到 TypeScript 的我,在回答那个问题的时候自然是回答不上的,现在有了AI的加持,可以多学习了,不仅问了下AI上面那个问题的答案,并且让AI列举了十道关于TypeScript那些偏僻的面试题

以下是关于 TypeScript 的一些偏僻的面试题😝

1. 泛型约束中的 “extends” 关键字有哪些用法和含义?

在 TypeScript 中,“extends” 关键字在泛型约束中使用,用来指定泛型类型的类型范围和约束条件,可以用于:

1. 类型的范围约束

在泛型定义中,使用 “extends” 关键字,指定一个类型约束范围,确保泛型所接收的类型必须是一个指定的类型。

例如:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

loggingIdentity('123'); // 输出 3

上面的例子中,“T extends Lengthwise” 说明泛型变量 “T” 必须满足 “Lengthwise” 接口的约束(即具有 length 属性)。

2. 多重约束

可以针对一个泛型变量指定多个约束条件。

例如:

function copyFields<T extends U, U>(target: T, source: U): T {
    for (let id in source) {
        target[id] = (<T>source)[id];
    }
    return target;
}

let x = { a:1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 }); //这个是可以的.
copyFields(x, { e: 10 }); // error,因为 source 上不存在 a 或 b 属性

上面的例子中,“T extends U” 说明泛型变量 “T” 必须满足 “U” 类型的约束,从而确定了泛型变量 “T” 的类型范围。

总之,“extends” 关键字用于定义泛型类型的约束条件,可以指定类型或接口,也可以定义多重约束。在使用泛型的时候,可以通过对泛型变量进行约束,确保所传入的参数符合预期类型。

2. 什么是交叉类型(Intersection Types)?如何在 TypeScript 中定义和使用它们?

交叉类型(Intersection Types)是 TypeScript 中表示多个类型的交集的一种方式。

简单地说,它可以将多个类型合并成一个类型。例如,假设有两个类型 A 和 B:

interface A {
  propA: string;
}

interface B {
  propB: number;
}

我们可以使用交叉类型将这两个类型合并成一个类型,定义一个新的类型 C:

type C = A & B;

交叉类型 C 包含了类型 AB 中的所有属性。因此,变量 c 可以包含 propApropB 两个属性:

const c: C = { propA: "hello", propB: 123 };

交叉类型也可以使用于继承:

class D implements A, B {
  propA = "hello";
  propB = 123;
}

在使用交叉类型时,需要注意以下几点:

  1. 只有需要合并类型的属性之间类型冲突时才需要使用交叉类型,否则使用普通类型即可。
  2. 交叉类型中所有类型的属性都必须满足相同的访问限制(public、private 或 protected)。
  3. Union 类型和 Intersection 类型的区别在于,Union 类型表示多个类型中的任意一个,而交叉类型表示多个类型的交集。

3. 请解释 “keyof” 关键字的含义和用法。

“keyof” 关键字是 TypeScript 中用来获取一个类型(通常是对象类型)所有键(即属性名)的名称的一种方式。

举个例子,假设有一个对象类型:

type Person = {
  name: string;
  age: number;
  gender: "male" | "female";
};

我们可以使用 “keyof” 关键字获取这个对象类型的所有键的名称:

type PersonKeys = keyof Person;
// 等价于 type PersonKeys = "name" | "age" | "gender"

这意味着我们可以使用 PersonKeys 来定义一个函数的参数类型:

function getProp(obj: Person, key: keyof Person): any {
  return obj[key];
}

在上面的例子中,参数 key 的类型指定为 “keyof Person”,即表示参数 key 必须为 Person 中定义过的属性名,而不是其他任何字符串。

此外,“keyof” 关键字还可以用于泛型类型或条件类型:

type PersonPropTypes<T> = {
  [K in keyof T]: T[K] extends string ? K : never;
};

在上面的例子中,我们使用了泛型 “T” 和 “keyof T”。“keyof T” 表示泛型 T 中所有属性名的联合类型,我们可以通过它来定义一个新的对象类型,该对象类型只包含泛型 T 中值为字符串类型的属性名。

总之,“keyof” 关键字是一种非常有用的 TypeScript 特性,它常用于为对象类型中的属性命名并且在编写通用代码时提供了更好的类型检查。

4. TypeScript 中的 “never” 类型有什么用途?请给出一个示例。

在 TypeScript 中,never 类型表示一个永远不会发生的值的类型。在实际应用中,never 类型常常用在联合类型或泛型中:

1. 当函数的返回类型为 never 时,表示该函数永远不会正常返回。

function throwError(message: string): never {
  throw new Error(message);
}

2. 在泛型中使用 never 类型可以帮助我们确保某些类型不会被传递进来,进而让编译器帮我们避免一些可能出现的错误。

type NonNullable<T> = T extends null | undefined ? never : T;

type MyRequired<T> = {
  [P in keyof T]-?: NonNullable<T[P]>;
};

上面的代码定义了一个泛型 NonNullable 和一个接口 MyRequired。其中,NonNullable 可以将 nullundefined 类型转化为 never 类型,而 MyRequired 可以将每个属性变为必需的,并且确保不会出现 nullundefined

总的来说,never 类型的主要用途是辅助开发人员进行类型检查和编写更加安全的代码。

5. 如何定义一个函数类型的 “this” 参数,以及在函数中使用它?

TypeScript 中可以使用 this 参数来显式地指定函数内部的 this 指向。在函数类型上,我们可以使用 this 参数来描述该函数期望的上下文对象类型。

具体的用法如下:

// 定义一个接口来描述需要使用 this 参数的函数类型
interface MyFunc {
  (this: Window, event: Event): void;
}

// 定义一个对象,在该对象上调用函数时需要使用 this 参数来指定上下文对象
const myObj = {
  handleClick: function(this: Window, event: Event) {
    console.log("event: ", event);
    console.log("this: ", this);
  }
};

// 在对象上调用函数时需要使用 call 或 apply 方法来指定上下文对象
myObj.handleClick.call(window, new Event("click"));

上面的代码定义了一个函数类型 MyFunc,它期望 this 指向 Window 对象。同时,我们也定义了一个对象 myObj,并在其上面定义了一个函数 handleClick,需要使用 this 参数来指定上下文对象。最后,在调用 myObj.handleClick 时,我们使用 call 方法来显式地指定 this 指向 window 对象。

需要注意的是,使用 this 参数需要显式地指定上下文对象的类型,这样可以在编译期间发现一些潜在的问题,让代码更加健壮。

6. 如果有一个类型是由两个类型 “A” 和 “B” 组成的,并且 “A” 和 “B” 中都有相同的属性名,那么 TypeScript 会怎么处理?

如果一个类型是由两个类型 “A” 和 “B” 组成的,并且 “A” 和 “B” 中都有相同的属性名,则 TypeScript 会合并这两个类型,并且将属性的类型合并为联合类型。

具体来说,TypeScript 会遵循以下规则:

  1. 对于非函数成员,如果两个类型中成员的类型相同,则最终类型中该成员的类型为这个相同的类型;如果两个类型中成员的类型不同,则最终类型中该成员的类型为两个成员的类型的联合类型。

  2. 对于函数成员,如果两个类型中成员的类型都为函数,则最终类型中该成员的类型为函数的重载列表。如果两个类型中函数成员的重载列表性质不同,则会出现编译时错误。

下面是一个示例,它展示了当类型 “A” 和类型 “B” 中都有一个属性 “name” 时,TypeScript 是如何合并它们的:

type A = {
  id: number;
  name: string | null;
};

type B = {
  name: string;
  age: number;
};

type C = A & B;

const c: C = {
  id: 1,
  name: "Tom",
  age: 18
};

在上面的代码中,我们定义了两个类型 “A” 和 “B”,它们都有一个属性 “name”。然后,我们定义了一个类型 “C”,使用交叉类型将类型 “A” 和 “B” 合并在一起。最后,我们声明了一个变量 c,它的类型为 “C”,并且给它赋值了一个包含 “id”、“name” 和 “age” 属性的对象。

在上面的代码中,属性 “name” 的类型为 string | nullstring,TypeScript 会将它们的类型合并为 string | null 类型。因此,类型 “C” 中属性 “name” 的类型为 string | null

总之,当一个类型由两个类型合并而成时,TypeScript 遵循一定的规则来进行合并,以便编译器能够正确地推断类型,从而提高代码的可靠性。

7. 如果有一个变量的类型是 “unknown”,那么我们在使用这个变量时需要注意什么?

8. TypeScript 中有哪些用于描述函数形参返回值异常的装饰器

如果一个变量的类型是 unknown,那么在使用这个变量时需要注意它的类型是未知的,因此我们需要进行类型检查或类型断言来避免类型错误。

具体来说,我们在使用 unknown 类型的变量时,需要遵循以下规则:

  1. unknown 类型变量不能直接赋值给其他类型的变量,除了 any 类型和 unknown 类型本身之外。这是因为 any 类型可以隐式转换为任何类型,而 unknown 类型的变量可以接受任何类型的值,但我们必须显式地对其进行类型检查或类型断言才能使用它。

  2. 在对 unknown 类型的变量进行类型检查时,我们需要使用类型保护来确定其实际类型、属性和方法的可用性,以免在运行时出现类型错误。

下面是一个示例,它展示了如何使用类型检查或类型断言来使用 unknown 类型的变量:

function printLength(str: unknown) {
  if (typeof str === "string") {
    console.log(str.length); // OK
  } else {
    console.log("The type of str is unknown.");
  }
}

function getValue(obj: { [key: string]: unknown }, key: string) {
  if (typeof obj[key] === "string") {
    console.log(obj[key].toUpperCase()); // OK
  } else {
    console.log("The type of obj[key] is unknown.");
  }
}

let x: unknown = "hello";
let y: string;
let z: number;

// y = x; // 报错,无法将类型“unknown”分配给类型“string”
y = typeof x === "string" ? x : ""; // OK,使用类型断言
// z = x; // 报错,无法将类型“unknown”分配给类型“number”
z = typeof x === "number" ? x : 0; // OK,使用类型断言

在上面的代码中,我们定义了一个函数 printLength,它接受一个 unknown 类型的参数,并在函数内部使用类型检查来确定参数的实际类型,从而避免类型错误。另外,我们还定义了一个函数 getValue,它接受一个对象和一个键名,用于获取该键对应的值,并在函数内部对该值进行类型检查。最后,我们声明了一个 unknown 类型的变量 x,并使用类型检查和类型断言来使用该变量。

总之,在使用 unknown 类型变量时,我们需要特别注意其类型是未知的,需要进行类型检查或类型断言才能使用,以避免类型错误。

9. TypeScript 中在类型声明中使用 “import” 语句,这有什么作用?

在 TypeScript 中,有许多用于描述函数形参、返回值和异常的装饰器,具体如下:

1. @Paramtypes(...types: Function[]):用于描述函数的形参类型。

function MyFunc(@Paramtypes(Number, String) arg1: any) {
  // ...
}

2. @ReturnType(type: Function):用于描述函数的返回值类型。

@ReturnType(Number)
function MyFunc() {
  // ...
}

3. @Throws(...types: any[]):用于描述函数可能抛出的异常类型。

function MyFunc() {
  @Throws(Error)
  function innerFunc() {
    // ...
  }
  // ...
}

4. @Asserts(expression: string):用于描述函数的运行结果是否满足某个条件。

function MyFunc() {
  @Asserts("result.length > 0")
  function innerFunc(): string[] {
    const result = ["a", "b", "c"];
    return result;
  }
  // ...
}

这些装饰器都是 TypeScript 中的实验性特性,需要在 tsconfig.json 文件中启用 “experimentalDecorators” 选项才能使用。

使用这些装饰器可以帮助我们更好地描述函数的类型信息,从而提高代码的可靠性和维护性。

10. 如果两个模块的接口名称相同,但结构不同,那么如何在 TypeScript 中合并这两个接口?

在 TypeScript 中,可以使用模块合并来合并两个模块中的接口,具体的方法如下:

假设我们有两个模块 Module1Module2,它们中都定义了名称为 SampleInterface 的接口,但是它们的结构不同。我们可以在同一文件中定义一个新的模块 ModuleCombined,并将模块 Module1Module2 合并到这个模块中,然后在模块 ModuleCombined 中定义一个同名的接口,这个接口的结构就是将模块 Module1Module2 中的同名接口的成员合并起来的结果。

下面是示例代码:

// module1.ts
export interface SampleInterface {
  id: number;
  name: string;
}

// module2.ts
export interface SampleInterface {
  age: number;
  address: string;
}

// moduleCombined.ts
import { SampleInterface as Interface1 } from './module1';
import { SampleInterface as Interface2 } from './module2';

interface SampleInterface extends Interface1, Interface2 {}

export { SampleInterface };

在上面的代码中,我们分别定义了两个模块 Module1Module2,它们中都定义了名称为 SampleInterface 的接口,但接口的结构不同。然后我们创建了一个新的模块 ModuleCombined,并将模块 Module1Module2 合并到这个模块中,最后在模块 ModuleCombined 中定义了同名的接口 SampleInterface,这个接口继承了模块 Module1Module2 中的同名接口,从而将两个接口中的成员合并起来。

这样,我们就成功地合并了模块 Module1Module2 中的接口,可以通过 ModuleCombined.SampleInterface 来使用这个接口。注意,我们通过 import { SampleInterface as Interface1 } from './module1';import { SampleInterface as Interface2 } from './module2'; 来避免名称冲突。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/20537.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第N2周:中文文本分类-Pytorch实现

目录 一、前言二、准备工作三、数据预处理1.加载数据2.构建词典3.生成数据批次和迭代器 三、模型构建1. 搭建模型2. 初始化模型3. 定义训练与评估函数 四、训练模型1. 拆分数据集并运行模型 一、前言 &#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客 …

不愧是腾讯 ,问的贼细

腾讯软件测试岗位的面试流程可能会因个人经验和公司而异&#xff0c;但通常情况下&#xff0c;腾讯软件测试的面试分为初试、二面、三面和四面。以下是每一轮面试可能涉及到的问题&#xff1a; 初试&#xff1a; 请介绍一下自己&#xff0c;以及为什么想要加入腾讯软件测试团…

时序预测 | MATLAB实现BO-CNN-LSTM贝叶斯优化卷积神经网络-长短期记忆网络时间序列预测

时序预测 | MATLAB实现BO-CNN-LSTM贝叶斯优化卷积神经网络-长短期记忆网络时间序列预测 目录 时序预测 | MATLAB实现BO-CNN-LSTM贝叶斯优化卷积神经网络-长短期记忆网络时间序列预测效果一览基本介绍模型搭建程序设计参考资料 效果一览 基本介绍 MATLAB实现BO-CNN-LSTM贝叶斯优…

操作系统实验二 进程(线程)同步

前言 实验二相比实验一难度有所提升&#xff0c;首先得先掌握好相应的理论知识&#xff08;读者-写者问题和消费者-生产者问题&#xff09;&#xff0c;才能在实验中得心应手。任务二的代码编写可以借鉴源码&#xff0c;所以我们要先读懂源码。 1.实验目的 掌握Linux环境下&a…

浅谈Redis

一、Redis的简介 1.开源免费的缓存中间件,性能高,读可达110000次/s,写可达81000次/s。 2.redis的单线程讨论&#xff1a; V4.0之前&#xff1a;是单线程的&#xff0c;所有任务处理都在一个线程内完成. V4.0&#xff1a;引入多线程&#xff0c;异步线程用于处理一些耗…

Linux三种网络模式 | 仅主机、桥接、NAT

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; Linux三种网络模式 仅主机模式&#xff1a;虚拟机只能访问物理机&#xff0c;不能上网 桥接模式&#xff1a;虚拟机和物理机连接同一网络&#xff0c;虚拟机和物理机…

Docker的四种网络模式

1.Host 模式 通常来讲&#xff0c;启动新的Docker容器&#xff0c;都会分配独立的Network Namespace隔离子系统&#xff0c;如果在运行是指定为host模式&#xff0c;那么Docker容器将不会获得一个独立的Network Namespace&#xff0c;而是和宿主机共用一个Network Namespace子…

山东专升本计算机第六章-数据库技术

数据库技术 SQL数据库与NOSQL数据库的区别 数据库管理系统 考点 6 数据库管理系统的组成和功能 组成 • 模式翻译 • 应用程序的翻译 • 交互式查询 • 数据的组织和存取 • 事务运行管理 • 数据库的维护 功能 • 数据定义功能 • 数据存取功能 • 数据库运行管理…

【图像基础知识】常见图像格式

文章目录 1 简介2 RGB3 BGR4 YUV4.1 YUV常见格式4.2 YUV420详解4.3 NV12 5 Gray6 图像格式之间的转换7 参考链接 原文来自于地平线开发者社区&#xff0c;未来会持续发布深度学习、板端部署的相关优质文章与视频&#xff0c;如果文章对您有帮助&#xff0c;麻烦给点个赞&#x…

Sentinel : 服务容错(降级熔断、流量整形)

什么是服务雪崩&#xff1f; 服务雪崩效应是一种因“服务提供者的不可用”&#xff08;原因&#xff09;导致“服务调用者不可用”&#xff08;结果&#xff09;&#xff0c;并将不可用逐渐放大的现象。 我来用一个模拟场景带你感受一下服务雪崩的厉害之处。假设我有一个微服…

20 散列表的查找

散列表的查找 简介&#xff1a;散列表&#xff08;也成哈希表&#xff09;是一种高效的数据结构&#xff0c;他可以在平均复杂度为O(1)的情况下实现查找、插入和删除操作。 哈希表的基本思想是根据关键字的值来计算其应存储的位置。这个计算过程就是通过哈希函数来实现的。 根…

深度学习-第T5周——运动鞋品牌识别

深度学习-第T5周——运动鞋品牌识别 深度学习-第T5周——运动鞋品牌识别一、前言二、我的环境三、前期工作1、导入数据集2、查看图片数目3、查看数据 四、数据预处理1、 加载数据1、设置图片格式2、划分训练集3、划分验证集4、查看标签 2、数据可视化3、检查数据4、配置数据集 …

西瓜书读书笔记整理(三)—— 第二章 模型评估与选择

第二章 模型评估与选择 第 2 章 模型评估与选择2.1 经验误差与过拟合1. 错误率 / 精度 / 误差2. 训练误差 / 经验误差 / 泛化误差3. 过拟合 / 欠拟合4. 学习能力5. 模型选择 2.2 评估方法1. 评估方法概述2. 留出法3. 交叉验证法4. 自助法5. 调参 / 最终模型 2.3 性能度量1. 回归…

Ada语言学习(1)Basic Knowledge

文章目录 说在前头命名注释数字变量变量类型signed integersEnumerationsFloating Points 类型重用&#xff08;继承&#xff09;类型转换 运算符属性&#xff08;Attributes&#xff09;练习 说在前头 本系列教程将会通过提问的方式来完成整个学习过程&#xff0c;因为当你能…

86盒IP对讲一键报警器

86盒IP对讲一键报警器 86盒IP对讲一键报警器&#xff1a;革命性保障生命安全的利器&#xff01; 随着科技的飞速发展&#xff0c;我们的生活变得越来越方便和智能化。而86盒IP对讲一键报警器更是在这种背景下应运而生。这款产品不仅无缝对接各种手机APP&#xff0c;也可以在智…

RabbitMQ --- 惰性队列、MQ集群

一、惰性队列 1.1、消息堆积问题 当生产者发送消息的速度超过了消费者处理消息的速度&#xff0c;就会导致队列中的消息堆积&#xff0c;直到队列存储消息达到上限。之后发送的消息就会成为死信&#xff0c;可能会被丢弃&#xff0c;这就是消息堆积问题。 解决消息堆积有三种…

互联网大厂测开面试记,二面被按地上血虐,所幸Offer已到手

在互联网做了几年之后&#xff0c;去大厂“镀镀金”是大部分人的首选。大厂不仅待遇高、福利好&#xff0c;更重要的是&#xff0c;它是对你专业能力的背书&#xff0c;大厂工作背景多少会给你的简历增加几分竞争力。 如何备战面试的&#xff1f; 第一步&#xff1a;准备简历 …

vue3回到上一个路由页面

学习链接 Vue Router获取当前页面由哪个路由跳转 在Vue3的setup中如何使用this beforeRouteEnter 在这个路由方法中不能访问到组件实例this&#xff0c;但是可以使用next里面的vm访问到组件实例&#xff0c;并通过vm.$data获取组件实例上的data数据getCurrentInstance 是vue3提…

51单片机也可以移植RTOS

说起RTOS移植&#xff0c;我们首先会想到32位单片机。 那么51单片机可以移植RTOS吗&#xff1f; 我的答案是&#xff0c;只要资源够用&#xff08;ROM空间、RAM空间&#xff09;&#xff0c;可以移植。 前提是你对RTOS的实现原理非常清楚&#xff0c;并且可以自己完成移植工作…

数据可视化大屏的页面布局以及自适应

在做数据可视化大屏之前&#xff0c;我们需要考虑到页面的布局问题以及页面缩放自适应问题&#xff0c;下面分别就这两个方面讲解。 页面布局 类似这种页面区块的明显划分&#xff0c;常用的布局方式有两种&#xff1a; 1、flex布局 2、grid布局 grid布局 grid布局可以按区块…