0%

今天来看看在JavaScript中如何控制对象的可访问性。对象的可访问性控制在常规业务代码中都用的并不多,一般都是底层框架在用,不过了解一下总是没有坏处的。

不可扩展对象

使用Object.preventExtensions()方法可以将一个对象设置为不可扩展,不可扩展的对象不能添加新属性,但可以修改或者删除已有属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"use strict"; // eslint-disable-line

const person1 = {
name: 'zdd',
};
console.log(Object.isExtensible(person1)); // true;

Object.preventExtensions(person1);
console.log(Object.isExtensible(person1)); // false

person1.name = 'Philip'; // OK
console.log(person1.name); // Philip

delete person1.name; // OK;
console.log(person1.name); // undefined.

person1.age = 18; // Error: Cannot add property age, object is not extensible

密封对象

使用Object.seal()方法可以将一个对象密封,密封后的对象不能添加、删除属性,但可以修改属性的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"use strict"; // eslint-disable-line

const person1 = {
name: 'zdd',
};
console.log(Object.isSealed(person1)); // false

Object.seal(person1);
console.log(Object.isSealed(person1)); // true

person1.name = 'Philip'; // OK
console.log(person1.name); // Philip

person1.age = 18; // Error, Seal object is non-extensible.
delete person1.name; // Error in strict mode.

冻结对象

使用Object.freeze()方法可以冻结一个对象,冻结后的对象不能添加、修改、删除属性,也不能修改属性的可枚举性、可配置性、可写性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use strict';

const person1 = {
name: 'zdd',
};
console.log(Object.isFrozen(person1)); // false

Object.freeze(person1);
console.log(Object.isFrozen(person1)); // true

person1.name = 'ddz'; // Error. object is frozen.
person1.age = 18; // Cannot add property age, object is not extensible
delete person1.name; // TypeError: Cannot delete property 'name' of #<Object>

console.log(person1);

总结

特性 不可扩展对象 (Object.preventExtensions) 密封对象 (Object.seal) 冻结对象 (Object.freeze)
是否可新增属性
是否可修改属性值
是否可删除属性
是否可更改原型链
是否可更改可枚举性
是否可更改可配置性
是否可更改可写性

今天我们来学习一下如何在JavaScript中实现原型继承,话不多说,直接上代码。

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
// 父类的构造函数
function Animal(name) {
this.name = name;
}

// 父类的方法
Animal.prototype.printName = function () {
console.log(this.name);
};

// 子类构造函数
function Dog(name, category) {
// 好用父类构造函数,初始化父类属性。
Animal.call(this, name);

// 初始化子类的属性
this.category = category;
}

// 设置 Dog 的原型为 Animal 的实例,建立原型链
Dog.prototype = Object.create(Animal.prototype);

// 修复子类的 constructor 指向(上面一行代码已经将constructor指向了Animal)
Dog.prototype.constructor = Dog;

// 子类独有的方法。
Dog.prototype.printCategory = function () {
console.log(`Dog name: ${this.name} barking`);
};

const dog = new Dog('dog', 'Pet');
dog.printName(); // 调用父类的方法
dog.printCategory(); // 调用自己的方法

今天在HackerRank上做Angular题目,偶然遇到一个循环固定次数的问题,这里记录一下。

假设需要在template里面循环count次,每次生成一个div,而count是后台api返回的一个变量,该如何实现呢?

方法一,使用数组

我能想到的最简单的方法,就是定义一个含有count个元素的数组,然后使用ngFor遍历这个数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export class AppComponent implements OnInit {
nums: number[] = [];

ngOnInit() {
this.fetchData().then(count => {
// 创建含有count个元素的数组,数组元素的值无所谓,我们只用到元素个数。
this.nums = Array(count).fill(0);
})
}

// 模拟后台api返回的count
fetchData() {
// Random int between 0 - 10
const randomInt = Math.floor((Math.random() * 10))
return Promise.resolve(randomInt);
}
}

然后使用ngFor遍历这个数组。这样会循环生成count个div元素。

1
2
3
<ng-container *ngFor="let n of nums; let i = index">
<div>{{i}}</div>
</ng-container>

方法二,在模板中构造数组

1
2
3
<div *ngFor="let i of [].constructor(count).keys()">
第 {{i + 1}} 次循环 <!-- 显示从1开始的序号 -->
</div>

注意:该方法会导致一个错误:需要研究一下,为什么?

1
core.mjs:6662 ERROR RuntimeError: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '[object Array Iterator]'. Current value: '[object Array Iterator]'. Expression location: _AppComponent component. Find more at

方法三,使用自定义指令

使用时,需要在模块或者独立组件的imports中引入RepeatDirective,否则会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 自定义指令代码
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
standalone: true,
selector: '[appRepeat]',
})
export class RepeatDirective {
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
) {}

@Input('appRepeat') set count(value: number) {
this.viewContainer.clear(); // 清空容器
for (let i = 0; i < value; i++) {
this.viewContainer.createEmbeddedView(this.templateRef, {
$implicit: i + 1, // 传递索引值
});
}
}
}

调用自定义指令

1
2
3
<div *appRepeat="count; let i">
这是第 {{ i }} 次循环
</div>

方法四:自定义管道

使用时,需要在模块或者独立组件的imports中引入RangePipe,否则会报错。

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

@Pipe({
standalone: true,
name: 'range'
})
export class RangePipe implements PipeTransform {
transform(value: number): number[] {
return Array.from({ length: value }, (_, i) => i);
}
}

使用自定义管道

1
2
3
<div *ngFor="let i of count | range">
第 {{i + 1}} 次循环 <!-- 显示从1开始的序号 -->
</div>

介绍

Pull和Push是两种数据流的传递方式,他们决定了数据如何从生产者传递到消费者。

Pull

Pull是拉模型,也就是消费者主动向生产者请求数据,消费者是主动的一方。在JavaScript中,所有的函数都是Pull模型的,函数相当于数据的生产者,函数调用者相当于数据的消费者。调用这个函数,就是在请求数据。

下面的代码中num.reduce函数用来求数组的和,const sum = nums.reduce(...)调用函数请求数据。

1
2
3
const nums = [1, 2, 3, 4, 5];
const sum = nums.reduce((acc, cur) => acc + cur, 0);
console.log(sum); // 15

Push

Push是推模型,由数据的生产者主动向外推送数据,生产者是主动的一方。在JavaScript中,Promise就是Push模型的,与函数不同,Promise决定什么时候发送数据。

下面代码中的p.then(console)会在Promise resolve之后执行,Promise决定了何时发送数据。

1
2
3
4
5
6
const p = new Promise((resolve) => {
setTimeout(() => {
resolve(42);
}, 1000);
});
p.then(console.log); // 42

RxJS中的Observable也是推模型的,Observable决定了何时发送数据。

References

  1. https://rxjs.dev/guide/observable

概述

今天来学习一下RxJS中的Subject,有人说它既是Observable,又是Observer,那么它到底是什么呢?先来看一下官方的定义:

1
Subject is equivalent to an EventEmitter, and the only way of multicasting a value or event to multiple Observers.

翻译一下:Subject等价于EventEmitter,是唯一一种将值或事件多播给多个观察者的方法。

Subject的特点

多播

下面的代码中,有两个订阅者,但是他们共享数据流,所以他们会输出相同的随机数。这就是Subject的多播特性。

1
2
3
4
5
6
import { Subject } from 'rxjs';

const subject = new Subject();
subject.subscribe((v) => console.log('observerA: ' + v));
subject.subscribe((v) => console.log('observerB: ' + v));
subject.next(Math.random());

但是如果改用observable, 那么每个订阅者都会收到一份数据,以下代码打印出不同的随机数。也就是说:对于普通的observable,每次订阅都会发起新的请求。一次订阅只能有一个观察者。

1
2
3
4
5
6
7
8
import { Observable } from 'rxjs';

const observable = new Observable((subscriber) => {
subscriber.next(Math.random());
});

observable.subscribe((v) => console.log(v));
observable.subscribe((v) => console.log(v));

总结

特性 Observable Subject
单播多播 单播 多播
数据产生时机 订阅时产生 由Subject.next()产生
冷热
是否是Observer
订阅时机的影响 每次订阅后产生新的数据 共享数据流,订阅后接收续生成的数据

今天来一篇RxJS的科普。

什么是RxJS

先看RxJS这个词,RxJS = Rx + JS,JS就是Javascript,那么Rx又是什么呢?

Rx = Reactive extension, 最早是由微软开发的一套软件库,用于处理异步数据流和事件流。后来,ReactiveX被移植到了多种编程语言中,如RxJS(Javascript)、RxJava(Java)、RxSwift(Swift)等。

下面是Wikipedia对ReactiveX的定义:
ReactiveX is an API for asynchronous programming with observable streams。

RxJS就是Rx的JavaScript版本。

RxJS的核心概念

Rx中使用了两个设计模式,观察者模式和迭代器模式。Rx还大量使用了函数式编程的思想。

Observable

这是RxJS中最终要的概念,没有之一,Observable,顾名思义,就是可观察对象,我们看一个现实中的例子,你在看风景,那么风景就是一个Observable,你在观察风景,风景发生了变化,你就会感知到,这就是Observable的概念。

Observer

Observer - 观察者,还是以现实中的例子来解释,你在看风景,你就是一个Observer。

Subscription

Subscription - 订阅,订阅是联系Observable和Observer的桥梁,我们常说,Observable发生变化时,Observer会收到通知,那么Observer如何收到通知呢?就是通过订阅机制来实现的。

Operators

Operators - 操作符,RxJS提供了许多操作符,用于对Observable进行操作,比如mapfilterreduce等。Operators都是纯函数,它们不会改变原始的Observable,而是返回一个新的Observable。

Subject

Subject - 主题,Subject是一种特殊的Observable,它允许将值多播给多个Observer。也就是说,它是用来实现多播的,同时,Subject也是Observer。

Scheduler

Scheduler - 调度器,RxJS提供了调度器,用于控制Observable何时开始发送通知。

References

  1. https://en.wikipedia.org/wiki/ReactiveX
  2. https://rxjs.dev/guide/overview

概述

RxJS中有许多和map相关的操作符,如mapmapTomergeMapswitchMap等,本文一一介绍。

map

map操作符是RxJS中最常用的操作符之一,它类似于数组的map方法,可以对Observable发出的每个值进行转换。

1

概述

今天来聊一聊Angular中的effect, 使用过React框架的同学对effect应该都不陌生,不过,Angular中的effect和React中的effect有所不同。

1
2
3
effect(() => {
console.log(`The current count is: ${count()}`);
});

effect函数特点

effect函数有如下特点:

  1. effect函数至少运行一次,无论effect函数中是否读取signal,也无论signal的值是否发生变化。
  2. 如果effect函数中读取了signal,那么当signal的值发生变化时,effect函数会再次运行。
  3. 如果effect函数中没有读取signal,那么effect函数只会运行一次。当signal变化时,effect函数不会再次运行。
  4. effect函数中不要更新signal的值,否则会导致死循环。(因为signal更新—>effect运行—>更新signal—>effect运行…)

比如下面这个effect函数,只会运行一次,因为effect函数中没有读取count

1
2
3
effect(() => {
console.log('This is a effect function');
});

比如下面这个effect函数,会导致死循环。

1
2
3
effect(() => {
count.set(count() + 1);
});

effect函数只能运行在injection context中(比如构造函数体内), 如果effect函数的使用位置不对(比如放到了ngOnInit函数中),那么Angular会报如下错误:关于injection context的更多信息,可以查看这里

1
core.mjs:6643 ERROR RuntimeError: NG0203: effect() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`. Find more at https://angular.dev/errors/NG0203

effect函数的使用场景

Effects are rarely needed in most application code, but may be useful in specific circumstances. Here are some examples of situations where an effect might be a good solution:

  • Logging data being displayed and when it changes, either for analytics or as a debugging tool.
  • Keeping data in sync with window.localStorage.
  • Adding custom DOM behavior that can’t be expressed with template syntax.
  • Performing custom rendering to a <canvas>, charting library, or other third party UI library.

Destroy effect

effect函数会在组件销毁时自动销毁,不需要手动清理。当然effect函数返回EffectRef对象,可以手动调用destroy方法销毁effect函数。

1
2
3
4
5
const effectRef = effect(() => {
console.log('This is a effect function');
});

effectRef.destroy();

untrack模式

https://angular.dev/guide/signals#reading-without-tracking-dependencies

cleanup function.

effect函数中可以定义一个清理函数onCleanup,当effect函数被销毁时,清理函数会被调用。下面的代码演示了如何使用onCleanup函数清理定时器。

1
2
3
4
5
6
7
8
9
10
effect((onCleanup) => {
const user = currentUser();
const timer = setTimeout(() => {
console.log(`1 second ago, the user became ${user}`);
}, 1000);

onCleanup(() => {
clearTimeout(timer);
});
});

Reference

  1. https://angular.dev/guide/signals#effects

请问以下代码输出什么?为什么?

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
import {Component} from "react";

interface DemoComponentState {
val: number;
}

export class DemoComponent extends Component<{}, DemoComponentState> {
constructor(props: {}) {
super(props);
this.state = {
val: 0,
};
}

componentDidMount() {
this.setState({val: this.state.val + 1}, () => {
console.log(this.state.val);
});

this.setState({val: this.state.val + 1}, () => {
console.log(this.state.val);
});

setTimeout(() => {
this.setState({val: this.state.val + 1}, () => {
console.log(this.state.val);
});

this.setState({val: this.state.val + 1}, () => {
console.log(this.state.val);
});
}, 0);
}

render() {
return null;
}
}

答案:1 1 2 2。
解析: