Angular 面试题及详细答案
Angular 作为 Google 维护的企业级前端框架,其面试考察通常覆盖核心概念、生命周期、依赖注入、状态管理、性能优化等维度。以下整理了高频面试题及深度解析,帮助理解原理而非单纯记忆。
一、基础概念与核心特性
1. Angular 和 AngularJS 的主要区别是什么?
Angular(通常指 Angular 2+,简称 Angular)是 AngularJS(Angular 1.x)的完全重写版本,核心差异如下:
对比维度 | AngularJS (1.x) | Angular (2+) |
---|---|---|
语言支持 | 仅支持 JavaScript | 推荐 TypeScript(强类型),也支持 JS |
架构模式 | MVC(Model-View-Controller) | 组件化架构(Component-Based) |
数据绑定 | 双向绑定(基于脏检查) | 双向绑定(基于 Zone.js)+ 单向绑定([] ) |
依赖注入 | 模块级注入,配置复杂 | 层级注入(模块/组件/指令),API 更清晰 |
性能 | 脏检查机制,大应用性能差 | 增量变更检测,性能更优 |
移动支持 | 无原生支持,需第三方框架(如 Ionic 1) | 原生支持 PWA、移动端适配,可配合 Ionic 4+ |
生态 | 老旧,社区活跃度低 | 活跃,官方维护路由、表单等模块 |
2. Angular 的核心模块(Modules)有哪些?各自的作用是什么?
Angular 中模块(NgModule)是组织代码的基本单元,每个 Angular 应用至少有一个根模块(AppModule
),核心模块分为:
模块名称 | 作用 |
---|---|
BrowserModule |
浏览器平台必备模块,提供 ngIf 、ngFor 等内置指令,仅根模块导入。 |
CommonModule |
包含 BrowserModule 的核心指令(无平台相关代码),子模块导入以复用指令。 |
FormsModule |
提供模板驱动表单支持(如 ngModel 、ngForm )。 |
ReactiveFormsModule |
提供响应式表单支持(如 FormGroup 、FormControl ),更适合复杂表单。 |
RouterModule |
路由核心模块,通过 forRoot() (根模块)/forChild() (子模块)配置路由。 |
HttpClientModule |
提供 HTTP 请求能力(替代 AngularJS 的 $http ),支持拦截器、TypeScript 类型。 |
3. 什么是 Angular 组件(Component)?它的核心组成部分有哪些?
组件是 Angular 应用的最小视图单元,负责封装 UI 结构、样式和逻辑,是“页面积木”。一个完整的组件由 4 部分组成:
装饰器(
@Component
):定义组件元数据,核心属性包括:selector
:组件的 HTML 标签名(如<app-user>
),用于在其他模板中引用。template/templateUrl
:组件的 HTML 模板(内联模板/外部文件路径)。styles/styleUrls
:组件的 CSS 样式(内联样式/外部文件路径),默认样式隔离(仅作用于当前组件)。providers
:在组件层级注入依赖(覆盖模块级注入)。
类(Class):包含组件的业务逻辑,如数据(
property
)、方法(method
),通过模板绑定与视图交互。模板(Template):HTML 结构,通过 Angular 模板语法(如
{{}}
、[]
、()
)绑定类中的数据和事件。样式(Styles):组件的样式,支持
scoped
(默认)、::ng-deep
(穿透子组件样式)等特性。
示例:
// user.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-user',
template: `
<h2>Hello, {{ userName }}!</h2>
<button (click)="changeName()">Change Name</button>
`,
styles: [`h2 { color: blue; }`]
})
export class UserComponent {
userName = 'Alice'; // 数据
changeName() { // 方法
this.userName = 'Bob';
}
}
二、模板语法与数据绑定
1. Angular 有哪些数据绑定方式?分别用于什么场景?
Angular 提供 4 种核心绑定方式,覆盖“数据从类到视图”“视图到类”“双向同步”的场景:
绑定类型 | 语法示例 | 方向 | 作用场景 |
---|---|---|---|
插值绑定 | {{ user.name }} |
类 → 视图 | 展示类中的数据(文本内容)。 |
属性绑定 | <img [src]="imgUrl"> |
类 → 视图 | 绑定 HTML 属性/组件输入属性(如 [disabled] )。 |
事件绑定 | <button (click)="onClick()"> |
视图 → 类 | 监听 DOM 事件/组件输出事件(如点击、输入)。 |
双向绑定 | <input [(ngModel)]="user.name"> |
类 ↔ 视图 | 数据在类和视图间同步(需导入 FormsModule )。 |
注意:双向绑定本质是“属性绑定 + 事件绑定”的语法糖,等价于 <input [value]="user.name" (input)="user.name = $event.target.value">
。
2. 什么是 *ngIf
和 *ngFor
?它们的区别和注意事项是什么?
两者都是 Angular 内置结构型指令(前缀 *
表示“修改 DOM 结构”):
*ngIf
:条件渲染
- 作用:根据表达式布尔值决定是否在 DOM 中添加/移除元素(而非隐藏)。
- 示例:
<div *ngIf="isShow">仅当 isShow 为 true 时显示</div>
- 注意:频繁切换可能触发 DOM 频繁创建/销毁,性能敏感场景可考虑
[hidden]
(仅隐藏,DOM 保留)。
*ngFor
:循环渲染
- 作用:根据数组/可迭代对象生成重复的 DOM 元素。
- 核心语法:
*ngFor="let item of list; let i = index; trackBy: trackByFn"
- 注意事项:
- 必须加
trackBy
:默认情况下,数组变化时*ngFor
会重新渲染所有元素;trackBy
通过唯一标识(如id
)复用 DOM,提升性能。 index
变量:获取当前循环的索引(从 0 开始)。
- 必须加
trackBy
示例:
// 组件类
trackByUserId(index: number, user: User): number {
return user.id; // 用用户唯一 ID 跟踪
}
// 模板
<div *ngFor="let user of userList; trackBy: trackByUserId">
{{ user.name }}
</div>
3. 什么是管道(Pipe)?常用的内置管道有哪些?
管道是 Angular 中用于转换模板数据格式的工具(如日期、货币、大小写转换),语法为 {{ 数据 | 管道名: 参数 }}
。
常用内置管道:
date
:格式化日期,示例:{{ today | date: 'yyyy-MM-dd HH:mm' }}
(输出:2024-05-20 14:30)。currency
:格式化货币,示例:{{ price | currency: 'CNY' }}
(输出:¥100.00)。uppercase/lowercase
:转换大小写,示例:{{ 'hello' | uppercase }}
(输出:HELLO)。async
:处理异步数据(如Observable
、Promise
),自动订阅/取消订阅,避免内存泄漏,示例:{{ user$ | async }}
。json
:将对象转为 JSON 字符串,用于调试,示例:{{ user | json }}
。
自定义管道:
通过 @Pipe
装饰器定义,需实现 PipeTransform
接口的 transform
方法。示例(首字母大写管道):
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'firstUpper' }) // 管道名称
export class FirstUpperPipe implements PipeTransform {
transform(value: string): string {
if (!value) return '';
return value[0].toUpperCase() + value.slice(1);
}
}
// 模板使用:{{ 'angular' | firstUpper }} → 输出 Angular
三、组件通信
1. 父组件如何向子组件传递数据?
通过输入属性(@Input()
) 实现,步骤如下:
子组件中用
@Input()
装饰器定义可接收的属性:// 子组件:child.component.ts import { Input } from '@angular/core'; export class ChildComponent { @Input() childName: string; // 父组件可传递的属性 @Input('alias') age: number; // 可选:设置别名,父组件用别名传递 }
父组件模板中通过属性绑定传递数据:
<!-- 父组件模板 --> <app-child [childName]="parentName" <!-- 无别名:直接用子组件属性名 --> [alias]="parentAge" <!-- 有别名:用别名传递 --> ></app-child>
(可选)子组件监听输入属性变化:
通过ngOnChanges
生命周期钩子(监听多个属性)或setter
方法(监听单个属性):// 方式1:setter 监听单个属性 @Input() set childName(name: string) { this._childName = name ? name.trim() : ''; } private _childName: string; // 方式2:ngOnChanges 监听多个属性 import { OnChanges, SimpleChanges } from '@angular/core'; export class ChildComponent implements OnChanges { @Input() childName: string; @Input() age: number; ngOnChanges(changes: SimpleChanges): void { // changes 包含所有变化的输入属性 if (changes['childName']) { console.log('childName 变化:', changes['childName'].currentValue); } } }
2. 子组件如何向父组件传递数据?
通过输出属性(@Output()
+ EventEmitter
) 实现,本质是“子组件触发事件,父组件监听事件”:
子组件中用
@Output()
装饰器定义事件发射器:// 子组件:child.component.ts import { Output, EventEmitter } from '@angular/core'; export class ChildComponent { // 定义输出事件(可选设置别名) @Output() sendMsg = new EventEmitter<string>(); // 子组件触发事件,传递数据 onClick() { this.sendMsg.emit('Hello from Child!'); // 发送数据 } }
父组件模板中通过事件绑定监听子组件事件:
<!-- 父组件模板 --> <app-child (sendMsg)="receiveMsg($event)"></app-child>
父组件定义方法接收数据:
// 父组件:parent.component.ts receiveMsg(msg: string): void { console.log('收到子组件消息:', msg); // 输出:收到子组件消息:Hello from Child! }
3. 非父子组件(如兄弟组件)如何通信?
非父子组件无直接关联,需通过“中间层”传递数据,常用方案有 3 种:
方案1:共享服务(推荐)
通过一个全局/模块级别的服务,利用 Observable
(如 Subject
)实现事件订阅/发布:
// 共享服务:msg.service.ts
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({ providedIn: 'root' }) // 全局注入
export class MsgService {
private msgSubject = new Subject<string>(); // 私有 Subject(避免外部直接调用)
msg$ = this.msgSubject.asObservable(); // 暴露为 Observable(仅允许订阅)
// 发送消息的方法
sendMsg(msg: string): void {
this.msgSubject.next(msg);
}
}
// 发送方组件(如 Brother1)
export class Brother1Component {
constructor(private msgService: MsgService) {}
send() {
this.msgService.sendMsg('Hi from Brother1!');
}
}
// 接收方组件(如 Brother2)
export class Brother2Component {
constructor(private msgService: MsgService) {}
ngOnInit() {
// 订阅消息
this.msgService.msg$.subscribe(msg => {
console.log('Brother2 收到:', msg);
});
}
}
方案2:通过父组件中转
兄弟组件通过共同的父组件传递:
- 兄弟 A → 父组件(
@Output()
); - 父组件 → 兄弟 B(
@Input()
)。
方案3:状态管理库(大型应用)
如 NgRx
(Angular 官方推荐,基于 Redux 思想),统一管理全局状态,适合复杂应用(如多组件共享大量数据)。
四、生命周期钩子
1. Angular 组件的生命周期钩子有哪些?按执行顺序说明作用。
Angular 组件从“创建”到“销毁”会触发一系列生命周期钩子,核心钩子按执行顺序如下:
钩子名称 | 执行时机 | 核心作用 |
---|---|---|
ngOnChanges |
输入属性(@Input() )变化时触发(首次赋值也触发) |
监听输入属性变化,处理数据更新逻辑。 |
ngOnInit |
组件初始化完成后触发(仅一次) | 执行初始化逻辑(如调用接口、订阅数据)。 |
ngDoCheck |
每次变更检测时触发(频率高) | 自定义变更检测逻辑(谨慎使用,避免性能问题)。 |
ngAfterContentInit |
组件内容投影(<ng-content> )初始化后触发(仅一次) |
操作投影内容(如获取投影的 DOM 元素)。 |
ngAfterContentChecked |
投影内容变更检测后触发 | 监听投影内容的变化。 |
ngAfterViewInit |
组件视图(模板)初始化后触发(仅一次) | 操作组件内部 DOM 元素(如通过 @ViewChild 获取元素)。 |
ngAfterViewChecked |
组件视图变更检测后触发 | 监听视图的变化。 |
ngOnDestroy |
组件销毁前触发(仅一次) | 清理资源(如取消 Observable 订阅、清除定时器),避免内存泄漏。 |
示例:组件初始化时调用接口,销毁时取消订阅:
import { OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { UserService } from './user.service';
export class UserComponent implements OnInit, OnDestroy {
private userSub: Subscription;
ngOnInit() {
this.userSub = this.userService.getUser().subscribe(user => {
console.log(user);
});
}
ngOnDestroy() {
this.userSub.unsubscribe(); // 清理订阅
}
}
五、依赖注入(DI)
1. 什么是 Angular 的依赖注入?它的核心作用是什么?
依赖注入(Dependency Injection,DI)是 Angular 的核心设计模式,指**“组件/服务的依赖由框架自动注入,而非手动创建”**。
核心作用:
- 解耦:组件无需关心依赖的创建逻辑,只需声明依赖,降低代码耦合度。
- 复用:依赖(如服务)可在多个组件间共享,避免重复代码。
- 测试友好:可轻松替换依赖的模拟实现(如测试时用模拟服务替代真实接口服务)。
核心概念:
- 注入器(Injector):负责创建和管理依赖实例的容器,Angular 有三级注入器(根注入器 → 模块注入器 → 组件注入器),层级越细优先级越高。
- 提供商(Provider):告诉注入器“如何创建依赖实例”,常用
useClass
(用类实例化)、useValue
(用固定值)、useFactory
(用工厂函数)。 - 依赖令牌(Token):注入器查找依赖的唯一标识(通常是服务类本身)。
2. 如何创建和使用一个服务(Service)?
服务是 Angular 中用于封装共享逻辑(如接口调用、数据处理) 的类,通过 @Injectable
装饰器标记为可注入。
步骤1:创建服务(用 Angular CLI 命令)
ng generate service user # 生成 user.service.ts
步骤2:定义服务逻辑
// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from './user.model';
@Injectable({
providedIn: 'root' // 根注入器:服务实例在整个应用中唯一
// 也可设置为模块:providedIn: UserModule → 仅该模块内共享
})
export class UserService {
constructor(private http: HttpClient) {} // 注入 HttpClient 依赖
// 封装接口调用逻辑
getUser(id: number): Observable<User> {
return this.http.get<User>(`https://api.example.com/users/${id}`);
}
}
步骤3:在组件中使用服务
组件只需在构造函数中声明依赖,Angular 会自动注入服务实例:
// user.component.ts
import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';
import { User } from './user.model';
@Component({ selector: 'app-user' })
export class UserComponent implements OnInit {
user: User;
// 声明依赖(无需手动 new UserService())
constructor(private userService: UserService) {}
ngOnInit() {
this.userService.getUser(1).subscribe(user => {
this.user = user;
});
}
}
六、路由(Routing)
1. Angular 路由的核心概念有哪些?如何配置基础路由?
Angular 路由(@angular/router
)负责管理组件间的导航,核心概念和配置步骤如下:
核心概念:
- 路由配置(Route):定义“路径 → 组件”的映射,如
{ path: 'user', component: UserComponent }
。 - 路由出口(
<router-outlet>
):路由组件的渲染容器,匹配的组件会替换该标签的内容。 - 路由链接(
<a routerLink="...">
):导航链接,替代原生<a href>
(避免页面刷新)。 - 激活路由(ActivatedRoute):获取当前路由的信息(如参数、查询参数)。
- 路由守卫(Route Guard):控制路由访问权限(如登录验证)。
基础路由配置步骤:
导入
RouterModule
和组件:// app.module.ts import { RouterModule } from '@angular/router'; import { HomeComponent } from './home.component'; import { UserComponent } from './user.component';
定义路由配置数组:
const routes = [ { path: '', redirectTo: '/home', pathMatch: 'full' }, // 默认路由(重定向到首页) { path: 'home', component: HomeComponent }, // 首页路由 { path: 'user/:id', component: UserComponent }, // 带参数的路由(:id 是参数占位符) { path: '**', component: NotFoundComponent } // 通配符路由(匹配未定义的路径,通常用于404页面) ];
在
@NgModule
的imports
中配置路由:@NgModule({ imports: [ BrowserModule, RouterModule.forRoot(routes) // 根模块用 forRoot() ], // ... })
在根组件模板中添加
<router-outlet>
和导航链接:<!-- app.component.html --> <nav> <a routerLink="/home">首页</a> <a routerLink="/user/1">用户1</a> </nav> <router-outlet></router-outlet> <!-- 路由组件将在这里渲染 -->
2. 如何在组件中获取路由参数?
通过 ActivatedRoute
服务获取路由参数(如 user/:id
中的 id
),常用两种方式:
方式1:快照(Snapshot)—— 参数不变时使用
适用于“参数一旦设置就不会变化”的场景(如从列表页进入详情页,参数固定):
// user.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
export class UserComponent implements OnInit {
userId: string;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
// 从快照中获取参数
this.userId = this.route.snapshot.paramMap.get('id');
console.log('用户ID:', this.userId);
}
}
方式2:订阅(Subscribe)—— 参数可能变化时使用
适用于“同一组件内参数可能变化”的场景(如详情页内切换用户,组件不销毁,仅参数变化):
export class UserComponent implements OnInit {
userId: string;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
// 订阅参数变化
this.route.paramMap.subscribe(params => {
this.userId = params.get('id');
console.log('用户ID(更新后):', this.userId);
});
}
}
补充:获取查询参数(如 user?id=1&name=alice
中的 id
和 name
):
// 快照方式
const id = this.route.snapshot.queryParamMap.get('id');
const name = this.route.snapshot.queryParamMap.get('name');
// 订阅方式
this.route.queryParamMap.subscribe(queryParams => {
const id = queryParams.get('id');
});
七、性能优化
1. Angular 应用有哪些常见的性能优化手段?
Angular 性能优化需从“变更检测、渲染、资源加载”等维度入手,常用手段如下:
优化变更检测:
- 使用
OnPush
变更检测策略:组件装饰器中设置changeDetection: ChangeDetectionStrategy.OnPush
,仅当输入属性(@Input()
)引用变化、组件内事件触发或手动调用markForCheck()
时,才执行变更检测,减少不必要的检测。 - 避免在模板中调用函数:模板中
{{ getUserName() }}
会在每次变更检测时执行,建议提前计算并存储为属性。
- 使用
减少 DOM 操作:
*ngFor
加trackBy
:复用 DOM 元素,避免频繁重新渲染(见“模板语法”部分)。- 虚拟滚动(Virtual Scrolling):使用
@angular/cdk/scrolling
的CdkVirtualScrollViewport
,仅渲染可视区域的列表项(适合万级以上数据)。
资源优化:
- 懒加载模块(Lazy Loading):路由配置中用
loadChildren
延迟加载模块,减少初始加载体积:const routes = [ { path: 'user', loadChildren: () => import('./user/user.module').then(m => m.UserModule) } ];
- 压缩代码:通过 Angular CLI 的
--prod
模式(ng build --prod
)自动压缩 JS/CSS/HTML,开启 Tree Shaking(移除未使用代码)。
- 懒加载模块(Lazy Loading):路由配置中用
内存泄漏防护:
- 取消订阅:
Observable
订阅后,在ngOnDestroy
中调用unsubscribe()
,或使用async
管道(自动取消订阅)。 - 清除定时器:
setTimeout
/setInterval
在ngOnDestroy
中调用clearTimeout
/clearInterval
。
- 取消订阅:
八、进阶问题
1. 什么是 Angular 拦截器(Interceptor)?如何实现一个 HTTP 拦截器?
拦截器是 Angular 中用于统一拦截和处理 HTTP 请求/响应的服务(如添加 Token、处理错误、显示加载动画),需实现 HttpInterceptor
接口。
实现步骤:
创建拦截器类:
// auth.interceptor.ts(添加请求 Token 的拦截器) import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; import { Observable } from 'rxjs'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} // 拦截请求的核心方法 intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { // 1. 获取本地存储的 Token const token = localStorage.getItem('token'); // 2. 克隆请求并添加 Token 头(请求对象不可变,需用 clone() 修改) if (token) { const authRequest = request.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); // 3. 继续处理修改后的请求 return next.handle(authRequest); } // 无 Token 时,直接处理原请求 return next.handle(request); } }
在模块中注册拦截器:
需在@NgModule
的providers
中用HTTP_INTERCEPTORS
令牌注册,多个拦截器按注册顺序执行:// app.module.ts import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true // 允许注册多个拦截器 } ] }) export class AppModule {}
2. 模板驱动表单和响应式表单的区别是什么?如何选择?
Angular 提供两种表单实现方式,核心差异在于“逻辑的存放位置”:
对比维度 | 模板驱动表单(Template-Driven) | 响应式表单(Reactive) |
---|---|---|
逻辑位置 | 逻辑在模板中(依赖指令如 ngModel ) |
逻辑在组件类中(依赖 FormGroup 等) |
数据模型 | 隐式模型(由 ngModel 维护) |
显式模型(组件类中定义 FormGroup ) |
验证方式 | 模板中添加验证指令(如 required ) |
类中定义验证器(如 Validators.required ) |
灵活性 | 简单场景友好,复杂场景难扩展 | 灵活,支持动态表单(如动态添加字段) |
测试友好性 | 难测试(需操作 DOM) | 易测试(直接测试类中的表单模型) |
适用场景 | 简单表单(如登录、注册) | 复杂表单(如多步骤表单、动态字段) |
选择建议:
- 简单表单(字段少、验证逻辑简单):选模板驱动表单,开发速度快。
- 复杂表单(动态字段、多验证规则、需频繁修改):选响应式表单,可维护性和扩展性更强。
3. 什么是 NgRx?它的核心组成部分有哪些?
NgRx 是 Angular 生态中的状态管理库,基于 Redux 思想和 RxJS,用于统一管理跨组件的全局状态(如用户信息、购物车数据),适合中大型应用。
核心组成部分(遵循“单向数据流”):
- Store:存储应用的全局状态(单一数据源),通过
select
方法获取状态,通过dispatch
方法触发动作。 - Action:描述“发生了什么”的纯对象(如
{ type: 'user/login', payload: { name: 'Alice' } }
),是状态变化的唯一原因。 - Reducer:纯函数,接收当前状态和 Action,返回新状态(不修改原状态),公式:
(state, action) => newState
。 - Effect:处理副作用(如接口调用、定时器),监听 Action 并触发异步操作,操作完成后派发新的 Action 更新状态。
- Selector:纯函数,从 Store 中提取指定状态(如
selectUser
提取用户信息),支持状态缓存和组合。
简单工作流:
- 组件通过
store.dispatch(action)
触发动作(如用户点击“登录”)。 - Effect 监听该 Action,调用登录接口(副作用),接口成功后派发
loginSuccess
Action。 - Reducer 接收
loginSuccess
Action,更新 Store 中的用户状态。 - 组件通过
store.select(selector)
订阅状态变化,更新视图。
以上面试题覆盖了 Angular 从基础到进阶的核心知识点,理解原理并结合实际项目经验,才能在面试中灵活应对。