4

Javascript interview question: make [1,2,3].sum() exact code run without using Prototype and Object.defineProperty, Object.defineProperties.

Since it was an interview question I am assuming there are ways to make it work?

Any help/pointing direction appreciated.

Thanks

gen_Eric
  • 223,194
  • 41
  • 299
  • 337
serkan
  • 6,885
  • 4
  • 41
  • 49
  • @Dai not really going to work - an array literal will not just lookup `Array` in the scope chain (that's what `new Array()` would do) - it actually goes and initialises a native array, regardless of what occupies `window.Array`. – VLAZ Nov 16 '20 at 19:58
  • Did you sign a non-disclosure and agree not to share the interview questions? – anthumchris Nov 16 '20 at 19:59
  • @VLAZ *Previously* (like, before 2010) browsers would use a user-provided `Array` constructor when it encounters literals. JavaScript array literals are not initialized by the parser/compiler but when encountered. – Dai Nov 16 '20 at 19:59
  • @AnthumChris, No, I did not. Are you familiarized with the question somehow? I guess you do. :) – serkan Nov 16 '20 at 20:02
  • 1
    What are the actual constraints? You aren't allowed to do *anything* with prototypes or you aren't allowed to use `prototype` as the property, or you aren't allowed to use the word "prototype"? Or what? – VLAZ Nov 16 '20 at 20:09
  • @serkan well, my first go-to would actually be `Object.getPrototypeOf()`. It *does* affect the prototype but it doesn't use the `.prototype` property. It's also fairly sensible choice, rather than a dirty hack. Mostly anything else would be some dirty hack one way or another. – VLAZ Nov 16 '20 at 20:18
  • 1
    I mean.. the correct answer they are looking for could be "there is no (good) way to do this" perhaps to see if you're willing to give something hacky vs. put your foot down – CrayonViolent Nov 16 '20 at 20:31

3 Answers3

9

Preface: Questions like these don't really show someone is a "good" programmer, it just means they're familiar with tricks in the language that do not lead to more-maintainable code. I'd be wary of working for a company or team that regularly uses tricks like this.

(And in my personal case: I worked on the Chakra JavaScript engine when I was an SE at Microsoft and I like to think that I know JavaScript/ECMAScript very well and I still had to think a long time about how this could be done without using prototype or defineProperty - so that's why I don't think this is a good technical interview question if they expected a straight-answer - but if this was an interview question that's meant to prompt you into asking questions of the interviewer then that's different).


Option 1: Global error handler:

Here's a horrible way:

window.addEventListener( 'error', function( e ) {
    
    if( e.error instanceof ErrorEvent || e.error instanceof TypeError ) {
        
        const msg = e.error.message;
        const suffixIdx = msg.indexOf( ".sum is not a function" );
        if( suffixIdx > -1 ) {
            const arrayStr = msg.substring( 0, suffixIdx );
            
            const arr = eval( arrayStr ); // <-- lolno
            const total = arr.reduce( ( sum, e ) => sum + e, 0 );
            console.log( total ); // 6
        }
    }
    
} );

[1,2,3].sum()

@NenadVracar has posted a simplified version that avoids eval, though it uses a local try:

try {
    [1,2,3].sum()
} catch (err) {
    const result = err.message
    .match(/\[(.*?)\]/)[1]
    .split(',')
    .reduce((r, e) => r + +e, 0)
    
  console.log(result)
}

Option 2: Override Array constructor

If you're using an older JavaScript engine (made prior to 2010 or ECMAScript 5) then a script that overrides the Array constructor will have that constructor used when the script encounters an array literal, and the .sum method could be added that way:

Array = function() { // <-- THIS WILL NOT WORK IN BROWSERS MADE AFTER 2010!
    this.sum = function() {
        var total = 0;
        for( var i = 0; i < this.length; i++ ) {
            total += this[i];
        }
        return total;
    };
};

let total = [1,2,3].sum();
console.log( total );

Option 3: Being sneaky with the prototype property:

As others have mentioned in the comments, you could still mutate the prototype member or use Object.defineProperty if you access those members as strings:

Array[ 'proto' + 'type' ].sum = function() {
    var total = 0;
    for( var i = 0; i < this.length; i++ ) {
        total += this[i];
    }
    return total;
};

let total = [1,2,3].sum();
console.log( total );
Dai
  • 141,631
  • 28
  • 261
  • 374
5

How much can we skirt the lines here?

Assuming that we want the following line to work [1, 2, 3].sum(); then we can very easily just make it do something. Note that due to the automatic semicolon insertion rules, it's not necessary what you have there to be an array. It might be array access with comma operator in it.

({3: {sum: () => console.log(6)}}) //<-- object

[1,2,3].sum(); //<-- array access

Or to make it more clear, here is the equivalent code:

const obj = {
  3: {
    sum: () => console.log(6)
  }
};

obj[3].sum(); //<-- array access

Since, I don't see a definition of what sum should do, the above covers all the requirements listed - no protoype shenanigans, no extra properties.

OK, technically, sum doesn't sum anything, but here is a workaround: define it like this

sum: (a, b) => a + b

Now, it's technically a function that sums two numbers. There is no requirement to sum the sequence 1, 2, 3 that appears before calling the sum, after all.

VLAZ
  • 26,331
  • 9
  • 49
  • 67
  • Oh, yes, the good old ASI ;) If it's allowed to put lines before the code, then it's a workable hack! – FZs Nov 16 '20 at 20:30
  • 1
    @FZs I'd assume it's allowed to add lines before. Otherwise I'm not even sure you'd be able to implement any actual hack. And it's their fault, they didn't do `;[1, 2, 3]` :P – VLAZ Nov 16 '20 at 20:32
  • That's true... (as long as they don't have a requirement that it should be a separate statement). Awesome answer anyway! – FZs Nov 16 '20 at 20:35
  • @FZs hey, I can only work with the requirements given here. It's likely that we can't really answer the question, though - perhaps it was meant to prompt a discussion into *what* exactly the goal is. Perhaps the expected outcome was to say "no, it's not possible". Who knows. I personally don't really like this as an interview question but hey. I'm on the opinion that the tasks at an interview should be practical and similar to what you'd normally encounter. FizzBuzz gets flack with good reason but it's still better than open ended questions like these. – VLAZ Nov 16 '20 at 20:39
  • I like your answer and don't want to criticize, just these are the things that came into my mind after looking at it... – FZs Nov 16 '20 at 20:41
  • @FZs as they should, honestly. – VLAZ Nov 16 '20 at 20:43
0

Use pizza, no Prototype needed

var knife = (s) => { return s.replace(/,/g, '') }, result, slice = 'codePointAt', bite = 'toString', mix = 'substring';
var pizza = '', peppers = knife(',,'), avocados = knife(',,,,,,,,'), cheese = String.fromCharCode;
[+pizza[slice](0)[bite](16)[mix](2,3)][avocados.split('').map((v) => +(v[slice](0)[bite](16).substring(2,5))).map((v) => +(v > 600 ? (v + '')[mix](0,2) : v - 440 )).reduce((a,b,i) => (i == 1 ? cheese(a) + cheese(b) : a + cheese(b)))][peppers.split('').map((v) => +(v[slice](0)[bite](16)[mix](2,5))).map((v, i) => (i == 0 ? v - 221 : i == 1 ? v - 219 : v - 227)).reduce((a,b,i) => (i == 1 ? cheese(a) + cheese(b) : a + cheese(b)))] = () => { return pizza[slice](0)[bite](8)[mix](2,4).split('').reduce((a,b) => +a+ +b);};
result = [1,2,3].sum();
// Show the result 
console.log(result);

The point of showing you this as a pizza/emoji example is that you can work around this kind of tests (similar to this).

Of course, the previous code can be simplified as this other more serious snippet, which also fulfills the purpose of the asked question using __proto__. This answer is similar to @VLAZ's answer but without the ; hack-thing he uses.

[3]['__proto__']['sum'] = () => { console.log('this runs!, answer is 6'); };
[1,2,3].sum();

/**
 * Here am using `__proto__` instead 
 * of `Prototype`, they are both almost the same 
 * but `__proto__` works for instances only.
 *
 **/
luiscla27
  • 4,956
  • 37
  • 49
  • ```__proto__``` is for instances, not the same thing. I get your point though. – serkan Nov 16 '20 at 20:08
  • If `__proto__` is not consider the same as `prototype` and can be accepted as answer, then `setPrototypeOf` it would be better solution: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf – StPaulis Nov 16 '20 at 20:26