
1. 先把修饰符和抽象类的关系理顺类设计其实是在定约束接触 TypeScript 一段时间后你会发现类的语法本身并不难class、constructor、extends、super翻来覆去就那几样。真正让代码变复杂的是约束。一个类里哪些属性对外可见、哪些内部私有、哪些允许子类改写、哪些必须由子类实现——这些约束一旦定错后面改起来就是牵一发动全身。抽象类和修饰符恰好就是这套约束的两种主要形态。用生活化的方式理解普通类是一个完整产品拿来就能用抽象类是一个半成品模具必须经过二次加工才能用接口则是一份验收清单只管你长什么样不管你怎么实现。修饰符更像是产品的封装等级全公开、仅内部、半公开。它们不是互相替代的关系而是层层递进的约束体系实际项目中几乎总是同时出现。为什么我要把这两样东西放在一起讲因为一个设计得好的基类通常同时具备三个特征用 protected 暴露必要的内部接口给子类、用 private 藏住无关实现、用 abstract 强制子类补齐缺失的行为。单独看任何一个知识点都简单组合起来才是真正的难点。这也是面试官最爱从修饰符一路追问到抽象类、再追问到接口的原因。这篇文章就按这个节奏来先修饰符再抽象类再接口最后落到真实项目里的一个 playwright 页面对象设计案例上。2. 访问修饰符public、private、protected 的边界感2.1 三兄弟的分工以及“编译期检查”这个关键背景先看一段最常见的演示代码class User { public name: string; private password: string; protected email: string; readonly id: number; static version: string v1.0; constructor(name: string, password: string, email: string, id: number) { this.name name; this.password password; this.email email; this.id id; } public checkPassword(input: string): boolean { return this.password input; } } const user new User(张三, pwd123, zhangexample.com, 1); user.name; // 可以public 默认对外开放 user.password; // 报错属性 password 是私有属性 user.email; // 报错属性 email 受保护只能在类及其子类中访问 user.id; // 可以读但不可以赋值 User.version; // 可以static 挂到类本身而不是实例 user.version; // 报错实例上访问不到 static 属性三兄弟的分工一句话就能概括public 谁都能碰private 只有本类能碰protected 本类加上所有子类能碰。但这里有个非常容易被忽略的背景——TypeScript 的访问修饰符只在编译阶段生效运行时根本没有这层限制。你把 private 的属性打印出来照样能看到它的值。这意味着什么意味着这些修饰符是写给开发者和编译器看的“约定”而不是真正意义上的安全机制。你用它来防止团队成员不小心乱改内部状态防止你三个月后自己忘记某个字段的用途。真要做运行时级的硬隔离得用 JavaScript 原生支持的#私有字段比如#password。TypeScript 从 3.8 版本开始也支持这种写法而且它是真正的强隔离delete都删不掉。我建议的实践是如果只是团队协作层面的封装用 private/protected 就够了可读性好错误提示清晰如果是要把核心机密和外部彻底隔开比如 SDK 内部的 token、密钥那就用#私有字段。两条路线可以混用不要因为它们长得像就觉得是一种东西。2.2 readonly 和 static两个很容易被讲漏的修饰符访问修饰符之外还有两个修饰符的出场率同样很高但很多人对它们的理解停留在“见过、用过、没想过”。readonly 的中文意思是“只读”它约束的是赋值时机。一个 readonly 属性只能在声明时、或者在构造函数里被赋值其他地方一律不能写。注意“构造函数里”这个表述要精确不是说子类构造函数里也能随便赋值而是说它只能在“自己声明所在的类”的构造函数里被赋值。一旦类实例化完成不管是在外部还是在子类里都不能再给它重新赋值了。这个特性特别适合那些业务上不允许变更的字段比如用户 id、订单号、创建时间。static 则是把属性或方法挂到类本身而不是挂在实例上。它最常见的用途是定义常量、工具方法、单例入口。比如一个配置类Config.BASE_URL、Config.get(timeout)。这里有个细节值得注意static 成员也可以和访问修饰符组合比如private static表示“类内部共享的私有工具”protected static表示“子类也能访问的公共基类常量”。这种组合在抽象类的设计里非常有用后面讲模板方法模式的时候你会看到它的实际价值。还有一点static 不具有多态性。子类可以定义同名的 static 成员但它们各自独立不存在“覆盖”的语义。如果你想要“不同子类返回不同常量”的效果更好的选择是把方法定义为实例方法或者用抽象属性让子类强制覆盖而不是依赖 static 的覆盖。2.3 参数属性一行代码省掉五遍重复声明接下来聊一个能立刻提升代码质感的语法参数属性Parameter Properties。它解决的是 TypeScript 类里一个著名的痛点——属性声明的重复劳动。传统写法是这样的class User { public name: string; private password: string; constructor(name: string, password: string) { this.name name; this.password password; } }一个属性要在声明处写一遍类型在参数处再写一遍类型还要在构造函数里手动赋值一次。三处改动任何一处漏了都会出问题。参数属性的写法直接把这些合并到构造函数参数里class User { constructor( public name: string, private password: string, protected email: string, readonly id: number, ) {} }只要在构造函数参数前面加上修饰符TypeScript 就会自动帮你完成“声明属性 类型标注 赋值”三件事。编译后的代码等价于前面的传统写法但源码简洁得多也从根本上杜绝了“忘了在构造函数里赋值”这类低级错误。不过要提醒一句不要为了炫耀语法把所有参数都改成参数属性。只有那些确实需要暴露为类属性的参数才值得这么写。如果某个参数只是构造时用一次、不需要存下来那就老老实实当普通参数用。另外参数属性和strictPropertyInitialization严格模式配合良好因为 TS 能明确看到它已经被赋值了不会报“属性没有初始化”的错。3. 抽象类真正的“半成品基类”3.1 抽象类为什么不能 new抽象方法又是什么访问修饰符管的是“谁能访问什么”抽象类管的则是“哪些行为必须由子类来确定”。抽象类用abstract关键字声明它可以包含普通类的一切属性、构造函数、具体方法。特殊之处在于它还可以包含抽象成员——只有方法签名和属性类型声明没有具体实现。任何包含抽象成员的类都必须在前面加上abstract而且这样的类不能直接new。为什么不能new因为抽象类里还有没实现的成员。如果允许实例化那调用到某个抽象方法时就只有一个空壳谁也不知道该执行什么逻辑。TypeScript 选择在编译期就拦死这个问题你的类里但凡有一个抽象成员实例化就直接报错。来看一个最经典的动物示例abstract class Animal { protected name: string; constructor(name: string) { this.name name; } // 抽象方法只有签名没有实现 abstract makeSound(): void; // 具体方法所有子类共享的实现 move(distance: number): void { console.log(${this.name} moved ${distance}m); } } class Dog extends Animal { constructor(name: string) { super(name); } // 子类必须实现抽象方法否则子类也会变成抽象类 makeSound(): void { console.log(Woof!); } } const dog new Dog(旺财); dog.makeSound(); // Woof! dog.move(10); // 旺财 moved 10m // const animal new Animal(x); // 报错无法创建抽象类的实例这个例子里makeSound被抽象出来了因为不同动物的叫声完全不同父类没法替子类决定。而move被实现出来了因为移动的逻辑所有动物都一样。抽象类的本质就是“把共性实现掉把差异留白”让子类只关注自己真正需要决定的部分。3.2 抽象类和普通类的四个核心区别面试的时候抽象类和普通类的区别几乎必考。光背结论没用你要能从设计意图上讲清楚第一实例化能力不同。普通类可以new抽象类不能直接new必须被子类继承后再实例化子类。抽象类的构造函数依然会被调用但只发生在super()的时候它是为子类服务而存在的。第二抽象方法的存在不同。普通类不可能有抽象方法或者说普通类的每个方法都必须有实现。抽象类可以包含抽象方法也可以包含具体方法甚至可以全部都是具体方法——注意这一点一个没有任何抽象成员的抽象类也是合法的这种情况等于你主动“禁止实例化”只允许继承。第三继承约束的强度不同。普通类的子类可以不重写任何方法完全复用父类代码就能用。抽象类的子类则必须实现所有抽象成员否则子类自己也得声明成抽象类。这就保证了“凡是某个业务规则的子类就一定拥有对应的行为”。第四从设计语义上看普通类描述的是一个“完整的东西”抽象类描述的是一个“不完整但可扩展的骨架”。你可以在抽象类里写固定的流程、公共的工具方法、统一的属性然后把变化的点设计成抽象成员强制每个子类去填。这四个区别背后是一条主线抽象类牺牲了“直接使用”的便利性换来了“子类质量”的保障。你想统一封装一套逻辑又不想让团队里有人绕过约束乱造实例抽象类就是最好的选择。3.3 模板方法模式抽象类最典型的用武之地抽象类最常见的实际应用是模板方法模式。这个模式的思想很简单把一套流程的骨架写好流程中的某些步骤交给子类去实现。举一个数据解析的例子。假设你要解析 CSV、JSON、XML 三种格式它们的整体流程都是“读取 → 清理 → 校验 → 转换”但校验规则和转换逻辑完全不同。这时抽象类可以这样设计interface ParsedResult { data: Recordstring, unknown[]; errors: string[]; } abstract class DataParser { // 模板方法定义固定流程子类不要重写 parse(raw: string): ParsedResult { const cleaned this.clean(raw); const validated this.validate(cleaned); return this.transform(validated); } // 共性实现所有格式都需要的清理逻辑 protected clean(raw: string): string { return raw.trim(); } // 抽象步骤校验规则由子类决定 protected abstract validate(cleaned: string): string; // 抽象步骤转换逻辑由子类决定 protected abstract transform(validated: string): ParsedResult; } class CsvParser extends DataParser { protected validate(cleaned: string): string { // 校验 CSV 的行数和列数 return cleaned; } protected transform(validated: string): ParsedResult { // 把 CSV 字符串转换成对象数组 return { data: [], errors: [] }; } }这个设计的好处是流程骨架被锁死在基类里子类不可能把“校验”和“转换”的顺序颠倒也不可能忘了实现某个步骤——因为忘了实现类根本没法通过编译。这就是抽象类比“普通类 约定注释”更可靠的地方。同时clean用protected保护起来外部不能随意调用中间步骤只能调parse这一个入口。你注意看这里parse方法本身没有加abstract它是完整实现validate和transform加了abstract是空实现。这种“完整骨架 留白节点”的组合就是模板方法模式的核心也是抽象类设计能力的直接体现。4. 抽象类 vs 接口不是二选一而是两种约束层级4.1 语义差异是什么 vs 能做什么网上关于“抽象类和接口的区别”的帖子已经多到看不过来但大部分都在罗列语法差异很少有人把语义讲透。我的理解是接口描述的是“一个对象应该长什么样”它是一份契约抽象类描述的是“一类对象应该是什么”它是一具骨架。你用接口去约束“这个东西能不能调用 fetch 方法”用抽象类去表达“这个东西是动物动物都会发出声音但怎么发声你子类决定”。生活里类比接口像招聘 JD写明岗位要求——会沟通、会写日报、会开会抽象类像入职后的师傅带教——公司流程、工具、团队规范都教给你了但你具体怎么产出得你自己摸索。JD 只关心你有没有这些能力带教关心你作为一个“这个团队的人”具备哪些公共基础和内部默契。这个差异带来一个实际影响接口可以用来约束任何对象不管它是类实例、普通对象字面量还是函数返回值而抽象类只能用来约束“类的继承体系”。如果你只是想声明一个函数的参数类型用接口如果你真的要抽公共逻辑、共享代码用抽象类。4.2 语法差异implements、继承、多继承语法上两者的差异更明显。一个类可以实现implements多个接口但只能继承extends一个抽象类。这个限制来自类的单继承模型——一个子类只能有一个父类但可以实现无数个接口。接口可以继承接口而且可以多继承interface BasicUser { name: string; age: number; } interface AdminUser extends BasicUser { role: admin; permissions: string[]; } interface SuperAdmin extends AdminUser, BasicUser { level: number; }抽象类继承抽象类也是合法的父子抽象类之间可以逐层细化抽象程度。但抽象类只能继承一个这个“单继承”的限制恰恰也是它设计价值的一部分——继承链越简单体系越好维护。还有一个语法细节抽象类可以有 constructor、可以有实例属性、可以有方法的实际实现这些接口统统做不到。接口里只能有成员的声明类型不能有赋值逻辑不能有构造函数。这也是为什么当你需要“公共属性和公共方法的状态”时抽象类是唯一合适的选择。接口描述能力抽象类提供基底能力两者用途截然不同。4.3 实战选择表与混合用法我在实际项目里判断用哪个通常会问三个问题我需要共享代码实现吗需要就选抽象类。我需要表达一个简单的形状契约吗需要就选接口。我又要共享编码又要约束形状两个一起用。混合用法很常见用抽象类做基类实现公共逻辑和状态用接口去描述对外暴露的能力让外部依赖结构而不是具体类。interface RepositoryT { find(id: number): PromiseT | null; save(item: T): Promisevoid; } abstract class BaseRepositoryT implements RepositoryT { protected abstract tableName: string; async find(id: number): PromiseT | null { // 公共逻辑基于 tableName 拼 SQL 查询 return null; } async save(item: T): Promisevoid { // 公共逻辑基于 tableName 拼 SQL 写入 } } class UserRepository extends BaseRepositoryUser { protected tableName users; }这个设计里RepositoryT是契约让调用方只依赖接口BaseRepositoryT是骨架把公共数据库逻辑收拢UserRepository只负责提供表名。这里既有 interface 的类型约束又有抽象类的实现共享还有 protected 修饰符保护内部细节四种知识点全部串起来了。这里有一点值得记住抽象类实现接口时不需要实现所有的接口成员可以把部分抽象成员继续留给子类。比如上面BaseRepository没有实现任何接口方法它把find和save作为普通方法写了实现如果我想让某些子类自己写find我可以把find声明为abstract让接口的契约转移到抽象类上子类再实现。这种“接口定义能力抽象类定义基座”的层级感是理解 TS 类型系统的关键。5. interface 的继承面试追问率最高的问题5.1 interface extends interface单继承和多继承“TypeScript interface 怎么继承”是近期搜得很多的问题它背后其实是大家对“继承”这个概念在不同类型之间的混淆。桥段通常是这样的你会用类继承知道抽象类继承但接口继承的更常用场景反而被忽略。接口继承接口是最简单也最常用的一种interface HasName { name: string; } interface HasAge { age: number; } interface Person extends HasName, HasAge { email: string; } const p: Person { name: 张三, age: 30, email: zhangexample.com, };这里的extends语法理解起来和类继承很像但有一个关键不同一个类只能继承一个类一个接口却可以继承多个接口。这相当于是 TypeScript 在类型层面帮你绕过了类的单继承限制达到了类似“多继承”的效果。如果你想让接口继承后覆盖父接口的成员需要满足一个规则子接口中重新声明的成员类型必须是可以赋值给父接口成员类型的。比如父接口定义了id: string子接口想改成id: number | string是可以的因为你扩展了类型范围但如果你改成id: number类型范围收窄了TS 会报错。这个规则很多人踩过坑建议面试前专门记一下。5.2 interface extends class冷门但面试官喜欢另一个容易让人愣住的点interface 居然可以 extends 一个 class。这在语义上不是“接口继承类的实现”而是“接口继承了类的结构和类型”。简单说接口可以从一个类中提取成员结构然后要求其他类或对象也满足这个结构。class Point { x: number 0; y: number 0; } interface Point3D extends Point { z: number; } const p3d: Point3D { x: 1, y: 2, z: 3, };这背后有个关键限制如果被继承的类有 private 或 protected 成员那么这个接口只能由该类的子类来实现普通对象字面量都不行。因为 private/protected 成员是类私有的结构外部对象没法满足。气人的是接口本身并不要求你给出那些私有成员的定义但实现它的类必须继承自那个类的子类才行。这个冷门知识在面试里的价值在于它能区分你是“背概念”还是“真正用过”。你不用在这里栽跟头——记住一个判断原则interface extends class 最大的使用场景是“用接口去表达某个类实例的某一部分形状”平时能不用就不用普通对象用Pick或者结构化类型就行了。5.3 接口继承与类继承的对比一句话总结各自位置把三种继承方式放在一张表里对比思路立刻就清楚了方式关键字数量限制是否共享实现典型用途类继承类extends只能一个是通用基类、公共逻辑复用抽象类继承类extends只能一个是模板方法、定义骨架接口继承接口extends可以多个否组合约束、扩展契约接口继承类extends可以多个否提取类的形状作为契约这个表最终落到一个核心认知继承不只有“代码复用”这一个目的。类继承和抽象类继承主要解决“实现共享”接口继承主要解决“类型组合”。面试的时候如果能把这个底层逻辑讲清楚比单纯背“接口可以多继承类不能”要有说服力得多。6. 实操案例用抽象类 修饰符设计一套 playwright 页面对象基类6.1 需求背景与设计思路聊了这么多理论接下来上一个我实际做过的完整案例。最近在用 TypeScript playwright 做一套端到端自动化测试页面很多每个页面都有不少重复操作打开 URL、等页面加载完成、定位元素、填表单、断言。一开始我每个 spec 文件里都写这些重复代码后来测试量上来后明显感觉到维护成本越来越高——一个公共等待策略的调整所有文件都要改一遍。重构的方向很明确抽一个页面对象的基类。所有页面都有的能力放基类打开页面、等待就绪、公共定位方法。每个页面各不相同的能力页面 URL、关键操作流程尽量用抽象成员约束住强制每个子类自己定义。同时用 protected 控制中间方法不被外部测试用例误调用——测试代码只需要暴露goto、login这样的高级动词不该看到底层定位细节。这里我用到的设计规则就是前面讲过的组合拳抽象类定义骨架、abstract 强制关键信息、protected 保护内部方法、readonly 固定依赖。6.2 基类与子类的完整实现先看基类import { Page, Locator } from playwright; abstract class BasePage { // readonly protected依赖固定只能内部和子类访问 protected readonly page: Page; constructor(page: Page) { this.page page; } // 抽象属性每个子类必须给出自己的 URL abstract get url(): string; // 公共方法打开页面延迟到子类 async goto(): Promisevoid { await this.page.goto(this.url); } // protected 方法封装底层定位外部不暴露 protected locator(selector: string): Locator { return this.page.locator(selector); } async waitForReady(): Promisevoid { await this.page.waitForLoadState(networkidle); } }再看两个子类class LoginPage extends BasePage { get url(): string { return https://example.com/login; } async login(username: string, password: string): Promisevoid { await this.goto(); await this.locator(#username).fill(username); await this.locator(#password).fill(password); await this.locator(#login-btn).click(); await this.waitForReady(); } } class DashboardPage extends BasePage { get url(): string { return https://example.com/dashboard; } async getWelcomeText(): Promisestring { await this.goto(); return this.locator(.welcome).innerText(); } }在测试用例里我只用了LoginPage和DashboardPage暴露出来的一层小 APIconst page await browser.newPage(); const loginPage new LoginPage(page); await loginPage.login(admin, 123456); const dashboardPage new DashboardPage(page); const welcome await dashboardPage.getWelcomeText(); expect(welcome).toContain(欢迎回来);外部拿不到loginPage.locator(#password)因为 locator 是 protected外部也无法绕过url属性随意跳转到别的地址因为url是抽象属性每个页面实例天然绑定自己的路径。这些约束都不是文档里约定的而是编译器强制执行的——团队里任何一个人假如在测试里写了dashboardPage.locator(...)TS 立刻报错根本走不到运行阶段。这就是抽象类加修饰符组合之后带来的“系统性防呆”。6.3 这套设计避开了哪些坑做这个重构时我踩过几个坑值得分享第一个坑把constructor里的page声明成普通属性而不是 readonly。有个测试文件里有人意外给page重新赋值了结果是整个测试会话的对象被替换后面所有操作都失效排查了快一小时。改成protected readonly page之后编译器立刻把这个错误拦截掉。这个教训让我养成了一个习惯依赖注入进来的核心对象一律声明为 readonly。第二个坑过度使用抽象成员。第一版设计里我几乎把所有方法都变成了 abstract比如getUsernameInput、getPasswordInput等等。结果子类代码堆成山每个页面都在重复实现相同的选择器逻辑。后来才明白抽象方法只应该留给“不同子类真正有差异的行为”相同的行为放基类实现就好。我把公共选择器逻辑放回基类子类只保留 URL 和高级操作动词代码量瞬间少了一半。第三个坑基类方法全部不设访问修饰符默认为 public。这让测试用例可以直接调用waitForReady、locator这些内部方法接口暴露面变大改起来也容易破坏封装。后来把内部方法统一改成 protected对外只留高级动词测试代码变得非常整洁可读性提升一个档次。这套模式不仅适用于 playwright凡是做页面对象模式Page Object Model的场景基本都能套用。它的核心收益是测试代码越来越薄页面结构变了只改页面类基类的公共逻辑只维护一份。7. 从“会用”到“讲清楚”面试题、常见雷区与我的建议7.1 高频面试题速查这几个问题别答漏基于近期搜得比较多的 typescript 面试关键词我整理了一个速查表。面试官问来问去核心就是这几道答案背后的逻辑你都能在这篇文章里找到面试问题关键回答要点抽象类和普通类有什么区别不能 new、可以有抽象成员、子类必须实现所有抽象成员、子类不实现就得继续是抽象类抽象类和接口有什么区别接口只约束形状抽象类还能共享实现接口可多实现抽象类只能单继承接口无构造函数无实现private 和 protected 区别private 仅本类protected 本类和子类运行时都不生效仅编译期readonly 能不能在构造函数外赋值不能在构造函数里赋值是最后的机会interface 怎么继承extends 接口可以多继承继承类时有私有成员的坑覆盖属性时不能收窄类型参数属性是什么构造参数加修饰符自动声明属性等于省略了声明和赋值两步一个类能不能同时 implements 多个接口能接口可以多实现抽象方法能不能有方法体不能只有签名如果加了实现它就不再是抽象方法了这些问题单独看都简单难的是面试官会连环追问。比如你先回答“抽象类不能 new”他会追问“那构造函数还有用吗”你先说“抽象方法没有实现”他会问“那子类不实现会怎样”。我的建议是把每个答案背后的原理链都过一遍而不是死记结论。7.2 实操中容易踩的雷四个高频报错场景日常写代码的时候我经常在同事的 PR 里看到下面这些典型错误。提前知道这些坑能帮你省下不少排查时间。第一个是“忘了实现抽象成员导致子类无法实例化”。TS 会报一条“Non-abstract class X does not implement inherited abstract member Y”的错误。解决办法有两个要么在子类里补上实现要么把子类也声明成 abstract。多数情况下你应该补实现因为如果子类都不实现那它大概率是不需要被实例化的中间层。第二个是“继承抽象类时调用 super 报错”。只要抽象类有构造函数子类构造函数就必须先调用super(...)。这个规则和普通类一样但很多人以为“抽象类不能 new 那就不用 super 了”这是误解。抽象类的构造函数由子类实例化时触发它照样能完成公共字段的初始化。第三个是“在外部访问 protected 成员被拦截”。这个问题最隐蔽子类可以通过this访问继承来的 protected 成员但不能用“父类实例”去访问。比如子类里写new Parent().protectedProp会报错因为 protected 的“可见范围”是当前实例的继承链而不是父类的所有实例。这是 TS 一个非常容易出现直觉偏差的规则。第四个是“interface 继承带私有成员的类后无法被对象字面量实现”。前面已经提到过如果类里有 private 成员那么这个接口只能由该类的子类实现。很多人被这条规则卡住后错误的解法是给接口也补一个私有成员——这是做不到的接口里根本不允许声明 private。正确的做法是重新设计类型结构别让接口直接继承带私有成员的类。7.3 最后分享一点我自己的使用心得写 TypeScript 这么多年我对抽象类和修饰符的态度有过一次明显转变。早期写代码的时候我恨不得把所有成员都标上 private把所有需要扩展的基类都写成 abstract恨不得让编译器替我约束一切。结果项目越写越累团队成员纷纷表示“这个 base class 动都不敢动”。后来我慢慢意识到抽象类和修饰符都是工具它们约束的是“设计边界”而不是“自由度”。如果基类里抽象成员太多、protected 方法太细子类实现的负担就会很重反而抑制了扩展性。好的抽象类应该像一张高完成度的图纸能画好的部分尽量画好留给别人的空白尽量小而精。抽象成员的数量控制在个位数每个抽象成员都有明确的语义而不是把每一个变体都漏出来让子类自己折腾。访问修饰符的使用也是一样不是越严越好而是“该公开的公开该保护的保护该私有的私有”边界清晰才是关键。如果你现在正准备 TypeScript 面试或者刚开始用 playwright 写自动化我给你的建议是先拿一个小项目重构练练手把你的公共基类抽出来试着把能做好的部分写在基类里把真正有差异的部分设计成抽象成员再想想每个方法应该对外还是对内。亲手做完这一轮你对抽象类和修饰符的理解会比背十篇面试题都扎实。