slinkhi
slinkhi

Reputation: 989

javascript referencing dynamic parent object

I have this object, a 3rd party tracking tool similar to google analytics. I want to extend it with my own "caching" function that saves the data from the previous tracking call so that I can reference stuff on the next tracking call if needed.

This is what I have so far, and it works:

// Current 3rd party tool, can't really mess with this. 
// It is loaded from an external script
window.someTool={/* stuff */};

// my code
someTool._cache=someTool._cache||{};
someTool._cache._get=function(variabl) {
  var length,index,variabl=(variabl||'').split('.'),
      cache=someTool&&someTool._cache&&someTool._cache._dataLayer||{};
  for (index=0,length=var.length;index<length;index++){
    cache=cache[variabl[index]];
    if (!cache) break;
  }
  return cache;
};

So then I have/do the following

// data layer output on initial page that gets wiped later
var dataLayer = {
  'page' : {
    'name' : 'foo',
    'lang' : 'en'
  },
  'events' : {
    'pageView' : true,
    'search' : true
  }
}

// I grab the initial data layer and save it here
someTool._cache._dataLayer = dataLayer;

This then allows me to do stuff like

someTool._cache._get('page'); // returns {'page':{'name':'foo','lang':'en'}
someTool._cache._get('page')['name']; // returns 'foo'
someTool._cache._get('page.lang'); // returns 'en'

So this works for me, but here comes the question/goal: I want to improve my _get function. Namely, I don't like that I have to hardcode someTool, or really even _cache, and if I can somehow swing it, _dataLayer.

Ideally, I'd like a reference of someTool._cache._dataLayer passed/exposed to _get (e.g. a parent type reference) so that if someTool,_cache, or _dataLayer were to change namespaces, I don't have to update _get. But I am not sure how to do that.

This is what I have so far:

(function(tool, cache, dataLayer) {
  var tool = tool || {},
      cache = cache || '_cache',
      dataLayer = dataLayer || '_dataLayer';

  dataLayer = tool[cache][dataLayer] || {};

  tool[cache]._get = function(property) {
    var length, index, property = (property || '').split('.');

    for (index = 0, length = property.length; index < length; index++) {
      dataLayer = dataLayer[property[index]];
      if (!dataLayer) break;
    }

    return dataLayer;
  };
})(someTool, '_cache', '_dataLayer');

This seems to work the first time I call it, e.g.

someTool._cache._get('page')['name']; // returns 'foo'

But after that, I get an error:

TypeError: someTool._cache._get(...) is undefined

I feel like it has something to do with dataLayer losing its reference or something, I dunno (though I'm not sure how it's working first time around..). Is what I am doing even possible, and if so, where am I going wrong? Or is what I originally have the best I can do?

Upvotes: 3

Views: 189

Answers (2)

nem035
nem035

Reputation: 35491

I feel like it has something to do with dataLayer losing its reference or something, I dunno (though I'm not sure how it's working first time around..).

The reason this is happening is because you are using the same dataLayer you initialize in the closure of _get to:

  • store information, and
  • to use as a temporary loop variable

If you look at your code:

(function(tool, cache, dataLayer) {
  // ...

  // Here you are initializing (or looking up) the dataLayer
  dataLayer = tool[cache][dataLayer] || {};

  tool[cache]._get = function(property) {
    // ...

    for (index = 0, length = property.length; index < length; index++) {
      // here you are overwriting the same dataLayer
      dataLayer = dataLayer[property[index]];
      if (!dataLayer) break;
    }

    return dataLayer;
  };
})(someTool, '_cache', '_dataLayer');

You can see that your loop will overwrite dataLayer on each iteration which means every lookup after the first will most likely be wrong.

Eventually, dataLayer will be overwritten with undefined, and then any further lookups will now break the code.

What you can do is use another variable for the loop iteration:

var temp;
for (index = 0, length = property.length; index < length; index++) {
  temp = dataLayer[property[index]];
  if (!temp) break;
}

return temp;

This will leave your dataLayer object intact.

Upvotes: 1

Jared Smith
Jared Smith

Reputation: 21984

Although your code is so obsfucated (one-character variable names, abuse of the comma operator, etc.) that its hard to tell for sure, it seems that you need to fix a few things before moving on.

  1. Properties prefixed with an underscore are meant to be private. They are subject to change, and by change I mean your app randomly breaking. Use the public API.

  2. Parsing strings out by hand is a lot of work for seemingly little gain. Is the use case for get('page.id') over get('page').id really so compelling?

  3. Your code is incomprehensible. This is the kind of output one would expect of a minifier: it makes it hard to understand what any of it does/is supposed to do.

  4. Unless a third-party API is so integral to your application that replacing it would require a rewrite no matter what (e.g. google maps) or so well-known that it has umpteen clones (jquery), its is generally a good idea to wrap third-party library calls so you can change the library later.

I realize this does not answer your question, but its way too long for a comment and it would be remiss of me to not point out the bright red targets (plural) you've painted on your feet prior to polishing your firearm.

As for your actual question (post-edit), you're on the right track. But I'd make it a curried function so that you can dynamically access different properties. We're going to ignore for one minute the huge mistake that is accessing private properties just to get the point across:

function accessDataCache(cache) {
  return function(dataLayer) {
    return function(namespaceObj) {
      return function(property) {
        return namespaceObj[cache][dataLayer][property];
      };
    };
  };
};

var getFn = accessDataCache('_cache')('_dataLayer')(someTool);
getFn('page');

You can now also mix and match if you need other stuff:

var getSomeOtherCachedThing = accessDataCache('_cache')('_someOtherThing')(someTool);

All of that is quite tedious to write out by hand, so I recommend using something like lodash or Ramda and .curry to achieve the effect:

var accessCacheData = R.curry(function(cache, dataLayer, namespaceObj, property) { 
  return namespaceObj[cache][dataLayer][property]; 
});

Upvotes: 0

Related Questions