Skip to content

Instantly share code, notes, and snippets.

@marcustyphoon
Created July 14, 2023 06:45
Show Gist options
  • Save marcustyphoon/e88d009306a74225344040108ecfe93e to your computer and use it in GitHub Desktop.
Save marcustyphoon/e88d009306a74225344040108ecfe93e to your computer and use it in GitHub Desktop.
/*
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