Why does adding a property to an object literal's prototype change its "type"?
Asked Answered
R

1

8

So I have a simple isPlainObject method that I use to test for JavaScript object literals:

var isPlainObject = function (obj) {
  return typeof obj === "object" && {}.toString.call(obj) === "[object Object]";
};

Now I have a plain object:

var obj = {'one': 1, 'two': 2, 'three': 3};

When I run it through the isPlainObject(obj) function it works as expected and returns true. My question arrives from adding a property to the object's prototype:

obj.constructor.prototype.four = 4;

Now when I run isPlainObject(obj) on obj it returns false. typeof obj returns object in both instances. The toString returns [object Number] in the second instance after I've added a property to the prototype.

What exactly has happened to change obj? What's going on?

EDIT: This only happens when tested within the confines of a QUnit function call.

test("each", function() {

    _.each([1, 2, 3], function(i,v) {
      equal(v, i + 1, 'each iterator provided index and value from array');
    });

    var obj = {'one': 1, 'two': 2, 'three': 3};
    console.log(_.isPlainObject(obj)); // => true
    obj.constructor.prototype.four = 4;
    console.log(_.isPlainObject(obj)); // => false

});

EDIT: This is the console.log I get when logging the arguments array-like object within isPlainObject.

Logging out the <code>arguments</code> object

Looking at the log would seem to indicate the array now has two arguments. But the length still reads 1.

Rasia answered 1/5, 2013 at 18:4 Comment(19)
What (weird) browser are you using? It shows true for me...?Fong
I'm testing within a QUnit test() call. And you're right I just ran the test in the console and everything works as expected. Why would running this test within QUnit have this effect. Some sort of scope issue?Rasia
It's true for me, too. What do typeof obj and {}.toString.call(obj) return for you?Cumquat
This question seems to show a lack of research effort. Your function has several pieces; have you investigated to see which piece is behaving differently from how you expect? (What is typeof obj now? What does {}.toString.call(obj) return now?)Pavla
You should not modifiy Object.prototype! (which obj.constructor.prototype is). Not sure how that affects your function, though.Undirected
I just tested this with QUinit (running in Chrome), and both isPlainObject calls return true. jsfiddle.net/mXHKEMelgar
Trust me... if the last 45 minutes is a lack of research effort then sure. Yes, I have investigated the individual components of the function. typeof obj returns "object" in both instances. The toString returns [object Number] in the second instance after I've added a property to the prototype.Rasia
@Xaxis: It's probably because modifying Object.prototype is causing errors.Melgar
@Haxmat No errors or exceptions of any kind are being thrown. I'm testing in Chrome by the way.Rasia
This is a complete mystery to me... I performed the test outside of QUnit and am having no issues. I guess I can live with this being an effect of QUnit. Thank you all. Anymore insights would still be appreciated.Rasia
@Rasia Still cannot reproduce your issue: jsfiddle.net/mXHKE/1 (prints true and true). Is my fiddle setting _.isPLainObject the same way your code does? You never specify exactly how you do that in your code.Nicotiana
@Nicotiana Note quite. The isPlainObject method differs in that in my library it calls a method that builds an arguments object. See: boilerjs.com/boiler.js ... search for isPlainObject =.Rasia
@Rasia Your problem has nothing to do with qunit -- here's a fiddle with no qunit at all but with the relvant parts of your library: jsfiddle.net/mXHKE/5. Your problem has to do with your own _.__args() function, which is incorrectly reporting the arguments after the prototype manipulation. (In my fiddle, you'll see that the first time you get { obj: {'one':1...} } and the second time you get {obj: 4}.)Nicotiana
@Nicotiana Yes indeed. I just came to this conclusion as well. Now I need to determine why.Rasia
@Nicotiana So as I think about the issue further. Yes my library is "breaking" something in its detection of the passed arguments when something is modified on the prototype of an object. But this still means that something about the "type" of the object is changing for my args() method to detect an argument of a different type once this modification has been made. Any clue as to what gives?Rasia
So I've learned that despite the length property of the arguments object being 1 when you iterate over it (the arguments array-like object) in in the second instance after the prototype has been modified, two iterations are encountered. One over the object. And one over the added property. ... WTF? I thought for in loops weren't supposed to iterate over object literals' prototypes?Rasia
The breakage is happening because Object now has a bonus enumerable value, which cause and extra loop to happen on for (var d in types) when looping over the properties of the types argument to __args. It happens again on for (var a in args) and anywhere you use for..in.Nicotiana
You need to check object.hasOwnProperty(prop) (e.g. types.hasOwnProperty(d)) for each loop or, better still, never modify Object.prototype!(!!) (Seriously! This is why you don't!) Voting to close as too localized, although it may be a duplicate of another "modified Object.prototype, now for..in doesn't work" question.Nicotiana
Yes, yes indeed. I wasn't modifying Object.prototype for anything other than a unit test I assure you. A unit test to determine if my each method was iterative over non-enumerable properties. I can't thank you enough! I'll come up with a solution in no time. ... Basically it comes down to my understanding of JavaScript. I was under the impression that properties added to an objects prototype were not enumerable. I was obviously way wrong about that.Rasia
S
1

By calling

({}).constructor.prototype

You're accessing the prototype for all objects. So essentially you were adding a "four" property to every native object.

If you wanted to extend your object instance you would ideally need a new constructor function, like:

var Count = function() {
  this.one = 1;
  this.two = 2;
  this.three = 3;
}

var obj = new Count();
obj.four = 4;

Or to extend the constructor function:

Count.prototype.four = 4;

Regardless... Not sure which this would break QUnit, but as another poster suggested hasOwnProperty should do the trick. Here's a similar question

Starry answered 18/5, 2013 at 11:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.