3

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?

nem035
  • 34,790
  • 6
  • 87
  • 99
slinkhi
  • 947
  • 4
  • 16
  • 32
  • 1
    One friendly suggestion. You should probably use more descriptive variable names and cleaner formatting, especially when you are asking someone else to read your code – nem035 Aug 03 '16 at 16:18
  • @nem035 okay sure, I have updated my code with expanded variable names, thanks – slinkhi Aug 03 '16 at 16:34
  • Actually, whitespace & formatting would help more than crowded names :). Just use the `tidy` option for a code snippet, that's what I did with my edit, it does the formatting for you. – nem035 Aug 03 '16 at 16:40
  • 1
    Also, in the first snippet, you are using `var` as a variable name which is invalid since `var` is a reserved word – nem035 Aug 03 '16 at 16:41
  • @nem035 okay sorry about that. I forgot `var` is reserved; i was just updating my code above by hand. I'm not actually using those expanded vars in my own code. I appreciate the formatting help, but it would also be nice to get some help on the actual problem ;) – slinkhi Aug 03 '16 at 16:46
  • my point was just that you have to make an effort to explain & demonstrate your problem clearly and then it will be much easier for anybody to help. Check out if my answer does the trick – nem035 Aug 03 '16 at 16:50
  • 1
    @nem035 I have clearly laid out my current solution *which works*, what I'd like to do to make it better, what i've tried, and the problem with it. And I even updated format at your request. I don't understand what more you need. Thanks for your help, I appreciate it – slinkhi Aug 03 '16 at 16:51
  • I'm glad to help mate, its just harder when the sample code look like it passed through an obfuscator and a minifier, with some copy paste errors :) – nem035 Aug 03 '16 at 16:53
  • 1
    I'm just so used to having to work with minified code it's all the same to me. But I get you. Not good for posting questions for others to help. That was definitely my mistake; why I fixed it :) – slinkhi Aug 03 '16 at 16:55

2 Answers2

1

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.

nem035
  • 34,790
  • 6
  • 87
  • 99
0

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]; 
});
Jared Smith
  • 19,721
  • 5
  • 45
  • 83
  • Thanks for the input. I understand the concerns and don't (necessarily) disagree with them. However, I left out a LOT of context/setup to focus on the actual problem at hand. A lot of your points aren't actually relevant to my situation. If i were to write a novel about it, perhaps you'd be a little more forgiving with your cautions ;) – slinkhi Aug 03 '16 at 17:08
  • btw regarding point#2 I don't actually *need* `_get('page.id')` functionality. I was just using the answers [here](http://stackoverflow.com/questions/359788/how-to-execute-a-javascript-function-when-i-have-its-name-as-a-string) as a reference, and i'm not some uber js ninja enough to disregard that effect. But I thought perhaps it might come in handy in the event one of the cached properties was some callback function that ultimately returned a value, or something. I dunno. – slinkhi Aug 03 '16 at 17:32
  • 1
    @slinkhi on your first point, fair enough, I'd just be careful how you post questions so people don't get the wrong idea, the kinds of people you actually *want* answers from generally wont let stuff like that pass without comment (nem035 and myself being cases in point). As to your second comment, that's a YAGNI, it adds complexity and failure cases to an otherwise simply function solely for speculative benefit. Calling functions by string name like that is a bit of a smell anyways, it means you probably didn't structure your code well. – Jared Smith Aug 03 '16 at 17:42