Hacker News new | past | comments | ask | show | jobs | submit login

I think it's a clever and elegant solution. Obviously, ymmv depending on the environment, or what exactly you're trying to undo/redo. But I like that it thinks of two stacks. StructuredClone is not something I've ever used or am familiar with... I'm used to storing and later de-referencing pointers to anonymous functions. But in your use case, a clone makes a lot more sense, and I'm kinda looking forward to playing with that next time I have a mess of event handlers.



I wonder if using structuredClone() has a performance penalty compared to a simple block-scoped closure though?

   let doFn, undoFn; {const newStroke = currentStroke; doFn = ()=>strokes.push(newStroke); undoFn = ()=>strokes.pop();}


I'm sure it has a massive performance penalty, but probably not as much as other runtime ways of deep-copying. It's doing something different than your code above.

In the code above, `newStroke` just makes a reference to `currentStroke`. Nothing is changed by creating a `const newStroke` and pushing it to the array. You're just pushing a reference to the original. In the OP's example, `currentStroke` is something that gets modified. If you used the code above, as soon as you modify `currentStroke`, every reference in the `strokes` array will change. The fact that you used `let` instead of `var` in the loop means you are creating a new constant, but that constant isn't preserving a snapshot of `currentStroke` unless you explicitly make it a new object. Otherwise it's just a reference. You need to deep-clone it in the undo function in order to preserve its previous state. Something like:

`let previousStroke = {a:currentStroke.a,b:currentStroke.b}` where `a` and `b` are primitives that are copied, not objects that are referenced, before you change `currentStroke`. If you did it manually, you'd have to keep keep recursively checking `a` and `b` all the way down to their primitives, and rebuild the object.

An easy way of doing this for objects without functions or classes in them is just `const previousStroke = JSON.parse(JSON.stringify(currentStroke));` but usually I've had to write custom re-initializers if for example `previousStroke.a` was a class instance.


> Nothing is changed by creating a `const newStroke` and pushing it to the array.

Not true. If you look carefully you will see the `const` and everything after it is wrapped in curly braces, and those braces create a block...

> Traditionally (before ES6)... blocks with curly braces do not create scopes... [but] blocks are finally treated as scopes in ES6, but only if you declare variables with let or const.https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guid...

So the curly braces with the use of const/let creates a new lexical scope, hence solving the issue. You can test it in the browser console to confirm;

  var stack=[], cmd="";
  cmd="success"; stack.push(()=>cmd);
  cmd="fail"; stack.push(()=>cmd);
  stack[0](); /* returns "fail" */

  var stack=[], cmd="";
  cmd="success"; {const c=cmd; stack.push(()=>c);}
  cmd="fail"; {const c=cmd; stack.push(()=>c);}
  stack[0](); /* returns "success" */
It's probably not a good idea to do it this way, because it's easily "overlooked". I would probably use a class or factory-function if I had the need.


TIL




Consider applying for YC's Summer 2025 batch! Applications are open till May 13

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: