Define custom array-like object with numeric access - javascript

How can I define a collection object in JavaScript that behaves like an array in that it provides access to its items through the numeric index operator? I'd like to allow this code:
// Create new object, already done
let collection = new MyCollection();
// Add items, already done
collection.append("a");
collection.append("b");
// Get number of items, already done
console.log(collection.length); // -> 2
// Access first item - how to implement?
console.log(collection[0]); // -> "a"
My collection class looks like this (only part of the code shown):
function MyCollection(array) {
// Create new array if none was passed
if (!array)
array = [];
this.array = array;
}
// Define iterator to support for/of loops over the array
MyCollection.prototype[Symbol.iterator] = function () {
const array = this.array;
let position = -1;
let isDone = false;
return {
next: () => {
position++;
if (position >= array.length)
isDone = true;
return { done: isDone, value: array[position] };
}
};
};
// Gets the number of items in the array.
Object.defineProperty(MyCollection.prototype, "length", {
get: function () {
return this.array.length;
}
});
// Adds an item to the end of the array.
MyCollection.prototype.append = function (item) {
this.array.push(item);
};
I think jQuery supports this index access, for example, but I can't find the relevant part in their code.

Your class can extend Array: class MyCollection extends Array {...} - you no longer need the internal array property, the iterator, or the custom length property, and you can define arbitrary methods like append which would simply call this.push(item);.
Edit: if you can't use the class keyword, you can do it the old school way:
// note - this is no different than doing class MyCollection extends Array {}
function MyCollection() { }
MyCollection.prototype = new Array();
MyCollection.prototype.constructor = MyCollection;
Or you can use JavaScript Proxies so that any time someone tries to read a property you can return the value they're looking for. This shouldn't be "slow" - though it's unavoidably slower than plain objects.
Edit: If you really want to make life difficult for yourself and everybody else using your code, you can update your append function to do the following:
MyCollection.prototype.append = function (item) {
this.array.push(item);
this[this.array.length - 1] = item;
};

Related

JavaScript/ES6: How to return multiple iterators from a class?

I'm implementing a doubly linked list as part of a programming exercise, and I would like to allow the developer to iterate through its nodes both forward and backwards using the for...in notation.
At its most basic, the data structure looks like this:
class DoublyLinkedList {
constructor(data) {
if (data) {
this.head = new DoublyLinkedListNode(data)
} else {
this.head = null
}
}
append = (data) => {
if (!this.head) {
this.prepend(data)
} else {
const newTail = new DoublyLinkedListNode(data)
let current = this.head
while(current.next) {
current = current.next
}
current.next = newTail
newTail.prev = current
}
}
}
Next I added the generator functions:
*values() {
let current = this.head
while (current) {
yield current.data;
current = current.next;
}
}
*valuesBackward() {
let currentForwards = this.head
while (currentForwards.next) {
currentForwards = currentForwards.next
}
const tail = currentForwards
let currentBackwards = tail
while (currentBackwards) {
yield currentBackwards.data
currentBackwards = currentBackwards.prev
}
}
I'm able to add a single, forwards iterator with the following added to the class:
[Symbol.iterator]() { return this.values()}
I tried adding both of the following to the class:
iterateForward = () => [Symbol.iterator] = () => this.valuesBackward()
iterateBackward = () => [Symbol.iterator] = () => this.valuesBackward()
And then tried to iterate using for (node in list.iterateForward()) but this failed with error TypeError: undefined is not a function.
I guess that made sense looking at the code so next I tried:
iterateForward = () => {
const vals = this.values()
const it = {
[Symbol.iterator]() {
return vals()
}
}
return it
}
This didn't error, but the iteration didn't work - the iterator ran zero times.
What am I missing here? Is it possible to achieve what I want?
This stuff regularly confuses me so here's a summary we can both refer to.
Here's the background
An iterable is an object that has the [Symbol.iterator] property and then you when you call that property as a function, it returns an iterator object.
The iterator object has a property .next() and each time you call that function, it returns the object with the expected properties {value: x, done: false}. The iterator object will typically keep the state of the iteration in this separate object (thus iterators can be independent of each other).
So to support multiple iterators, you create multiple methods where each method returns an object that is a different iterable, each has it's own [Symbol.iterator] that when called returns a different iterator object.
So, to recap, you:
Call a method that returns an iterable
The iterable is an object that has the [Symbol.iterator] property on it and has access to the original object's data.
When you call the function in that [Symbol.iterator] property, you get an iterator object.
The iterator object contains a .next() method that gets you each item in the sequence and it does that by returning an object like this {value: x, done: false} each time you call .next().
You can skip step 1 and just have your core object have the [Symbol.iterator] property on it. That essentially becomes your default iteration. If you do:
for (let x of myObj) {
console.log(x);
}
it will access myObj[Symbol.iterator]() to get the iterator. But, if you want to have more than one way to iterate your collection, then you create separate functions that each return their own iterable (their own object with their own [Symbol.iterator] property on them).
In an array, you've got .entries() and .values() as an example of two methods that return different iterables, that make different iterators.
let x = ['a', 'b', 'c'];
for (let v of x.values()) {
console.log(v);
}
This gives output:
'a'
'b'
'c'
Or, for .entries():
let x = ['a', 'b', 'c'];
for (let v of x.entries()) {
console.log(v);
}
[0, "a"]
[1, "b"]
[2, "c"]
So, each of .values() and .entries() returns a different object that each have a different [Symbol.iterator] that when called as a function returns a different iterator function for their unique sequence.
And, in the case of the array, .values() returns a function that when called give you the exact same iterator as just iterating the array directly (e.g. the [Symbol.iterator] property on the array itself).
Now, for your specific situation
You want to create two methods, let's say .forward() and .backward() that each create an object with a [Symbol.iterator] property that is a function that when called return their unique iterator object.
So, obj.forward() would return an object with a [Symbol.iterator] property that is a function that when called returns the iterator object with the appropriate .next() property to iterate forward and the appropriate starting state.
So, obj.backward() would return an object with a [Symbol.iterator] property that is a function that when called returns the iterator object with the appropriate .next() property to iterate backward and the appropriate starting state.
Here's an example using an array:
class myArray extends Array {
forward() {
return this; // core object already has appropriate forward
// [Symbol.iterator] property, so we can use it
}
backward() {
return {
[Symbol.iterator]: () => {
let i = this.length - 1; // maintains state in closure
return {
next: () => { // get next item in iteration
if (i < 0) {
return {done: true};
} else {
return {value: this[i--], done: false};
}
}
}
}
}
}
}
let x = new myArray('a', 'b', 'c');
console.log("Forward using default iterator")
for (let v of x) {
console.log(v);
}
console.log("\nUsing .forward()")
for (let v of x.forward()) {
console.log(v);
}
console.log("\nUsing .backward()")
for (let v of x.backward()) {
console.log(v);
}

How do I make work filter function on Class extended from Array

I try to create a custom Array class by extending the generic Array one. On this class I want to create a custom find / filter. When I call the custom find it works well, but when I call the filter I get a problem with the iterator.
There is my code
class MyClass extends Array {
constructor(inArray){
super();
this.push(...inArray);
}
myFind(callback) {
return this.find(callback);
}
myFilter(callback) {
return this.filter(callback);
}
}
//Works
console.log(new MyClass(["a","b"]).myFind(item => item === "a"));
//Do not work
console.log(new MyClass(["a","b"]).myFilter(item => item === "a"));
The difference is, that .filter(...) creates a new array. And that array is very special, as it got the same type as the array it is called on:
(new MyClass(["a", "b"])).filter(/*...*/) // MyClass(...)
so when you call filter, it calls the arrays constructor by passing the arrays length first:
Array.prototype.filter = function(cb) {
const result = new this.constructor(0); // <---
for(const el of this)
if(cb(el) result.push(el);
return result;
};
Now that constructor is:
constructor(inArray){ // inArray = 0
super();
this.push(...inArray); // ...0
}
and spreading 0 is impossible. To resolve that, make the constructor behave just as the arrays constructor, or just don't add your own constructor.
Try add:
class MyClass extends Array {
constructor(inArray){
super();
if(inArray !== 0 ) this.push(...inArray); // Condition
}
myFind(callback) {
return this.find(callback);
}
myFilter(callback) {
return this.filter(callback);
}
}
The problem is that it is very dangerous to overwrite the ARRAY class
constructor, the function of the filter is to create an empty
array and then fill it with the matches it finds, so when creating an
empty array inArray === 0, this causes this.push (... inArray) give an
error.

Return object in array with specific property [duplicate]

This question already has answers here:
Find object by id in an array of JavaScript objects
(36 answers)
Closed 4 months ago.
I'm fairly new with writing my own JS functions, and I'm struggling with this one.
I want to run through an array of objects, find an object that matches a particular ID, and then return that object.
So far this is what I have:
var findTeam = function() {
$scope.extraTeamData.forEach(team) {
if(team.team_id === $scope.whichTeam) { return team }
}
$scope.thisTeam = team;
};
$scope.teamDetails is my array, and the $scope.whichTeam variable holds the correct ID which I am checking against.
Ultimately I want to be able to assign the object that results from the function to the $scope.thisTeam variable, so I can call its properties in the view.
Any help would be appreciated.
Thanks.
You could use Array#some which ends the iteration if found
var findTeam = function() {
$scope.extraTeamData.some(function (team) {
if (team.team_id === $scope.whichTeam) {
$scope.thisTeam = team;
return true;
}
});
};
Move your $scope.thisTeam = team; to within the if check.
var findTeam = function() {
$scope.teamDetails.forEach(team) {
if(team.team_id === $scope.whichTeam) {
$scope.thisTeam = team;
}
}
};
$scope.team = $scope.teamDetails.filter(function (team) {
return team.team_id === $scope.whichTeam;
})[0];
You need to use filter method of array. It creates new array elements that match given predicate (function that return boolean value). Then you simply take first value.
You can also use find, but it is not implemented in every browser yet.
It would look something like this:
$scope.team = $scope.teamDetails.find(function (team) {
return team.team_id === $scope.whichTeam;
});

indexOf for array with different objects

I have an array of users.
When I click on button "Add New", I want to add new object to this array only if it doen't exist there:
var newUser = { 'creating': true, 'editMode': true };
if ($scope.users.indexOf(newUser) < 0) {
$scope.users.push(newUser);
}
but indexOf always return -1.
Is this because array contain "different" objects?
I suppose each time you're going to call "Add New", it'll work (hard to say without more code). For the simple reason that each instance of newUser is a different one.
The indexOf calls check for exactly this instance of newUser. It doesn't check the property values, just for the reference to the instance.
e.g. :
var user = {a : "b"};
var users = [];
users.push(user);
users.indexOf(user); // returns 0, reference the user created at the top, inserted beforehand
user = {a : "b"};
users.indexOf(user); // returns -1, reference the user created just above, not yet inserted
If you want to check for instance, you'll have to make a check on a property (name, id, ...)
If you want to have a collection of unique values you should use ECMASCRIPT-6 Set
If you need to stay legacy, otherwise, you need to use arrays...
var Set = (function() {
function Set() {}
Set.prototype.has = function(val) {
return !!this._list.filter(i => i.id === val.id).length;
};
Set.prototype.get = function(val) {
if(!val) {
return this._list;
}
return this._list.filter(i => i.id === val.id).pop();
};
Set.prototype.add = function(val) {
if(!this.has(val)) {
this._list.push(val)
}
return this;
}
return Set;
})();

Ways to extend Array object in javascript

i try to extend Array object in javascript with some user friendly methods like Array.Add() instead Array.push() etc...
i implement 3 ways to do this.
unfortunetly the 3rd way is not working and i want to ask why? and how to do it work.
//------------- 1st way
Array.prototype.Add=function(element){
this.push(element);
};
var list1 = new Array();
list1.Add("Hello world");
alert(list1[0]);
//------------- 2nd way
function Array2 () {
//some other properties and methods
};
Array2.prototype = new Array;
Array2.prototype.Add = function(element){
this.push(element);
};
var list2 = new Array2;
list2.Add(123);
alert(list2[0]);
//------------- 3rd way
function Array3 () {
this.prototype = new Array;
this.Add = function(element){
this.push(element);
};
};
var list3 = new Array3;
list3.Add(456); //push is not a function
alert(list3[0]); // undefined
in 3rd way i want to extend the Array object internally Array3 class.
How to do this so not to get "push is not a function" and "undefined"?
Here i add a 4th way.
//------------- 4th way
function Array4 () {
//some other properties and methods
this.Add = function(element){
this.push(element);
};
};
Array4.prototype = new Array();
var list4 = new Array4();
list4.Add(789);
alert(list4[0]);
Here again i have to use prototype.
I hoped to avoid to use extra lines outside class constructor as Array4.prototype.
I wanted to have a compact defined class with all pieces in one place.
But i think i cant do it otherwise.
ES6
class SubArray extends Array {
last() {
return this[this.length - 1];
}
}
var sub = new SubArray(1, 2, 3);
sub // [1, 2, 3]
sub instanceof SubArray; // true
sub instanceof Array; // true
Using __proto__
(old answer, not recommended, may cause performance issues)
function SubArray() {
var arr = [ ];
arr.push.apply(arr, arguments);
arr.__proto__ = SubArray.prototype;
return arr;
}
SubArray.prototype = new Array;
Now you can add your methods to SubArray
SubArray.prototype.last = function() {
return this[this.length - 1];
};
Initialize like normal Arrays
var sub = new SubArray(1, 2, 3);
Behaves like normal Arrays
sub instanceof SubArray; // true
sub instanceof Array; // true
Method names should be lowercase. Prototype should not be modified in the constructor.
function Array3() { };
Array3.prototype = new Array;
Array3.prototype.add = Array3.prototype.push
in CoffeeScript
class Array3 extends Array
add: (item)->
#push(item)
If you don't like that syntax, and you HAVE to extend it from within the constructor,
Your only option is:
// define this once somewhere
// you can also change this to accept multiple arguments
function extend(x, y){
for(var key in y) {
if (y.hasOwnProperty(key)) {
x[key] = y[key];
}
}
return x;
}
function Array3() {
extend(this, Array.prototype);
extend(this, {
Add: function(item) {
return this.push(item)
}
});
};
You could also do this
ArrayExtenstions = {
Add: function() {
}
}
extend(ArrayExtenstions, Array.prototype);
function Array3() { }
Array3.prototype = ArrayExtenstions;
In olden days, 'prototype.js' used to have a Class.create method. You could wrap all this is a method like that
var Array3 = Class.create(Array, {
construct: function() {
},
Add: function() {
}
});
For more info on this and how to implement, look in the prototype.js source code
A while ago I read the book Javascript Ninja written by John Resig, the creator of jQuery.
He proposed a way to mimic array-like methods with a plain JS object. Basically, only length is required.
var obj = {
length: 0, //only length is required to mimic an Array
add: function(elem){
Array.prototype.push.call(this, elem);
},
filter: function(callback) {
return Array.prototype.filter.call(this, callback); //or provide your own implemetation
}
};
obj.add('a');
obj.add('b');
console.log(obj.length); //2
console.log(obj[0], obj[1]); //'a', 'b'
I don't mean it's good or bad. It's an original way of doing Array operations. The benefit is that you do not extend the Array prototype.
Keep in mind that obj is a plain object, it's not an Array. Therefore obj instanceof Array will return false. Think obj as a façade.
If that code is of interest to you, read the excerpt Listing 4.10 Simulating array-like methods.
In your third example you're just creating a new property named prototype for the object Array3. When you do new Array3 which should be new Array3(), you're instantiating that object into variable list3. Therefore, the Add method won't work because this, which is the object in question, doesn't have a valid method push. Hope you understand.
Edit: Check out Understanding JavaScript Context to learn more about this.
You can also use this way in ES6:
Object.assign(Array.prototype, {
unique() {
return this.filter((value, index, array) => {
return array.indexOf(value) === index;
});
}
});
Result:
let x = [0,1,2,3,2,3];
let y = x.unique();
console.log(y); // => [0,1,2,3]
Are you trying to do something more complicated then just add an alias for "push" called "Add"?
If not, it would probably be best to avoid doing this. The reason I suggest this is a bad idea is that because Array is a builtin javascript type, modifying it will cause all scripts Array type to have your new "Add" method. The potential for name clashes with another third party are high and could cause the third party script to lose its method in favour of your one.
My general rule is to make a helper function to work on the Array's if it doesnt exist somewhere already and only extend Array if its extremely necessary.
You CANNOT extend the Array Object in JavaScript.
Instead, what you can do is define an object that will contain a list of functions that perform on the Array, and inject these functions into that Array instance and return this new Array instance. What you shouldn't do is changing the Array.prototype to include your custom functions upon the list.
Example:
function MyArray() {
var tmp_array = Object.create(Array.prototype);
tmp_array = (Array.apply(tmp_array, arguments) || tmp_array);
//Now extend tmp_array
for( var meth in MyArray.prototype )
if(MyArray.prototype.hasOwnProperty(meth))
tmp_array[meth] = MyArray.prototype[meth];
return (tmp_array);
}
//Now define the prototype chain.
MyArray.prototype = {
customFunction: function() { return "blah blah"; },
customMetaData: "Blah Blah",
}
Just a sample code, you can modify it and use however you want. But the underlying concept I recommend you to follow remains the same.
var SubArray = function() {
var arrInst = new Array(...arguments); // spread arguments object
/* Object.getPrototypeOf(arrInst) === Array.prototype */
Object.setPrototypeOf(arrInst, SubArray.prototype); //redirectionA
return arrInst; // now instanceof SubArray
};
SubArray.prototype = {
// SubArray.prototype.constructor = SubArray;
constructor: SubArray,
// methods avilable for all instances of SubArray
add: function(element){return this.push(element);},
...
};
Object.setPrototypeOf(SubArray.prototype, Array.prototype); //redirectionB
var subArr = new SubArray(1, 2);
subArr.add(3); subArr[2]; // 3
The answer is a compact workaround which works as intended in all supporting browsers.

Categories