Binding scope to item in forEach loop

Viewed 159

This is extremely hard for me to explain in the title so I am just going to give an example to explain better:

I want to pass a function such as ()=>{console.log(this.operatorValue)}, as a parameter to recursive traverseCriteria function.

traverseCriteria Function

  function traverseCriteria(arr, parameters,fn) {
        for (const item of arr) {
          if (Array.isArray(item))
          this.traverseCriteria(item, parameters)
          else if (typeof item === 'object'){
              item.operatorValue = parameters[item.Property]
             console.log(item.operatorValue)  ///<-- REPLACE THIS WITH fn()
            //fn()
          }
        }
        return arr
      }

In the above function I want to be able to replace console.log(item.operatorValue) with fn();

    const criteria =  [
            "and",
            {
              "Collection": "persons",
              "Property": "phone",
              "operator": "eq",
              "operatorValue": "23138213"
            },
            {
              "Collection": "persondetails",
              "Property": "country",
              "operator": "eq",
              "operatorValue": "Russia"
            }
          ]
    const parameters = { phone: "23138213", "country": "Russia" };

function call:

  traverseCriteria(criteria,parameters,()=>{console.log(this.operatorValue)}); 
4 Answers

You can use .call or .apply to set the this value of a function:

fn.call(item)

However, keep in mind that arrow functions don't have their own this value, so to make this work you have to pass a "normal" function.

You just pass item.operatorValue as argument to your function

  function traverseCriteria(arr, parameters, fn) {
        for (const item of arr) {
          if (Array.isArray(item))
          this.traverseCriteria(item, parameters)
          else if (typeof item === 'object'){
              item.operatorValue = parameters[item.Property]
            fn(item)
          }
        }
        return arr
      }

and then call it

traverseCriteria(criteria, parameters,(item) => { console.log(item.opValue) }); 

The value of this inside an arrow function remains the same throughout the lifecycle of the function and is always bound to the value of this in the closest non-arrow parent function.

Arrow function cannot be used every where, not for this case, passing normal function will work!.

Great article: https://medium.com/better-programming/difference-between-regular-functions-and-arrow-functions-f65639aba256#:~:text=Unlike%20regular%20functions%2C%20arrow%20functions,closest%20non%2Darrow%20parent%20function.

For me the most valuable approach was to separate the actual processing of a criteria item from the traverseCriteria method thats sole responsibility should be the iteration of a criteria array structure via a context aware forEach.

Thus the footprint of traverseCriteria remains small and one can focus on the actual processing task. The implementation of the latter then can be more easily refactored to e.g. the OP's goals.

Since it was not quite clear what those goals actually are, the approach provides two solutions to what was possibly ask for. First, this additionally passed handler could be understood to be a method that operates upon a criteria item. In this case one just applies the specific criteria item via call to this handler. Second, and this is what I would propose, one should see this handler as the most generic form of an item specific callback. Thus one, snapshot-like, passes all the relevant data to it. How to implement such a callback handler then is left to whoever consumes the traverseCriteria method.

function processContextualItem(item, idx, arr) {
  const { target, params, itemHandler, genericCallback } = this;
  if (Array.isArray(item)) {

    target.traverseCriteria(item, params, itemHandler, genericCallback);

  } else if (item && (typeof item === 'object')) {

    item.operatorValue = params[item.Property];

    // I want to be able to replace
    // console.log(item.operatorValue)
    // with fn();

    // e.g. *item specific* with bound item reference.
    itemHandler.call(item/*, idx, arr, .., ...*/);

    // e.g. *most generic* for any kind of "callback".
    genericCallback(item, idx, arr, params, target);
  }
}


function traverseCriteria(arr, params, itemHandler, genericCallback) {
  arr.forEach(processContextualItem, {
    target: this,
    params,
    itemHandler,
    genericCallback
  });
  return arr;
}
const testType = {
  foo: "foo",
  bar: "bar",
  traverseCriteria
};


function itemMethod() {
  console.log('\n this.operatorValue :', this.operatorValue);
}
function genericCallback(item, idx, arr, params, target) {
  console.log('\n item.operatorValue :', item.operatorValue);
  console.log('\n idx, arr, params, target:', idx, arr, params, target);
}
const params = { phone: "23138213", "country": "Russia" };

const criteria =  [
  "foo", {
    "Collection": "persons",
    "Property": "phone",
    "operator": "eq",
    "operatorValue": "23138213"
  }, {
    "Collection": "persondetails",
    "Property": "country",
    "operator": "eq",
    "operatorValue": "Russia"
  }, [
    "bar", {
      "Collection": "persons",
      "Property": "phone",
      "operator": "eq",
      "operatorValue": "23138213"
    },
    "baz"
  ]
];


testType.traverseCriteria(criteria, params, itemMethod, genericCallback);
.as-console-wrapper { min-height: 100%!important; top: 0; }

Related