Implementing Private Variables In JavaScript
JavaScript (or ECMAScript) is the programming language that powers the web. Created in May 1995 by Brendan Eich, it’s found its place as a widely-used and versatile technology. Despite its success, it’s been met with its fair share of criticism, especially for idiosyncrasies. Things like objects being casted to string form when used as indices, 1 == "1"
returning true, or the notoriously confusing this
keyword. A particularly interesting quirk though, is the existence of various techniques for variable privacy.
In its current state, there is no "direct” way to create a private variable in JavaScript. In other languages, you can use the private
keyword or double-underscores and everything works, but variable privacy in JavaScript carries characteristics that make it seem more akin to an emergent trait of the language rather than an intended functionality. Let’s introduce some background to our problem.
The "var” keyword
Before 2015, there was essentially one way to create a variable, and that was the var
keyword. var
is function-scoped, meaning that variables instantiated with the keyword would only be accessible to code within the function. When outside of a function, or "global” essentially, the variable will be accessible to anything executed after the definition of the variable. If you try to access the variable in the same scope before its definition, you will get undefined
rather than an error. This is due to the way the var
keyword "hoists."
// Define "a" in global scope
var a = 123;
// Define "b" in function scope
(function() {
console.log(b); //=> Returns "undefined" instead of an error due to hoisting.
var b = 456;
})();
console.log(a); // => 123
console.log(b); // Throws "ReferenceError" exception, because "b" cannot be accessed from outside the function scope.
The birth of ES6 variables
In 2015, ES6/ES2015 was made official, and with it came two new variable keywords: let
and const
. Both were block-scoped, meaning that variables created with the keywords would be accessible from anything within the same pair of braces. Same as with var
, but the let
and const
variables could not be accessed outside of block scope with loops, functions, if statements, braces, etc.
const a = 123;
// Block scope example #1
if (true) {
const b = 345;
}
// Block scope example #2
{
const c = 678;
}
console.log(a); // 123
console.log(b); // Throws "ReferenceError" because "b" cannot be accessed from outside the block scope.
console.log(c); // Throws "ReferenceError" because "b" cannot be accessed from outside the block scope.
Since code outside of the scope cannot access the variables, we get an emergent trait of privacy. We’re going to cover some techniques for implementing it in different ways.
Using functions
Since functions in JavaScript also are blocks, all variable keywords work with them. In addition, we can implement a very useful design pattern called the "module.”
The Module Design Pattern
Google relies on the Oxford Dictionary to define a "module":
Any of a number of distinct but interrelated units from which a program may be built up or into which a complex activity may be analyzed.
—"Module" Definition 1.2
The module design pattern is very useful in JavaScript because it combines public and private components and it allows us to break a program into smaller components, only exposing what another part of the program should be able to access through a process called "encapsulation.” Through this method, we expose only what needs to be used and can hide the rest of the implementation that doesn’t need to be seen. We can take advantage of function scope to implement this.
const CarModule = () => {
let milesDriven = 0;
let speed = 0;
const accelerate = (amount) => {
speed += amount;
milesDriven += speed;
}
const getMilesDriven = () => milesDriven;
// Using the "return" keyword, you can control what gets
// exposed and what gets hidden. In this case, we expose
// only the accelerate() and getMilesDriven() function.
return {
accelerate,
getMilesDriven
}
};
const testCarModule = CarModule();
testCarModule.accelerate(5);
testCarModule.accelerate(4);
console.log(testCarModule.getMilesDriven());
With this, we can get the number of miles driven, as well as the amount of acceleration, but since the user doesn’t need access to the speed in this case, we can hide it by only exposing the accelerate()
and getMilesDriven()
method. Essentially, speed
is a private variable, as it is only accessible to code inside of the same block scope. The benefit to private variables begins to become clear in this situation. When you remove the ability to access a variable, function, or any other internal component, you reduce the surface area for errors resulting from someone else mistakenly using something that wasn’t meant to be.
The alternative way
In this second example, you’ll notice the addition of the this
keyword. There’s a difference between the ES6 arrow function ( =>
) and the traditional function(){}
. With the function
keyword, you can use this
, which will be bound to the function
itself, whereas arrow functions don’t allow any kind of use of the this
keyword. Both are equally-valid ways to create the module. The core idea is to expose parts that should be accessed and leave other parts that should not be interacted with, hence both public and private data.
function CarModule() {
let milesDriven = 0;
let speed = 0;
// In this case, we instead use the "this" keyword,
// which refers to CarModule
this.accelerate = (amount) => {
speed += amount;
milesDriven += speed;
}
this.getMilesDriven = () => milesDriven;
}
const testCarModule = new CarModule();
testCarModule.accelerate(5);
testCarModule.accelerate(4);
console.log(testCarModule.getMilesDriven());
Enter ES6 Classes
Classes were another addition that came with ES6. Classes are essentially syntactic sugar — in other words, still a function, but potentially "sweetening” it into a form that’s easier to express. With classes, variable privacy is (as of now) close to impossible without making some major changes to the code.
Let’s take a look at an example class.
class CarModule {
/*
milesDriven = 0;
speed = 0;
*/
constructor() {
this.milesDriven = 0;
this.speed = 0;
}
accelerate(amount) {
this.speed += amount;
this.milesDriven += this.speed;
}
getMilesDriven() {
return this.milesDriven;
}
}
const testCarModule = new CarModule();
testCarModule.accelerate(5);
testCarModule.accelerate(4);
console.log(testCarModule.getMilesDriven());
One of the first things that stands out is that the milesDriven
and speed
variable are inside of a constructor()
function. Note that you can also define the variables outside of the constructor (as shown in the code comment), but they are functionally the same regardless. The problem is that these variables will be public and accessible to elements outside of the class.
Let’s look at some ways to work around that.
Using an underscore
In cases where privacy is to prevent collaborators from making some catastrophic mistake, prefixing variables with an underscore (_
), despite still being "visible” to the outside, can be sufficient to signal to a developer, "Don’t touch this variable.” So, for example, we now have the following:
// This is the new constructor for the class. Note that it could
// also be expressed as the following outside of constructor().
/*
_milesDriven = 0;
_speed = 0;
*/
constructor() {
this._milesDriven = 0;
this._speed = 0;
}
While this does
work for its specific use case, it’s still safe to say that it’s less than ideal on many levels. You can still access the variable but you also have to modify the variable name on top of that.
Putting everything inside the constructor
Technically, there is a method for variable privacy in a class that you can use right now, and that’s placing all variables and methods inside the constructor()
function. Let’s take a look.
class CarModule {
constructor() {
let milesDriven = 0;
let speed = 0;
this.accelerate = (amount) => {
speed += amount;
milesDriven += speed;
}
this.getMilesDriven = () => milesDriven;
}
}
const testCarModule = new CarModule();
testCarModule.accelerate(5);
testCarModule.accelerate(4);
console.log(testCarModule.getMilesDriven());
console.log(testCarModule.speed); // undefined -- We have true variable privacy now.
This method accomplishes true variable privacy in the sense that there is no way to directly access any variables that aren’t intentionally exposed. The problem is that we now have, well, code that doesn’t look all that great compared to what we had before, in addition to the fact that it defeats the benefits of the syntactic sugar we had with classes. At this point, we might as well be using the function()
method.
Using WeakMap
There’s another, more creative way to go about making a private variable, and that’s using WeakMap()
. Although it may sound similar to Map
, the two are very different. While maps can take any type of value as a key, a WeakMap
only take objects and deletes the values in the WeakMap
when the object key is garbage collected. In addition, a WeakMap
cannot be iterated through, meaning that you must have access to the reference to an object key in order to access a value. This makes it rather useful for creating private variables, since the variables are effectively invisible.
class CarModule {
constructor() {
this.data = new WeakMap();
this.data.set(this, {
milesDriven: 0,
speed: 0
});
}
accelerate(amount) {
// In this version, we instead create a WeakMap and
// use the "this" keyword as a key, which is not likely
// to be used accidentally as a key to the WeakMap.
const data = this.data.get(this);
const speed = data.speed + amount;
const milesDriven = data.milesDriven + data.speed;
this.data.set({ speed, milesDriven });
}
this.getMilesDriven = () => this.data.get(this).milesDriven;
}
const testCarModule = new CarModule();
testCarModule.accelerate(5);
testCarModule.accelerate(4);
console.log(testCarModule.getMilesDriven());
console.log(testCarModule.data); //=> WeakMap { [items unknown] } -- This data cannot be accessed easily from the outside!
This solution is good at preventing an accidental usage of the data, but it isn’t truly private, since it can still be accessed from outside the scope by substituting this
with CarModule
. In addition, it adds a fair amount of complexity to the mix and, therefore, isn’t the most elegant solution.
Using symbols to prevent collisions
If the intent is to prevent name collisions, there is a useful solution using Symbol
. These are essentially instances that can behave as unique values that will never be equal to anything else, except its own unique instance. Here’s an example of it in action:
class CarModule {
constructor() {
this.speedKey = Symbol("speedKey");
this.milesDrivenKey = Symbol("milesDrivenKey");
this[this.speedKey] = 0;
this[this.milesDrivenKey] = 0;
}
accelerate(amount) {
// It's virtually impossible for this data to be
// accidentally accessed. By no means is it private,
// but it's well out of the way of anyone who would
// be implementing this module.
this[this.speedKey] += amount;
this[this.milesDrivenKey] += this[this.speedKey];
}
getMilesDriven() {
return this[this.milesDrivenKey];
}
}
const testCarModule = new CarModule();
testCarModule.accelerate(5);
testCarModule.accelerate(4);
console.log(testCarModule.getMilesDriven());
console.log(testCarModule.speed); // => undefined -- we would need to access the internal keys to access the variable.
Like the underscore solution, this method more or less relies on naming conventions to prevent confusion.
TC39 private class field proposal
Recently, a new proposal was introduced that would introduce private variables to classes. It’s rather simple: put a #
before the name of a variable, and it becomes private. No extra structural changes needed.
class CarModule {
#speed = 0
#milesDriven = 0
accelerate(amount) {
// It's virtually impossible for this data to be
// accidentally accessed. By no means is it private,
// but it's well out of the way of anyone who would
// be implementing this module.
this.#speed += amount;
this.#milesDriven += speed;
}
getMilesDriven() {
return this.#milesDriven;
}
}
const testCarModule = new CarModule();
testCarModule.accelerate(5);
testCarModule.accelerate(4);
console.log(testCarModule.getMilesDriven());
console.log(testCarModule.speed); //=> undefined -- we would need to access the internal keys to access the variable.
The private class field proposal is not standard and cannot be done without using Babel as of this writing, so you’ll have to wait a bit for it to be usable on major browsers, Node, etc.
Conclusion
That sums up the various ways you can implement private variables in JavaScript. There isn’t a single "correct” way to do it. These will work for different needs, existing codebases, and other constraints. While each has advantages and disadvantages, ultimately, all methods are equally valid as long as they effectively solve your problem.
Thanks for reading! I hope this provides some insight into how scope and variable privacy can be applied to improve your JavaScript code. This is a powerful technique and can support so many different methods and make your code more usable and bug-free. Try out some new examples for yourself and get a better feel.
The post Implementing Private Variables In JavaScript appeared first on CSS-Tricks.
from CSS-Tricks http://bit.ly/2MnVr49
Comments
Post a Comment