Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript - Create Iterable Abstraction Layer

Assuming I have an object like this:

var result = {
    items: [
        'item1', 'item2', 'item3'
    ]
}

I'm looking for a way create an object that would abstract my result object and yet would remain compatible to it:

function wrapper(result) {
    this.items = ???
}

In order to able able to do this

var p = new wrapper(result);

for(var x = 0; x < p.items.length; x++) {
    console.log(p[x]) // = item1, item2, item3
}

This can be easily accomplished in PHP using ArrayIterator. I was wondering if it can be done in JS, specifically in NodeJS.

Just to clarify:

Imagine the original object is a result from a db query. However, the formatting of that result object cannot be fed into the consumer as-is and needs to be adapted. In this case I did not dwell on the adaptation but for the sake of the example assume each value needs to be uppercased.

Additionally, the original list is very large. I could obviously iterate, copy all values and uppercase them, but in most cases it will not be required for all values, therefore it would be inefficient to do this for all items every time. The idea of the wrapper is to act as a proxy that I would be able to iterate. The wrapper will in turn retrieve the result from the original object, and modify it on the fly.

like image 671
Tomasz Rakowski Avatar asked Aug 07 '26 16:08

Tomasz Rakowski


1 Answers

If you're using Node 4 or 5 you can consider using ES6's Symbol.Iterator. It's the closest thing to PHP's ArrayIterator that I can think of.

Here's an example of a use-case which you describe in your post:

'use strict';

let result = {
  items: [
    'item1', 'item2', 'item3'
  ],
  [Symbol.iterator](cb) {
    let index = 0;
    return {
      next: () => {
        let value = this.items[index];
        let done = index >= this.items.length;
        // Note that arrow functions won't bind `this.items` to cb's `this`.
        if (typeof cb === 'function') {
          return cb.call(this.items, value, done, this.items, index++);
        } else {
          index++;
          return { value, done };
        }
      }
    }
  }
}

let cb = (value, done, items, index) => {
  // Modify original array.
  items[index] = items[index] && items[index].toUpperCase();
  value = items[index];
  return { value, done };
};

let iterator1 = result[Symbol.iterator](cb);
let iterator2 = result[Symbol.iterator]();
console.log(iterator1.next()); // { value: 'ITEM1', done: false }
console.log(iterator2.next()); // { value: 'ITEM1', done: false }
console.log(iterator1.next()); // { value: 'ITEM2', done: false }
console.log(iterator1.next()); // { value: 'ITEM3', done: false }
console.log(result.items); // [ 'ITEM1', 'ITEM2', 'ITEM3' ]

Note that you can define multiple iterators to iterate concurrently, modify the referenced array, and add your own methods to the iterator to emulate ArrayIterator as you please.

I should also mention that the for-of construct works well with this:

'use strict';

let result = {
  items: [
    'item1', 'item2', 'item3'
  ]
}

let cb = (value, done, items, index) => {
  // Modify original array.
  items[index] = items[index] && items[index].toUpperCase();
  value = items[index];
  return { value, done };
};

let modifiableIterableIterator = {
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        let value = result.items[index];
        let done = index >= result.items.length;
        // Note that arrow functions won't bind `this.items` to cb's `this`.
        if (typeof cb === 'function') {
          return cb.call(result.items, value, done, result.items, index++);
        } else {
          index++;
          return { value, done };
        }
      }
    }
  }
};

for (let item of modifiableIterableIterator) console.log(item);

modifiableIterableIterator[Symbol.iterator] is the same as the previous result[Symbol.iterator], except there is no more cb parameter. When passing an iterable iterator (an iterable iterator is one that defines a next method, and Symbol.iterator on the object), no arguments are passed to Symbol.iterator, so we just make the callback explicit from within the method. The loop will perform the same operations as before: uppercase all the values and modify the array in-place.

There are some design decisions about how this can be structured properly, but this is up to you and the way your application is structured.


Since you want to support the "older" way to iterate while maintaining this behavior, the only way you can do this by overloading [] is to use ES6's Proxy object. This allows you to do some metaprogramming. Unfortunately, the native support is lacking, but you can use shims if you need to (I got this working on Node 5 using the harmony-reflect module and passing --harmony_proxies on the command-line).

let proxy = new Proxy(result.items, {
  get(target, index) {
    target[index] = target[index] && target[index].toUpperCase();
    return target[index];
  }
});
for (let x = 0; x < result.items.length; x++) console.log(proxy[x]); // 'ITEMx'
console.log(result.items); // [ 'ITEM1', 'ITEM2', 'ITEM3' ]

Combine the new iterators feature and a Proxy, and you have a solution which works with your existing implementation and future-proof's your code.

like image 123
Anthony Calandra Avatar answered Aug 10 '26 06:08

Anthony Calandra



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!