0%

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!

Introduction

arguments是一个类数组对象,包含了函数调用时传入的所有参数。arguments对象只有在函数内部才可以使用。

访问arguments对象

1
2
3
4
5
6
function add(a, b) {
console.log(arguments[0], arguments[1]);
return a + b;
}

add(1, 2); // output: 1 2

通过arguments对象可以改变函数参数的值

  1. 改变arguments时,函数参数的值也会改变。
1
2
3
4
5
6
function add(a, b) {
arguments[0] = 10;
console.log(a);
}

add(1, 2); // output: 10
  1. 改变函数参数时,arguments对象的值也会改变。
1
2
3
4
5
6
function add(a, b) {
a = 10;
console.log(arguments[0]);
}

add(1, 2); // output: 10

Introduction

try

catch

JavaScript中的catch不能按类型捕获异常,只能捕获所有异常。如果需要按类型捕获异常,可以使用if语句判断异常类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
myFunction();
} catch (e) {
if (e instanceof ReferenceError) {
console.error(`ReferenceError:${e.message}`);
} else if (e instanceof TypeError) {
console.error(`TypeError: ${e.message}`);
} else if (e instanceof SyntaxError) {
console.error(`SyntaxError: ${e.message}`);
} else if (e instanceof Error) {
console.error(`Error: ${e.message}`);
}
}

Java等其他类型的语言不同,JS不能像下面这样捕获异常。JS中一个try只能对应一个catch

1
2
3
4
5
6
7
8
9
10
11
try {
myFunction();
} catch (ReferenceError) {
console.error(`ReferenceError:${e.message}`);
} catch (TypeError) {
console.error(`TypeError: ${e.message}`);
} catch (SyntaxError) {
console.error(`SyntaxError: ${e.message}`);
} catch (Error) {
console.error(`Error: ${e.message}`);
}

finally

returnfinally同时存在时,finally会在return之前执行。

1
2
3
4
5
6
7
function test() {
try {
return 1;
} finally {
console.log('finally');
}
}

Introduction

Arrow function couldn’t be a constructor

Arrow function doesn’t have its own this binding

Arrow function has no prototype property

Arrow function has no arguments object

Arrow function has no hoisting.

Arrow function is not suitable for call, apply, bind methods

Introduction

JavaScript模板字符串是ES6新增的一种字符串形式,它可以让我们在字符串中插入变量,而不需要使用+号连接字符串,这样使得字符串的拼接更加简洁。以下是模板字符串的特点。

Interpolation

模板字符串中可以插入变量,使用${}包裹变量名。

1
2
3
const name = 'zdd';
const str = `Hello, ${name}`;
console.log(str); // Hello, zdd

Multiple lines of string

模板字符串可以跨行,这样我们就可以直接在字符串中换行,而不需要使用\n

1
2
3
const str = `Hello
World`;
console.log(str);

Tagged template

模板字符串还支持标签模板,即在模板字符串前面加上一个函数名,这个函数会处理模板字符串的内容。所以,这个模板字符串的应用场景是什么?在项目中好像没见过这么用的。

1
2
3
4
5
6
7
8
9
function tag(strings, ...values) {
console.log(strings); // ['Hello, ', '']
console.log(values); // ['zdd']
return 'Hello, ' + values[0];
}

const name = 'zdd';
const str = tag`Hello, ${name}`;
console.log(str); // Hello, zdd

Introduction

There are some questions about RxJS that are often asked in interviews. Let’s take a look at them.

What’s the difference between Promise and Observable?

  1. Promise是一对一的,而Observable可以是一对多的。(一个Observer可以有多个订阅者)
  2. Promise不能取消(配合某些Library也许可以),而Observable可以。
  3. Promise定义后立即执行,而Observable是惰性的。订阅后才执行。
  4. Promise可以配合async/await使用,而Observable不行。
  5. Promise没有操作符,而Observable有很多操作符,比如map,retry等,在处理复杂请求时更加方便。

Introduction

ngDoCheck runs before every time Angular checks a component’s template for changes.

常见的误解

很多人误以为,只要这个函数调用了,那么就证明Angular对当前组件进行了变更检测,这是一个常见的误解。注意看这个函数的定义:它是在每次Angular检查组件的模板变化之前运行的。所以不能以这个函数的调用作为Angular进行了变更检测的依据。

我们来看一个实际的例子:

  1. 一个父组件ParentComponent, 采用ChangeDetectionStrategyDefault模式
  2. 一个子组件ChildComponent,采用ChangeDetectionStrategy.OnPush模式
  3. 当父组件处理点击事件时,子组件的ngDoCheck函数会被调用吗?
  4. 再添加一个孙子组件GrandChildComponent,采用ChangeDetectionStrategy.OnPush模式,当父组件处理点击事件时,孙子组件的ngDoCheck函数会被调用吗?

ParentComponent采用默认的ChangeDetectionStrategyDefault模式,并且添加了一个按钮,当我们点击这个按钮时,Angular会触发变更检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// parent.component.ts
@Component({
selector: 'app-parent',
standalone: true,
imports: [
ChildComponent
],
template: `
<p>parent works!</p>
<app-child />
<button (click)="onParentButtonClick()">Parent</button>
`,
styleUrl: './parent.component.scss'
})
export class ParentComponent {
onParentButtonClick() {
console.log(`click in ParentComponent`);
}
}

ChildComponent采用ChangeDetectionStrategy.OnPush模式,只有当Input属性发生变化时,或者响应自身事件,或者手动触发了变更检测时,ngDoCheck函数才会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// child.component.ts
@Component({
selector: 'app-child',
standalone: true,
imports: [
GrandChildComponent
],
template: `
<p>child works!</p>
<app-grand-child />
`,
styleUrl: './child.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChildComponent implements DoCheck {
ngDoCheck(): void {
console.log(`ngDoCheck in ChildComponent`);
}
}

GrandChildComponent同样采用了ChangeDetectionStrategy.OnPush模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// grand-child.component.ts
@Component({
selector: 'app-grand-child',
standalone: true,
imports: [],
template: `<p>grand-child works!</p>`,
styleUrl: './grand-child.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GrandChildComponent implements DoCheck {
ngDoCheck(): void {
console.log(`ngDoCheck in GrandChildComponent`);
}
}

当我们点击Parent按钮时,控制台输出包含如下一行,说明ChildComponent的ngDoCheck函数被调用了。

1
ngDoCheck in ChildComponent

可是我们明明指定了ChangeDetectionStrategy.OnPush模式,为什么会调用ngDoCheck函数呢?难道OnPush模式失效了吗?

其实不然,正如前面所说的,ngDoCheck函数是在每次Angular检查组件的模板变化之前运行的。所以,即使ChildComponent采用了ChangeDetectionStrategy.OnPush模式,ngDoCheck也被调用了,但是这并不意味着Angular对ChildComponent进行了变更检测。

这种情况只发生在OnPush根组件上,上面的GrandChildComponent并没有被调用,因为它是ChildComponent的子组件,所以它的ngDoCheck不会调用。

有此类行为的生命周期函数还有ngAfterViewChecked,无论Angular是否进行了变更检测,这个函数都会被调用。

如何确定Angular是否进行了变更检测?

对于一个组件来说,我如何确定Angular是否对它进行了变更检测呢?这个问题,其实困扰了我很久,以前我一直以为ngDoCheck函数的调用就是Angular进行了变更检测的标志,由上面的结论可知,这是不准确的。而其他生命周期函数也无法准确的告诉我们Angular是否进行了变更检测。

真的没办法了吗?

有的!其实之所以有这个困惑,还是对Angular变更检测理解不够深入,Angular的变更检测到底做了什么?其中必然有一个步骤是对template进行检查,如果template中绑定的值发生了变化,那么Angular就会更新视图。所以,我们可以通过template是否发生变化来判断Angular是否进行了变更检测。

代码很简单,只要在ChildComponent的模板中插入一个随机值即可,如果Angular进行了变更检测,那么每次这个值都会变化。如果这个值没有变,那么Angular就没有进行变更检测。

1
2
3
4
5
6
7
8
9
10
11
12
@Component({
selector: 'app-child',
standalone: true,
imports: [
GrandChildComponent
],
template: `
<p>child works! {{Math.random()}}</p>
<app-grand-child />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})

这时我们再点击Parent按钮,发现ChildComponent的模板中的随机值是不变的,这证明Angular没有进行变更检测。

ngDoCheck到底怎么用?

其实ngDoCheck是在Angular进行变更检测之前给用户一个机会,执行一些自定义逻辑。注意看官网的这句话:

1
You can use this lifecycle hook to manually check for state changes outside of Angular's normal change detection, manually updating the component's state.

所以到底要怎么使用它?恕我经验不够,我至今还未在项目中实际使用过ngDoCheck函数。但是我在网上找到了一些资料,可以参考一下:

  1. 使用第三方库时,如果你的组件中使用了第三方库,而第三方库改变了DOM状态,但是Angular没有检测到,这时可以使用ngDoCheck函数来手动检测状态变化。
  2. 自定义变更检测的逻辑,比如在某些条件下不进行变更检测,可以在ngDoCheck函数中实现。
  3. 子组件接收一个对象作为Input属性,而父组件只改变了对象的属性,这时ngOnChanges函数不会被调用的,这时可以使用ngDoCheck函数来检测对象的属性变化。
    说实话,这个例子非常的牵强,父组件只该变对象的一个属性这不是好的编程习惯,这种情况应该直接传递一个新的对象给子组件。但是作为例子,我们还是说一下这个情况。

使用第三方库

组件中使用第三方库改变了DOM状态,但是Angular没有检测到,这时可以使用ngDoCheck函数来手动检测状态变化。

1
2
3
ngDoCheck() {
this.cdf.markForCheck();
}

自定义变更检测逻辑

假设有一个父组件ParentComponent,一个子组件ChildComponent,父组件给子组件传递一个User对象,我们将User的年龄显示到子组件页面上,我们希望达到一个效果,如果年龄小于50岁时,子组件不进行变更检测,这种情况就需要使用ngDoCheck函数。

父组件定义如下,注意在onParentButtonClick函数中要重新赋值一个新的User对象,而不是改变User对象的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component({
selector: 'app-parent',
standalone: true,
imports: [
ChildComponent
],
template: `
<p>parent works!</p>
<app-child [user]="user" />
<button (click)="onParentButtonClick()">Parent</button>
`,
styleUrl: './parent.component.scss'
})
export class ParentComponent {
user: User = {
id: 1, name: 'Philip', age: 40,
}

onParentButtonClick() {
const age = Math.floor(Math.random() * 100) + 1;
this.user = {...this.user, age};
}
}

子组件定义如下,我们需要在ngDoCheck中自定义变更检测的逻辑。(注意,ngDoCheck在每次ngOnChanges之后调用。), 如果年龄小于50岁时,我们是用ChangeDetectorRef.detach()函数来停止变更检测,大于50岁时,我们使用ChangeDetectorRef.reattach()函数来恢复变更检测。

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
@Component({
selector: 'app-child',
standalone: true,
imports: [
GrandChildComponent
],
template: `
<p>child works!</p>
<p>{{user?.age}}</p>
<app-grand-child/>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChildComponent implements DoCheck, OnChanges {
@Input() user: User | null = null;
currentAge: number | null = null;

constructor(private cdf: ChangeDetectorRef) {
}

ngOnChanges(changes: SimpleChanges): void {
this.currentAge = changes['user'].currentValue.age;
}

ngDoCheck(): void {
if (this.currentAge! < 50) {
this.cdf.detach(); // stop change detection
} else {
this.cdf.reattach(); // restore change detection
}
}
}

注意:这个例子十分牵强,现实中不会有这种情况的。

子组件接收对象作为Input属性,父组件只改变了对象的属性

User类型定义:

1
2
3
4
5
export interface User {
id: number;
name: string;
age: number;
}

父组件定义如下,初始化时,我们传递一个user对象给ChildComponent, 点击按钮时,改变user对象的age属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component({
selector: 'app-parent',
standalone: true,
imports: [
ChildComponent
],
template: `
<p>parent works!</p>
<app-child [user]="user" />
<button (click)="onParentButtonClick()">Parent</button>
`,
styleUrl: './parent.component.scss'
})
export class ParentComponent {
user: User = {
id: 1, name: 'Philip', age: 40,
}

onParentButtonClick() {
this.user.age = 18;
}
}

子组件定义如下,当父组件点击按钮时,子组件的页面上,age值并没有变化,还是40.
子组件的ngDoCheck函数会被调用,我们可以在ngDoCheck函数中检测user对象的属性变化,并打印出变化的值。
但是子组件的ngOnChanges函数不会被调用,因为父组件只改变了user对象的属性,而没有改变user对象本身。而ngOnChanges比较的是对象的引用,而不是对象内部的值。

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
@Component({
selector: 'app-child',
standalone: true,
imports: [
GrandChildComponent
],
template: `
<p>child works!</p>
<p>{{user?.age}}</p>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChildComponent implements OnInit, DoCheck, OnChanges {
@Input() user: User | null = null;
differ: any;

constructor(private differs: KeyValueDiffers) {
}

ngOnInit() {
this.differ = this.differs.find(this.user).create();
}

ngOnChanges(changes: SimpleChanges): void {
console.log(changes[0].currentValue.age);
}

ngDoCheck(): void {
console.log(`ngDoCheck in ChildComponent`);
const userChanges = this.differ.diff(this.user);
if (userChanges) {
userChanges.forEachChangedItem((changeRecord: any) => {
console.log('item changed : ' + changeRecord.key + ' ' + JSON.stringify(changeRecord.currentValue))
});
userChanges.forEachAddedItem((changeRecord: any) => {
console.log('item added : ' + changeRecord.key + ' ' + JSON.stringify(changeRecord.currentValue))
});
}
}
}

如果我们想让子组件页面上的age值变化,那么只需要手动触发一次变更检测即可。此时再点击父组件中的按钮,子组件的age值会变为18。

1
2
3
4
5
constructor(private cdf: ChangeDetectorRef) {}

ngDoCheck(): void {
this.cdf.markForCheck();
}

References

  1. https://angular.dev/guide/components/lifecycle#ngdocheck
  2. https://stackoverflow.com/a/45522199/1487475
  3. https://medium.com/@tehseen_ullah786/here-is-why-we-should-use-ngdocheck-in-angular-28bc98a86d85#:~:text=Integration%20with%20third%2Dparty%20libraries,and%20updates%20the%20view%20accordingly.

Introduction

ngOnChanges是Angular中非常重要的一个生命周期函数,今天我们来详细讨论一下它的用法。

Angular什么时候调用ngOnChanges

ngOnChanges是一个生命周期钩子,当Angular检测到组件的输入属性发生变化时,就会调用ngOnChanges函数。这意味着,只有当组件的输入属性发生变化时,ngOnChanges才会被调用。

我们什么时候需要重写ngOnChanges

通常情况下,我们不需要重写ngOnChanges函数,因为Angular会自动调用它。但是,以下几种情况都需要重写ngOnChanges函数:

  1. 当我们使用了OnPush时,且需要根据Input变化做相应操作时,那么就要响应ngOnChanges
  2. 当我们需要在组件的输入属性发生变化时执行一些自定义逻辑时,我们就需要重写ngOnChanges函数。

配合OnPush

假设有一个父组件:ParentComponent,一个子组件ChildComponent,父组件中调用一个API,返回值作为Input传递给子组件,子组件要根据这个Input继续调用另外一个API,这时候就需要在子组件中重写ngOnChanges函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component({
selector: 'app-child',
standalone: true,
imports: [],
template: `
<p>Child Component</p>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})

export class ChildComponent implements OnChanges {
@Input() data: any;

ngOnChanges(changes: SimpleChanges) {
if (changes.data) {
this.fetchData();
}
}

fetchData() {
// Call API
}
}

自定义逻辑