关于前端工程师成长,需要两个视角。一是立足标准,系统性总结和整理前端知识,建立自己的认知和方法论;二是放眼团队,从业务和工程角度思考前端团队的价值和发展需要。只有这样做,才能够持续发展,在高速发展的技术和工程浪潮中稳稳立足。

方法

建立知识架构

建立具有逻辑性和完备性的知识目录。

img

追本溯源

JavaScript 中的反射与 Proxy 与 Reflect

JavaScript 中的反射与 Proxy 与 Reflect

反射(Reflect)是指程序运行时访问、检测和修改本身状态和行为的能力。在 ES6 中引入 Reflect 之前,常用 for...in 实现反射,而在引入 Reflect 之后,反射机制在 JavaScript 中得到了更大的延伸与应用。

Reflect

Reflect 是一个全局对象,但与其它全局对象不同,它并不是一个构造函数,所以不能通过 new 来对其进行调用。

而且它所有的属性和方法都是静态的(类似的还有 Math)。

1
2
3
4
5
6
7
8
9
10
// ** 检测一个对象是否有特定的属性
const obj = { A: '1', B: '2' };
Reflect.has(obj, 'A'); // true
Reflect.has(obj. 'C'); // false

// ** 返回对象自身的键 has 与 in 运算符作用相同
Reflect.ownKeys(obj); // ['A', 'B']

// ** 为对象添加新的属性
Reflect.set(obj, 'C', '3'); // true

Proxy

Proxy 基于目标对象创建一个代理对象,可以通过代理对象实现对于基本操作的拦截和自定义。

1
2
3
// target 要代理的对象,可以是任何对象,包括函数
// handler 代理配置,带有扑捉器的对象
const proxy = new Proxy(targe, handler);

对 proxy 对象进行操作,如果 handler 中包含了对应的捕捉器,则扑捉器会拦截这个操作,并对其进行处理。如果没有相应的捕捉器,则操作会被转发到被代理的对象。

对被代理对象的操作和相应的捕捉器名称如下:

被代理对象的操作 捕捉器
Object.getPrototypeOf() handler.getPrototypeOf()
Object.setPrototypeOf() handler.setPrototypeOf()
Object.isExtensible() handler.isExtensible()
Object.preventExtensions() handler.preventExtensions()
Object.getOwnPropertyDescriptor() handler.getOwnPropertyDescriptor()
Object.defineProperty() handler.defineProperty()
in handler.has()
属性读取 handler.get()
属性赋值 handler.set()
delete handler.deleteProperty()
Object.getOwnPropertyNames()Object.getOwnPropertySymbols() handler.ownKeys()
函数调用 handler.apply()
new handler.construct()

不变量

为了确保语言功能和行为的一致性,代理对象的捕捉器的返回是有一定限制的,被称为不变量。

  • set() ,如果设置属性值成功,需要返回 true,否则返回 false

  • deleteProperty(),如果属性删除成功,需要返回 true,否则返回 false

  • getPrototypeOf() 必须返回被代理对象的原型

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
"use strict";

let origin = {
name: 'Mike',
age: 29,
_isMarried: false
};

origin = new Proxy(origin, {
isExtensible: function(obj) {
return Reflect.isExtensible(obj);
},
has: function(obj, prop) {
return Reflect.has(obj, prop);
},
get: function get(obj, prop, receiver) {
if (Reflect.has(obj, prop)) {
if (prop.startsWith('_')) return 'Access denied';
return Reflect.get(obj, prop, receiver);
} else {
return "No ".concat(prop, " find in target");
}
},
set: function set(obj, prop, value, receiver) {
if (Reflect.has(obj, prop)) {
if (prop.startsWith('_')) return 'Access denied';
Reflect.set(obj, prop, value, receiver);
return true;
} else {
return false;
}
}
});

console.log(origin._isMarried); // Access denied
console.log(origin._isMarried = '1'); // Access denied

function constructPoint(numX, numY) {
return [numX, numY];
}

const funcProxy = new Proxy(constructPoint, {
apply: function(obj, thisArg, argumentsList) {
return argumentsList.join('-');
}
});

console.log(funcProxy(1, 2)); // 1-2

对于每个可被 Proxy 捕获的内部方法,在 Reflect 中都有一个对应的方法,其名称和参数与 Proxy 捕捉器相同。

所以,我们可以使用 Reflect 来将操作转发给原始对象。

Proxy 的局限

许多内建对象,例如 MapSetDatePromise 等,都使用了所谓的“内部插槽”。

它们类似于属性,但仅限于内部使用,仅用于规范目的。例如,Map 将项目(item)存储在 [[MapData]] 中。内建方法可以直接访问它们,而不通过 [[Get]]/[[Set]] 内部方法。所以 Proxy 无法拦截它们。

1
2
3
4
5
6
7
8
9
10
11
let map = new Map();

let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});

proxy.set('test', 1);
alert(proxy.get('test')); // 1(工作了!)

类似的,类的私有属性也是通过内部插槽而不是 getset 方法,采用上面的方法依旧有效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class User {
#name = "Guest";

getName() {
return this.#name;
}
}

let user = new User();

user = new Proxy(user, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});

alert(user.getName()); // Guest

Vue 3 原理解析 - 数据侦测原理

Vue 3 原理解析 - 数据侦测原理

Vue 2.x 及之前的版本中实现数据的可响应,需要对 ObjectArray 分别进行处理:

  • Object 类型通过 Object.definePropery 把属性转换成 getter/setter ,这个过程需要递归侦测所有的对象 key 来实现深度侦测
  • Array 类型通过对改变数组自身的几个方法进行拦截来实现对数组的可响应

而在 Vue3 中则是通过 Proxy 实现数据读取和设置拦截,在捕捉器中实现数据依赖收集和触发视图更新的操作。

Proxy

Proxy 是 ES6 中新引入的特性。

Proxy 正如其含字面意义 - “代理” 所表明的那样,它是对象与对象之间的一层代理,程序可以通过 Proxy 来访问或操作目标对象,进而可以实现基本操作的拦截和自定义。

基本语法如下:

1
const proxy = new Proxy(target, handler);

参数:

  • target 要代理的原始对象,可以是任何类型的对象,包括原生数组、函数甚至另一个代理对象
  • handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理的行为

举个栗子:

1
2
3
4
5
let const obj = {};
const proxyHandler = {};
const objProxy = new Proxy(obj, proxyHandler);
// 赋值操作被转发到 obj 对象
objProxy.a = 37;

handler 中可以设置对原始对象各种操作的捕获器,在捕获器中可以自定义修改操作的行为。

1
2
3
4
5
6
7
const obj = { a: 'A' };
const proxy = new Proxy(obj, {
get: (o, p) => {
if (p === 'a') return 'This is A';
return o[p];
}
});

有时我们并不知道目标对象的具体类型,这种情况下使用 Reflect 返回 trap (捕捉器)相应的默认行为。

Reflect 是一个内置的不可构造的非函数对象,它提供了拦截 JavaScript 各种操作的方法。

1
2
3
4
5
6
7
8
9
10
let target = {};
const proxyObj = new Proxy(target, {
get: (obj, prop) => {
return Reflect.get(obj, prop, receiver);
},
set: (obj, prop, value) => {
Reflect.set(obj, prop, value, receiver);
return true;
}
});

对于 set 操作,可能会引起代理对象的属性更改,导致 set 多次执行。比如当代理对象是数组时,执行 push 操作,会多次触发 set,同时也引发 get 操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let array = [ 1, 3 ];

const proxyHandler = {
get: (target, prop, receiver) => {
console.log('Get value : ', prop, Reflect.get(target, prop, receiver));
return Reflect.get(target, prop, receiver);
},
set: (target, prop, value, receiver) => {
console.log('Set value :', prop, value);
return Reflect.set(target, prop, value, receiver);
}
};

const objProxy = new Proxy(array, proxyHandler);

objProxy.push(4);

// Get value : push [Function: push]
// Get value : length 2
// Set value : 2 4
// Set value : length 3

当要代理的对象是多层结构时,Proxy 的代理只能到第一层,即不能感知操作对象内部的 set 操作,但是 get 会被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let obj = { 'foo': 'foo', 'bar': { 'name': 1 } };

const proxyHandler = {
get: (target, prop, receiver) => {
console.log('Get value : ', prop, Reflect.get(target, prop, receiver));
return Reflect.get(target, prop, receiver);
},
set: (target, prop, value, receiver) => {
console.log('Set value :', prop, value);
Reflect.set(target, prop, value, receiver);
return true;
}
};

const objProxy = new Proxy(obj, proxyHandler);

console.log(objProxy.bar.name);

// Get value : bar { name: 1 }
// 1

一句话总结:Proxy 是 ES6 引入的新特性,可以使用 Proxy 间接访问或操作其代理的对象。

Vue 3 中响应式数据实现

Vue 3 中响应式系统的 API 主要有

  • reactive
  • ref
  • computed
  • readonly
  • watchEffect
  • watch

其中 reactive 是最核心的 API

reactive

Vue 3 中是用全局的 WeakMap 来存储正在追踪的响应式对象,如 reactiveMapshallowReactiveMapreadonlyMapshallowReadonlyMap

1
2
3
4
export const reactiveMap = new WeakMap<Target, any>()
export const shallowReactiveMap = new WeakMap<Target, any>()
export const readonlyMap = new WeakMap<Target, any>()
export const shallowReadonlyMap = new WeakMap<Target, any>()

此外还定义了代理对象的可响应操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 普通对象,如 Object 、Array 的拦截器
// 包含了当对代理对象进行取值、复制、删除属性等操作时的捕获器
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
};

// 集合对象,如果 Map/WeakMap/Set/WeakSet 的拦截器
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}

reactive 创建响应式对象的流程如下:

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
/**
* 生成一个原始对象的响应式副本,这种转换是深层次的,会影响到所有嵌套属性
*/
function reactive(target: object) {
// 如果目标对象是只读的,直接返回目标对象
if (target && (target as Target).__v_isReadonly) {
return target
}
// 否则就在目标对象的基础上创建响应式对象
return createReactiveObject(
target, // 目标代理对象
false, // 是否只读
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}

/**
* 创建响应式对象
*/
function createReactiveObject(
target: Target, // 原始对象
isReadonly: boolean, // 是否只读
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any> // 正在追踪的可响应对象的集合
) {
// 如果 target 不是对象类型,返回原值
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 如果 taret 已经是一个响应式对象,返回原值
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 如果 target 已经有一个代理对象,返回已经存在的代理对象
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 只有特定类型的对象才可以被观测
// 包括 Array/Object/Map/Set/WeakMap/WeakSet,不是则返回 TargetType.INVALID
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
// Map/Set/WeakMap/WeakSet 被认为是 COLLECTION 类型,handler 和其它类型不同
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}

上面提到了对于多层对象的代理,set 并不能感知到内层对象的变化,但是 get 会被触发,Vue 3 利用这个原理,再对内层数据进行一次代理。

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
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) {
// 如果 key 是可响应标志 IS_REACTIVE,返回 !isReadonly
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
// 如果 key 是只读标志 IS_READONLY,返回 isReadonly
return isReadonly
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}

const targetIsArray = isArray(target)

// 如果目标是数组
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}

const res = Reflect.get(target, key, receiver)

// 判断 key 是否是 Symbol
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}

// 如果可读,追踪
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}

// 如果是浅层响应,直接返回得到的值
if (shallow) {
return res
}

// 判断是否 Ref
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}

if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}

return res
}
}

使用 reactive 创建了响应式对象后,改变响应式对象的属性操作,会被侦测,并即时地在目标对象上触发对应的响应。

set 方法,即赋值来举例,就是:

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
const set = /*#__PURE__*/ createSetter()
const shallowSet = /*#__PURE__*/ createSetter(true)

function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 取得旧的值 oldValue
let oldValue = (target as any)[key]

// 如果不是浅层响应模式,需要进行一系列的判断,才能赋值
if (!shallow) {
value = toRaw(value)
oldValue = toRaw(oldValue)
// 如果 target 不是数组,旧值是 Ref
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}

const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
// 在 target 上触发 ADD 或者 SET 响应
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}

ref

由于 Proxy 代理的目标需是对象形式,不能对简单类型进行代理,为此 Vue 3 中使用 ref 函数为简单类型的值生成了一个包装,这样就可以通过 recative 函数构建响应式数据了。

包装对象的实现如下:

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
class RefImpl<T> {
private _value: T
private _rawValue: T

public dep?: Dep = undefined
public readonly __v_isRef = true

constructor(value: T, public readonly _shallow = false) {
this._rawValue = _shallow ? value : toRaw(value)
this._value = _shallow ? value : convert(value)
}

get value() {
trackRefValue(this)
return this._value
}

set value(newVal) {
newVal = this._shallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
triggerRefValue(this, newVal)
}
}
}
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
interface Ref<T = any> {
[RefSymbol]: true
value: T
}

function ref(value?: unknown) {
return createRef(value)
}

function createRef(rawValue: unknown, shallow = false) {
// 判断是否是 Ref,如果是 Ref 则直接返回
if (isRef(rawValue)) {
return rawValue
}
// 判断是否是浅代理
// convert = (val) => isObject(val) ? reactive(val) : val
let value = shallow ? rawValue : convert(rawValue)

//
const r = {
__v_isRef: true,
get value() {
track(r, TrackOpTypes.GET, 'value')
return value
},
set value(newVal) {
if (hasChanged(toRaw(newVal), rawValue)) {
rawValue = newVal
value = shallow ? newVal : convert(newVal)
trigger(
r,
TriggerOpTypes.SET,
'value',
__DEV__ ? { newValue: newVal } : void 0
)
}
}
}
return r
}

computed

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
function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>

// 如果传入的 getterOrOptions 是函数,则将其设置为计算属性的 getter
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
// 如果只传入了 getter 函数,将计算属性是不可修改的 readonly
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
// 如果 getterOrOptions 是个对象,则计算属性是可修改的
getter = getterOrOptions.get
setter = getterOrOptions.set
}

let dirty = true
let value: T
let computed: ComputedRef<T>

const runner = effect(getter, {
lazy: true,
// 将 effect 标记为 computed,则其在执行时拥有更高的优先级
computed: true,
scheduler: () => {
if (!dirty) {
dirty = true
trigger(computed, TriggerOpTypes.SET, 'value')
}
}
})

// 构建一个 computed 对象
computed = {
__v_isRef: true,
// 将 effect 暴露,使得计算属性能够 stop
effect: runner,
get value() {
if (dirty) {
value = runner()
dirty = false
}
// 追踪
track(computed, TrackOpTypes.GET, 'value')
// 返回最新值
return value
},
set value(newValue: T) {
setter(newValue)
}
} as any

return computed
}

effect

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
function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}

function createReactiveEffect<T = any>(
fn: (...args: any[]) => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn(...args)
}
// 如果 effectStack 不存在当前 effect
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
// 将当前 effect 加入 effectStack
effectStack.push(effect)
// 将当前 effect 设置为 activeEffect
activeEffect = effect
return fn(...args)
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
关于 triggertrack

trigger 用来当观测值发生发生变化时通知观察者

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
function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// targetMap 存储了所有被追踪的对象,如果当前对象没有被追踪,直接返回
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}

const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || !shouldTrack) {
if (effect.options.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
} else {
// the effect mutated its own dependency during its execution.
// this can be caused by operations like foo.value++
// do not trigger or we end in an infinite loop
}
})
}
}

// 如果当前触发类型为 CLEAR
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
const isAddOrDelete =
type === TriggerOpTypes.ADD ||
(type === TriggerOpTypes.DELETE && !isArray(target))
if (
isAddOrDelete ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}
if (isAddOrDelete && target instanceof Map) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}

const run = (effect: ReactiveEffect) => {
// 在开发环境下,调试触发器
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}

// Important: computed effects must be run first so that computed getters
// can be invalidated before any normal effects that depend on them are run.
computedRunners.forEach(run)
effects.forEach(run)
}
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
function track(target: object, type: TrackOpTypes, key: unknown) {
if (!shouldTrack || activeEffect === undefined) {
return
}
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}

TypeScript 概览

TypeScript

TypeScript 是 JavaScript 类型的超集,它可以编译成纯 JavaScript,可以在任何浏览器、任何计算机和任何操作系统上运行,并且是开源的。

基础类型

TypeScript 支持与 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
// boolean
let isDone: boolean = false;
// number
// JavaScript 和 TypeScript 中的数字不是浮点类型 floating point 就是大整数类型 BigInteger
let decLiteral: number = 6;
let bigLiteral: number = 100n;
// string
let name: string = 'bob';
// Array
let list: number[] = [1,2,3];
let list: Array<number> = [1,2,3];
// Tuple
// 元组类型要求值类型、顺序和个数一一对应
let tuple: [string, string] = ['string', 'string'];
// enum
enum Color {Red = 0, Green, Blue}
let c: Color = Color.Green;
// Any
let source: any = 4
// Void
let unusable: void = undefined;
// Null & Undefined
let u: undefined = undefined;
let n: null = null;
// Never
function error(message: string): never {
throw new Error(message);
}
// 类型断言
// 明确知道值的类型时可使用类型断言
let strValue: any = 'str';
let length: number = (<string>strValue).length;
let length: number = (strValue as string).length

变量声明

  • var

    可以通过 var 关键字定义变量,但是 var 声明可以在包含它的函数、模块、命名空间或全局作用域内部任何位置被访问,包含它的代码块对此没有影响,这可能会引发一些错误,比如多次声明同一个变量并不会报错。

  • let

    当用 let 声明一个变量,它使用的是词法作用域或块作用域。不同于使用 var 声明的变量那样可以在包含它们的函数外访问,块作用域变量在包含它们的块之外是不能访问的。

  • const

    const 拥有与 let 相同的作用域规则,但是声明的变量被赋值后不能再改变。

每次进入一个作用域时,它创建了一个变量的环境,就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。当 let 声明出现在循环体里时拥有完全不同的行为,不仅是在循环里引入了一个新的变量环境,而是针对每次迭代都会创建这样一个新作用域。

接口

TypeScript 的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做 “鸭式辨型法”或”结构性子类型化”。 在 TypeScript 里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

JavaScript 为开发者提供了非常大的灵活性:比如你可以为一个初始化为整数的变量赋值一个函数。但同时这种不确定性也会带来很多的麻烦,TypeScript 内置的接口就可以用来解决这个问题。

接口帮助我们在赋值和传递参数时进行类型检查,确保我们给变量的赋值符合变量的类型或接收参数正确(接口在某种程度上表示了变量或函数对于赋值给它的值或传递给它的参数的一种期望和要求)。

可选属性

接口里的属性不全都是必需的,有些是只在某些条件下存在,或者根本不存在,可以为接口定义可选属性来实现这些需求。

1
2
3
4
interface OptionAttribute {
optionNum?: number;
optionStr?: string;
}

可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。

只读属性

如果希望对象的某些属性只有在刚创建的时候修改其值,可以通过在属性名前用 readonly 来指定属性为只读属性。

1
2
3
4
interface ReadonlyAttribute {
readonly num: number;
readonly str: string;
}

TypeScript 具有 ReadonlyArray<T> 类型,它与 Array<T> 类似,只是把所有可变方法去掉了,因此可以保证数组创建后再也不能被修改。

readonly vs const

使用 const 来修饰变量,使用 readonly 来修饰属性

额外的类型检查

TypeScript 中,对象字面量赋值给变量或作为参数进行传递的时候,会经过 “额外属性检查”,如果一个对象字面量存在任何 “目标类型” 不包含的属性时,会发生错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface SquareConfig { 
width: number;
color?: string;
}

function createSquare(config: SquareConfig): { color: string, area: number } {
let newSquare = { color: 'blue', area: 0 };
if (config.width) newSquare.area = config.width * config.width;
if (config.color) newSquare.color = config.color;
return newSquare;
}

// error: height not expected in type SquareConfig
let mySquare = createSquare({ color: 'green', width: 10, height: 10 });

使用 ‘类型断言’ 可以绕开额外属性检查

1
let mySquare = createSquare({ color: 'green', width: 10, height: 10 } as SquareConfig);

但是更好的做法是在接口中提供一个字符串索引签名

1
2
3
4
5
interface SquareConfig { 
width: number;
color?: string;
[propName: string]: any;
}

函数类型

接口能够描述 JavaScript 中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。

参数列表里的每个参数都需要名字和类型。

1
2
3
interface TypeFunc {
(source: string, subString: string): boolean;
}

这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。

1
2
3
4
5
let mySearchFunc: TypeFunc;
mySearchFunc = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}

对于函数类型的类型检查来说,函数的参数名不需要与接口定义里的名字相匹配。

函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。如果没有指定类型,TypeScript 的类型系统会推断出参数类型,而函数的返回值类型是通过其返回值推断出来的。

可索引的类型

与使用接口描述函数类型差不多,我们也可以描述那些能够通过索引得到的类型。可索引类型具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。

1
2
3
4
5
6
7
8
// 表明了当用 number 去索引 StringArr 时会得到 string 类型的返回值
interface StringArr {
[index: number]: string;
}

let myArr: StringArr;
myArr = ["A","B"];
let myStr: string = myArr[0];

索引签名共有两种类型:字符串和数字。可以同时使用这两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。这是因为当使用 number 来索引时,TypeScript 会将它转换为 string 然后再去索引对象。

1
2
3
4
interface StringArray { 
[index: string]: string;
[index: number]: string;
}

一般将索引签名设置为只读,这样可以防止给索引赋值。

类类型

TypeScript 中的接口也能够用来明确的强制一个类去符合某种协议/契约。

1
2
3
4
5
6
7
8
9
10
11
12
interface ClockInterface {
currentTime: Date;
setTime:(d: Date);
}

class Clock implements ClockInterface {
currentTime: Date;
setTime:(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}

接口描述了类的公共部分,而不是公共和私有两部分,它不会帮你检查类是否具有某些私有成员。

类静态部分与实例部分的区别

类具有两个类型 :静态部分的类型和实例的类型。

当一个类实现了一个接口时,只对其实例部分进行类型检查。constructor 存在于类的静态部分,不在检查范围内。因此我们应该直接操作类的静态部分。

可以认为类的静态部分指的是类本身,实例部分指的是类实例化出来的对象。

继承接口

和类一样,接口也可以相互继承。这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

1
2
3
4
5
6
7
8
9
10
11
interface Father {
familyName: string;
}

interface Son extends Father {
givenName: string;
}

let aPerson = <Son>{};
aPerson.familyName = 'S';
aPerson.givenName = 'T';

一个接口可以继承多个接口,创建出多个接口的合成接口。

1
2
3
4
5
6
7
8
9
10
11
interface Shape {
color: string;
}

interface Stroke {
penWidth: number;
}

interface Square extends Shape, Stroke {
width: number;
}

混合类型

接口能够描述 JavaScript 中丰富的类型,可以使用 混合类型 来使某个对象具有多个类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Counter {
(start: number): number;
interval: number;
reset(): void;
}

function getCounter(): Counter {
let counter = <Counter>function (start: number) {};
counter.interval = 1;
counter.reset = function () {};
return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 2;

接口继承类

当接口继承了一个类类型时,它会继承类的成员但不继承其实现。就好像接口声明了所有类中存在的成员,但没有提供具体实现一样。接口同样会继承到类的 privateprotected 成员。这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现。

当你有一个庞大的继承结构时这很有用,但要指出的是你的代码只在子类拥有特定的属性时起作用。

ECMAScript 2015 , 也就是 ECMAScript 6 开始, JavaScript 中也能使用基于类的面向对象的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义一个类
class Person {
familyName: string;
constructor(familyName: string, givenName: string, age: number) {
this.familyName = familyName;
}
say() {
return 'Hello, my name is ' + this.familyName + ' ' + this.givenName;
}
move(distanceInMeters = 0) {
console.log('${this.name} moved ${distanceInMeters}m.');
}
}
// 创建类实例
lei aPerson = new Person('Wang', 'XiaoHong', 22);

在引用任何一个类的成员时都需要使用 this ,它表示我们访问的是类的成员。

继承

TypeScript 中允许使用继承来扩展现有的类。

1
2
3
4
5
6
7
8
class Student extends Person {
constructor(familyName: string, givenName: string, age: number) {
super(familyName, givenName, age);
}
move(distanceInMeters = 5) {
super.move(distanceInMeters);
}
}

如果子类包含了构造函数,它必须调用 super() ,它会执行基类的构造函数。在构造函数中访问 this 的属性之前,一定要先调用 super()

子类从父类中继承属性和方法

修饰符

  • public

    publicTypeScript 中类成员的默认属性

  • private

    类成员声明为 private 时,不能在生声明它的类的外部访问

  • protected

    类成员声明为 protected 时,在派生类中仍可访问

    构造函数也能被标记为 protected ,这意味着它不能在包含它的类外被实例化,但是能被继承

  • readonly

    通过 readonly 属性将属性设置为只读,只读属性必须在声明时或构造函数里被初始化

参数属性

参数属性通过给构造函数参数前添加一个访问限定符来声明。

1
2
3
4
class Student {
constructor(public familyName: string, public givenName: string, public age: number){
}
}

通过这个特性我们可以方便的在一个地方定义并初始化一个成员。

存取器

TypeScript 支持通过 getters/setters 来截取对对象成员的访问,它们可以有效的控制对对象成员的访问。

1
2
3
4
5
6
7
8
9
10
11
class student {
private _familyName: string;

get familyName(): string {
return this._familyNmae;
}

set familyName(fName: string) {
this._familyName = fName;
}
}

需要注意的是,存取器只支持 ECMAScript 5 或更高,其次,只带有 getter 存取器被推断为 readonly

静态属性

类的实例成员是那些仅当类被实例化时才会被初始化的属性,而类的静态成员存在于类本身上面而不是类的实例上。实例想要访问静态属性时,需要在其加上类名。

1
2
3
4
5
6
7
class student {
static school = 'ts';
...
getStudentSchool() {
return student.school;
}
}

抽象类

抽象类一般作为其它派生类的基类使用,一般不会被实例化。不同于接口,抽象类可以包含成员的实现细节。

使用 abstract 关键字定义抽象类和在抽象类中定义抽象方法。

1
2
3
abstract class Human {
abstract function_name: return_type;
}

抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。

抽象方法的语法与接口语法相似,两者都是定义方法签名但不包含方法体。然而,抽象方法必须包含 abstract 关键字并且可以包含访问修饰符。

函数

函数是 JavaScript 应用程序的基础。 它帮助你实现抽象层,模拟类,信息隐藏和模块。 在 TypeScript 里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义行为的地方。 TypeScriptJavaScript 函数添加了额外的功能,让我们可以更容易地使用。

函数类型

1
2
3
function fun(arg_1: arg_type, arg_2: arg_type, ...): return_type {
...
}

TypeScript 能够通过返回语句自动推断出返回值类型,因此函数返回类型一般是省略的。

1
let fun_var_name: (arg_1: arg_type, arg_2: arg_type, ...) => return_type;

函数类型包括两部分,参数类型和返回值类型。而且只要参数类型是匹配的,就认为它是有效的函数类型,而不在乎参数名是否准确。

可选参数和默认参数

JavaScript 中函数的每个参数都是可选的,可传可不传,没有传参的时候它的值就是 undefined。而 TypeScript 中函数的每个参数默认情况下都是必须的,编译器检查用户是否为每个参数都传入了值,还会假设只有这些参数被传递进函数-即传递给一个函数的参数个数必须与函数期望的参数个数一致。

1
2
3
4
5
6
let myAdd: (x: number, y: number) => number = function (x: number, y: number) { 
return x + y;
}

const sum = myAdd(1); // error: Expected 2 arugements, but got 1
const sum1 = myAdd(1, 2, 3); // error: Expected 2 arugements, but got 3

TypeScript 中,可以在参数名称旁使用 ? 实现可选参数的功能。

1
2
3
4
5
6
7
// 可选参数
function buildStudent(name: string, grade: number, gender?: string) {
return name + ' ' + grade + ' ' + (gender ? gender : '');
}

const student_1 = buildStudent('Tom', 2);
const student_2 = buildStudent('Jerry', 2, 'male');

需要注意的是,可选参数必须跟在可选参数后面。

TypeScript 中,当用户没有传递这个参数或传递的值是 undefined 时,我们可以为参数提供一个默认值。在所有必须参数后面的带默认初始化的参数都是可选的,与可选参数一样,在调用函数的时候可以省略-即可选参数与末尾的默认参数共享参数类型。

需要注意的是,如果带默认值的参数出现在必须参数之前,用户必须明确的传入 undefined 值来获得默认值。

剩余参数

如果你想同时操作多个参数或不知道会有多少个参数传递进来,在 JavaScript 中,你可以通过 arguments 来访问所有传入的参数,在 TypeScript 中,你则可以将所有参数收集到一个变量里。

1
2
3
4
5
6
// 剩余参数
function buildChildren(name: string, age: number, ...restOfProperty: string[]) {
return `${name} ${age} ${restOfProperty.join(' ')}`;
}

const children = buildChildren('Mike', 12, 'male', 'grade 3');

泛型

泛型是程序设计语言的一种特性,是程序员在编写代码时定义的一些可变部分,这些部分在使用前必须做出指明。

1
2
3
4
5
6
7
// 类型变量 T 会捕获用户传入的类型,之后我们就可以使用者个类型
function identity<T>(arg: T): T {
return arg;
}

const strId = identity('JavaScript');
const numId = identity(12);

泛型变量

使用泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型-即你必须把这些参数当做是任意或所有类型。

泛型函数

1
2
3
4
5
6
7
function combineFunc<T,K>(x: T, y: K): [T, K] {
return [T, K];
}

let combine: <T,K>(x: T, y: K) => [T,K] = combineFunc;
// or
let combine: { <T,K>(x: T, y: K): [T, K] } = combineFunc;

泛型接口

1
2
3
4
5
interface CombineFunc {
<T,K>(x: T, y: K): [T, K];
}

let combine: CombineFunc = (x, y) => [x, y];

泛型类

1
2
3
4
5
6
class CombineFunC {
combine: <T,K>(x: T, y: K) => [T, K];
}

let combineC: CombineFunC = new CombineFunC<number, number>();
combineC.combine = (x, y) => [x, y];

枚举

使用枚举可以定义一些带名字的常量,清晰的表达意图或创建一组有区别的用例。TypeScript 支持数字的和基于字符串的枚举。

数字枚举

数字枚举默认的枚举值是从 0 开始自增长的,如果你为第一个枚举名字指定了一个数字枚举值,则会从这个数字值开始自增长。

1
2
3
4
5
6
enum Status {
DELETED = -1,
VALID,
UNPAID,
PAID
}

字符串枚举

字符串枚举中,每个成员都需要使用字符串字面量或另外一个字符串枚举成员进行初始化。

需要注意的是,字符串枚举没有自增长行为。

1
2
3
4
5
6
enum StatusTip {
DELETED = 'Order has been deleted.',
VALID = 'Order is valid.',
UNPAID = 'Order is unpaid.',
PAID = 'Order is paid'
}

异构枚举

枚举可以混合字符串和数字成员。

1
2
3
4
enum HeterogeneousEnum {
NO = 0,
YES = 'Yes'
}

计算的和常量成员

每个枚举成员都带有一个值,它可以是常量或计算出来的。当满足如下条件时,枚举成员被当做常量:

  • 它是枚举的第一个成员,且没有初始化器,此时它的值为 0

  • 它不带有初始化器且它之前的枚举成员是一个数字常量,此时它的值为上一个枚举成员的值 + 1

  • 枚举成员使用常量枚举表达式初始化。常量枚举表达式是 TypeScript 表达式的子集,它可以在编译阶段求值。当一个表达式满足以下条件时,它就是一个常量枚举表达式:

    • 一个枚举表达式字面量

    • 一个对之前定义的常量枚举成员的引用

    • 带括号的常量枚举表达式

    • 一元运算符 + - ~ 其中之一应用在了常量枚举表达式

    • 常量枚举表达式作为二元运算符 +, -, *, /, %, <<, >>, >>>, &, |, ^ 的操作对象(若常量枚举表达式求值后为 NaNInfinity,则会在编译时报错)

      1
      2
      3
      4
      5
      6
      7
      enum FileAccess {
      None,
      Read = 1 << 1,
      Write = 1 << 2,
      ReadWrite = Read | Write,
      G = '1024'.length
      }

联合枚举与枚举成员的类型

存在一种特殊的非计算的常量枚举成员的子集:字面量枚举成员。字面量枚举成员是指不带有初始值的常量枚举成员或是被初始化为

  • 任何字符串字面量
  • 任何数字字面量
  • 应用了一元 - 符号的数字字面量

当所有枚举成员都拥有字面量枚举值时,它就带有了一种特殊的语义。

运行时枚举

枚举是在运行时真正存在的对象

反向映射

数字枚举成员具有从枚举值到枚举名字的反向映射。

1
2
3
4
5
6
7
8
enum Status {
DELETED = -1,
VALID,
UNPAID,
PAID
}

console.log(Status[0]); // VALID

const 枚举

常量枚举只能使用常量枚举表达式,并且不同于常规的枚举,它们在编译阶段会被删除-常量枚举成员会在使用的地方被内联进来,这是因为常量枚举不允许包含计算成员。

1
2
3
4
5
6
7
8
const enum Directions {
Up,
Down,
Left,
Right
}

let directions = [ Directions.Up, Directions.Down, Directions.Left, Directions.Right ];

类型推论

类型推论是指能够在编译期间自动推导出值的类型的能力。一般发生在初始化变量和成员,设置默认参数值和决定函数返回值时。

最佳通用类型

当需要从几个表达式中推断类型时,会使用这几个表达式的类型来推断出一个最合适的通用类型。

上下文类型

当表达式的类型与所处的位置相关时,类型推论按照上下文归类来推论类型。当然,如果表达式包含了明确的类型信息,上下文的类型会被忽略。

类型兼容性

TypeScript 中的类型兼容性是基于结构子类型的-结构子类型是一种只使用其成员来描述类型的方式。

原始类型和对象类型的兼容性

如果 x 要兼容 y, 那么 y 至少需要具有与 x 相同的属性。

1
2
3
4
5
// 如果 x 要兼容 y, y 至少要与 x 拥有相同的属性
let x = { name: 'x' };
let y = { name: 'y', alias: 'Y' };
x = y;
y = x; // Property 'alias' is missing in type '{ name: string; }' but required in type '{ name: string; alias: string; }

函数的类型兼容性

如果函数 x 要兼容函数 y,那么 x 至少要与 y 具有相同的参数并且返回值类型相同。

1
2
3
4
5
6
7
8
let funCom = (x: number, y: number) => x + y;
let funAbc = (x: number, y: number) => [x, y];
let funSub = (x: number) => x;
let funDef = (x: number, y: number) => x * y;
funCom = funSub;
funSub = funCom; // Type '(x: number, y: number) => number' is not assignable to type '(x: number) => number'
funCom = funAbc; // Type '(x: number, y: number) => number[]' is not assignable to type '(x: number, y: number) => number'.Type 'number[]' is not assignable to type 'number'
funCom = funDef;

类的兼容性

比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class W {
static sign: string = 'AAAAA';
pro: string = 'W';
constructor(name: string){}
}

class Y {
pro: string = 'W';
constructor(length: number) {}
}

let wC = new W('W');
let yC = new Y(2);

wC = yC; // OK
yC = wC; // OK

类的私有成员和受保护成员会影响兼容性。 当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 同样地,这条规则也适用于包含受保护成员实例的类型检查。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。

泛型的兼容性

1
2
3
4
5
6
interface NotEmpty<T> {
}

let gA: NotEmpty<number>;
let gB: NotEmpty<string>;
gA = gB; // ok
1
2
3
4
5
6
7
interface NotEmpty<T> {
data: T;
}

let gA: NotEmpty<number>;
let gB: NotEmpty<string>;
gA = gB; // Error

高级类型

交叉类型

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

联合类型

联合类型表示一个值可以是几种类型之一。

1
2
3
4
5
6
7
8
/ 联合类型
// 联合类型表示一个值可以是几个类型之一
function pad(padding: number | string) {
return padding;
}

console.log(pad(1));
console.log(pad('A'));

如果一个值是联合类型,我们只能访问联合类型所有类型里共有的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Elephant {
name: string;
walk();
}

interface Shark {
name: string;
swim();
}

function getAnimal(): Elephant | Shark {
return { name: 'Elephant', walk() { console.log('walk') } } as Elephant;
}

let aAnimal = getAnimal();
console.log(aAnimal.name);
aAnimal.walk();
aAnimal.swim();

JavaScript 操作符

位操作符

ECMAScript 中的所有数值都以 IEEE 754 64 位格式存储,但位操作并不直接应用到 64 位表示,而是先把值转换为 32 位整数,再进行位操作,之后再把结果转换为 64 位。对开发者而言,就好像只有 32 位整数一样,因 为 64 位整数存储格式是不可见的。这让二进制操作变得与其他语言中类似。但这个转换也导致了一个奇特的副作用,即特殊值 NaNInfinity 在位操作中都会被当成 0 处理。

有符号整数使用 32 位的前 31 位表示整数值。第 32 位表示数值的符号,如 0 表示正,1 表示负。这 一位称为符号位(sign bit),它的值决定了数值其余部分的格式。正值以真正的二进制格式存储,即 31 位中的每一位都代表 2 的幂。第一位(称为第 0 位)表示 20 ,第二位表示 21 ,依此类推。如果一个位是空的,则以 0填充,相当于忽略不计。

负值以一种称为二补数(或补码)的二进制编码存储。一个数值的二补数通过如下 3 个步骤计算 得到:

  1. 确定绝对值的二进制表示

  2. 找到数值的一补数(或反码),换句话说,就是每个 0 都变成 1,每个 1 都变成 0

  3. 给结果加 1

默认情况下,ECMAScript 中的所有整数都表示为有符号数。不过,确实存在无符号整数。对无符号整数来说,第 32 位不表示符号,因为只有正值。无符号整数比有符号整数的范围更大,因为符号位被用来表示数值了。

1
2
3
4
5
6
console.log(1 << 10); // 1024
console.log(-1 << 10); // -1024
console.log(1024 >> 10); // 1
console.log(-1024 >> 10); // -1
console.log(1024 >>> 10); // 1
console.log(-1024 >>> 10); // 4194303

相等操作符

全等和不全等操作符与相等和不相等操作符类似,只不过它们在比较相等时不转换操作数。全等操 作符由 3 个等于号(===)表示,只有两个操作数在不转换的前提下相等才返回 true