JavaScript is a powerful language that allows us to reason about our business domain using a wide variety of tools, such as classes.
class User {
#firstname;
#lastname;
constructor(firstname, lastname) {
this.#firstname = firstname;
this.#lastname = lastname;
}
getFullName() {
return `${this.#firstname}${this.#lastname}`;
}
getFirstname() {
return this.#firstname;
}
getLastname() {
return this.#lastname;
}
}
Working with a server can be quite interesting, especially when sharing data between a JavaScript application and a server.
For that matter, a common language that is used to send data from and to a server is JSON.
const user = new User("John", "Doe");
const data = {
user
};
const serializedData = JSON.stringify(data);
console.log(serializedData);
Except, in this case, this code would output the following result.
// {"user":{}}
Why? Because our User
class has private fields, and a class is simply a function that when instanciated, will create an object.
In this case, since all properties of this class are private (prefixed with a hash symbol), the created object is empty, hence why JSON.stringify
cannot output an object with our properties.
Although our class has private members, it can still access its data through accessors or methods.
console.log(user.getFullName());
The above code will output the following result.
JohnDOE
So everything is working, but we can make it better by accounting for JSON.stringify
calls by updating our class.
class User {
#firstname;
#lastname;
constructor(firstname, lastname) {
this.#firstname = firstname;
this.#lastname = lastname;
}
getFullName() {
return `${this.#firstname}${this.#lastname}`;
}
getFirstname() {
return this.#firstname;
}
getLastname() {
return this.#lastname;
}
toJSON() {
return {
firstname: this.#firstname,
lastname: this.#lastname
};
}
}
This fixes our issue and we can now embed our user in our stringified data.
{"user":{"firstname":"John","lastname":"Doe"}}
Now, let's imagine we are using this class from a library, and the author of this library don't want to include the necessary code to make it stringifiable.
We could monkey patch this class, but this would put a high risk on our code since an upgrade to this class whenever the author feels like adding a toJSON
method could potentially break a lot of thing on a precarious update.
What we could do instead is use a replacer function, which is the third argument that can receive the JSON.stringify
function.
class User {
#firstname;
#lastname;
constructor(firstname, lastname) {
this.#firstname = firstname;
this.#lastname = lastname;
}
getFullName() {
return `${this.#firstname}${this.#lastname}`;
}
getFirstname() {
return this.#firstname;
}
getLastname() {
return this.#lastname;
}
}
const user = new User("John", "Doe");
const data = {
user
};
/**
* @param {string} key
* @param {unknown} value
*/
const replacerFunction = (key, value) => {
if (value instanceof User) {
return {
firstname: user.getFirstname(),
lastname: user.getLastname()
};
}
return value;
};
const serializedData = JSON.stringify(data, replacerFunction);
console.log(serializedData);
Now, if we run this code, we should see the following output.
{"user":{"firstname":"John","lastname":"Doe"}}
As you can see, the replacer function is a function that gets the key and value of the object that you want to stringify.
Each time this function parses a couple of key/pair values, it calls our function (hence why this function takes two arguments), and we can decide based on a condition what to do with the value.
In any case, you must return a value that is not stringified yet, this will be taken care of by the JSON.stringify
after you have replaced all necessary values.
The replacer function uses a design pattern that is called a Visitor, the principle is relatively easy to understand.
Fun fact, this is the same design pattern that is used by tools like ESLint to analyse the code that has been parsed by a parser like TypeScript to give you warning about what is wrong in your code.
export default {
meta: {
type: "problem",
docs: {
description: "Enforce that a variable named `foo` can only be assigned a value of 'bar'."
},
fixable: "code",
schema: []
},
create(context) {
return {
VariableDeclarator(node) {
if (node.parent.kind === "const") {
if (node.id.type === "Identifier" && node.id.name === "foo") {
if (node.init && node.init.type === "Literal" && node.init.value !== "bar") {
context.report({
node,
message: 'Value other than "bar" assigned to `const foo`. Unexpected value: {{ notBar }}.',
data: {
notBar: node.init.value
},
fix(fixer) {
return fixer.replaceText(node.init, '"bar"');
}
});
}
}
}
}
};
}
};
That's it! I hope you learned one or two things. If you want to share some though, I'll gladly read your take in the comment section.
Thanks for reading & stay curious!
Top comments (0)