const a = {x:1};
console.log({...a}.x);
// 1
console.log(window.Math);
// Math {abs: ƒ, acos: ƒ, acosh: ƒ, asin: ƒ, asinh: ƒ, …}
console.log({...window}.Math);
// undefined
I don't understand why {...a}.x evaluates 1, but {...window}.Math evaluates undefined.
That's because Math is not enumerable.
The ECMA-2018 (ES9) specs is a little hard to read. MDN and a proposal page stated: {...obj} creates a new object for all of obj's (1) own and (2) enumerable properties. Math is window's own property, but not enumerable:
console.log(window.hasOwnProperty("Math"));
console.log(Object.getOwnPropertyDescriptor(window, "Math"));
You can reproduce the situation with an object:
obj = {};
Object.defineProperty(obj, 'x', {
enumerable: false,
value: 123
});
console.log(obj); // Google Chrome prints { x: 123 } but StackOverflow prints {} and Node.js prints {} too
console.log(obj.x);
console.log(({...obj}).x);
{...Window} is using the spread operator to make a copy into a new object. Math is not enumerable so it won't be copied to the new object.
You can test it by your self:
const a = {}
Object.defineProperty(a, "prop1", { value: "enumerable", enumerable: true })
Object.defineProperty(a, "prop2", { value: "not enumerable", enumerable: false })
Then copy your object:
{...a}.prop1 //"enumerable"
{...a}.prop2 // undefined
Related
In the console, I get abc despite setting {writable:false}. Could you explain how changing metadata works?
let portfolio = {
myFirstName: "Bob",
myLastName: "Alice",
myAge: 26,
aboutMe: function() {
return ("First Name: " + this.myFirstName + "; Last Name: " + this.myLastName + "; Age: " + this.myAge + ";");
}
};
Object.defineProperty(portfolio, "myFirstName", { writable: false });
Object.defineProperty(portfolio, "myFirstName", { value: "abc" });
console.log(portfolio.myFirstName);
in your 2nd line Object.defineProperty(portfolio, "myFirstName", { value: "abc" }); you are defining the property again.
You are not assigning a value. You are tossing out the old property and replacing it with a brand spanking new one. (Technically incorrect, it goes through a lot of steps to evaluate and apply values to property properties, but for simple understanding, I believe this suffices as understanding, as it feels like a new one in this scenario. Please read the link if you wish to have the complex truth)
To assign a new value use portfolio.myFirstName = "value here" and you see it's write protected.
let portfolio = {
myFirstName: "Bob",
myLastName: "Alice",
myAge: 26,
aboutMe: function() {
return ("First Name: " + this.myFirstName + "; Last Name: " + this.myLastName + "; Age: " + this.myAge + ";");
}
};
Object.defineProperty(portfolio, "myFirstName", { writable: false });
portfolio.myFirstName = "Alice";
console.log(portfolio.myFirstName);
to prevent the workaround, call Object.freeze() on the object after modifying its property. This will also have other side effects like not being able to edit the values of other properties.
let portfolio = {
myFirstName: "Bob",
myLastName: "Alice",
myAge: 26,
aboutMe: function() {
return ("First Name: " + this.myFirstName + "; Last Name: " + this.myLastName + "; Age: " + this.myAge + ";");
}
};
Object.defineProperty(portfolio, "myFirstName", { writable: false });
Object.freeze(portfolio);
Object.defineProperty(portfolio, "myFirstName", {value:"abc"});
console.log(portfolio.myFirstName);
writable: false only has an effect on Object.defineProperty if configurable is also set to false. See step 7 of the ValidateAndApplyPropertyDescriptor algorithm:
Else if IsDataDescriptor(current) and IsDataDescriptor(Desc) are both true, then
If current.[[Configurable]] is false and current.[[Writable]] is false, then
If Desc.[[Writable]] is present and Desc.[[Writable]] is true, return false.
If Desc.[[Value]] is present and SameValue(Desc.[[Value]], current.[[Value]]) is false, return false.
Return true.
That's likely because as long as a property is configurable, nothing stops you from changing the value of writable back to true, e.g.
Object.defineProperty(
portfolio,
"myFirstName",
{value: "abc", writable: true}
);
Note that any property declared as part of an object literal automatically has {writable: true, configurable: true, enumerable: true}.
Examples
Can't assign because writable and configurable are both false:
var obj = {};
Object.defineProperty(obj, 'test', {
value: 42,
configurable: false,
writable: false,
enumerable: true,
});
console.log(obj);
Object.defineProperty(obj, 'test', {value: 21});
console.log(obj);
Can assign a value because writable or configurable are true:
var obj = {};
Object.defineProperty(obj, 'test', {
value: 42,
configurable: true,
writable: false,
enumerable: true
});
console.log(obj);
Object.defineProperty(obj, 'test', {value: 21});
console.log(obj);
var obj = {};
Object.defineProperty(obj, 'test', {
value: 42,
configurable: false,
writable: true,
enumerable: true
});
console.log(obj);
Object.defineProperty(obj, 'test', {value: 21});
console.log(obj);
Lastly, if writable and configurable are both false but if the new value is the same as the current value, no error will be thrown since no change is actually being made to the property:
var obj = {};
Object.defineProperty(obj, 'test', {
value: 42,
configurable: false,
writable: false,
enumerable: true,
});
console.log(obj);
Object.defineProperty(obj, 'test', {value: 42});
console.log(obj);
Setting writable: false will work as expected for normal assignments (foo.bar = 42) because such assignments go through OrdinarySetWithOwnDescriptor which check the writable value of an existing property descriptor first.
You're working around that access restriction to hard-set the property. That only impacts the portfolio.myFirstName mutator.
You really can't block access to defineProperty like that. It's too low-level. That's probably a good thing, though, since it is dependable.
I have initialized an object that I wish to add to dynamically. Specifically I want to add array of objects to this object. I have tried the below but neither work.. is there a way to do this correctly? The final output should be object1.property1[0].value = 42 and object1.property1[0].writable = false.
const object1 = {};
Object.defineProperty(object1, 'property1', '' );
object1.property1 = [{value: 42, writable: false}];
const object1 = {};
Object.defineProperty(object1, 'property1', [{value: 42, writable: false}] );
Try using value property from descriptor argument:
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: [{
value: 42,
writable: false
}]
});
console.log(object1.property1[0].value);
console.log(object1.property1[0].writable);
The object descriptor must be declared as follow:
{value: [{value: 42}], writable: false}
const object1 = {};
Object.defineProperty(object1, 'property1', {value: [{value: 42}], writable: false});
console.log(object1.property1[0].value)
Since you specified you wanted to add an array of objects to the object, this solution is similar to other answers, but should demonstrate adding an array with more than one element:
const obj = {};
Object.defineProperty(obj, 'property', {
value: [{
value: 42,
writable: false
},
{
value: 55,
writable: false
}
]
});
console.log(obj.property[0].value);
console.log(obj.property[0].writable);
console.log(obj.property[1].value);
console.log(obj.property[1].writable);
Of course, your writable isn't going to do anything, since it'll be treated as just another property. Due to the fact that your property is an array of objects (e.g., [{...},{...}]), what you may want to do is iterate over that array and freeze those objects:
obj.property.forEach(o=>Object.freeze(o))
obj.property[0].value=33; // when frozen you won't be able to update the value
This question already has answers here:
Why is JSON.stringify not serializing prototype values?
(3 answers)
Closed 8 years ago.
I have one prototype of this structure:
function MyObj() { }
MyObj.prototype = {
prop1: {
prop11: null,
prop12: null,
prop13: null,
},
prop2: {
prop21: null,
prop22: null,
prop23: null,
},
prop3: {
prop31: [],
prop32: '',
prop34: [],
},
prop4: {
prop41: null,
},
}
When I call JSON.stringify(myObjInstance), I get {}, why?
This happens because prop1 through prop4 are properties of the prototype and not of the instantiated object.
You can compare it to something like:
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
// add property to bag
}
}
Only the properties of the object itself are used.
Because JSON.stringify only includes an object's own properties (specifically, own enumerable properties), not properties an object inherits from its prototypes or any of its own properties that are non-enumerable.
So for example: Live Copy | Live Source
function Foo() { }
Foo.prototype.inherited = true;
var f = new Foo();
Object.defineProperty(f, "ownNonEnumerable", {
value: true
});
f.ownEnumerable = true;
console.log(f.inherited); // true
console.log(f.ownNonEnumerable); // true
console.log(f.ownEnumerable); // true
console.log(JSON.stringify(f)); // {"ownEnumerable": true}
JSON.stringify will only include a property if it can be found with hasOwnProperty, and
new MyObj().hasOwnProperty("prop1") === false;
Try this instead:
JSON.stringify(MyObj.prototype);
JSON.stringify(obj) will output the properties that the object itself has, not its prototype. Its prototype is a different object.
What you're looking for would be something like this
JSON.stringify(MyObj.prototype)
I had the same issue, when I start working with JSON.stringify. In order to create a string the object you create needs to of typeof == object. Otherwise it won't work.
Try to alert the typeOf of MyObj. If it not Object that won't work.
MyObj = {
prop1: {
prop11: null,
prop12: null,
prop13: null,
},
prop2: {
prop21: null,
prop22: null,
prop23: null,
},
prop3: {
prop31: [],
prop32: '',
prop34: [],
},
prop4: {
prop41: null,
},
}
I'm serializing objects to JSON strings with JavaScript,
I noticed only enumerable object properties get serialized:
var a = Object.create(null,{
x: { writable:true, configurable:true, value: "hello",enumerable:false },
y: { writable:true, configurable:true, value: "hello",enumerable:true }
});
document.write(JSON.stringify(a)); //result is {"y":"hello"}
[pen]
I'm wondering why that is? I've searched through the MDN page, the json2 parser documentation.
I could not find this behavior documented any-where.
I suspect this is the result of using for... in loops that only go through [[enumerable]] properties (at least in the case of json2). This can probably be done with something like Object.getOwnPropertyNames that returns both enumerable, and non-enumerable properties.
That might be problematic to serialize though (due to deserialization).
tl;dr
Why does JSON.stringify only serialize enumerable properties?
Is this behavior documented anywhere?
How can I implement serializing non-enumerable properties myself?
It's specified in the ES5 spec.
If Type(value) is Object, and IsCallable(value) is false
If the [[Class]] internal property of value is "Array" then
Return the result of calling the abstract operation JA with argument value.
Else, return the result of calling the abstract operation JO with argument value.
So, let's look at JO. Here's the relevant section:
Let K be an internal List of Strings consisting of the names of all the own properties of value whose [[Enumerable]] attribute is true. The ordering of the Strings should be the same as that used by the Object.keys standard built-in function.
As #ThiefMaster answered above, it's specified in the spec
however, if you know the names of the non-enumerable properties you like to be serialized ahead of time, you can achieve it by passing a replacer function as the second param to JSON.stringify() (documentation on MDN), like so
var o = {
prop: 'propval',
}
Object.defineProperty(o, 'propHidden', {
value: 'propHiddenVal',
enumerable: false,
writable: true,
configurable: true
});
var s = JSON.stringify(o, (key, val) => {
if (!key) {
// Initially, the replacer function is called with an empty string as key representing the object being stringified. It is then called for each property on the object or array being stringified.
if (typeof val === 'object' && val.hasOwnProperty('propHidden')) {
Object.defineProperty(val, 'propHidden', {
value: val.propHidden,
enumerable: true,
writable: true,
configurable: true
});
}
}
return val;
});
console.log(s);
You can achieve a generic JSON.stringify() that includes non-enumerables with code like below. But first, some notes:
This code has not been thoroughly tested for possible bugs or unexpected behavior; also, it turns functions into basic objects. Treat accordingly.
copyEnumerable() is the function to pay attention to. It does not itself internally call JSON.stringify() (you can do that yourself, with the output object) mainly because recursive calls to JSON.stringify() (due to nested objects) will yield uglier, unwanted results.
I'm only checking for object and function types to be treated specially; I believe that these are the only types which need to be handled specially... Note that JS Arrays ([]) fall into this category (objects), as well as classes (functions), and null does not. Since null is of type object, I included the !! check.
The stopRecursiveCopy Set is just to make sure it's not doing infinite recursion.
I'm using a trick with the 2 extra parameters to JSON.stringify() calls to make it output something formatted prettier, for easy reading.
The code is in an easy format to try out and tweak in the console:
{
o = {};
d = {};
Object.defineProperties(o, {
a: {
value: 5,
enumerable: false
},
b: {
value: "test",
enumerable: false
},
c: {
value: {},
enumerable: false
}
});
Object.defineProperty(o.c, "d", {
value: 7,
enumerable: false
});
//The code to use (after careful consideration!).
function isObject(testMe) {
return ((typeof(testMe) === "object" && !!testMe) ||
typeof(testMe) === "function");
}
let stopRecursiveCopy = new Set();
function copyEnumerable(obj) {
if (!isObject(obj)) {
return obj;
}
let enumerableCopy = {};
for (let key of Object.getOwnPropertyNames(obj)) {
if (isObject(obj[key])) {
if (!stopRecursiveCopy.has(obj[key])) {
stopRecursiveCopy.add(obj[key]);
enumerableCopy[key] = copyEnumerable(obj[key]);
stopRecursiveCopy.delete(obj[key]);
}
} else {
enumerableCopy[key] = obj[key];
}
}
return enumerableCopy;
}
console.log(JSON.stringify(copyEnumerable(o), null, " "));
Object.defineProperty(copyEnumerable, "test", {
value: 10,
enumerable: false
});
console.log(JSON.stringify(copyEnumerable(copyEnumerable), null, " "));
Object.defineProperty(o, "f", {
value: copyEnumerable,
enumerable: false
});
console.log(JSON.stringify(copyEnumerable(o), null, " "));
}
which outputs:
//o
{
"a": 5,
"b": "test",
"c": {
"d": 7
}
}
//copyEnumerable itself
{
"test": 10,
"prototype": {
"constructor": {
"test": 10,
"length": 1,
"name": "copyEnumerable"
}
},
"length": 1,
"name": "copyEnumerable"
}
//o again, but with `f: copyEnumerable` added
{
"a": 5,
"b": "test",
"c": {
"d": 7
},
"f": {
"test": 10,
"prototype": {},
"length": 1,
"name": "copyEnumerable"
}
}
Pertinent links:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof
https://stackoverflow.com/a/14706877/1599699
https://stackoverflow.com/a/18538851/1599699
In what usages of Object.create do you want to set enumerable to true?
A property of an object should be enumerable if you want to be able to have access to it when you iterate through all the objects properties. Example:
var obj = {prop1: 'val1', prop2:'val2'};
for (var prop in obj){
console.log(prop, obj[prop]);
}
In this type of instantiation, enumerable is always true, this will give you an output of:
prop1 val1
prop2 val2
If you would have used Object.create() like so:
obj = Object.create({}, { prop1: { value: 'val1', enumerable: true}, prop2: { value: 'val2', enumerable: false} });
your for loop would only access the prop1, not the prop2. Using Object.create() the properties are set with enumerable = false by default.