去往js函数式编程(8)完
冻结
如果我们希望避免程序员意外或故意修改对象的可能性,冻结对象是一个有效的解决方案。在对象被冻结之后,任何修改它的尝试都会静默失败。javascript 不会报告错误或抛出异常,但也不会修改对象。这种解决方案只有一个问题:冻结对象是一个浅层操作,它仅冻结属性本身,类似于 const 声明的作用。如果任何属性本身是对象或数组,并且具有进一步的对象或数组作为属性,依次类推,它们仍然可以被修改。
let myObj3 = {
d: 22,
m: 9,
o: { c: 'MVD', i: 'UY', f: { a: 56 } }
}
Object.freeze(myObj3)
console.log(myObj3) // {d:22, m:9, o:{c:"MVD", i:"UY", f:{ a:56}}}
//这只是部分成功,当我们尝试更改一些属性时可以看到:
myObj3.d = 8888 //不起作用,与之前一样
myObj3.o.f.a = 9999 //糟糕,起作用了!
console.log(myObj3) // {d:22, m:9, o:{c:"MVD", i:"UY", f:{ a:9999}}}
如果我们想要实现真正的不可变性,我们需要编写一个可以冻结对象所有层级的例程。幸运的是,通过应用递归,很容易实现这一点。主要思想是先冻结对象本身,然后递归冻结每个属性。我们必须确保只冻结对象自身的属性,不要干扰对象的原型。
const deepFreeze = (obj) => {
if (obj && typeof obj === 'object' && !Object.isFrozen(obj)) {
Object.freeze(obj)
Object.getOwnPropertyNames(obj).forEach((prop) => deepFreeze(obj[prop]))
}
return obj
}
如果一个对象包含对自身的引用会发生什么?如果我们跳过冻结已经被冻结的对象,我们可以避免这个问题:由于它们所引用的对象已经被冻结,因此后向循环引用将被忽略。
如果禁止修改对象,那么你必须创建一个新对象。例如,如果你使用 Redux,reducer 是一个函数,它接收当前状态和一个动作(本质上是一个带有新数据的对象),并生成新的状态。为了结束这一切,我们还应该冻结返回的对象,就像我们对原始状态做的那样。但让我们从头开始:如何克隆一个对象?
let oldObject = {
d: 22,
m: 9,
o: { c: 'MVD', i: 'UY', f: { a: 56 } }
}
let newObject = {
d: oldObject.d,
m: oldObject.m,
o: { c: oldObject.o.c, i: oldObject.o.i, f: { a: oldObject.o.f.a } }
}
你可以使用 Object.assign()或扩展运算符来创建浅复制对象:要创建再次是浅数组的副本,你可以使用 slice()或扩展运算符,如果一个对象或数组包含对象,我们会遇到冻结时的相同问题:对象是按引用复制的,这意味着在新对象中的更改也会影响到旧对象。
let newObject1 = Object.assign({}, myObj)
let newObject2 = { ...myObj }
let myArray = [1, 2, 3, 4]
let newArray1 = myArray.slice()
let newArray2 = [...myArray]
let oldObject = {
d: 22,
m: 9,
o: { c: 'MVD', i: 'UY', f: { a: 56 } }
}
let newObject = Object.assign({}, oldObject)
newObject.d = 8888;
new newObject.o.f.a = 9999;
console.log(newObject);
// {d:8888, m:9, o: {c:"MVD", i:"UY", f: {a:9999}}} -- ok
console.log(oldObject);
// {d:22, m:9, o: {c:"MVD", i:"UY", f: {a:9999}}} -- oops!!
在这种情况下,请注意当我们更改 newObject 的某些属性时会发生什么。更改 newObject.d 是可以的,但更改 newObject.o.f.a 也会影响到 oldObject,因为 newObject.o 和 oldObject.o 实际上是对同一个对象的引用。对于有一个简单的基于 JSON 的解决方案。如果我们对原始对象进行 stringify(),然后解析 parse()结果,我们将得到一个与旧对象完全独立的新对象:
const jsonCopy = (obj) => JSON.parse(JSON.stringify(obj))
使用 JSON.stringify(),我们可以将对象转换为字符串。然后,JSON.parse()会将该字符串创建一个对象;简单!这适用于数组和对象,但存在一个问题。如果对象的任何属性具有构造函数,它们不会被调用:结果始终由普通的 js 对象组成。
let myDate = new Date()
let newDate = jsonCopy(myDate)
console.log(typeof myDate, typeof newDate)
模式通常以四个基本要素描述:简洁的名称,用于描述问题,解决方案和后果。模式适用的上下文:需要解决方案的特定情况,可能还有一些额外的条件必须满足。解决方案:列出解决给定情况所需的元素。应用模式的后果。你可能从解决方案中获得一些收益,但也可能带来一些损失。
检测多次点击
假设出于某种原因,你决定用户应该能够多次点击某个元素,并且点击次数在某种程度上具有意义并产生某种特殊结果。浏览器非常擅长检测单机或双击,但三次火更多次点击并不那么容易获得。
<html>
<head>
<title>多次点击示例</title>
<script type="text/javascript" src="rxjs.umd.js"></script>
</head>
<body>
<span id="mySpan">多次点击此文本(快速点击)</span>
<script>
const { fromEvent, pipe } = rxjs;
const { buffer, filter } = rxjs.operators;
const spanClick$ = fromEvent(
document.getElementById("mySpan"),
"click"
);
spanClick$
.pipe(
buffer(spanClick$.pipe(debounceTime(250))),
map(list =>list.length),
filter(x =>x >= 3)
)
.subscribe(e =>{
console.log(${e} 次点击于 ${new Date()});
});
</script>
</body>
</html>