07 Deep Understanding of Object's Private and Static Properties

07 Deep Understanding of Object’s Private and Static Properties #

Hello, I am Ishikawa.

In the previous lessons, we discussed functional programming, starting from the basics of input, computation, and output, then moving on to the possible side effects that may occur during the process, and how to manage side effects using pure functions and immutability. We gained a systematic understanding of these topics. Then, we explored how to respond to the unknown and make changes based on events by combining reactive programming with functional programming.

Starting from this lesson, let’s delve deeper into JavaScript object construction and object-oriented programming.

In Lesson 1, we were first introduced to objects and object-oriented programming. An object is like a person, born with attributes and functionalities. For example, skin color and height are our attributes, and being able to cry and laugh are our functionalities. Interacting with other objects is the essence of object-oriented design. Today, we will start by discussing how to create an object.

Image

In object-oriented design, the properties of an object are crucial because they determine what the object is and what it can do. An object can have public properties that can be accessed by others, as well as private properties that are not exposed and cannot be accessed casually.

In addition to public and private properties, there are also static properties. Static properties belong to a class, not individual objects. This may sound a bit confusing, so let’s use an analogy. For example, China has a population of 1.4 billion. The “1.4 billion population” is an attribute of the country class, but we cannot say that each Chinese person has the attribute of “1.4 billion population.”

Similarly, static properties include both public and private attributes. For example, a company, as an organization class, usually discloses the number of employees as its public static property to showcase its scale. However, the company would not disclose some operational data because it is sensitive and only provided during audits or specific occasions. This operational data would be a private static property.

The support for creating private and static properties was introduced in the ECMAScript specification in June 2022, which is 25 years after the introduction of JavaScript (although almost all mainstream browsers support these two features, except for the retired Internet Explorer). However, before this, people tried to achieve similar functionalities through other means.

In today’s lesson, we will explore the underlying logic and applications of private and static properties.

How to create private properties? #

In object-oriented programming, there is an important concept of creating private properties.

As we can see, unlike Java, when creating a Widget object in JavaScript, whether using class, object literals, or function constructors, in general, after defining properties and methods, they can be publicly accessed without any restrictions.

// Example 1: class
class WidgetA {
  constructor() {
    this.appName = "Weather App";
  }
  getName() {
    return this.appName;
  }
}
var widget1 = new WidgetA();
console.log(widget1.appName); // Returns "Weather App"
console.log(widget1.getName()); // Returns "Weather App"

// Example 2: object literals
var WidgetB = {
  appName: "Weather App",
  getName: function() {
    return this.appName;
  }
}

console.log(WidgetB.appName); // Returns "Weather App"
console.log(WidgetB.getName()); // Returns "Weather App"

// Example 3: function constructor
function WidgetC() {
  this.appName = "Weather App";
  this.getName = function() {
    return "Weather App";
  };
}

var widget3 = new WidgetC();
console.log(widget3.appName); // Returns "Weather App"
console.log(widget3.getName()); // Returns "Weather App"

Create private properties using the # symbol #

So how can we create private properties in objects? According to the latest ES13 specification, we can use the # symbol to define a private property.

First, we declare a #appName private property. In the constructor, we assign it the value “Weather App”. When we try to access appName directly, we will see that the returned result is undefined. However, if we use the getName method, we can access the value of appName.

class WidgetD {
  #appName;
  constructor() {
    this.#appName = "Weather App";
  }
  getName() {
    return this.#appName;
  }
}

var widget4 = new WidgetD();
console.log(widget4.appName); // Returns undefined
console.log(widget4.getName()); // Returns "Weather App"

So now, let’s take a look at how engineers achieved private properties before the introduction of #. There are three main ways: using closures, WeakMap, and Symbol.

Create private properties using closures and IIFE #

First, let’s see how to create private properties in object literals. Yes, closures, which we mentioned earlier in functional programming, come into play here.

First, we declare a variable WidgetE. Then we create an immediately invoked function expression (IIFE). Inside this expression, we first assign the appName variable to the value “Weather App”.

Next, we assign WidgetE within the function, where we define the getName method. This method returns the appName from the outer function.

At this point, when we try to access WidgetE.appName, we will find that we cannot access the variable declared inside the nested function. However, when we use the getName method, taking advantage of the fact that nested functions can access variables from the outer function, we can obtain the corresponding return value.

// Object literals
var WidgetE;
(function() {
  var appName = "Weather App";
  WidgetE = {
    getName: function() {
      return appName;
    }
  };
}());
WidgetE.appName; // Returns undefined
WidgetE.getName(); // Returns "Weather App"

Okay, let’s take a look at how to create private properties using the constructor function.

Here, we can also use closures we have learned to directly code. This example actually seems simpler than the previous one. We first define a function in which we declare a variable appName, and then create an expression function getName that returns appName.

// Constructor function
function WidgetF() {
  var appName = "Weather Application";
  this.getName = function(){
    return appName;
  }
}
var widget6 = new WidgetF();
console.log(widget6.appName); // Returns undefined
console.log(widget6.getName()); // Returns "Weather Application"

At this point, we can create a new function widget6 through function construction. However, there is no result from accessing appName with this newly constructed object because appName is encapsulated inside WidgetF. However, widget6 can access appName through getName. Similarly, this uses the feature of closures to access variables outside the function.

However, there is still a problem in this example. Every time we create a new object, the private property is recreated, resulting in redundant work and redundant memory usage. To solve this problem, assign common properties and functionality to the prototype, so that objects created by the same constructor can share these hidden properties.

For example, let’s look at the following example. We assign a function returning an object to the prototype of WidgetG. The function contains private properties and the object returned contains a method to get the property. This way, after creating an object widget7, we can see that it can get the device supported by the weather application.

function WidgetG() {
  var appName = "Weather Application";
  this.getName = function(){
    return appName;
  }
}
WidgetG.prototype = (function(){
  var model = "Supports Android";
  return {
    getModel: function(){
      return model;
    }   
  }
}());
var widget7 = new WidgetG();
console.log(widget7.getName()); // Returns "Weather Application"
console.log(widget7.getModel()); // Returns "Supports Android"

Creating Private Properties using WeakMap #

In ES6, JavaScript introduced the Set and Map data structures. Set and Map are mainly used for data restructuring and storage. Set uses the data structure of a set, and Map uses the data structure of a dictionary. Map has extremely fast lookup speed, and in the subsequent lessons when we discuss data structures and algorithms, we will explain it in detail. Here, let’s take a look at WeakMap. Its feature is that it only accepts objects as keys, the keys are weakly referenced, and the values can be anything.

In the example below, we first declare a WidgetG variable. Then, we establish a block scope, inside which we declare a privateProps variable of type WeakMap. Then, we assign a function declaration to WidgetG. In it, we set the key name of the WeakMap to this, and the appName in the value to “Weather Application”. Next, based on the prototype of WidgetF, we create a getName method that returns the value of appName.

Using this method, we can achieve encapsulation of appName and access to the private property values through getName.

var WidgetH;
{
  let privateProps = new WeakMap();
  
  WidgetH = function(){
    privateProps.set(this,{appName : "Weather Application"});
  }
  
  WidgetH.prototype.getName = function(){
    return privateProps.get(this).appName;
  }
}
  
var widget8 = new WidgetH();
console.log(widget8.appName); // Returns undefined
console.log(widget8.getName()); // Returns "Weather Application"

Creating Private Properties using Symbol #

Symbol is also a new data type introduced in ES6. We can use it to name the keys of object properties.

Let’s look at an example. Similar to the previous example, we establish a block scope, but the difference here is that we replace privateProps from WeakMap with Symbol to implement private properties.

var WidgetI;
{
  let privateProps = Symbol();
  
  WidgetI = function(){
    this[privateProps] = {appName : "Weather Application"};
  }
  
  WidgetI.prototype.getName = function(){
    return this[privateProps].appName;
  }
}
  
var widget9 = new WidgetI();
console.log(widget9.getName()); // Returns "Weather Application"

How to Create Static Properties? #

Previously, we mentioned that static properties belong to the constructor function, not the instantiated objects. Now let’s see how to implement static properties in JavaScript.

Creating Public Static Properties #

First, let’s see how to create public static properties using the keyword static. As shown in the code below, when we directly access appName and getName on WidgetJ, we can see that the result is “Weather App”. However, if we create a widget10 using WidgetJ, we can see that the result is undefined. This indicates that static properties can only be accessed by the class itself.

class WidgetJ {
  static appName = "Weather App";
  static getName(){
    return this.appName;
  } 
}

console.log(WidgetJ.appName); // Returns "Weather App"
console.log(WidgetJ.getName()); // Returns "Weather App"

var widget10 = new WidgetJ();
console.log(widget10.appName); // Returns undefined
console.log(widget10.getName()); // Returns undefined

Creating Private Static Properties #

Now that we have covered public static properties, let’s take a look at private static properties. Private static properties, as the name suggests, are not only for the constructor to use, but are also encapsulated within the object being constructed.

To implement private static properties, we simply combine the # symbol and the static keyword.

class WidgetM {
  static #appName = "Weather App";
  static staticGetName(){
    return WidgetM.#appName; 
  }
  instanceGetName(){
    return WidgetM.#appName; 
  }
}

console.log(WidgetM.staticGetName()); // Returns "Weather App"

var widget13 = new WidgetM();
console.log(widget13.instanceGetName()); // Returns "Weather App"

Summary #

In this lesson, we further understand object construction through the use of private and static properties within objects, building upon the foundation covered in the first lesson. Importantly, we also explore how to achieve the same functionality by removing the syntactic sugar of private properties and utilizing lower-level approaches such as closures in functional programming, prototypes within objects, Map and Symbol in value types. In the following two lessons, we will continue to extend our understanding from individual objects to the “production relationship” between objects, further deepening our comprehension of object-oriented programming patterns.

Thought-provoking question #

Today, we have attempted to implement private attributes in objects in a more low-level way by removing syntactic sugar. Can you try to remove the syntactic sugar for static attributes to achieve similar functionality?

Feel free to share your answer, exchange learning experiences, or ask questions in the comments section. If you find it helpful, you are also welcome to share today’s content with more friends.