0%

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);
});

介绍

ES6以后有了class关键字,可以方便地实现类的继承。但是JavaScript是一门单继承的语言,即一个类只能继承一个类。但是有时候我们需要多继承,这时候我们可以使用混入(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!

组合

组合也是一种实现多重继承的方式,即通过组合多个类的实例来实现多继承。下面的代码使用组合来实现鸭子可以飞和游泳的功能。

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("I can fly!"); }
}

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

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之前执行。以下代码先输出finally,然后返回1

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