2

I have task to receive unique element in order from string as parameter. I do not understand how this function uniqueElements returns ['A','B','C','B'].

var word = "AAAABBBBCCBB";
var uniqueElements = function(word) {
    return [].filter.call(word, (function(elem,index) {
        return word[index-1] !== elem
    }));
}    
uniqueElements(word);      
//it should return ['A','B','C','B']

I checked and read the following sources:

MDN Mozilla and JS Blog but without success.

Hawk360
  • 161
  • 1
  • 1
  • 7

7 Answers7

3

The aim of [].filter.call here is to apply the filter method on a string. Naively you would first try to do this:

return word.filter(function(elem,index) {
    return word[index-1] !== elem;
});

... which -- if word were an array -- would return the characters in it that satisfy the condition word[index-1] !== elem (which is short for word[index-1] !== word[index]), i.e. each character that is different from the character that precedes it.

But, the filter method only exists on objects that inherit from the Array prototype, which is not the case for strings.

However, filter itself can deal with array-like objects, i.e. objects that have a length and potentially have properties that are numerical. To call filter without a true Array object, you can use call on that function, and then provide the context (i.e. the array-like object) as first argument. The other argument(s) remain the same. So you first need to know where to find the filter method... It is on the Array.prototype, so you would reference it like this:

Array.prototype.filter

But as all arrays have access to this method, it is shorter to just take an empty array, and reference its filter method:

[].filter

Either of the two will work. Now you have to execute the call method on it, and supply the array-like object you want to iterate over:

[].filter.call(words, ...)

The rest is like you would do with a true array: you provide the callback function:

return [].filter.call(word, function(elem,index) {
    return word[index-1] !== elem
});

The return value is no longer a string. It is -- like filter always returns -- an array: a real one -- it is created by the filter method.

ES6 way:

In ES6 you can write this in a more readable way, as there now is the Array.from method, which can turn an array-like object to a true array. And then you can just continue with the filter method:

return Array.from(word).filter(function(elem,index) {
    return word[index-1] !== elem
});

The spread operator is yet another alternative provided by ES6:

return [...word].filter(function(elem,index) {
    return word[index-1] !== elem
});
trincot
  • 317,000
  • 35
  • 244
  • 286
1

It's performing an explicit call() on the filter method on the array object so they can pass in a string. They're just using [] instead of Array because it's shorter syntax or someone was trying to be clever when they wrote it.

Normally, the filter() method would affect the array's own contents. However, by call()-ing it, you can change the context by passing in another array, or in this case, a string (which is treated as an array of characters). It then runs the supplied function for each element, keeping the element if the return value of the function is true.

Soviut
  • 88,194
  • 49
  • 192
  • 260
  • This misses the whole point that they are doing it in order to apply `filter` to a string. –  Nov 10 '16 at 16:01
  • it's kinda extraordinary that you can pass a string as this reference and filter function will perform split('') as expected. Is it reliable behavior? – ganqqwerty Sep 04 '17 at 00:17
  • 1
    @ganqqwerty it's not calling split, it's iterating over each letter in the string. Strings are iterable because under the hood they're really just character arrays. – Soviut Sep 04 '17 at 00:23
1
var uniqueElements = function(word) {
    return [].filter.call(word, (function(elem,index) {
        return word[index-1] !== elem
    }));
}    

is equivalent to

var uniqueElements = function(word) {
    return word.split("").filter(function(elem,index) {
        return word[index-1] !== elem
    });
}    

which should already be clearer: it turns word into an array of char then filters every char that is different from the previous one. This explains why "B" is present twice in the result.

To get unique elements, you can do

var uniqueElements = function(word) {
    var res = []
    word.split("").forEach(function(val){
        if (res.indexOf(val)===-1) res.push(val);
    });
    return res
}  
solendil
  • 8,432
  • 4
  • 28
  • 29
0

If you don't understand what a code do, you can log actions ! IE

var word = "AAAABBBBCCBB";
function uniqueElements(word){
    return [].filter.call(word,(
        function(elem,index) { 
        console.log("Index : "+index+" | Elem : " +elem+" | CompareTo "+word[index-1]);
            return word[index-1] !== elem;
        }
    ));
}
uniqueElements(word); 

You will get the following input :

Index : 0 | Elem : A | CompareTo undefined
Index : 1 | Elem : A | CompareTo A
Index : 2 | Elem : A | CompareTo A
Index : 3 | Elem : A | CompareTo A
Index : 4 | Elem : B | CompareTo A
Index : 5 | Elem : B | CompareTo B
Index : 6 | Elem : B | CompareTo B
Index : 7 | Elem : B | CompareTo B
Index : 8 | Elem : C | CompareTo B
Index : 9 | Elem : C | CompareTo C
Index : 10 | Elem : B | CompareTo C
Index : 11 | Elem : B | CompareTo B

Using this, you can check that every element who aren't equal than the one before are send to your array.

Few answers/comments already pointed out how [].filter.call() work.

If you want to get a String instead of an array, simply add .join("") IE

var word = "AAAABBBBCCBB";
function uniqueElements(word){
    return [].filter.call(word,(
        function(elem,index) { 
        console.log("Index : "+index+" | Elem : " +elem+" | CompareTo "+word[index-1]);
            return word[index-1] !== elem;
        }
    )).join("");
}
uniqueElements(word); 
Aks
  • 395
  • 3
  • 11
0

I've split the code into smaller parts with inline comments for better understanding.

var word = "AAAABBBBCCBB";
var uniqueElements = function(word){
    // this is the filter function
    // this function will get each element of the string with its index
    var filterFunction = function(elem, index) {
        // the elements who pass the following condition are kept
        // the condition - 
        // if the character at the previous index (index-1) is not equal to the 
        // current element, then keep the element
        return word[index-1] !== elem;
    }

    // the below is equivalent to Array.prototype.filter.call(context, filterFunction)
    return [].filter.call(word, filterFunction);
}
console.log(uniqueElements(word));
Raghudevan Shankar
  • 1,083
  • 9
  • 18
0

I would do this O(n) time by utilizing Array.prototype.reduce() as follows;

var str = "AAAABBBBCCBB",
uniques = Array.prototype.reduce.call(str, (p,c,i) => i-1 ? p[p.length-1] !== c ? (p.push(c),p)
                                                                                : p
                                                          : [c]);
console.log(uniques);
Redu
  • 25,060
  • 6
  • 56
  • 76
-1

var word = "AAAABBBBCCBB";
var unique = word.split('').filter(function(item, i, ar) {
  return ar.indexOf(item) === i;
}).join('');
kukkuz
  • 41,512
  • 6
  • 59
  • 95
samnu pel
  • 914
  • 5
  • 12