65.9K
CodeProject is changing. Read more.
Home

JavaScript How to Access Private Fields from Chained Extended Classes

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2 votes)

Jan 14, 2023

CPOL

1 min read

viewsIcon

9553

downloadIcon

36

Access private fields from Chained Extended classes

Introduction

While working with JavaScript class inheritance, I found something Interesting I would love to share. A way to access private class fields from classes that didn't define them, only extended them, while still keeping them private to the outside world.

Bypass errors like:
Uncaught SyntaxError: Private field '#active' must be declared in an enclosing class

Chained Extended Classes

Below is an example of creating chained multi inheritance of JavaScript classes:

// Foo3 class to extend from
class foo3 {

    // Private Fields
    #active = false;
    
    get active() { return this.#active; }
    set active(value) { this.#active = value; }
    
    constructor() { }
    
    $(Class, Name, Value = undefined) {
            if (Class!=='foo3') { if (super.$) 
               { return super.$(Class, Name, Value); } return; }
            
            switch (Name) {
                    case '#active':
                        if (typeof Value === 'undefined') {
                                return this.#active;
                        } else {
                                this.#active = Value;
                        }
                        break;
                    default: if (super.$) 
                    { return super.$(Class, Name, Value); } break;
            }
        }
}

// Foo2 class to extend from
class foo2 extends foo3 {

    // Private Fields
    #active = 'no';
    
    get active() { return this.#active; }
    set active(value) { this.#active = value; }
    
    constructor() { super(); }
    
    $(Class, Name, Value = undefined) {
            if (Class!=='foo2') { if (super.$) 
               { return super.$(Class, Name, Value); } return; }
            
            switch (Name) {
                    case '#active':
                        if (typeof Value === 'undefined') {
                                return this.#active;
                        } else {
                                this.#active = Value;
                        }
                        break;
                    default: if (super.$) 
                    { return super.$(Class, Name, Value); } break;
            }
        }
}

// Foo class with chained extended: foo <- foo2 <- foo3
class foo extends foo2 {
    
    constructor() { super(); }
    
    // Prevent outside direct access to method by override.
    $(Class, Name, Value = undefined) {
            throw new Error(`This method is for protected use!`);
    }
        
    test() {
         // Sets foo2 #active
         super.$('foo2', '#active', 'yes');
         
         // Sets foo3 #active
         super.$('foo3', '#active', true);
         
         // Gets foo2 #active
        const foo2_active = super.$('foo2','#active'); 
        
        // Gets foo3 #active
        const foo3_active = super.$('foo3','#active'); 
        
        console.log(`Print Original Values`);
        console.log(`Private - foo2.#active = 'no'`);
        console.log('Private - foo3.#active = false');
        console.log(`Public - this.active = 'no'`); 
        
        console.log(`\nPrint Changed Fields`);
        console.log(`Private - foo2.#active = '${foo2_active}'`);
        console.log(`Private - foo3.#active = ${foo3_active}`);
        console.log(`Public - this.active = '${this.active}'`); 
        
        // Set this active
        this.active = 'off';
        
        console.log(`\nReprint after setting: this.active = 'off'`);
        console.log(`Private - foo2.#active = '${foo2_active}'`);
        console.log(`Private - foo3.#active = ${foo3_active}`);
        console.log(`Public - this.active = '${this.active}'`); 
    }
}

const f = new foo();
f.test();

Super Recursion Method for Private Access

Notice the public method named '$' per each class. This method takes 3 params.

$(Class, Name, Value = undefined)

  1. Class: The string name for the extended class that you would like to access a private field.
  2. Name: The name of the private field to access.
  3. Value: The value to set the private field to.

This special method allows you to get and set private fields directly per class.

     $(Class, Name, Value = undefined) {
            if (Class!=='foo2') 
               { if (super.$) return super.$(Class, Name, Value); return; }
            
            switch (Name) {
                    case '#active':
                        if (typeof Value === 'undefined') {
                                return this.#active;
                        } else {
                                this.#active = Value;
                        }
                        break;
                    default: if (super.$) return super.$(Class, Name, Value);
            }
        }

A couple of things you need to change when creating your own class access. The class name and private fields you want to directly access. You must define everything you want to use. There is no access to private members like this['#active']. Only access like this.#active. So you must declare everything you want to use per string case. You only need to add the private fields from each class.

Console Output from Above Code


Print Original Values
Private - foo2.#active = 'no'
Private - foo3.#active = false
Public - this.active = 'no'
 
Print Changed Fields
Private - foo2.#active = 'yes'
Private - foo3.#active = true
Public - this.active = 'yes'

 
Reprint after setting: this.active = 'off'
Private - foo2.#active = 'yes'
Private - foo3.#active = true
Public - this.active = 'off'

Override Extended Private Method

Protected methods can look like below.

    // Protected Function
    #message = (str) => {
        console.log(`foo2:message('${str}')`);
    }

I also changed the super recursive method to handle private functions and remove having to write string class name. Now, just say the name in the signature call.

New Super Recursive $ Method

Added comment to instruct you about function parameters.

   /**
    * For accessing private members directly.
    * @param {function} Class - The class name where the field exists.
    * @param {string} Member - The member name.
    * @param {any} Value - The value to set the field to. optional
    * @returns {any} - Returns private field if Value is undefined.
    */
    $(Class, Member, Value = undefined) {

        // Jumper
        if (!(Object.getPrototypeOf(this) instanceof Class) && super.$) { return super.$(Class, Member, Value); }

        switch (Member) {

            case '#active':
                if (typeof Value === 'undefined') { return this.#active; } else { this.#active = Value; }
                break;

            case '#message':
                if (typeof Value === 'undefined') { return this.#message; } else { this.#message = Value; }
                break;

            default: if (super.$) { return super.$(Class, Member, Value); } break;
        }


    }

Example Call to $ Method

// Change field
super.$(foo3, '#active', true);

// Override method
super.$(foo2, '#message', this.#message);

Example 2 Output

foo2:message('Hello from foo2_test()')
Overriding foo2:#message() with foo:message()
foo:message('Hello from foo2_test()')

this.active = no
this.active2 = false
this.active = true
this.active2 = Yes

Uncaught Error: This method is for protected use!
    at foo.$ (test2.html:100:15)
    at test2.html:131:5

History

  • 16th January, 2023: Override Private Function
  • 14th January, 2023: Initial version