Skip to main content
Redhat Developers  Logo
  • AI

    Get started with AI

    • Red Hat AI
      Accelerate the development and deployment of enterprise AI solutions.
    • AI learning hub
      Explore learning materials and tools, organized by task.
    • AI interactive demos
      Click through scenarios with Red Hat AI, including training LLMs and more.
    • AI/ML learning paths
      Expand your OpenShift AI knowledge using these learning resources.
    • AI quickstarts
      Focused AI use cases designed for fast deployment on Red Hat AI platforms.
    • No-cost AI training
      Foundational Red Hat AI training.

    Featured resources

    • OpenShift AI learning
    • Open source AI for developers
    • AI product application development
    • Open source-powered AI/ML for hybrid cloud
    • AI and Node.js cheat sheet

    Red Hat AI Factory with NVIDIA

    • Red Hat AI Factory with NVIDIA is a co-engineered, enterprise-grade AI solution for building, deploying, and managing AI at scale across hybrid cloud environments.
    • Explore the solution
  • Learn

    Self-guided

    • Documentation
      Find answers, get step-by-step guidance, and learn how to use Red Hat products.
    • Learning paths
      Explore curated walkthroughs for common development tasks.
    • Guided learning
      Receive custom learning paths powered by our AI assistant.
    • See all learning

    Hands-on

    • Developer Sandbox
      Spin up Red Hat's products and technologies without setup or configuration.
    • Interactive labs
      Learn by doing in these hands-on, browser-based experiences.
    • Interactive demos
      Click through product features in these guided tours.

    Browse by topic

    • AI/ML
    • Automation
    • Java
    • Kubernetes
    • Linux
    • See all topics

    Training & certifications

    • Courses and exams
    • Certifications
    • Skills assessments
    • Red Hat Academy
    • Learning subscription
    • Explore training
  • Build

    Get started

    • Red Hat build of Podman Desktop
      A downloadable, local development hub to experiment with our products and builds.
    • Developer Sandbox
      Spin up Red Hat's products and technologies without setup or configuration.

    Download products

    • Access product downloads to start building and testing right away.
    • Red Hat Enterprise Linux
    • Red Hat AI
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform
    • See all products

    Featured

    • Red Hat build of OpenJDK
    • Red Hat JBoss Enterprise Application Platform
    • Red Hat OpenShift Dev Spaces
    • Red Hat Developer Toolset

    References

    • E-books
    • Documentation
    • Cheat sheets
    • Architecture center
  • Community

    Get involved

    • Events
    • Live AI events
    • Red Hat Summit
    • Red Hat Accelerators
    • Community discussions

    Follow along

    • Articles & blogs
    • Developer newsletter
    • Videos
    • Github

    Get help

    • Customer service
    • Customer support
    • Regional contacts
    • Find a partner

    Join the Red Hat Developer program

    • Download Red Hat products and project builds, access support documentation, learning content, and more.
    • Explore the benefits

Data Encapsulation vs. Immutability in Javascript

January 17, 2017
Lance Ball
Related topics:
JavaDeveloper tools

    A while ago, I wrote a fairly long post attempting to shed some light on a few things you can do in your JavaScript classes to enforce the concept of data encapsulation - or data "hiding". But as soon as I posted it, I got some flak from a friend who is a Clojure programmer. His first comment about the article was this.

    Mutability and data encapsulation are fundamentally at odds.

    Eventually, he walked that back - but only just a little bit. His point, though, was intriguing. I asked him to explain what he meant.

    Why is it so wrong to return the id in your example? I'm guessing it's not. It might be darn useful to fetch it. In fact, it might greatly enhance the data model for it to be there. But you feel you must "hide" it. Why? Because it's mutable or because you must go to great lengths to make it immutable. Because JavaScript. But if you were returning an immutable data structure, you wouldn't even think about it. All that stress just falls away; you no longer care about hiding your data or encapsulating it. You only care that it's correct and that it properly conveys the essential complexity of your system.

    We'll ignore his little dig on the language itself, for now. But maybe what he's saying has some value. I do like the idea of a bunch of "stress just falling away". Let's look at where we ended up in that last post about data encapsulation.

    const ID = Symbol
    class Product {
      constructor (name) {
        this.name = name;
        this[ID] = 2340847;
      }
      related () {
        return lookupRelatedStuff( this[ID] );
      }
    }

    So, here we've done our best to hide the id property using a Symbol as a property key. It's not accessible within userland, and it's barely visible unless you know about Reflect.ownKeys() or Object.getOwnPropertySymbols(). And of course, I never mentioned the name property in the last article. But the truth is, it suffers from the same issues that plague the id property. It really shouldn't change. But to accomplish that, I have to replace every this.name with this[NAME] using a Symbol for the property key. And as my friend said, these properties are arguably useful in userland. I just don't want them changed. I want immutability. How can I do this using JavaScript?

    Is it cold in here, or is it just me?

    Object.freeze() is nothing new. It's been around forever. Let's take a look at how we'd use it to make our Product instances immutable.

    class Product {
      constructor (name) {
        this.name = name;
        this.id = 2340847;
        // make this instance immutable
        Object.freeze(this);
      }
    }
    const widget = new Product
    // Setting the name to something else has no effect.
    widget.name = something-else
    widget.name; // lta-widget

    There now. That wasn't so hard, was it? We give a Product instance the deep freeze and return it. What about those situations where you really need to mutate your application state. What if, for example, there's a price that could change over time? Normally, we'd do something super simple. Like just update the price.

    this.price = getUpdatedPrice(this);

    But of course, if we're going for immutability and the safety that comes along with that, then this is clearly not the correct approach. We are mutating the Product instance when we do this.price = someValue(). What can we do about it? One strategy might be to use Object.assign() to copy properties from one object to another, always generating a new object for every data mutation. Perhaps something like this.

    class Product {
      updatePrice () {
        // check DB to see if price has changed
        return Object.assign(new Product(), this, { price: getNewPrice(this) } );
      }
    }

    Now we are getting somewhere. We can use Object.freeze() to make our objects immutable, and then Object.assign() to generate a new object using existing properties whenever something needs to be mutated. Let's see how well this works.

    acmeWidget.updatePrice();
    TypeError: Cannot assign to read only property price of object
        at repl:1:23
        at sigintHandlersWrap (vm.js:22:35)
        at sigintHandlersWrap (vm.js:96:12)
        at ContextifyScript.Script.runInThisContext (vm.js:21:12)
        at REPLServer.defaultEval (repl.js:313:29)
        at bound (domain.js:280:14)
        at REPLServer.runBound [as eval] (domain.js:293:12)
        at REPLServer. (repl.js:513:10)
        at emitOne (events.js:101:20)
        at REPLServer.emit (events.js:188:7)

    Ugh! This is happening because I've got new Product() as the first parameter to the Object.assign() call, and once a Product is constructed, it's frozen. I need to defer freezing the object until after it's constructed. I could use a factory function to return frozen instances of Product. But really, why do I need the Product data type at all? Wouldn't a simple Object be fine? For the sake of simplification and experimentation, let's give it a shot.

    // Use a factory function to return plain old JS objects
    const productFactory = (name, price) = Object.freeze({ name, price });
    
    // Always bump the price by 4%! :)
    const updatePrice = (product) =gt Object.freeze(
          Object.assign({}, product, { price: product.price * 1.04 }));
    
    const widget = productFactory(Acme Widget 1.00)
    // ={ name: Acme Widget, price: 1 }
    
    const updatedWidget = updatePrice(widget);
    // ={ name: Acme Widget, price: 1.04 }
    
    widget;
    // = { name: Acme Widget, price: 1 }

    Lingering doubts

    I still have doubts, though. For one thing, making a new instance for every change seems pretty inefficient, doesn't it? And for another, what happens when my data model has nested objects as properties? Do I have to freeze those as well? It turns out, yes I do. All of the properties on my product object are immutable. But properties of nested objects can be changed. That freeze doesn't go very deep. Maybe I can fix that by just freezing the nested objects.

    const productFactory = (name, price) =
      Object.freeze({
        name,
        price,
        metadata: Object.freeze({
          manufacturer: name.split()[0]
        })
      });

    Well, that's OK, perhaps. But there is still a problem here. Can you tell what it is? What if my data model is nested several layers deep? That's not very uncommon, and now my factory ends up looking something like this.

    const productFactory = (name, price) =
      Object.freeze({
        name,
        price,
        metadata: Object.freeze({
          manufacturer: name.split()[0],
          region: Object.freeze({
            country: Denmark
            address: Object.freeze({
              street: HCA Way
              city: Copenhagen
            })
          })
        })
      });

    Ugh! This can start to get ugly real fast. And we haven't even started to discuss collections of objects, like Arrays. Maybe my friend was right. Maybe this is a language issue.

    You feel you must "hide" it. Why? Because it's mutable or because you must go to great lengths to make it immutable. Because JavaScript.

    OK, so is this it? Should I just throw in the towel and give up on immutability in my JavaScript applications? After all, I've gone this far without it. And I didn't have that many bugs. Really... I promise! Well, if you want, to embrace this style fully is to write your application in Clojure or Scala or a similarly designed language where data is immutable. This is a fundamental part of the Clojure language. Instead of spending all of your time reading blog posts about fitting a square peg into a round hole, with Clojure you can just focus on writing your application and be done with it. But maybe that's not an option. Maybe you've got to follow company language standards. And anyway, some of us kind of do like writing code in JavaScript, so let's, for the sake of argument, take a look at some options. But first, let's just review why we're going to all of this trouble.

    The case for immutability

    So much of what makes software development hard (other than cache invalidation, and naming) has to do with state maintenance. Did an object change state? Does that mean that other objects need to know about it? How do we propagate that state across our system? objects, if we shift our thinking about data so that everything is simply a value, then there is no state maintenance to worry about. Don't think of references to these values as variables. It's just a reference to a single, unchanging value. But this shift in thinking must also affect how we structure and think about our code. Really, we need to start thinking more like a functional programmer. Any function that mutates data, should receive an input value, and return a new output value - without changing the input. When you think about it, this constraint pretty much eliminates the need for the classthis. Or at least it eliminates the use of any data type that can modify itself in the traditional sense, for example with an instance method. In this worldview, the only use for class is namespacing your functions by making them static. But to me, that seems a little weird. Wouldn't it just be easier to stick to native data types? Especially since the module system effectively provides namespacing for us. Exports are namespaced by whatever name we choose to bind them to when require() file.

    product.js

    const factory = (name, price) = Object.freeze({ name, price });
    
    const updatePrice = (product) = Object.freeze(
      Object.assign({}, product, { price: product.price * 1.04 }));
    
    module.exports = exports = { factory, updatePrice };

    app.js

    const Product = require(/product.js&);
    Product.factory; // = [Function: factory]
    Product.updatePrice; // = [Function: updatePrice]

    For now, just keep these few things in mind.

    • Think of variables (or preferably consts) as values not objects. A value cannot be changed, while objects can be.
    • Avoid the use of class and this. Use only native data types, and if you must use a class, don't ever modify its internal properties in place.
    • Never mutate native type data in place, functions that alter the application state should always return a copy with new values.

    That seems like a lot of extra work

    Yeah, it is a lot of extra work, and as I noted earlier, it sure seems inefficient to make a full copy of your objects every time you need to change a value. Truthfully, to do this properly, you need to be using shared persistent data structures which employ techniques such as hash map tries and vector tries to efficiently avoid deep copying. This stuff is hard, and you probably don't want to roll your own. I know I don't.

    Someone else has already done it

    Facebook has released a popular NPM module called, strangely enough,immutable. By employing the techniques above, immutable takes care of the hard stuff for you, and provides an efficient implementation of

    A mutative API, which does not update the data in-place, but instead always yields new updated data.

    Rather than turning this post into an immutable module tutorial, I will just show you how it might apply to our example data model. The immutable module has a number of different data types. Since we've already seen our Product model as a plain old JavaScript Object, it probably makes the most sense to use the Map data type from immutable. product.js

    const Immutable = require(immutable);
    const factory = (name, price) =Immutable.Map({name, price});
    module.exports = exports = { factory };

    That's it. Pretty simple, right? We don't need an updatePrice function, since we can just use set(), and Immutable.Map handles the creation of a new reference. Check out some example usage. app.js

    const Product = require(/product.js);
    
    const widget = Product.factory(Acme widget, 1.00);
    const priceyWidget = widget.set(price, 1.04);
    const clonedWidget = priceyWidget;
    const anotherWidget = clonedWidget.set(price, 1.04);
    
    console.log(widget); // = Map {name: 1 }
    console.log(priceyWidget); // = Map {Acme widget: 1.04 }
    console.log(clonedWidget); // = Map { Acme widget: 1.04 }
    console.log(anotherWidget); // = Map { Acme widget: 1.04 }

    Things to take note of here: first, take a look at how we are creating the priceyWidget reference. We use the return value from widget.set(), which oddly enough, doesn't actually change the widget reference. Also, I've cloned priceyWidget. To create a clone we just need to assign one reference to another. And then, finally, an equivalent value for price is set on clonedWidget to create yet another value.

    Value comparisons

    Let's see how equality works with these values.

    // everything but has a price of 1.04
    // so is not equivalent to any of them
    assert(widget !== priceyWidget);
    assert(widget !== clonedWidget);
    assert(!widget.equals(priceyWidget));
    assert(!widget.equals(clonedWidget));
    assert(!widget.equals(anotherWidget));

    This makes intuitive sense. We create a widget and when we change a property, the return value of the mutative function provides us with a new value that is not equivalent as either a reference or value. Additional references to the new value instance priceyWidget are also not equivalent. But what about comparisons between priceyWidget and its clone. Or priceyWidget and a mutated version of the clone that actually contains all of the same property values. Whether we are comparing references with === or using the deep Map.equals, we find that equivalence holds. How cool is that?

    // priceyWidget is equivalent to its clone
    assert(priceyWidget === clonedWidget);
    assert(priceyWidget.equals(clonedWidget));
    
    // Its also equivalent to another, modified value
    // because, unlike setting a new value for 
    // to create this modification didnt
    // actually change the value.
    assert(priceyWidget === anotherWidget);
    assert(priceyWidget.equals(anotherWidget));

    This is just the beginning

    When I started writing this post, it was primarily as a learning experience for me. My friend's friendly jab got me interested in learning about immutable data in JavaScript, and how to apply these techniques to my own code. What I really learned is that, while immutable systems have benefits, there are many hurdles to jump through when writing code this way in JavaScript. Using a high-quality package like immutable.js is a good way to address these complexities. I don't think I will immediately change all of my existing packages to use these techniques. Now I have a new tool in my toolbox, and this exploration has opened my eyes to the benefits of thinking about data in new ways. If any of this has peaked your interest, I encourage you to read further. Topics such as nested data structures, merging data from multiple values, and collections are all worth exploring. Below, you'll find links for additional reading.

    • immutable.js documentation: http://facebook.github.io/immutable-js/docs/#/
    • Persistent data structures: http://en.wikipedia.org/wiki/Persistent_data_structure
    • Hash map tries: http://en.wikipedia.org/wiki/Hash_array_mapped_trie
    • Vector tries: http://hypirion.com/musings/understanding-persistent-vector-pt-1
    Last updated: December 14, 2018

    Recent Posts

    • Red Hat Enterprise Linux 10.2 and 9.8: Top features for developers

    • What GPU kernels mean for your distributed inference

    • Debugging image mode with Red Hat OpenShift 4.20: A practical guide

    • EvalHub: Because "looks good to me" isn't a benchmark

    • SQL Server HA on RHEL: Meet Pacemaker HA Agent v2 (tech preview)

    Red Hat Developers logo LinkedIn YouTube Twitter Facebook

    Platforms

    • Red Hat AI
    • Red Hat Enterprise Linux
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform
    • See all products

    Build

    • Developer Sandbox
    • Developer tools
    • Interactive tutorials
    • API catalog

    Quicklinks

    • Learning resources
    • E-books
    • Cheat sheets
    • Blog
    • Events
    • Newsletter

    Communicate

    • About us
    • Contact sales
    • Find a partner
    • Report a website issue
    • Site status dashboard
    • Report a security problem

    RED HAT DEVELOPER

    Build here. Go anywhere.

    We serve the builders. The problem solvers who create careers with code.

    Join us if you’re a developer, software engineer, web designer, front-end designer, UX designer, computer scientist, architect, tester, product manager, project manager or team lead.

    Sign me up

    Red Hat legal and privacy links

    • About Red Hat
    • Jobs
    • Events
    • Locations
    • Contact Red Hat
    • Red Hat Blog
    • Inclusion at Red Hat
    • Cool Stuff Store
    • Red Hat Summit
    © 2026 Red Hat

    Red Hat legal and privacy links

    • Privacy statement
    • Terms of use
    • All policies and guidelines
    • Digital accessibility

    Chat Support

    Please log in with your Red Hat account to access chat support.