Freezing the elements of an array - javascript

I have an array of certain primitive elements:
const array = [1, 2, 3]
I want to be able to temporarily freeze an element of this array and prevent it from being modified. But the moment the element can be allowed to be modified, there should be a way to unfreeze the value.
Is there a way to do so?
Off-topic for those who flag this question as duplicate:
The question is about freezing the elements of an array, not the entire array. It is not a duplicate of a question regarding freezing the entire array.

You don't want to freeze the value, you want to freeze the property that holds the value (the "1" property of the array, in your case). You'd use Object.defineProperty to redefine the property without the writable flag. To make it writable again, you redefine it with writable: true:
const array = [1, 2, 3];
console.log("A", array.join(", ")); // 1, 2, 3
// Freeze it
Object.defineProperty(array, "1", {
value: array[1],
writable: false, // For emphasis (this is the default)
enumerable: true,
configurable: true
});
console.log("B1", array.join(", ")); // 1, 2, 3
array[1] = 42; // <== Doesn't change it
console.log("B2", array.join(", ")); // 1, 2, 3 (still)
// Thaw it
Object.defineProperty(array, "1", {
value: array[1],
writable: true,
enumerable: true,
configurable: true
});
console.log("C1", array.join(", ")); // 1, 2, 3
array[1] = 42; // <== Changes it
console.log("C2", array.join(", ")); // 1, 42, 3 (changed!)
That assignment would be an exception if the code doing the assignment were running in strict mode:
"use strict";
const array = [1, 2, 3];
console.log("A", array.join(", ")); // 1, 2, 3
// Freeze it
Object.defineProperty(array, "1", {
value: array[1],
writable: false, // For emphasis (this is the default)
enumerable: true,
configurable: true
});
console.log("B1", array.join(", ")); // 1, 2, 3
array[1] = 42; // <== Doesn't change it
console.log("B2", array.join(", ")); // 1, 2, 3 (still)
// Thaw it
Object.defineProperty(array, "1", {
value: array[1],
writable: true,
enumerable: true,
configurable: true
});
console.log("C1", array.join(", ")); // 1, 2, 3
array[1] = 42; // <== Changes it
console.log("C2", array.join(", ")); // 1, 42, 3 (changed!)
But note that if your code can redefine it to make it writable, anyone else's code can, too.
Alternately, give it a getter and setter, make it non-configurable (so no one else can redefie it), and maintain a flag:
const array = [1, 2, 3];
let elementValue = array[1];
let writable = true;
Object.defineProperty(array, "1", {
get: function() {
return elementValue;
},
set: function(newValue) {
if (writable) {
elementValue = newValue;
}
},
enumerable: true,
configurable: false // Again, emphasis
});
console.log("A", array.join(", ")); // 1, 2, 3
array[1] = 42;
console.log("B", array.join(", ")); // 1, 42, 3 -- it changed
writable = false;
array[1] = 67;
console.log("C", array.join(", ")); // 1, 42, 3 -- didn't change
writable = true;
array[1] = 94;
console.log("D", array.join(", ")); // 1, 94, 3 -- changed
Naturally, you'd hide some of that and just expose the array itself.

Related

How to sort array of objects with boolean values: true, false and null

Hi i have an array of objects that i want to sort based on a boolean that one of the objects has. However normally there would be either true or false but in this case we also check on null values because sometimes the data has not been set and in that case we wanna show that it has yet to be set with an icon.
Here's an example of the array:
const arrayOfObjects = [
{
id: 69,
boolean: true,
name: 'foo',
},
{
id: 42,
boolean: false,
name: 'bar',
},
{
id: 666,
boolean: null,
name: 'foo',
},
{
id: 420,
boolean: false,
name: 'bar',
},
{
id: 2,
boolean: null,
name: 'foo',
},
{
id: 123,
boolean: true,
name: 'foo',
},
]
So what i tried first was:
arrayOfObjects.sort((a, b) => b.boolean - a.boolean);
This sets the objects that are true at the front but the objects with false or null are scattered.
Then i tried:
arrayOfObjects.sort((a, b, c) => (c.boolean - b.boolean) - a.boolean);
This just didn't work at all.
I couldn't really find a case that was similar enough to base a solution off of it so hopefully i can find it here.
If you like to use a custom sorting, you could take an object with the wanted sorting, like
const
order = { true: 1, null: 2, false: 3 };
data = [{ id: 69, boolean: true, name: 'foo' }, { id: 42, boolean: false, name: 'bar' }, { id: 666, boolean: null, name: 'foo' }, { id: 420, boolean: false, name: 'bar' }, { id: 2, boolean: null, name: 'foo' }, { id: 123, boolean: true, name: 'foo' }];
data.sort((a, b) => order[a.boolean] - order[b.boolean]);
console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }
If you have unknown values and want to move them to bottom, you could add another key with a large value, like
order = { true: 1, null: 2, false: 3, bottom: Number.MAX_VALUE };
Usage:
data.sort((a, b) =>
(order[a.boolean] || order.bottom) - (order[b.boolean] || order.bottom)
);
You can check for the null explicitly ...
let list = [{i: 0, boolean: true}, { i: 1, boolean: null}, { i:2, boolean: false}, { i: 4, boolean: true}]
function cpBoolWithNull(a,b) {
//if both are null return 0 to maintain a stable sort
//if only one is null return 0 or 1 depending on the value of the other
if (a.boolean === null) return b.boolean === null ? 0 : b.boolean ? 1 : -1;
if (b.boolean === null) return a.boolean ? -1 : 1;
//if both are different from null, sort true before false
return b.boolean - a.boolean
}
console.log(list.sort(cpBoolWithNull));
This will sort true ... null ... false If you need a differnt order, adjust the return values.
I think that you can have a type checker with JS with this simple script.
let array =[true, false, null];
function check(i){
if (array[i] != null||array[i]!=false){
if (array[i]!=null || array[i]!=true)document.write(" Array item"+" "+i+" "+"has the value of boolean false. ");
if (array[i]!=true||array[i]!=false)document.write(" Array item"+" "+i+" "+"has the value of boolean true. ");
if (array[i] != true || array[i] != false )document.write(" Array item"+" "+i+" "+"has the value of object null. ");
document.write("<br>")
}
}
check(0);
You can comment out the other text when it is not needed.

How does changing metadata in JavaScript work?

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.

Given an object how can I get the name used to describe it?

I have a JSON object(mainObj) which in turn has objects (say obj1, obj2, obj3). What I am trying to achieve is when I check for a condition iterating through every obj in the mainObj and if it holds true, I want to add only the name of that obj in an array of String. Something like,
for(obj in mainObj){
if(obj holds condition){
add the descriptor of the obj (in string format) to an array (not the entire obj)
}
You can use Object.keys() to iterate over your object keys, then use Array.filter() to filter the keys, here I am checking if the inner objects have a property show and if this property is truthy:
const mainObj = {
obj1: { show: true, a: 1 },
obj2: { show: false, a: 2 },
obj3: { a: 3 },
obj4: { show: true, b: 1 }
};
const result = Object.keys(mainObj).filter(key => mainObj[key].show);
console.log(result);
If you want to use a for-in loop, you have to make sure the property is part of the object and is not inherited from its protype chain using Object.hasOwnProperty():
const mainObj = {
obj1: { show: true, a: 1 },
obj2: { show: false, a: 2 },
obj3: { a: 3 },
obj4: { show: true, b: 1 }
};
const result = [];
for (const prop in mainObj) {
if (mainObj.hasOwnProperty(prop) && mainObj[prop].show) {
result.push(prop);
}
}
console.log(result);

how to transform an object to an array based on key/value?

I have an object with few properties true/false, I need to return an array with only the property name with true.
I have tried Object.entries but not sure how to create an array now.
const inputs = {
a: true,
b: true,
c: false
}
// result should be ['a','b']
// i have tried so far this one with no success
// const result = Object.entries(inputs).map((x, idx) => console.log(x))
Object.keys(inputs).filter(key => inputs[key])
Use my JSFiddle for the code.
Use the filter array method, described by MDN as
creates a new array with all elements that pass the test implemented by the provided function
Call it on the Object.keys, a built in array for objects:
returns an array of a given object's property names
Source: Object.keys()
So, to put it together, it would look like
const inputs = {
a: true,
b: true,
c: false
}
console.log(inputs); // Output: {a: true, b: true, c: false}
const arr = Object.keys(inputs).filter(keyName => inputs[keyName]);
console.log(arr); // Output: ["a", "b"]
For give only keys of object use filter:
Like this:
const inputs = {
a: true,
b: true,
c: false
};
var true_inputs = Object.keys(inputs).filter(key => inputs[key]);
console.log(true_inputs);
Or Jquery map:
const inputs = {
a: true,
b: true,
c: false
};
var true_inputs = $.map(inputs, function(n, i) { if(n) return i });
console.log(true_inputs);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
For to get the whole object use for .. in:
Like this:
const inputs = {
a: true,
b: true,
c: false
};
var true_inputs = {};
for(var key in inputs){
if(inputs[key])
true_inputs[key]=inputs[key];
}
console.log(inputs);
console.log(true_inputs);

Why does JSON.stringify not serialize non-enumerable properties?

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

Categories

Resources