这是我在 jQuery 源代码中发现的一个模式.
你可能从未听说过这种模式,因为这个名字是我编的。
在源代码中,我清楚地记得有这样写的代码(为了简单起见,我省略了很多代码。如果你感兴趣,你可以查看它的源代码):
var elemData = someInitialValue
...
elemData.events = elemData = function(){};
...
elemData.events = {};
当时我不明白为什么是 elemData.events
被赋值了两次。 因为最后一个赋值会覆盖第一个赋值。我不知道这是不是jQuery 维护者犯的一个错误。
事实证明这不是一个错误。它运用了:
- 赋值作为表达式
- 操作符优先级
我将在这篇文章中解释这是如何工作的。也就是说,我认为这是JavaScript语言中一个模糊的角落。尽管它看起来简洁,但您通常不需要编写这样的代码。
赋值也是一个表达式
我们每天都使用赋值来给变量赋值,但我们可能不知道,赋值除了是语句外,也是表达式。这样的表达式计算得到的值是right-hand-side(RHS)的值。
所以我们可以这样写代码:
let x
if(x = 1) { // 1 is truthy
console.log(1) // 1
}
赋值操作符 =
是右结合的:
let a, b
a = b = 2 // the same as a = ( b = 2)
console.log(a) // 2
console.log(b) // 2
操作符优先级
回到那个令人费解的 Jquery 代码: elemData.events = elemData = function(){};
; 它包含了两种操作符: 两个赋值操作符和一个成员访问操作符
当混合使用不同类型的操作符时,操作符优先级决定了哪种类型的操作符优先。
根据操作符优先级表, 成员访问操作符(member access) 是 18,赋值操作符(assignment)是 2。所以说访问操作符优先级更高。这符合我们的直觉,比如 obj.prop = 1
,首先执行的是 obj.prop
, 解析出属性 prop
的引用,然后再赋值。
Old elemData vs new elemData
让我们重新回顾一下神秘的jQuery代码片段:
var elemData = someInitialValue // 1
// ...
elemData.events = elemData = function(){}; // 2
// ...
elemData.events = {}; // 3
- Line 1: 首先,(old)
elemData
指向initialValue
- Line 2: (old)elemData.events 指向 initialValue.events;然后右到左赋值,(new)elemData 重新被赋予 function(){},并丢失调之前的 initialValue 引用;最后,elemData 的值赋值给 initialValue.events,即:
initialValue.events = elemData = function(){};
- Line 3: (new)elemData.events 指向 {}
这让我想起 for in 循环: 当我们在循环中途改变对象的绑定(即,给变量重新赋值)时,被枚举的属性不会突然改变:
let obj = {a: 1, b: 2, c: 3}
let obj2 = {d: 1, e: 2, f: 3}
for(const prop in obj ) {
console.log(prop) // a, b, c
obj = obj2
}
console.log(obj) // { d: 1, e: 2, f: 3 }
其他示例
可以用这个模式写一个链表:
let i = 0, root = { index: i }, node = root
while (i < 10) {
node.next = node = {} // `node` in `node.next` is the old `node`
node.index = ++i // `node` in `node.index` is the new `node`
}
node = root
do {
console.log(node.index) // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
} while ((node = node.next))