0%

To Do List

  1. typescript tag not found on github pages tags.

  2. Angular, AOT vs JIT and Ivy

  3. Angular, How ngc works?

  4. Angular, ngc vs ngcc.

  5. Angular change detection

    1. https://angularindepth.com/posts/1513/from-zone-js-to-zoneless-angular-and-back-how-it-all-works
    2. https://blog.angular-university.io/how-does-angular-2-change-detection-really-work/
    3. https://justangular.com/blog/a-new-era-for-angular-zoneless-change-detection?source=post_page-----babdeb6bc84e--------------------------------
  6. Angular zone

    1. https://dev.to/vivekdogra02/angular-zonejs-change-detection-understanding-the-core-concepts-16ek
    2. https://angularindepth.com/posts/1135/i-reverse-engineered-zones-zone-js-and-here-is-what-ive-found
    3. https://justangular.com/blog/a-change-detection-zone-js-zoneless-local-change-detection-and-signals-story
    4. https://justangular.com/blog/its-ok-to-use-function-calls-in-angular-templates
  7. Angular polyfill(under project root)

  8. Angular data binding.

  9. What’s new in Angular 15

  10. What’s new in Angular 16

  11. What’s new in Angular 17

  12. angular.json - projects - architect - build - options - tsConfig

  13. Angular router: resolver = https://medium.com/@singhkrishnapal0818/angular-resolver-with-example-cf23f6d38f7f

  14. Never use function call in angular template: https://medium.com/showpad-engineering/why-you-should-never-use-function-calls-in-angular-template-expressions-e1a50f9c0496

  15. https://medium.com/ngconf/accessing-route-params-in-angular-1f8e12770617

  16. Angular service scope providedIn: ‘root’, ‘platform’, ‘any’ - https://dev.to/christiankohler/improved-dependeny-injection-with-the-new-providedin-scopes-any-and-platform-30bb, https://angular.io/guide/providers#limiting-provider-scope-with-components

  17. Good angular post:

    1. https://justangular.com/blog
    2. https://blog.angular.io/
    3. https://angulararchitects.io/en/blog
  18. What is browserlist in Angular: https://github.com/browserslist/browserslist

  19. Angular, what is matrix parameters: https://stackoverflow.com/questions/2048121/url-matrix-parameters-vs-query-parameters

SSR in Angular

多页应用
http://www.guoxue.com/shibu/24shi/hansu/hsu_002.htm

  1. https://medium.com/@nishtha.viitorcloud/ssr-server-side-rendering-in-angular-a68bee12cb0d
  2. https://medium.com/@mbleigh/when-should-i-server-side-render-c2a383ff2d0f
  3. https://solutionshub.epam.com/blog/post/what-is-server-side-rendering

Module Federation

  1. code example: https://github.com/module-federation/module-federation-examples
  2. https://medium.com/swlh/webpack-5-module-federation-a-game-changer-to-javascript-architecture-bcdd30e02669 - need pay
  3. https://medium.com/@scriptedalchemy/understanding-webpack-module-federation-a-deep-dive-efe5c55bf366
  4. https://www.angulararchitects.io/en/blog/micro-frontends-with-modern-angular-part-1-standalone-and-esbuild/
  5. https://module-federation.io/
  6. https://www.youtube.com/watch?v=-ei6RqZilYI
  7. https://vugar-005.medium.com/module-federation-series-part-1-a-little-in-depth-258f331bc11e

Native module federation

https://github.com/angular-architects/module-federation-plugin/blob/main/libs/native-federation/README.md

Angular Elements

Angular elements are Angular components packaged as custom elements (also called Web Components), a web standard for defining new HTML elements in a framework-agnostic way

Pure ESM package

https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c

Angular CLI

https://angular.dev/cli

dynamic import

https://medium.com/unalai/%E8%AA%8D%E8%AD%98-dynamic-import-3a6f75da2fc9
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import

Angular lazy loading

https://angular.dev/guide/ngmodules/lazy-loading

Write tools

ASCII tree
markdown table generator, convert table cell contains multiple lines of code to markdown text, see What's new in angular 17 for details

本文讲解一下如何在Angular项目中使用router

路由配置(NgModule based apps)

创建一个Angular项目

1
ng new angular-router

在创建过程中,Angular CLI会询问是否使用router,选择Yes

创建三个组件。

使用WebStorm或者VSCode打开上面创建的项目,呼出Terminal,执行以下命令。

1
2
3
ng generate component home
ng generate component product
ng generate component about

然后打开app-routing.module.ts文件,创建路由。注意,path不能以/开头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// app-routing.module.ts
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {HomeComponent} from "./home/home.component";
import {ProductComponent} from "./product/product.component";
import {AboutComponent} from "./about/about.component";

// Add Routes here
const routes: Routes = [
{path: '', redirectTo: 'home', pathMatch: 'full'},
{path: 'home', component: HomeComponent},
{path: 'product', component: ProductComponent},
{path: 'about', component: AboutComponent}
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}

添加导航链接

打开app.component.html文件,添加导航链接。注意,routerLink的值要和path一致。如果是根路由,可以使用/。文档末尾的router-outlet是用来显示路由对应的组件的。当点击某一个链接的时候,Angular会找到对应的组件,并将其加载到router-outlet中。所以router-outlet相当于一个占位符,与html中的slot类似。

1
2
3
4
5
6
7
8
9
<h1>Angular Router Demo</h1>
<nav>
<ul>
<li><a routerLink="/home">Home</a></li>
<li><a routerLink="/product">Product</a></li>
<li><a routerLink="/about">About</a></li>
</ul>
</nav>
<router-outlet></router-outlet>

运行项目

Terminal中执行以下命令。打开浏览器,输入http://localhost:4200,就可以看到导航链接了。依次点击页面上的Home,Product,About链接,可以看到页面跳转了,并且浏览器中的地址也发生了变化。

1
ng serve

基于ROUTES的路由配置

ROUTES是Angular内置的一个Injection Token,在一些比较老的Angular app中,也有使用ROUTES来配置路由的,现举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@NgModule({
declarations: [
AppComponent,
ProductComponent,
ParentComponent,
ChildComponent,
HomeComponent,
AboutComponent,
],
imports: [
BrowserModule,
RouterModule.forRoot([]), // This line is must!
],
providers: [
{
provide: ROUTES,
useFactory: () => [
{path: 'home', component: HomeComponent},
{path: 'product', component: ProductComponent},
{path: 'about', component: AboutComponent}
],
multi: true, // This line is must!
},
],
bootstrap: [AppComponent]
})
export class AppModule {
}

useFactory后面跟的是一个工厂函数,这个函数我们直接返回一个路由数组。
使用这种方法后,我们就不需要路由文件app-routing.module.ts了,可以将它删除掉。

路由配置(Standalone apps)

基于standalone组件的app,路由配置稍有不同。

  1. main.ts文件中启动app时会传递appConfig进去,我们的路由配置就在这个appConfig中。
1
2
3
4
5
6
7
// main.ts
import {bootstrapApplication} from '@angular/platform-browser';
import {appConfig} from './app/app.config';
import {AppComponent} from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
  1. app.config.ts文件中通过provideRouter函数提供路由。
1
2
3
4
5
6
7
8
9
10
// app.config.ts
import {provideRouter} from '@angular/router';
import {routes} from './app.routes';

export const appConfig: ApplicationConfig = {
providers: [
// ...
provideRouter(routes),
]
};
  1. app.routes.ts文件中定义路由。
1
2
3
4
5
6
// app.routes.ts
export const routes: Routes = [
{path: 'home', component: HomeComponent},
{path: 'product', component: ProductComponent},
{path: 'about', component: AboutComponent}
];

可见,路由的结构是十分清晰的。

添加404页面

如果我们在浏览器中随意输入一个不存在的路径,比如http://localhost:4200/abc,会发现页面是空白的。这是因为我们没有处理404页面。我们可以在app-routing.module.ts中添加一个**路径,用来匹配所有路径,然后将其重定向到一个404组件。

创建404页面组件

1
ng generate component not-found

修改404页面内容

打开not-found.component.html,输入以下内容。

1
<p>404: Not Found!</p>

添加404路由

1
2
3
4
5
6
7
// app-routing.module.ts
const routes: Routes = [
{path: 'home', component: HomeComponent},
{path: 'product', component: ProductComponent},
{path: 'about', component: AboutComponent},
{path: '**', component: NotFoundComponent}
];

此时,如果在浏览器里面输入一个不存在的路径,比如http://localhost:4200/abc,则会显示404: Not Found!

嵌套路由

如果我们想要在product组件中添加两个子路由,product-aproduct-a,可以在app-routing.module.ts中添加如下代码。

新建两个组件

1
2
ng generate component product-a
ng generate component product-b

在product页面添加router-outlet

这个router-outlet用来显示ProductA和ProductB对应的的组件。

1
2
3
<!-- product.component.html -->
<p>product works!</p>
<router-outlet></router-outlet>

添加子路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const routes: Routes = [
{path: 'home', component: HomeComponent},
{
path: 'product',
// pathMatch: 'full', 注意这一句不能写!否则子路由无法匹配,会导致404!
component: ProductComponent,
children: [
{path: 'a', component: ProductAComponent},
{path: 'b', component: ProductBComponent}
]
},
{path: 'about', component: AboutComponent},
{path: 'about-me', component: AboutmeComponent},
{path: '**', component: NotFoundComponent}
];

此时,如果在浏览器中输入http://localhost:4200/product/a,会显示ProductA works!,输入http://localhost:4200/product/b,会显示ProductB works!。同时也会显示product works!,因为product.component.html中除了有<router-outlet></router-outlet>。还有<p>product works!</p>

严格匹配

上面的例子中,product的path不能使用严格匹配选项pathMath: 'full',否则子路由无法匹配,会导致404。这是为什么呢?
因为匹配子路由的时候,必须先匹配父级路由,而由于父级路由使用了pathMatch: 'full',所以无法匹配,父级路由无法匹配导致子路由的匹配无法进行,所以导致了404!

如果你非要使用pathMatch: 'full',可以如下定义路由表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const routes: Routes = [
{path: 'home', component: HomeComponent},
{
path: 'product',
pathMatch: 'full',
component: ProductComponent,
},
{
path: 'product/a', component: ProductAComponent
},
{path: 'product/b', component: ProductBComponent},
{path: 'about', component: AboutComponent},
{path: 'about-me', component: AboutmeComponent},
{path: '**', component: NotFoundComponent}
];

这样就可以使用pathMatch: 'full'了。但是这么做也有一个弊端,就是a和b从逻辑上讲不再是product子路由了。而是平级路由,只不过路径中包含了字符串product而已。这会导致product页面的内容无法显示。通常父级路由都会显示一些公共信息,差异化的信息放到子路由中。如果你使用了pathMatch: 'full',则会导致父级页面内容无法显示。请酌情使用此方法。

注意,如果你使用了嵌套路由,或者lazy load, 请谨慎使用pathMatch: 'full'。详情请看这里:detail

路由拆分

如果product中的子路由越来越多,都放到一个文件中会显得很臃肿。这时候我们可以将子路由拆分到单独的文件中。将子路由定义到product组件中。

基于NgModule的路由拆分

进入product文件夹,执行以下命令。

1
2
ng generate module product-routing --flat
ng generate module product --flat

--flat选项表示不创建文件夹。

product-routing.module.ts中添加如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// product-routing.module.ts
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ProductBComponent} from "../product-b/product-b.component";
import {ProductAComponent} from "../product-a/product-a.component";
import {ProductComponent} from "./product.component";
import {RouterModule, Routes} from "@angular/router";

const routes: Routes = [
{
path: '', // 注意这里不能写product,因为`app-routing.module.ts`中已经定义了product路由, 并且指定了path: 'product'
component: ProductComponent,
children: [
{path: 'a', component: ProductAComponent},
{path: 'b', component: ProductBComponent}
]
}
];

@NgModule({
imports: [
CommonModule,
RouterModule.forChild(routes),
],
exports: [RouterModule]
})
export class ProductRoutingModule {
}

product.module.ts文件中添加如下代码,导入ProductRoutingModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ProductComponent} from './product.component';
import {ProductRoutingModule} from './product-routing.module';
import {ProductAComponent} from '../product-a/product-a.component';
import {ProductBComponent} from '../product-b/product-b.component';

@NgModule({
declarations: [
ProductComponent,
ProductAComponent,
ProductBComponent
],
imports: [
CommonModule,
ProductRoutingModule
]
})
export class ProductModule {
}

修改app-routing.module.ts
注意,这里必须使用loadChildren, 否则子路由无法显示。

1
2
3
4
5
// app-routing.module.ts
const routes: Routes = [
{path: 'product', loadChildren: () => import('./product/product.module').then(m => m.ProductModule)},
// ...
];

这样就可以将product中的子路由拆分到单独的文件中了。

基于Standalone组件的路由拆分

如果你的组件是standalone的,没有对应的module文件,那么可以使用如下方法拆分路由。注意loadChildren中加载的是路由文件,而不是module文件,也不是component文件。

1
2
// app.routers.ts
{path: 'product', loadChildren: () => import('./product/product.routers').then((routes) => routes.productRouters)},

在Product组件文件夹下新建路由文件product.routers.ts,添加如下代码。

1
2
3
4
5
6
7
8
9
// product.routers.ts
import {Routes} from "@angular/router";
import {ProductComponent} from "./product.component";
import {ProductDetailComponent} from "../product-detail/product-detail.component";

export const productRouters: Routes = [
{path: '', component: ProductComponent},
{path: 'product-detail', component: ProductDetailComponent},
]

注意这种方式下,点击product-detail链接时,会显示product-detail组件。product组件的内容不会显示。

如果要显示product组件的内容,需要将product-detail组件定义为product组件的子路由。如下所示。这种方式在导航时很方便,比如左侧是导航菜单,点击不同菜单显示不同的组件。对于整个页面来说,导航菜单一直显示,而右侧的内容会随着导航的变化而变化。

1
2
3
4
5
6
7
8
9
10
11
12
// product.routers.ts
import {Routes} from "@angular/router";
import {ProductComponent} from "./product.component";
import {ProductDetailComponent} from "../product-detail/product-detail.component";

export const productRouters: Routes = [
{
path: '', component: ProductComponent, children: [
{path: 'product-detail', component: ProductDetailComponent},
]
},
]

监听路由变化

如果我们想要在路由变化的时候做一些事情,比如打印路由信息,可以在app.component.ts中添加如下代码。此时会监听整个app所有路由的变化,如果只想监控某个组件的路由变化,那么可以在该组件中添监听代码。

1
2
3
4
5
6
7
// app.component.ts
constructor(private router: Router) {}
ngOnInit() {
this.router.events.subscribe((event) => {
console.log('Route:', event);
});
}

路由变化时会依次触发以下事件:

  • NavigationStart
  • RoutesRecognized
  • GuardsCheckStart
  • ChildActivationStart
  • ActivationStart
  • GuardsCheckEnd
  • ResolveStart
  • ResolveEnd
  • ActivationEnd
  • ChildActivationEnd
  • NavigationEnd

我们一般在NavigationEnd事件中做一些事情。

1
2
3
4
5
router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
// do something
}
});

注意

即使使用了skipLocationChange选项,也可以通过以上方法获取完整的路由信息,因为skipLocationChange只是不改变浏览器地址栏的URL,但是路由信息还是会变化的。

获取路由信息

如果想要获取路由对应的url,路由中的参数等信息,可以在组件中注入ActivatedRoute服务。

使用ActivatedRoute.snapshot获取路由信息

1
2
// app.routers.ts
{ path: 'product/:id', component: ProductDetailComponent },
1
2
<!-- product.component.html -->
<p>This is product: {{id}}</p>
1
2
3
4
5
6
7
8
9
10
11
12
// product-detail.component.ts
export class ProductDetailComponent implements OnInit {
id: number = -1;
constructor(private activatedRoute: ActivatedRoute) {}

ngOnInit() {
const id = this.activatedRoute.snapshot.paramMap.get('id');
if (id) {
this.id = parseInt(id);
}
}
}

使用ActivatedRoute.params订阅路由变化

1
2
3
4
5
6
7
8
9
10
11
// product-detail.component.ts
export class ProductDetailComponent implements OnInit {
id: number = -1;
constructor(private activatedRoute: ActivatedRoute) {}

ngOnInit() {
this.activatedRoute.params.subscribe((params) => {
this.id = parseInt(params.id);
});
}
}

snapshop和params的区别:

  • snapshot只会获取一次路由信息,它是静态的,获取之后就不会再变化了。
  • params会订阅路由变化,当路由变化时,会重新获取路由信息。

params和queryParams的区别:

  • params: 获取路由中的参数,比如/product/1中的1
  • queryParams: 获取路由中的查询参数,比如/product?id=1&name=zdd中的id, name
  • matrixParameters: 获取路由中的矩阵参数,比如/product;id=1;name=zdd中的id, name

queryParams和matrixParameters的区别

  • queryParams: 使用?开头,使用&分隔每组参数,比如/product?id=1&name=zdd
  • matrixParameters: 使用;开头,使用;分隔每组参数,比如/product;id=1;name=zdd
  • 两者都以=分隔键值对。

路由守卫

待整理

路由传递数据

定义Router时发送数据

1
2
3
4
5
6
// app.routers.ts
{
path: 'product',
component: ProductComponent,
data: {category: 'digital'} // send data to component
},

组件中接收数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// product.component.ts
export class ProductComponent implements OnInit {
constructor(private activatedRoute: ActivatedRoute) {}
ngOnInit() {
const {category} = this.activatedRoute.snapshot.data;
console.log(category);

// or

this.activatedRoute.data.subscribe((data) => {
const {category} = data;
console.log(category);
})
}
}
1
this.router.navigate(['/product'], {state: {category: 'digital'}});

组件中接收数据

After Angular 16, must call this on constructor or you will get null, see here for detail

1
2
3
4
5
6
7
8
// product.component.ts
export class ProductComponent implements OnInit {
constructor(private router: Router) {
const state = this.router.getCurrentNavigation()?.extras.state;
const {category} = state || {category: 'none'};
console.log(category);
}
}

定义routerLink时发送数据

1
<a [routerLink]="['/product']" [state]="{category: 'digital'}">Product</a>

组件中接收数据

1
2
3
4
5
6
7
8
// product.component.ts
export class ProductComponent implements OnInit {
constructor(private router: Router) {
const state = this.router.getCurrentNavigation()?.extras.state;
const {category} = state || {category: 'none'};
console.log(category);
}
}

保持Fragment不丢失

Url中#后面紧跟的内容为Fragment,比如http://localhost:4200/book#b123中的b123就是Fragment

首先在Component中定义Fragment变量

1
2
3
export class AppComponent implements OnInit {
bookId = 'b123';
}

在routerLink中添加Fragment

1
<a [routerLink]="['book', {}]" [fragment]="bookId">Book</a>

在navigate中添加Fragment

1
this.router.navigate(['book'], {fragment: this.bookId});

激活后路由为:http://localhost:4200/book#b123

保持Fragment不变

当我们从一个带Fragment的路由跳转到其他路由时(比如辅助路由),Fragment会丢失。如果我们想要保持Fragment不变,可以在navigate中添加preserveFragment选项。

1
this.router.navigate([{outlets: {primary: 'book/book1/book2', detail: 'book-detail'}}, {preserveFragment: true}]);

此时路由变为:http://localhost:4200/book(detail:book-detail//list:book-list)#b123 - 注意Fragment在url末尾。

注意关闭辅助路由时,也要添加此选项,否则Fragment仍会丢失。

1
this.router.navigate([{outlets: {list: null}}], {preserveFragment: true}); 

此时路由变为:http://localhost:4200/book(detail:book-detail)#b123

1
this.router.navigate([{outlets: {detail: null}}], {preserveFragment: true});

此时路由变为:http://localhost:4200/book#b123

路由三要素

  1. 路由对应的路径及组件 - Routes配置文件
  2. 路由显示的位置 - Outlet
  3. 路由的触发方式
    • 点击触发,比如routerLink, button等。
    • 编程触发,比如navigatenavigateByUrl等。
    • 动态加载,比如动态创建组件并加载。

待商榷:

  1. 没有module时如何拆分router文件? - 可以直接使用路由module文件,不需要组件对应的module文件。目前standalone组件无法做到路由文件的拆分,必须有module支持。

https://www.angulararchitects.io/en/blog/routing-and-lazy-loading-with-standalone-components/

Angular中如何实现嵌套路由。

需求,一个Angular App,根路由包含三个组件,home, product, aboutproduct组件下有两个子路由product-listproduct-detail。如何实现这个需求呢?

首先定义路由文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { HelpComponent } from './help/help.component';
import { AbcComponent } from './home/abc/abc.component';
import { DefComponent } from './home/def/def.component';

const routes: Routes = [
{
path: 'home',
component: HomeComponent,

},
{
path: 'about',
component: AboutComponent,
},
{
path: 'product',
component: HelpComponent,
children: [
{
path: 'product-list',
component: AbcComponent,
},
{
path: 'product-detail',
component: DefComponent,
},
],
},
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

然后在template文件中定义导航链接:

1
2
3
4
5
6
7
<!-- app.component.html -->
<nav>
<a routerLink="/home">Home</a>
<a routerLink="/about">About</a>
<a routerLink="/product">Product</a>
</nav>
<router-outlet></router-outlet>

最后在根模块中引入路由模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// app.component.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
import { ProductComponent } from './product.component';

@NgModule({
declarations: [
AppComponent,
AboutComponent,
ProductComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}

product组件的模板文件中定义子路由导航:

1
2
3
4
5
6
<!-- product.component.html -->
<nav>
<a routerLink="/product/product-list">Product List</a>
<a routerLink="/product/product-detail">Product Detail</a>
</nav>
<router-outlet></router-outlet>

策略模式

策略模式是一种行为设计模式,它定义了一系列算法,将每个算法封装到一个类中,并使它们可以相互替换。策略模式让算法独立于使用它的客户端。

现实中的例子

假设我们经营一家电影院,目前是销售淡季,我们推出了各种折扣,学生票八折,儿童票九折,VIP会员半价。我们要设计一个系统,根据不同的折扣算法计算电影票的价格。

不假思索的你可能写下了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
enum UserType {
Regular,
Student,
Child,
VIP,
}

class MovieTicket {
private ticketPrice = 100;
private userType?: UserType;

setUserType(userType: UserType) {
this.userType = userType;
}

getPrice() {
switch (this.userType) {
case UserType.Student:
return this.ticketPrice * 0.8;
case UserType.Child:
return this.ticketPrice * 0.9;
case UserType.VIP:
return this.ticketPrice * 0.5;
default:
return this.ticketPrice;
}
}
}

const ticket = new MovieTicket();
ticket.setUserType(UserType.Student);
console.log(ticket.getPrice());

我们来看看这段代码有什么问题:

  1. 违反了开闭原则。如果我们要添加新的折扣类型,比如老年人票,就需要修改MovieTicket类,违反了开闭原则。
  2. 违反了单一职责原则。MovieTicket类不仅负责计算价格,还负责判断折扣类型,违反了单一职责原则。
  3. 耦合性太高,不易维护。如果折扣算法发生变化,需要修改MovieTicket类,不易维护。
  4. 其他类如果想要使用折扣算法的话,只能复制粘贴一份,导致代码重复。

下面我们通过策略模式来重构这段代码,策略模式的精髓就是将算法和使用算法的类解耦,使得算法可以独立于使用它的类进行变化。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
interface Discount {
getDiscount(): number;
}

class StudentDiscount implements Discount {
getDiscount(): number {
return 0.8;
}
}

class ChildrenDiscount implements Discount {
getDiscount(): number {
return 0.9;
}
}

class VIPDiscount implements Discount {
getDiscount(): number {
return 0.5;
}
}

class MovieTicket {
private ticketPrice = 100;
private discount?: Discount;

setDiscount(discount: Discount) {
this.discount = discount;
}

getPrice() {
if (this.discount) {
return this.ticketPrice * this.discount.getDiscount();
}

throw new Error("No discount set");
}
}

const ticket = new MovieTicket();
ticket.setDiscount(new StudentDiscount());
console.log(ticket.getPrice());

代码解析:

  1. 首先定义一个接口Discount,它有一个getDiscount方法,返回折扣值。所有具体的折扣类都要实现这个接口。
  2. 然后定义三个具体的折扣类:StudentDiscountChildrenDiscountVIPDiscount。在每个折扣类中,我们实现了getDiscount方法,返回不同的折扣值。
  3. 最后定义一个MovieTicket类,它有一个ticketPrice属性,表示电影票的价格,还有一个discount属性,表示折扣。我们定义了一个setDiscount方法,用来设置折扣。getPrice方法用来计算折扣后的价格。

这种实现使得折扣算法和使用折扣的类解耦,使得折扣算法可以独立于使用它的类进行变化。好处如下:

  1. 便于扩展,可以很容易地添加新的折扣类。只要这个类实现了Discount接口,就可以被MovieTicket类使用。
  2. 由于折扣算法和使用折扣的类解耦,所以折扣算法可以独立于使用它的类进行变化。如果折扣算法发生变化,只需要修改折扣类即可,不需要修改使用折扣的类, 比如儿童票从9折改成8折,只需要修改ChildrenDiscount类使其返回0.8即可。
  3. 如果有其他类想使用折扣算法的话,也可以直接使用。

以上代码其实使用了依赖注入的方式,MovieTicket类依赖于Discount接口,我们通过Setter的方式将具体的折扣类注入到MovieTicket类中。

注意这里discount属性是可选的,因为我们在getPrice方法中判断了discount是否存在,如果不存在就抛出异常。这样做是为了防止忘记设置折扣而导致的错误。如果不设置为可选的话会报错:Property 'discount' has no initializer and is not definitely assigned in the constructor. 由于TypeScript配置文件默认开启了strictPropertyInitialization选项导致的。

策略模式UML图

image

  • Context:消费策略的类相当于上例中的MovieTicket类,内涵一个抽象策略类,相当于上例中的Discount接口。
  • Strategy:抽象策略类,相当于上例中的Discount接口。
  • ConcreteStrategy:具体策略类,相当于上例中的StudentDiscountChildrenDiscountVIPDiscount类。
  • execute:执行策略的方法,相当于上例中的getDiscount方法。

策略模式的应用场景

  1. Sorting Algorithms: If you have a collection of data that can be sorted in several ways, you can use the Strategy pattern to switch between sorting algorithms (like quicksort, merge sort, heap sort) at runtime.
  2. Payment Methods: In an e-commerce application, you can use the Strategy pattern to switch between different payment methods (like credit card, PayPal, Bitcoin) at runtime.
  3. Compression Algorithms: If you have a file that can be compressed using different algorithms (like zip, rar, 7z), you can use the Strategy pattern to choose the compression algorithm at runtime.
  4. Travel Planning: If you have a travel planning application, you can use the Strategy pattern to switch between different travel strategies (like by car, by train, by plane) at runtime.

ES5

strict mode

1
"use strict";

JSON

1
2
3
const obj = { a: 1, b: 2, c: 3 };
const json = JSON.stringify(obj);
console.log(json); // {"a":1,"b":2,"c":3}

New array methods:

  • forEach
  • map
  • filter
  • reduce
  • reduceRight

Property getters and setters

1
2
3
4
5
6
7
8
9
10
11
const obj = {
get a() {
return this._a;
},
set a(value) {
this._a = value;
}
};

obj.a = 1;
console.log(obj.a); // 1

Function binding: bind

1
2
3
4
function foo() {
console.log(this);
}
foo.bind({ a: 1 });

ES6(ECMAScript 2015)

ES7(ECMAScript 2016)

Array.prototype.includes

1
2
const arr = [1, 2, 3];
console.log(arr.includes(2)); // true

Exponentiation Operator

1
console.log(2 ** 3); // 8

ES8(ECMAScript 2017)

Object.values

1
2
const obj = { a: 1, b: 2, c: 3 };
console.log(Object.values(obj)); // [1, 2, 3]

Object.entries

1
2
const obj = { a: 1, b: 2, c: 3 };
console.log(Object.entries(obj)); // [["a", 1], ["b", 2], ["c", 3]]

Object.getOwnPropertyDescriptors

1
2
3
4
5
6
7
const obj = { a: 1, b: 2, c: 3 };
console.log(Object.getOwnPropertyDescriptors(obj));
// {
// a: { value: 1, writable: true, enumerable: true, configurable: true },
// b: { value: 2, writable: true, enumerable: true, configurable: true },
// c: { value: 3, writable: true, enumerable: true, configurable: true }
// }

String padding

1
2
console.log("hello".padStart(10, "123")); // 12312hello
console.log("hello".padEnd(10, "123")); // hello12312

Trailing commas in function parameter lists and calls

1
2
3
4
function foo(a, b, c,) {
console.log(a, b, c);
}
foo(1, 2, 3);

ES9(ECMAScript 2018)

Asynchronous Iteration

ES10(ECMAScript 2019)

Array.prototype.flat()

1
2
const arr = [1, 2, [3, 4, [5, 6]]];
console.log(arr.flat()); // [1, 2, 3, 4, [5, 6]]

Array.prototype.flatMap()

1
2
3
const arr = [1, 2, 3];
console.log(arr.map(x => [x * 2])); // [[2], [4], [6]]
console.log(arr.flatMap(x => [x * 2])); // [2, 4, 6]

Object.fromEntries()

1
2
3
const obj = { a: 1, b: 2, c: 3 };
console.log(Object.entries(obj)); // [["a", 1], ["b", 2], ["c", 3]]
console.log(Object.fromEntries(Object.entries(obj))); // { a: 1, b: 2, c: 3 }

String.prototype.trimStart() and String.prototype.trimEnd()

1
2
3
const str = "  hello  ";
console.log(str.trimStart()); // "hello "
console.log(str.trimEnd()); // " hello"

Optional Catch Binding

1
2
3
4
5
try {
throw new Error("error");
} catch {
console.log("caught");
}

JSON.stringify() replacer parameter

The second arguments [“a”, “b”] is a replacer array, which specifies the properties to include in the JSON string.

1
2
const obj = { a: 1, b: 2, c: 3 };
console.log(JSON.stringify(obj, ["a", "b"])); // {"a":1,"b":2}

JSON.stringify() space parameter

The third argument 2 is a space parameter, which specifies the number of spaces to use for indentation.

1
2
3
4
5
6
7
const obj = { a: 1, b: 2, c: 3 };
console.log(JSON.stringify(obj, null, 2));
// {
// "a": 1,
// "b": 2,
// "c": 3
// }

The output is {"a":1,"b":2,"c":3} without the third argument.

ES11(ECMAScript 2020)

BigInt

1
2
3
4
5
const max = Number.MAX_SAFE_INTEGER;
console.log(max); // 9007199254740991
console.log(max + 1); // 9007199254740992
console.log(max + 2); // 9007199254740992
console.log(max + 3n); // 9007199254740994n

Dynamic import

1
const module = await import('./module.js');

Optional chaining operator (?.)

1
2
const obj = { a: { b: { c: 1 } } };
console.log(obj.a?.b?.c); // 1

Nullish coalescing operator (??)

  • leftExpr ?? rightExpr, this expression will return its right-hand side operand when the left-hand side operand is null or undefined. Otherwise, it will return its left-hand side operand.
  • leftExpr || rightExpr, return true if and only if one of the operands is true. Otherwise return false.
1
2
3
4
5
6
7
8
9
const obj = { a: 0, b: "", c: null, d: undefined };
console.log(obj.a || "default"); // default
console.log(obj.b || "default"); // default
console.log(obj.c || "default"); // default
console.log(obj.d || "default"); // default
console.log(obj.a ?? "default"); // 0
console.log(obj.b ?? "default"); // ""(empty string)
console.log(obj.c ?? "default"); // default
console.log(obj.d ?? "default"); // default

Promise.allSettled()

1
2
3
4
5
6
7
8
const promise1 = new Promise((resolve, reject) => setTimeout(resolve, 1000));
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 1000));
Promise.allSettled([promise1, promise2])
.then(results => console.log(results));
// [
// { status: "fulfilled", value: undefined },
// { status: "rejected", reason: undefined }
// ]

globalThis

1
console.log(globalThis);

String.prototype.matchAll()

1
2
3
4
5
6
const str = "hello world";
const regex = /\w+/g;
const matches = str.matchAll(regex);
for (const match of matches) {
console.log(match);
}

ES12(ECMAScript 2021)

Numeric separators

1
2
const num = 1_000_000;
console.log(num); // 1000000

String.prototype.replaceAll()

1
2
const str = "hello world";
console.log(str.replaceAll("o", "0")); // hell0 w0rld

Promise.any()

1
2
3
4
const promise1 = new Promise((resolve, reject) => setTimeout(reject, 1000));
const promise2 = new Promise((resolve, reject) => setTimeout(resolve, 500));
Promise.any([promise1, promise2])
.then(value => console.log(value));

WeakRef

1
2
3
4
let obj = { name: "John" };
const weakRef = new WeakRef(obj);
obj = null;
console.log(weakRef.deref()); // { name: "John" }

FinalizationRegistry

1
2
3
4
let obj = { name: "John" };
const finalizationRegistry = new FinalizationRegistry(key => console.log(key));
finalizationRegistry.register(obj, "custom key");
obj = null;

ES13(ECMAScript 2022)

Top-level await

Private instance fields, methods, and accessors

Static class fields and methods

Static class initialization blocks

Error.cause

Array, String, and TypedArray .at() method

Object.hasOwn()

The Object.hasOwn() static method returns true if the specified object has the indicated property as its own property. If the property is inherited, or does not exist, the method returns false.

1
2
3
4
const obj = { a: 1};
console.log(Object.hasOwn(obj, "a")); // true
console.log(Object.hasOwn(obj, "b")); // false
console.log(Object.hasOwn(obj, "toString")); // false

Object.hasOwn is intended as a replacement of Object.prototype.hasOwnProperty.

RegExp match /d flag

References:

Prefer const over let and var

Bad

1
2
3
4
var array = [1, 2, 3];
for (let num of array) {
console.log(num);
}

Good

1
2
3
4
const array = [1, 2, 3];
for (const num of array) {
console.log(num);
}

Note, If you access array by index, you can’t use const:

1
2
3
4
const array = [1, 2, 3];
for (let i = 0; i < array.length; i++) {
console.log(array[i]);
}

在JavaScript中,apply, call, bind是三个非常重要的方法,它们的一个重要作用是在调用函数时改变this的值。注意:箭头函数的this值无法修改。

  1. Function.prototype.apply()
  2. Function.prototype.call()
  3. Function.prototype.bind()

apply

假设我们有一个函数用来计算两数之和:

1
2
3
function add(a, b) {
return a + b;
}

我们可以正常调用这个函数:

1
add(1, 2); // 3

也可以使用apply方法来调用这个函数,第一个参数是this的值,第二个参数是一个数组,数组中的元素是函数的参数。这里我们不需要this,所以第一个参数传入null

1
add.apply(null, [1, 2]); // 3

call

call方法和apply方法类似,只是参数的传递方式不同,call方法的参数是一个个传递的,而不是通过数组传递。还是以上面的求和函数为例,使用call方法调用,第一个参数是this的值,后面的参数是函数的参数。这里我们不需要this,所以第一个参数传入null

1
add.call(null, 1, 2); // 3

bind

bind的调用方式与call类似,参数也是一个一个传递的。但是与callapply直接调用函数不同,bind会创建一个新的函数并返回该函数,所以在使用bind时,我们通常会用一个变量接收bind返回的函数,再通过这个变量调用函数。

1
2
3
fn.call(thisValue, arg1, arg2, ...);
fn.apply(thisValue, [arg1, arg2, ...]);
fn.bind(thisValue)(arg1, arg2, ...);

还记得前面的例子吗?将对象方法赋值给普通函数时,this指向了全局对象,而全局对象上没有name属性,所以输出undefined

1
2
3
4
5
6
7
8
9
const person = {
name: 'Philip',
sayName() {
console.log(this.name);
},
};

const sayName = person.sayName;
sayName(); // undefined

我们只要用bind修改一下this,让它绑定到person对象,就可以输出Philip了。

1
2
3
4
5
6
7
8
9
10
const person = {
name: 'Philip',
sayName() {
console.log(this.name);
},
};

// 通过sayName接收bind返回的函数。
const sayName = person.sayName.bind(person);
sayName(); // Philip

call, apply, bind的使用场景

我们知道,函数中有一个隐含对象arguments,包含了函数调用时传入的所有参数。它是一个类数组对象,我们无法对它使用数组的方法,比如map, filter, reduce等。但是我们可以通过call, apply, bind方法将数组的方法应用到arguments上。

比如下面这个求和函数,在函数体内部,我们可以使用reduce方法来计算所有参数的和。

1
2
3
function sum() {
return Array.prototype.reduce.call(arguments, (acc, cur) => acc + cur, 0);
}

注意,上面的例子仅作演示只用,一个更加规范的写法是:

1
2
3
function sum(...args) {
return args.reduce((acc, cur) => acc + cur, 0);
}

再看一个class中使用bind的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class CountDown {
count = 0;
interval = null;

constructor(count) {
this.count = count;
}

decrement() {
console.log(this.count);
this.count -= 1;
if (this.count < 0) {
console.log("Done");
clearInterval(this.interval);
}
}

start() {
this.interval = setInterval(this.decrement, 1000);
}
}

const foo = new CountDown(10);
foo.start();

上面的代码中,this.interval是一个定时器,每隔1秒调用this.decrement方法,但是setInterval这个函数比较特殊,当我们将一个函数传递给setInterval时,这个函数的this值会指向全局对象,所以this.count会输出undefined。详情请参考这里

解决方法有两种,一种是在start方法中使用bind方法,将this绑定到CountDown类。

1
2
3
start() {
this.interval = setInterval(this.decrement.bind(this), 1000);
}

另一种方法是将setInterval的回调函数改为箭头函数,因为箭头函数没有自己的this binding, 所以this指向其parent scope,也就是CountDown。个人推荐使用这种方法。

1
2
3
start() {
this.interval = setInterval(() => this.decrement(), 1000);
}

当然,也可以将decrement方法改为箭头函数,因为箭头函数没有自己的this binding, 所以this指向其parent scope,也就是CountDown

1
2
3
4
5
6
7
8
decrement = () => {
console.log(this.count);
this.count -= 1;
if (this.count < 0) {
console.log("Done");
clearInterval(this.interval);
}
}

Conclusion

  • applycall的作用是改变函数的this值,apply的参数是一个数组,call的参数是一个个传递的。这里有一个小窍门,applya开头的,数组array也是a开头的,所以apply的参数是一个数组。
  • bind传递参数的方法和call类似(这个经常被忽略),但是bind会创建一个新的函数并返回该函数,所以在使用bind时,我们通常会用一个变量接收bind返回的函数,再通过这个变量调用函数。

What is functional programming?

Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing state and mutable data.

Functional programming concepts

Pure functions

A function is called pure function if it returns the same result for the same arguments and has no side effects.

And here is the full definition of a pure function from WikiPedia

In computer programming, a pure function is a function that has the following properties

the function return values are identical for identical arguments (no variation with local static variables, non-local variables, mutable reference arguments or input streams, i.e., referential transparency), and
the function has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or input/output streams).

pure function

1
2
3
function add(a, b) {
return a + b;
}

non-pure function

In the following example, the calculateTax function depends on the external variable taxRate. This means that if taxRate changes, the output of calculateTax changes even if the input remains the same. Therefore, calculateTax is not a pure function.

1
2
3
4
const taxRate = 0.1;
function calculateTax(amount) {
return amount * taxRate;
}

Is the following function pure?

1
2
3
function mergeArray(arr1, arr2) {
return arr1.push(...arr2);
}

No, the function mergeArray is not pure because it modifies the arr1 array in place using the push method.

How to make the mergeArray function pure?

1
2
3
function mergeArray(arr1, arr2) {
return [...arr1, ...arr2];
}

Immutable data

In functional programming, data is immutable, which means it cannot be changed once it’s created. Instead of modifying existing data, we create new data with the desired changes.

Side effects

A function is said to have a side effect if it modifies some state or variable outside its scope.

1
2
3
4
5
6
7
8
let count = 0;

function incrementCount() {
count += 1;
}

incrementCount();
console.log(count); // Outputs: 1

In this example, the function incrementCount has a side effect because it modifies the global variable count. This is a side effect because it’s changing state outside of its own scope.

Referential transparency

A function is called referentially transparent if it can be replaced with its value without changing the program’s behavior.

1
2
3
4
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // Outputs: 5

In this example, the function add is referentially transparent because we can replace add(2, 3) with 5 without changing the program’s behavior.

First-class functions

In JavaScript, functions are first-class citizens. It means we can assign functions to variables, pass functions as arguments to other functions, and return functions from other functions.

function as variable

1
2
3
4
5
const add = function(a, b) {
return a + b;
};

console.log(add(2, 3)); // Outputs: 5

function as argument

1
2
3
4
5
6
7
8
const nums = [1, 2, 3, 4, 5];

function isEven(num) {
return num % 2 === 0;
}

const evens = nums.filter(isEven);
console.log(evens);

function as return value

1
2
3
4
5
6
7
8
function createGreeter() {
return function(name) {
return `Hello, ${name}!`;
};
}

const greet = createGreeter();
console.log(greet('John'));

Higher-order functions

A function that takes one or more functions as arguments or returns a function is called a higher-order function.
There are many functions in javascript that are higher-order functions, such as map, filter, reduce, forEach, sort, etc.

declarative vs imperative

Declarative programming is a programming paradigm that expresses the logic of a computation without describing its control flow. In contrast, imperative programming is a programming paradigm that uses statements that change a program’s state.

Example:
Given an array of numbers, multiply all even numbers by 10 and return the sum of them.

Imperative way:

1
2
3
4
5
6
7
8
9
10
const numbers = [1, 2, 3, 4, 5, 6];

let sum = 0;
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
sum += numbers[i] * 10;
}
}

console.log(sum); // 60

Declarative way:

1
2
3
4
5
6
7
8
const numbers = [1, 2, 3, 4, 5, 6];

const sum = numbers
.filter(num => num % 2 === 0)
.map(num => num * 10)
.reduce((acc, num) => acc + num, 0);

console.log(sum); // 60

Benefits of functional programming

  • Easy to test
  • Easy to run in parallel(concurrency)
  • Easy to cache

References

Wikipedia

Dependency injection(DI) - 是Angular的核心概念之一 is a design pattern that allows us to remove the hard-coded dependencies and make our application loosely coupled, maintainable, and testable. It is a way to achieve Inversion of Control (IoC) in our application.

DI is widely used in Angular, React, and other modern frameworks. In Angular, we can inject services, components, and other dependencies into our components, services, and directives.

Some terms:

  • Dependency 依赖
  • Injector 注入器
  • Registry 注册中心
  • DI - Dependency Injection
  • IoC - Inversion of Control

Ways to implement DI:

  • Constructor Injection
  • Setter Injection
  • Interface Injection

Angular Dependency Injection

Angular通过一个叫做Injector(注入器)的服务来实现DI。注入器相当于依赖的生产者和依赖的消费者之间的桥梁。Angular会在启动时创建一个应用级别的注入器(也叫root injector),当然也会创建其他的injector(如果需要的话)。

当需要一个依赖时,Angular首先会检查其注册中心是否有这个依赖,如果有则直接返回,如果没有则会根据依赖的类型创建一个实例并返回。同时将其加入到注册中心中,以便下次使用。

在依赖注入中有两个主要的角色,一个是依赖的生产者,比如用来获取后台数据的Service,一个是依赖的消费者,比如使用这个ServiceComponent。而Injector就是这两者之间的桥梁。有了Injector,依赖生产者和依赖消费者之间便实现了解耦。

Inversion of control

控制反转 - 到底怎么个反转呢?
在没有依赖注入之前,对象自己管理自己的依赖。

1
2
3
4
5
6
class Car {
private engine: Engine;
constructor() {
this.engine = new Engine(); // 直接创建依赖
}
}

有了依赖注入之后,对象不在负责依赖的管理,而是交给Angular框架来管理,把管理依赖的职责由对象转移到外层框架,依赖的查找、注入都由框架负责,这就是控制反转。

1
2
3
4
5
6
7
8
@Component({
selector: 'app-car',
template: '...',
providers: [Engine] // 依赖配置在外部, 如果依赖有{provideIn: 'root'},则不需要配置这里
})
class CarComponent {
constructor(private engine: Engine) {} // 依赖由 Angular 注入
}

Provide a dependency

在Angular中,有三种方式提供一个依赖。

provideIn

创建依赖

1
2
3
4
@Injectable({
providedIn: 'root'
})
class HeroService {}

使用依赖

1
constructor(private heroService: HeroService) {}

使用provideIn:'root'方式提供的依赖有如下特点:

  1. 该依赖是全局共享的的,只有一个实例。
  2. 所有组件、指令、服务等都可以使用该依赖。
  3. 支持tree shaking, 如果该依赖没有被使用,则不会被打包到最终的bundle中。

providers

创建依赖

1
2
@Injectable() // <--- 注意这里没有provideIn
class HeroService {}

使用依赖

1
2
3
4
@NgModule({
providers: [HeroService]
})
class AppModule {}

使用providers方式提供的依赖有如下特点:

  1. 每个使用该依赖的模块对应的实例都会创建一个该依赖的实例。
  2. 该依赖是局部的,只有在提供了providers的模块中才能使用。
  3. 不支持tree shaking, 无论是否使用,都会被打包到最终的bundle中。
  4. 此依赖实例的生命周期和组件相同,当组件销毁时,该依赖实例也销毁。

注意,如果当前组件引入了一个依赖,比如一个Service,那么这个依赖:

  • 在当前模块可用
  • 在当前模块的模板中的所有组件中也可用

举个例子:当前组件是ProductComponent,其模板中引用了ProductDetailComponent。ProductComponent引入了一个ProductService,那么:

ApplicationConfig

创建依赖

1
2
@Injectable() // <--- 注意这里没有provideIn
class HeroService {}

使用依赖

1
2
3
4
5
6
export const appConfig: ApplicationConfig = {
providers: [
{ provide: HeroService },
]
};
bootstrapApplication(AppComponent, appConfig)

使用ApplicationConfig方式提供的依赖有如下特点:

  1. 该依赖是全局共享的的,只有一个实例。
  2. 所有组件、指令、服务等都可以使用该依赖。
  3. 不支持tree shaking, 无论是否使用,都会被打包到最终的bundle中。

此方式与providedIn: 'root'颇为相似,不同的是这种方式不支持tree shaking。

NgModule based configuration

创建依赖

1
2
@Injectable() // <--- 注意这里没有provideIn
class HeroService {}

使用依赖

1
2
3
4
@NgModule({
providers: [HeroService]
})
class AppModule {}

A service provided in a module is available to all declarations of the module, or to any other modules which share the same ModuleInjector. To understand all edge-cases, see Hierarchical injectors.

Pros of DI:

  • Loose coupling
  • Easy to test
  • Easy to maintain

references:

JavaScript Tricks

1. typeof null === 'object'. This is a known bug in JavaScript.

2. typeof NaN === 'number'. But, this is true.

3. ... perform a shallow copy(only the first level is copied by value, other levels is copied by reference).

1
2
3
4
5
6
7
8
const arr = [1, 2, [3, 4]];
const newArr = [...arr];

newArr[1] = 3;
console.log(arr); // [1, 2, [3, 4]]

newArr[2][0] = 5;
console.log(arr); // [1, 2, [5, 4]]

4. There is no pass by reference in JavaScript, only pass by value.

5. In JavaScript, everything is object - this is not true.

6. type print or window.print in Browser console, you will get a print dialog.

7. |0 can be used to remove the fractional part of a number.

It has the same effect as Math.floor().

1
2
3
const num = 3.14;
console.log(num | 0); // 3
console.log(Math.floor(num)); // 3

8. !! can be used to convert a value to a boolean.

1
2
3
const a = 1;
console.log(!!a); // true
console.log(!!0); // false

9. How to quit node.js REPL?

1
> .exit
1
> process.exit()