0%

Introduction

Angular app build后会生成如下文件结构,其中main/polyfills/runtime文件中的xxx是Angular随机生成的hash值。

1
2
3
4
5
6
7
8
9
10
dist/
├─ angular-15/
│ ├─ assets/
│ ├─ 3rdpartylicenses.txt
│ ├─ favicon.ico
│ ├─ index.html
│ ├─ main.xxx.js
│ ├─ polyfills.xxx.js
│ ├─ runtime.xxx.js
│ ├─ styles.xxx.css

今天我们来分析一下runtime.js文件的内容。(注意,由于Angular编译后的代码是压缩过的,所以以下源码是使用ng server命令本地启动app后在浏览器中查看的。)

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({});
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
/******/
/************************************************************************/
/******/ /* webpack/runtime/chunk loaded */
/******/ (() => {
/******/ var deferred = [];
/******/ __webpack_require__.O = (result, chunkIds, fn, priority) => {
/******/ if(chunkIds) {
/******/ priority = priority || 0;
/******/ for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
/******/ deferred[i] = [chunkIds, fn, priority];
/******/ return;
/******/ }
/******/ var notFulfilled = Infinity;
/******/ for (var i = 0; i < deferred.length; i++) {
/******/ var [chunkIds, fn, priority] = deferred[i];
/******/ var fulfilled = true;
/******/ for (var j = 0; j < chunkIds.length; j++) {
/******/ if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {
/******/ chunkIds.splice(j--, 1);
/******/ } else {
/******/ fulfilled = false;
/******/ if(priority < notFulfilled) notFulfilled = priority;
/******/ }
/******/ }
/******/ if(fulfilled) {
/******/ deferred.splice(i--, 1)
/******/ var r = fn();
/******/ if (r !== undefined) result = r;
/******/ }
/******/ }
/******/ return result;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => ("14f1d830efa2381d")
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/jsonp chunk loading */
/******/ (() => {
/******/ // no baseURI
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "runtime": 0
/******/ };
/******/
/******/ // no chunk on demand loading
/******/
/******/ // no prefetching
/******/
/******/ // no preloaded
/******/
/******/ // no HMR
/******/
/******/ // no HMR manifest
/******/
/******/ __webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);
/******/
/******/ // install a JSONP callback for chunk loading
/******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
/******/ var [chunkIds, moreModules, runtime] = data;
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0;
/******/ if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
/******/ for(moduleId in moreModules) {
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(runtime) var result = runtime(__webpack_require__);
/******/ }
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/ installedChunks[chunkId][0]();
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ return __webpack_require__.O(result);
/******/ }
/******/
/******/ var chunkLoadingGlobal = self["webpackChunkangular_15"] = self["webpackChunkangular_15"] || [];
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
/******/ })();
/******/
/************************************************************************/
/******/
/******/
/******/ })()
;
//# sourceMappingURL=runtime.js.map

Introduction

字符串操作是JS中非常常见的操作,本文将介绍JS中搜索字符串的几种方法。

判断是否包含某个字符串

String.prototype.indexOf()

在ES5中,我们通常使用indexOf方法来查找字符串中的子串。indexOf方法返回给定子串的第一个索引,如果不存在,则返回 -1indexOf方法的语法如下:

1
2
indexOf(searchValue)
indexOf(searchValue, fromIndex)

String.prototype.includes()

includes方法是ES6引入的,相比indexOf方法,includes方法更加直观。includes方法用于判断一个字符串是否包含另一个字符串。includes方法的语法如下:

String.prototype.startsWith()

startsWith也是ES6引入的方法,用于判断一个字符串是否以另一个字符串开头。startsWith方法的语法如下:

String.prototype.endsWith()

endsWith也是ES6引入的方法,用于判断一个字符串是否以另一个字符串结尾。endsWith方法的语法如下:

includes, startsWith, endsWith方法都可以传递第二个参数,表示从指定位置开始搜索。个人感觉没有必要。

Introduction

今天我们一起研究一下Angular App是如何运行的,今天主要讨论的是在生产环境下,Angular App是如何运行的。既然限定了生产环境,那么就不讨论开发环境相关的东西了,我们直接看编译后的代码如何运行。

Create Angular app

首先,我们需要创建一个Angular项目,这里我们使用Angular CLI来创建一个项目,具体的步骤可以参考这篇文章

Build Angular App

然后,我们使用ng build --prod命令来构建我们的Angular项目,这个命令会将我们的项目编译成生产环境下的代码。编译完成后,我们会得到一个dist目录,里面包含了我们的项目的所有文件,如下图所示:

dist

众所周知,index.html是前端项目的入口文件,我们看看这个文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Angular15</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.ef46db3751d8e999.css">
</head>
<body>
<app-root></app-root>
<script src="runtime.17ade4109dbace19.js" type="module"></script>
<script src="polyfills.7488d7e9e1657922.js" type="module"></script>
<script src="main.301f0813b18ba60a.js" type="module"></script>
</body>
</html>

该文件主要执行以下操作:

  1. 加载styles.ef46db3751d8e999.css样式文件 - 这是项目打包后的样式文件。
  2. 加载runtime.17ade4109dbace19.js文件 - 这个文件的作用是初始化webpack相关的设置,比如加载模块的方法,为后续的代码运行做准备。
  3. 加载polyfills.7488d7e9e1657922.js文件 - 这个文件主要是加载一些polyfills,用来兼容一些老的浏览器,当然也做一些额外的操作,比如zone.js的monkey patch。
  4. 加载main.301f0813b18ba60a.js文件 - 这个文件是我们的项目的主要代码,我们书写的所有组件,服务等代码都会被编译到这个文件。

由于runtime/polyfills/main这三个文件打包后都是混淆的,我们无法查看其代码,所以我们还是借助开发环境,使用ng serve命令来查看这三个文件的内容。

以下是runtime.js的内容:
该文件主要是初始化一些Webpack的相关函数,用来为后续加载polyfillsmain文件做准备。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({});
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
/******/
/************************************************************************/
/******/ /* webpack/runtime/chunk loaded */
/******/ (() => {
/******/ var deferred = [];
/******/ __webpack_require__.O = (result, chunkIds, fn, priority) => {
/******/ if(chunkIds) {
/******/ priority = priority || 0;
/******/ for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
/******/ deferred[i] = [chunkIds, fn, priority];
/******/ return;
/******/ }
/******/ var notFulfilled = Infinity;
/******/ for (var i = 0; i < deferred.length; i++) {
/******/ var [chunkIds, fn, priority] = deferred[i];
/******/ var fulfilled = true;
/******/ for (var j = 0; j < chunkIds.length; j++) {
/******/ if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {
/******/ chunkIds.splice(j--, 1);
/******/ } else {
/******/ fulfilled = false;
/******/ if(priority < notFulfilled) notFulfilled = priority;
/******/ }
/******/ }
/******/ if(fulfilled) {
/******/ deferred.splice(i--, 1)
/******/ var r = fn();
/******/ if (r !== undefined) result = r;
/******/ }
/******/ }
/******/ return result;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => ("14f1d830efa2381d")
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/jsonp chunk loading */
/******/ (() => {
/******/ // no baseURI
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "runtime": 0
/******/ };
/******/
/******/ // no chunk on demand loading
/******/
/******/ // no prefetching
/******/
/******/ // no preloaded
/******/
/******/ // no HMR
/******/
/******/ // no HMR manifest
/******/
/******/ __webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);
/******/
/******/ // install a JSONP callback for chunk loading
/******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
/******/ var [chunkIds, moreModules, runtime] = data;
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0;
/******/ if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
/******/ for(moduleId in moreModules) {
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(runtime) var result = runtime(__webpack_require__);
/******/ }
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/ installedChunks[chunkId][0]();
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ return __webpack_require__.O(result);
/******/ }
/******/
/******/ var chunkLoadingGlobal = self["webpackChunkangular_15"] = self["webpackChunkangular_15"] || [];
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
/******/ })();
/******/
/************************************************************************/
/******/
/******/
/******/ })()
;
//# sourceMappingURL=runtime.js.map

注意代码末尾的self["webpackChunkangular_15"], 这里angular_15是我们的项目名称,webpackChunk是前缀。self是浏览器Window对象的一个属性。它就指向Window对象本身。详情参考这里:https://developer.mozilla.org/en-US/docs/Web/API/Window/self

其余代码我就不细说了,大伙有空自己看吧,都比较简单。

Introduction

JS中Object类型转换为Primitive类型的过程,称为ToPrimitive操作。这篇文章主要介绍一下这个过程。

转换流程

  1. 调用对象的[Symbol.toPrimitive]方法,如果返回的是Primitive类型,则返回。
  2. 否则,调用对象的valueOf方法,如果返回的是Primitive类型,则返回。
  3. 否则,调用对象的toString方法,如果返回的是Primitive类型,则返回。
  4. 否则,抛出TypeError异常。

[]{}

注意:[]{}在转换为Primitive类型时,会有很大的不同。

  1. 转换为Number类型时,[]会转换为0{}会转换为NaN
  2. 转换为String类型时,[]会转换为''{}会转换为'[object Object]'
  3. 转换为Boolean类型时,[]会转换为false{}会转换为true

之所以造成这么大的区别就是因为[]{}的和toString()方法的返回值不同。

  • []toString()方法返回的是'',而valueOf()方法返回的是[]
  • {}toString()方法返回的是'[object Object]',而valueOf()方法返回的是{}

因为[]能转换为空字符串,而空字符串可以转换为0false,所以[]能转换为0false。而{}则不能。

References

  1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#type_coercion
  2. https://tc39.es/ecma262/multipage/abstract-operations.html#sec-toprimitive

Angular更新检测是自动执行的,但是有些情况下我们需要手动触发更新检测,Angular提供以下方法来手动触发更新检测:

  • detectChanges(): Forces an immediate check for the component and its children.
  • markForCheck(): Marks the component to be checked in the next cycle (useful with OnPush).
  • detach()/reattach(): Temporarily disables or re-enables change detection for a component.

When to use detectChanges

The follow cases are when you should use detectChanges:

1. When change detector is detached from current component.

1
2
3
4
5
6
7
8
ngDoCheck(): void {
if (this.currentAge! < 50) {
this.cdf.detach(); // stop change detection
this.cdf.markForCheck(); // not work, use detectChanges instead.
} else {
this.cdf.reattach(); // restore change detection
}
}

2. An update is happened, but its not in Angular zone, for example: 3rd party libraries.

1
2
3
4
myFunction(){
someFunctionThatIsRunByAThirdPartyCode();
this.cd.detectChanges();
}

Note that we can also fix this by wrapping the third party code in setTimeout or NgZone.run:

1
2
3
4
5
6
myFunction(){
setTimeout(() => {
someFunctionThatIsRunByAThirdPartyCode();
this.cd.detectChanges();
});
}
1
2
3
4
5
myFunction(){
this.ngZone.run(() => {
someFunctionThatIsRunByAThirdPartyCode();
});
}

Angular has monkey patched setTimeout, and will do the change detection after the setTimeout is finished.

3. There are also cases where you update the model after the change detection cycle is finished, where in those cases you get this dreaded error: "Expression has changed after it was checked";

When to use markForCheck

The most common case to use markForCheck is when your component use OnPush change detection strategy and you want to trigger change detection for the component and its ancestors.

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
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit} from '@angular/core';
import 'reflect-metadata';
import {ProductDetailComponent} from "../product-detail/product-detail.component";
import {CardComponent} from "../card/card.component";
import {HoverDirective} from "../hover.directive";
import {Dir1Directive} from "../dir1.directive";
import {OrderService} from "../order.service";
import {Dir2Directive} from "../dir2.directive";
import {ProductItem} from "./product.model";

@Component({
selector: 'app-product',
standalone: true,
providers: [],
templateUrl: './product.component.html',
styleUrl: './product.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush, // OnPush mode
})
export class ProductComponent implements OnInit {
product: ProductItem | null = null;

constructor(private orderService: OrderService, private cdf: ChangeDetectorRef) {
}

ngOnInit(): void {
this.orderService.fetchData(1).then(product => {
this.product = product;
this.cdf.markForCheck(); // <--- must
})
}
}

Difference between detectChanges and markForCheck

  1. detectChanges triggers change detection for the component and its children.
  2. markForCheck marks the component and its ancestors for change detection, but it doesn’t trigger change detection immediately.
  3. detectChanges still work even when change detector is detached, but markForCheck doesn’t.

References

  1. https://angular.dev/api/core/ChangeDetectorRef#markForCheck

在Angular中,我们可以通过useClassuseValueuseFactoryuseExisting等属性来指定依赖注入的提供者,但是这些属性只能指定一个提供者,如果我们需要指定多个提供者,该怎么办呢?这时候就需要使用multi属性了。

比如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
import { InjectionToken, NgModule } from '@angular/core';

export const MY_MULTI_TOKEN = new InjectionToken<string[]>('MyMultiToken');

@NgModule({
providers: [
{ provide: MY_MULTI_TOKEN, useValue: 'Value1', multi: true },
{ provide: MY_MULTI_TOKEN, useValue: 'Value2', multi: true },
],
})
export class AppModule { }

对于MY_MULTI_TOKEN,我们多次使用useValue属性来指定提供者,然后通过multi: true来指定这是一个多提供者。Angular最终会将这些值放到一个数组中返回。所以MY_MULTI_TOKEN最终的值是['Value1', 'Value2']

Angular内置的ROUTEStoken也是multi的,从它的定义就可以看出来,它返回的是Route[]的数组。

1
export declare const ROUTES: InjectionToken<Route[][]>;

指定前端项目的Node版本

可以在package.json中指定前端项目的Node版本,这样其他人在安装依赖时就会自动安装指定版本的Node。

以下配置要求Node版本大于等于14.0.0。

1
2
3
4
5
{
"engines": {
"node": ">=14.0.0"
}
}

也可以指定更加复杂的版本:

1
2
3
"engines": {
"node": "^18.13.0 || ^20.9.0"
},

如果Node版本不符合要求,那么在运行npm install时会报错:

1
error angular-realworld@0.0.0: The engine "node" is incompatible with this module. Expected version "^18.13.0 || ^20.9.0". Got "22.12.0"

关于版本好,这里多说几句,Node.js采用的是SemVer规范,即Semantic Versioning,版本号由三部分组成:major.minor.patch,例如20.0.0

  • ^ - 表示锁定主版本号,例如^20.0.0表示只能更新到20.x.x版本。
  • ~ - 表示锁定主/次版本号,例如~20.9.0表示只能更新到20.9.x版本。

Introduction

搜索数组中的元素是我们在日常开发中经常遇到的问题,本文将介绍 JavaScript 中搜索数组元素的几种方法。

Array.prototype.indexOf()

在ES6之前,我们通常使用indexOf方法来查找数组中的元素的下标。此函数多用于判断数组中是否存在某元素。
Array.prototype.indexOf() 方法返回给定元素的第一个索引,如果不存在,则返回 -1indexOf方法的语法如下:

1
2
indexOf(searchElement)
indexOf(searchElement, fromIndex)
  • searchElement:要查找的元素。
  • fromIndex:从该索引处开始查找。如果该值大于或等于数组的长度,则 indexOf 返回 -1,表示未找到。
1
2
3
const arr = [1, 2, 3, 4, 5];
const result = arr.indexOf(3);
console.log(result); // output: 2

indexOf的弊端是只能查找有确定值的元素,无法按条件查找,比如查找大于3的元素下标。

Array.prototype.find()

ES6引入了find方法,相比indexOf方法,find方法使用一个条件函数来查找数组中的元素。
Array.prototype.find() 方法返回数组中满足条件的第一个元素的值。如果找不到责则返回 undefinedfind一旦找到元素,立即停止查找并返回。find方法的语法如下:

1
2
find(callbackFn)
find(callbackFn, thisArg)

callbackFn 函数接收三个参数:

  • element:数组中当前正在处理的元素。
  • index:数组中当前正在处理的元素的索引。
  • array:调用 find 方法的数组。

以下代码查找数组中等于3的元素。

1
2
3
const arr = [1, 2, 3, 4, 5];
const result = arr.find(item => item === 3);
console.log(result); // output: 3

以下代码查找数组中大于3的元素。

1
2
3
const arr = [1, 2, 3, 4, 5];
const result = arr.find(item => item > 3);
console.log(result); // output: 4

由此可见find相比indexOf更加灵活:

  1. indexOf只能查找元素的下标,而find可以查找元素本身。
  2. indexOf只能查找有确定值的元素下标,而find可以按条件查找。

如果想按条件查找元素的下标该怎么办呢?这时候就需要用到findIndex方法。

Array.prototype.findIndex()

Array.prototype.findIndex() 该方法与find方法类似,只不过它不是返回元素,而是返回元素的下标。找不到则返回 -1findIndex一旦找到一个匹配,立即停止查找并返回。findIndex方法的语法如下:

1
2
findIndex(callbackFn)
findIndex(callbackFn, thisArg)

callbackFn 函数接收三个参数:

  • element:数组中当前正在处理的元素。
  • index:数组中当前正在处理的元素的索引。
  • array:调用 findIndex 方法的数组。
1
2
3
const arr = [1, 2, 3, 4, 5];
const result = arr.findIndex(item => item > 3);
console.log(result); // output: 3

Array.prototype.includes()

Array.prototype.includes() 方法用来判断一个数组是否包含一个指定的值,如果是返回 true,否则返回 falseincludes方法的语法如下:

1
2
includes(searchElement)
includes(searchElement, fromIndex)
  • searchElement:要查找的元素。
  • fromIndex:从该索引处开始查找。如果该值大于或等于数组的长度,则 includes 返回 false,表示未找到。
1
2
3
const arr = [1, 2, 3, 4, 5];
const result = arr.includes(3);
console.log(result); // output: true

注意:includes方法只能判断特定值,而不能按条件判断,比如判断数组中是否有大于3的元素,include做不到。

总结

比较方式:

  1. findfindIndex 方法是通过回调函数来判断元素是否满足条件。
  2. indexOf使用strict equal ===来判断元素是否相等。
  3. includes方法是通过 SameValueZero 算法来判断元素是否相等。

Sparsed Array的处理方式:

  1. indexOf会跳过稀疏数组中的空位。
  2. findIndexincludes 方法不会跳过稀疏数组中的空位。

undefinedNaN的处理方式:

  1. indexOf方法无法正确处理undefinedNaN
    1
    2
    [NaN].indexOf(NaN); // output: -1
    Array(1).indexOf(undefined); // output: -1
  2. includes方法可以正确处理undefinedNaN
    1
    2
    [NaN].includes(NaN); // true
    Array(1).includes(undefined); // true
  3. find/findIndex方法可以正确处理undefinedNaN吗?这取决于回调函数的具体实现。
    1
    2
    [NaN].find(x => Number.isNaN(x)); // OK
    Array(1).findIndex(x => x === undefined); // OK

使用场景

  1. 如果只需判断某元素是否在数组中,使用includes方法。
  2. 如果需要对找到的元素做进一步处理,那么使用find方法。
  3. 如果需要按确定值查找元素下标,使用indexOf方法。
  4. 如果需要根据条件查找数组中的元素的下标,使用findIndex方法。

Introduction

有时候我们需要对一组数据进行相同的测试,这时候可以使用it.each

Syntax

1
2
3
4
5
6
7
8
9
10
const info = [
{name: 'zdd', age: 18},
{name: 'zdd1', age: 19},
{name: 'zdd2', age: 20},
];

it.each(info)('test %s', (name, age) => {
expect(name).toMatch(/zdd/);
expect(age).toBeGreaterThan(17);
});

Introduction

ES6以后有了class关键字,可以方便地实现类的继承。但是JavaScript是一门单继承的语言,即一个类只能继承一个类。但是有时候我们需要多继承,这时候我们可以使用混入(mixin)来实现多继承。

Mixin

Mixin是一种实现多继承的方式,即将多个类的方法混入到一个类中。下面是一个简单的Mixin实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function mix(...mixins) {
class Mix {}
for (let mixin of mixins) {
Object.assign(Mix.prototype, mixin);
}
return Mix;
}

// 定义两个mixin
const Flyable = {
fly() { console.log("I can fly!"); }
};

const Swimmable = {
swim() { console.log("I can swim!"); }
};

// 创建一个使用mixin的类
class Duck extends mix(Flyable, Swimmable) {}

let duck = new Duck();
duck.fly(); // 输出: I can fly!
duck.swim(); // 输出: I can swim!

Composition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class FlyBehavior {
fly() { console.log("Flying with wings!"); }
}

class SwimBehavior {
swim() { console.log("Swimming!"); }
}

class Duck {
constructor() {
this.flyBehavior = new FlyBehavior();
this.swimBehavior = new SwimBehavior();
}

performFly() { this.flyBehavior.fly(); }
performSwim() { this.swimBehavior.swim(); }
}

let duck = new Duck();
duck.performFly(); // 输出: Flying with wings!
duck.performSwim(); // 输出: Swimming!