Created
July 14, 2023 06:45
-
-
Save marcustyphoon/e88d009306a74225344040108ecfe93e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
The Firefox and Chromium developer consoles have a non-obvious effect when you | |
call console.log on an object that you mutate. Here's some example code to | |
illustrate this: | |
*/ | |
const myAccount = { | |
name: 'marcus', | |
balance: 0 | |
} | |
const test = () => { | |
for (let count = 0; count < 3; count++) { | |
myAccount.balance += 1; | |
// we call console.log directly on an object that is getting mutated | |
console.log(myAccount); | |
} | |
} | |
setTimeout(test, 1000); | |
/* | |
If one runs this code snippet with the Chromium or Firefox developer console | |
open while the incrementing loop and log fire, the resulting console entries | |
will be interactable previews of the myAccount object that initially look | |
roughly like this: | |
firefox: | |
▶ Object { name: "marcus", balance: 1 } | |
▶ Object { name: "marcus", balance: 2 } | |
▶ Object { name: "marcus", balance: 3 } | |
chromium: | |
▶ {name: 'marcus', balance: 1} | |
▶ {name: 'marcus', balance: 2} | |
▶ {name: 'marcus', balance: 3} | |
But if you click those dropdowns... | |
firefox: | |
▼ Object { name: "marcus", balance: 1 } | |
balance: 3 | |
name: "marcus" | |
▼ Object { name: "marcus", balance: 2 } | |
balance: 3 | |
name: "marcus" | |
▼ Object { name: "marcus", balance: 3 } | |
balance: 3 | |
name: "marcus" | |
chromium: | |
▼ {name: 'marcus', balance: 1} 🅸 | |
balance: 3 | |
name: "marcus" | |
▼ {name: 'marcus', balance: 2} 🅸 | |
balance: 3 | |
name: "marcus" | |
▼ {name: 'marcus', balance: 3} 🅸 | |
balance: 3 | |
name: "marcus" | |
Weird, right? In fact, if one runs this code snippet *without* the developer | |
console open, or closes the developer console before the incrementing loop and | |
log fire, then when one does eventually opens the developer console, they'll | |
initially see: | |
firefox: | |
▶ Object { name: "marcus", balance: 3 } | |
▶ Object { name: "marcus", balance: 3 } | |
▶ Object { name: "marcus", balance: 3 } | |
chromium: | |
▶ Object | |
▶ Object | |
▶ Object | |
Q: What gives? | |
A: Interactable console entries can be views of something (a javascript object) | |
that can change over time. | |
Every time you *call* console.log(), your browser doesn't generate a new deep | |
copy of the current state of whatever you chose to log; it just stores a pointer | |
to it in the console history. | |
Every time the developer console tries to *render* one of these entries, or when | |
you interact with the interactive entry to display more of its data, the current | |
state of the object is used to generate the rendered information. Thus, once our | |
incrementing loop is finished and and myAccount.balance is 3, clicking to expand | |
the interactive preview or even causing the preview to be created in the first | |
place by opening dev tools will reference the current value, not the value when | |
console.log was called. | |
The little 🅸 icon that Chromium displays hints at this; it has this tooltip: | |
"This value was evaluated upon first expanding. It may have changed since then." | |
This, of course, is not true in the case where console.log outputs | |
non-interactive text at the time it is called. In node, for example, console | |
output is non-interactive text, so you will always get 1, 2, 3 instead of | |
3, 3, 3 with code like this. | |
This therefore means we can ensure that we definitely log the current state of | |
an object by logging text instead of a pointer to the object itself: | |
*/ | |
const myAccount = { | |
name: 'marcus', | |
balance: 0 | |
} | |
const test = () => { | |
for (let count = 0; count < 3; count++) { | |
myAccount.balance += 1; | |
// compute a string right now and give it to console.log | |
console.log(JSON.stringify(myAccount)); | |
} | |
} | |
setTimeout(test, 1000); | |
/* | |
Result, in all cases: | |
{"name":"marcus","balance":1} | |
{"name":"marcus","balance":2} | |
{"name":"marcus","balance":3} | |
Note that this doesn't work on non-serializable objects and that it obviously | |
uses a bunch of extra memory, so avoid it when not investigating a problem. (I | |
mean, avoid logging stuff randomly in general.) | |
Anyway, this view is kind of inconvenient, though, if the object is really big. | |
We don't have the nice interactive tree view of our object any more! A nice way | |
to improve this is to make our object data back into an object by parsing the | |
JSON: in other words, we're now making a deep copy of the object we want to log | |
when we want to log it. | |
(The structuredClone global function is a better way to do this, and you | |
probably have access to it. It's faster and can do circular object references | |
and some non-serializable stuff.) | |
*/ | |
const myAccount = { | |
name: 'marcus', | |
balance: 0 | |
} | |
const test = () => { | |
for (let count = 0; count < 3; count++) { | |
myAccount.balance += 1; | |
// make a deep copy of the current state of the object and log that | |
console.log(JSON.parse(JSON.stringify(myAccount))); | |
// better way to do this if your browser version is from ≥2022 | |
// console.log(structuredClone(myAccount)); | |
} | |
} | |
setTimeout(test, 1000); | |
/* | |
Result: What you would expect. | |
▼ {name: 'marcus', balance: 1} 🅸 | |
balance: 1 | |
name: "marcus" | |
▼ {name: 'marcus', balance: 2} 🅸 | |
balance: 2 | |
name: "marcus" | |
▼ {name: 'marcus', balance: 3} 🅸 | |
balance: 3 | |
name: "marcus" | |
Now, this kind of thing is completely irrelevant if you're writing code in a | |
framework that uses immutable javascript principles (like if you're using | |
redux): if you never mutate your objects, logging one of them will always have | |
the same result no matter when the developer console renders the result! | |
-------------------------------------------------------------------------------- | |
Fun aside: the ability to interact with javascript objects in the console goes | |
both ways. Try this, for example: | |
*/ | |
let myWeirdAccount = { | |
name: 'marcus', | |
get balance() { | |
this.name = 'whoa you changed my name!'; | |
return Math.random(); | |
} | |
} | |
console.log(myWeirdAccount); | |
console.log(myWeirdAccount); | |
/* | |
Here's what that looks like once you click the arrow to expand the object: | |
firefox: | |
▼ Object { name: "marcus", balance: Getter } | |
balance: >> | |
▶ name: "marcus" | |
▶ <get balance()>: function balance() | |
▼ Object { name: "marcus", balance: Getter } | |
balance: >> | |
▶ name: "marcus" | |
▶ <get balance()>: function balance() | |
chromium: | |
▼ {name: 'marcus'} 🅸 | |
balance: (...) | |
name: "marcus" | |
▶ get balance: f balance() | |
▼ {name: 'marcus'} 🅸 | |
balance: (...) | |
name: "marcus" | |
▶ get balance: f balance() | |
You can click those >> or (...) text areas to invoke the getter, in real time, in the console! Here, | |
for example, I expanded the first one, invoked the getter, and then expanded the second one: | |
▼ {name: 'marcus'} 🅸 | |
balance: 0.954183525722754 | |
name: "marcus" | |
▶ get balance: f balance() | |
▼ {name: 'marcus'} 🅸 | |
balance: (...) | |
name: "whoa you changed my name!" | |
▶ get balance: f balance() | |
Wild, huh. | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment