Scope of this in Object Method

I’m fairly new to javascipt and coming from PHP programming I’m having difficulty getting my head around scope within objects. Of course in PHP this used anywhere within a class always refers to the object instantiated from it. In javascript this is not the case.
The following is my first javascript class. It’s a piece of form control to enable a group of input “switches” (such as radios) to control the appearance (or not) of other inputs that depend on the switch selected.
It was fairly simple to write a script without objects to do this for a particular form. But I wanted a more generic, reusable script that could be used on dynamically created forms (coming from PHP/MySql) where the names and number of switchable inputs could vary.
So the idea was to create a class, then instantiate an object from it, passing in the data for the specific form.
This is what I came up with, and it does work, but I’m not happy with it.

class Iswitch {
	
	constructor(fDepends){
		this.data = JSON.parse(fDepends);
	
		this.switches = [];
		this.depends = [];
	
		for (const value of this.data.switches){
			let sw = document.getElementById(value);
			sw.addEventListener('click', this.change);
			this.switches.push(sw) ;		
		}
		for (const value of this.data.depends){
			let dep = document.getElementById(value);
			this.depends.push(dep) ;		
		}
	}
	
	hide = function(elem){		// Hide an element by addign a class
		elem.classList.add('hide');
	}
	show = function(elem){		// Show an element by removing a class
		elem.classList.remove('hide');
	}
	
	change(){	
		console.log(this); // "this" is the target input element!!
		const sel = event.target.id;
		for(const [key,value] of formSwitch.switches.entries()){	// Find the event target switch
			if( sel === value.id ){
				formSwitch.show(formSwitch.depends[key]); // Show target's dependant input
			}
			else{
				formSwitch.hide(formSwitch.depends[key]); // Hide the dependant inputs of all other switches
			}
		}
	}
}

// JSON Data to be changable depending on form
const formSwitch = new Iswitch('{"switches": ["depRad", "indRad"], "depends": ["dep", "ind"]}');

The bit I don’t like is in the change method. To access the arrays of switches and dependants I have to refer to the object by its variable name, as in: formSwitch.switches.entries()
This just seems wrong to my mind, as I should like the class to be independent of the names of variables outside of it, so I can call the object anything and the methods will work without having to edit its code.
I initially used this in place of the variable name thinking it would refer to the object itself, but that didn’t work of course. When I added the console.log(this) line I discovered to my surprise that this in the scope of the method is the input element that called it via the listener.
Doing bit of reading it seems you can’t (easily) get the objects variable name within its methods. So I’m wondering: what is the best/correct way to do it?

Yeah this in JavaScript is a bit of a funny thing. It has a slightly different context than what you might be use to in other OOP languages. If you check out MDN, they have a great line that sums it up for most cases…

In most cases, the value of this is determined by how a function is called (runtime binding).

As you discovered, since the input element is the one triggering the listener, this is the input element. You can change that behavior in a few different ways, depending on the version of JS you are using. I suggest reading this page from MDN to get an idea of the problem and possible solutions. It may be that the bind() method approach is what you would need.

I hope this helps. :wink:

2 Likes

Bind has done it thank you.
I had already looked into using bind, but I wasn’t doing right so it never worked. But following that reference I saw how it’s done. I added it to the call in the listener:-

sw.addEventListener('click', this.change.bind(this));

Now I can use this in the change method and remove any reference to the object’s variable name.

change(){	
		const sel = event.target.id;
		for(const [key,value] of this.switches.entries()){	// Find the event target switch
			if( sel === value.id ){
				this.show(this.depends[key]); // Show target's dependant input
			}
			else{
				this.hide(this.depends[key]); // Hide the dependant inputs of all other switches
			}
		}
	}

Awesome! Congrats on getting it fixed. :slight_smile:

This is the more modern approach using arrow functions.

sw.addEventListener('click', () => this.change());

Also one word on JavaScript. JavaScript is a very different language from php in many ways. Two major differences are that it is prototype based (not oop) and asynchronous by nature and design. Therefore, programming in JavaScript like you are programming in php is only going to lead you down a dark path of horrible nightmares and poorly constructed code.

One of the most powerful features of JavaScript is the asynchronous design. Providing the capability to harness hybrid programming methodologies like object-oriented and functional, reactive programming. Modern JavaScript programming is really driven my the ability to manipulate async streams using promise chaining. Elegant well constructed code looks very different when thinking in async streams rather than synchronous objected-oriented programming.

Promise chaining

https://javascript.info/promise-chaining

Threw me for a second, until checking out the context. Maybe worth pointing out that ‘this’ inside of an arrow function is lexically scoped. Arrow functions do not have their own binding to this, rather they have to look up the scope chain.

So for instance you couldn’t do

const myObj = {name: 'Fred'}
const getName = () => this.name

const boundGetName  = getName.bind(myObj)

Maybe I am wrong, but I would have thought Javascript at it’s core is synchronous, with some asynchronous features baked in (promises) and other features xmlhttprequest, setTimeout etc being browser features.

I mean this as far as execution top to bottom is a synchronous procedure, no?

fetch('http://example.com/movies.json')
  .then(response => response.json())
  .then(data => console.log(data));

Promises are returned immediately, their then methods are called immediately, but much like with addEventlistener, callbacks are registered to be called at a later date.

Maybe I’m splitting hairs, but just find that statement a bit confusing.

Straight from the horses mouth.

google says that:

JavaScript is a single-threaded, non-blocking, asynchronous , concurrent programming language with lots of flexibility.

A http request is an asynchronous operation and promises are asynchronous.

Promises are not resolved immediately. They can be resolved at any time. They are a macro operation registered with the internal event loop.

Yes, but their setup isn’t, was the point I was making there. And http requests are a browser feature not a javascript built in, fetch being a two pronged procedure.

Javascript is capable of performing asynchronous procedures, but is synchronous.

I could google and come up with

Spoiler: at its base, JavaScript is a synchronous, blocking, single-threaded language.

Or MDN where synchronous and asynchronous Javascript is discussed

So you are saying google is wrong.

I think the problem is google is using the wrong page to resolve the answer to the question.

I’m not discussing resolving, fullfillment, or settling.

A promise object is immediately returned though. The .then is immediately called on that promise and a callback registered. A new promise is immediately returned from that .then and so on. That was the point I was making.

Anyway I don’t want to completely dump on your comments, I think your discussions on streams and reactive programming are very interesting. Certainly I want to know more.

1 Like

No, you are correct. The event loop is what makes async programming possible which is a feature. However, the event loop is synchronous. With that I will say that real power of JavaScript is harnessing its asynchronous capabilities using event driven, reactive asynchronous programming. One of the best ways to learn this is by using rxjs. RxJS takes promises a step even further providing a complete solution to treating applications as streams of data that can manipulated, transformed, augmented, etc using reactive, functional programming.

https://rxjs.dev/

RxJS is a fundamental building block of Angular and in my professional opinion one of things that makes Angular more powerful than just React or Vue application. Although RxJS and reactive programming isn’t tied directly to any framework and can be used in JavaScript application. The Angular gods at google just made an early on decision with v2+ to cement RxJS and reactive programming into the technology stack. I feel like I have shared the angular link a tad to much so will restrain myself here.

2 Likes

This is an example of harnessing async reactive functional programming. This is an example pulled from a project of mine using Typescript and RxJS.

export const s3EntityCrudAdaptorPluginFactory = (platformId: Object, authFacade: AuthFacade, cognitoSettings: CognitoSettings, paramsEvaluatorService: ParamEvaluatorService, http: HttpClient, asyncApiCallHelperSvc: AsyncApiCallHelperService, hostName?: string, protocol?: string) => {
  return new CrudAdaptorPlugin<string>({
    id: 'aws_s3_entity',
    title: 'AWS S3 Entity',
    create: ({ object, identity, params }: CrudOperationInput) => of({ success: false }).pipe(
      map(() => buildClient(authFacade, cognitoSettings)),
      switchMap(s3 => identity({ object }).pipe(
        map(({ identity }) => ({ s3, identity }))
      )),
      switchMap(({ s3, identity }) => params && Object.keys(params).length !== 0 ? forkJoin(Object.keys(params).map(name => paramsEvaluatorService.paramValue(params[name], new Map<string, any>()).pipe(map(v => ({ [name]: v }))))).pipe(
        map(groups => groups.reduce((p, c) => ({ ...p, ...c }), {})), // default options go here instead of empty object.
        map(options => ({ s3, identity, options }))
      ): of({ s3, identity, options: {} })),
      map(({ s3, identity, options }) => {
        const name = options.prefix + identity + '.json';
        const command = new PutObjectCommand({
          Bucket: options.bucket,
          Key: name,
          Body: JSON.stringify(object),
          ContentType: 'application/json',
          CacheControl: `ETag: ${uuid.v4()}` // cache could be part of adaptor options - for now KISS
        });
        return { s3, command };
      }),
      switchMap(({ s3, command }) => new Observable<CrudOperationResponse>(obs => {
        s3.send(command).then(res => {
          console.log('sent');
          console.log(res);
          obs.next({ success: true });
          obs.complete();
        }).catch(e => {
          console.log('error')
          console.log(e);
          obs.next({ success: false })
          obs.complete();
        });
      }))
    ),
    read: ({ }: CrudOperationInput) => of<CrudOperationResponse>({ success: false }),
    update: ({ object, identity, params }: CrudOperationInput) => of({ success: false }).pipe(
      map(() => buildClient(authFacade, cognitoSettings)),
      switchMap(s3 => identity({ object }).pipe(
        map(({ identity }) => ({ s3, identity }))
      )),
      switchMap(({ s3, identity }) => params && Object.keys(params).length !== 0 ? forkJoin(Object.keys(params).map(name => paramsEvaluatorService.paramValue(params[name], new Map<string, any>()).pipe(map(v => ({ [name]: v }))))).pipe(
        map(groups => groups.reduce((p, c) => ({ ...p, ...c }), {})), // default options go here instead of empty object.
        map(options => ({ s3, identity, options }))
      ): of({ s3, identity, options: {} })),
      map(({ s3, identity, options }) => {
        const name = options.prefix + identity + '.json';
        const command = new PutObjectCommand({
          Bucket: options.bucket,
          Key: name,
          Body: JSON.stringify(object),
          ContentType: 'application/json',
          CacheControl: `ETag: ${uuid.v4()}` // cache could be part of adaptor options - for now KISS
        });
        return { s3, command };
      }),
      switchMap(({ s3, command }) => new Observable<CrudOperationResponse>(obs => {
        s3.send(command).then(res => {
          console.log('sent');
          console.log(res);
          obs.next({ success: true });
          obs.complete();
        }).catch(e => {
          console.log('error')
          console.log(e);
          obs.next({ success: false })
          obs.complete();
        });
      }))
    ),
    delete: ({ }: CrudOperationInput) => of<CrudOperationResponse>({ success: false }),
    query: ({ rule, params }: CrudCollectionOperationInput) => of({ entities: [], success: false }).pipe(
      map(() => ({ identityCondition: (rule.conditions as AllConditions).all.map(c => (c as AnyConditions).any.find(c2 => (c2 as ConditionProperties).fact === 'identity')).find(c => !!c) })),
      switchMap(({ identityCondition }) => iif(
        () =>  identityCondition !== undefined && (identityCondition as ConditionProperties).fact === 'identity',
        of({ entities: [], success: false }).pipe(
          map(() => buildClient(authFacade, cognitoSettings)),
          switchMap( s3 => params && Object.keys(params).length !== 0 ? forkJoin(Object.keys(params).map(name => paramsEvaluatorService.paramValue(params[name], new Map<string, any>()).pipe(map(v => ({ [name]: v }))))).pipe(
            map(groups => groups.reduce((p, c) => ({ ...p, ...c }), {})), // default options go here instead of empty object.
            map(options => ({ s3, options }))
          ): of({ s3, options: {} })),
          /*map(({ s3, options }) => {
            const name = options.prefix + (identityCondition as ConditionProperties).value + '.json';
            const command = new GetObjectCommand({
              Bucket: options.bucket,
              Key: name
            });
            return { s3, command };
          }),*/
          /*switchMap(({ s3, command }) => new Observable<CrudCollectionOperationResponse>(obs => {
            s3.send(command)
              .then(res => new Response(res.Body as ReadableStream, {}))
              .then(res => res.json())
              .then(entity => {
              console.log('sent');
              // console.log(res);
              obs.next({ success: true, entities: [ entity ] });
              obs.complete();
            }).catch(e => {
              console.log('error')
              console.log(e);
              obs.next({ success: false, entities: [] })
              obs.complete();
            });
          }))*/
          switchMap(({ options }) => createSignedHttpRequest({
              method: "GET",
              headers: {
                "Content-Type": "application/json",
                host: `${options.bucket}.s3.amazonaws.com`,
              },
              hostname: `${options.bucket}.s3.amazonaws.com`,
              path: `${options.prefix}${(identityCondition as ConditionProperties).value}.json`,
              protocol: 'https:',
              service: "s3",
              cognitoSettings: cognitoSettings,
              authFacade: authFacade
            }).pipe(
              map(signedHttpRequest => ({ signedHttpRequest, options }))
            )
          ),
          switchMap(( { signedHttpRequest, options }) => {
            // if (!isPlatformServer(platformId)) {
              delete signedHttpRequest.headers.host;
            // }
            // const url = `${ isPlatformServer(platformId) ? '' : '/opensearch' }${signedHttpRequest.path}`;
            const url = `${ isPlatformServer(platformId) ? /*'http://localhost:4000'*/ `${protocol}://${hostName}` : '' }/awproxy/s3/${options.bucket}${signedHttpRequest.path}`;
            console.log('url', url);
            return asyncApiCallHelperSvc.doTask(http.get(url, { headers: signedHttpRequest.headers, withCredentials: true }).toPromise()).pipe(
              catchError(() => of(undefined)),
              map(res => ({ res, options }))
            );
            /*return http.get(url, { headers: signedHttpRequest.headers, withCredentials: true }).pipe(
              map(res => ({ res, options }))
            );*/
          }),
          tap(({ res }) => console.log(`panelpage id ${res ? (res as any).id : 'undefined'}`)),
          map(({ res }) => ({ entities: res ? [ res ] : [], success: res ? true : false }))
        ),
        // Only implemented for GetObject (single object by identity) at the moment.
        of({ entities: [], success: false })
      ))
    )
  });
};

All of the functions return observables which is is basically supercharged promise compatible with all the RxJS operations. Therefore, the initial stream of data which is just a simple object literal can be manipulated, replaced, and transformed as it passes down the streams pipeline. No state is maintained and each operation is a small, discrete operation that either be synchronous like map or async like switchMap which replaced the observable with a brand new one. That brand new observable replaces the existing stream with a new values which advance down the pipeline and so on and so forth. Observables can be chained together just like a promise indefinately until the application ceases or stops.

With this mentality functions effectively become collections of sequential reactive, functional operations that further manipulate a stream or even replace or augment it.

This is an example of leveraging reactive programming to handle dom events.

      const nav$ = fromEvent(this.el.nativeElement, 'click').pipe(
        //filter(evt => (evt as any).target.closest('a') !== null),
        tap(() => alert('Hello'))
      );

https://rxjs.dev/api/index/function/fromEvent

Learning reactive programming is highly sought skill for many tech employers that could possibly separate someone from the rest of the heap that say they can write JavaScript. The same is true for Typescript many React and Vue applications even use it as part of their stack. I have seen considerable number of posting that use those ecosystems not Angular but desire Typescript and even reactive programming.

1 Like

Hi @SamA74,

First off I would recommend looking into the following linter standardjs

It really is useful, and actually works quite well as a teaching aid.

I have the less strict variant standardx running in vscode, and it immediately picked up on the following.

hide = function(elem){		// Hide an element by addign a class
    elem.classList.add('hide');
}

Would be better written in short form

class Iswitch {
    constructor() {}

    hide(elem) {
        // do stuff here
    }
}

Point of note, hide is added to the classes’ prototype. If you console.dir(Iswitch) without instantiating, you will see the following

class Iswitch
    length: 0
    name: "Iswitch"
    v prototype:
        > constructor: class Iswitch
        > hide: ƒ hide(elem) // <---

Inside your change method I had a nice red squiggly line under ‘event’ in event.target.id. Hovering over that revealed ‘event’ is not defined. change should be written with the event parameter.

change (event) {
    const sel = event.target.id
}

Other changes included changing your lets to consts and a bit of cleanup on formatting. Note this can be done automatically on save.

As Martyr2 pointed out, you have a binding issue. One solution, albeit all a bit ugly, is to use function.bind()

sw.addEventListener('click', this.change.bind(this))

That should fix your change method being bound to the dom element.

The Array.entries() iterator you have used is quite interesting :slight_smile:

In JS we tend to just think in terms of indexes. So change could be written instead using Array.forEach

The first argument passed by forEach is the current element in the array, the second argument is the current index.

    change (event) {
        const selected = event.target.id
        const switches = this.switches
        const dependInputs = this.depends

        switches.forEach(
            // second parameter is the current index
            (switchElement, currIndex) => {
                if (selected === switchElement.id) {
                    this.show(dependInputs[currIndex])
                } else {
                    this.hide(dependInputs[currIndex])
                }
            }
        )
    }

Personally I kind of dislike the ‘this’ keyword. As you have found yourself, it can lead to confusion and I think doesn’t particularly improve the readability of the code.

If show and hide are not to be made publicly available on the Iswitch instance, you could just declare them outside of the class

const hide  = elem => elem.classList.add('hide')

const show  = elem => elem.classList.remove('hide')

class Iswitch {
    constructor (fDepends) {
    ...
    }

    change (event) {
        const selected = event.target.id
        const switches = this.switches
        const dependInputs = this.depends

        switches.forEach(
            (switchElement, currIndex) => {
                if (selected === switchElement.id) {
                    show(dependInputs[currIndex]) // no 'this' needed
                } else {
                    hide(dependInputs[currIndex])
                }
            }
        )
    }
}

If Iswitch is then exported as a module, then essentially hide and show will then become private methods.

Edit: This isn’t isn’t tested (fdepends etc) so my change method may need a further look at

1 Like

This was solved at post #3 by adding bind() to the call in the listener.
Though this snippet worked just as well too.

But as I’m new to javascript I was going to ask next if any javascript experts could critique the code and possibly show how they may improve upon it. So this is helpful feedback.

That was just a throw-back to an earlier version of the script where I defined iswitch using a function rather than a class.

const iswitch = function(fDepends){
      this.data = JSON.parse(fDepends);
     // Etc, Etc...
}

In that version the methods had to be defined like: this.hide = function(elem){ ... }
I just forgot to remove all the redundant code when I changed to using a class.
That’s another thing I’m finding confusing, how the syntax changes depending on context like that.

I was struggling to find a way of accessing both the value and index of an array in a loop. That is what i eventually came up with.

I tried your version of the change method. It works with the exception of using event as an argument. That gives me an error that event is undefined. But leaving the brackets empty works just fine.

That is something I had thought about. As those are quite generic functions that may be reused elsewhere throughout a site, not just in the context of a form. For example they may be used for a navigation.

Regarding the subject of async, promises and the like, it is something I had already read about. It may be my beginner ignorance, but I don’t see how that relates to this particular problem. Though it is no doubt something I will look further into as I move on to more advanced js use.

@SamA74

There is something else wrong there then.

Here I have written a little demo, where I bind the handler’s this to an array of fruit names.

On changing the checkboxes I use the event object to get the value of the input which I use as an index on our this object and output it along with the checked status.

const fruit = ['apple', 'banana', 'strawberry']

const switchHandler = function (event) {
    const target = event.target
    const index = target.value
    const checked = target.checked

    output.textContent = (`${this[index]} ${checked}`)
}

const switches = document.querySelector('#switches')
const output = document.querySelector('#output')

switches.addEventListener('change', switchHandler.bind(fruit))

Demo in codepen.

Something else in your script needs looking at. As you can see from the demo, the handler still receives an event argument as well as being bound to another context.

1 Like

I’ll take closer look at your Pen later when I have more time. But from a brief glance it looks a lot slimmer than mine.
Strangely when I tried making a Pen of what I have here (one version) using your version of the change method, it’s not working in Codepen. But I have a version using my original change which shows it working.

Here is your script using my change method taking an event argument. Show and hide moved outside (utils)

There are some question marks, so will come back to this.

So something really threw me with @SamA74’s script. This is probably basic common knowledge, but for years I have relied on an event object being passed to the handler as an argument. I wasn’t quite sure how SamA74 was accessing the event object without doing that.

I wondered if it was being added globally, so a bit of testing revealed it was.

const handler = function() {
    console.dir(window.event) // pointerevent: { ..... }
}

console.dir(window.event) // undefined

document.addEventListener('click', handler)

If you read through MDN or the various other sources relating to handlers and the event object there is no mention of this, just that it is passed as an argument.

Then a google for ‘js event object added to window’ revealed this

Apparently this is depreciated and not surprisingly to be avoided.

You should avoid using this property in new code, and should instead use the Event passed into the event handler function. This property is not universally supported and even when supported introduces potential fragility to your code.

Note: This property can be fragile, in that there may be situations in which the returned Event is not the expected value. In addition, Window.event is not accurate for events dispatched within shadow trees.

So there you go, or there I go. Off to read about shadow trees now :smiley:

1 Like

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.