Object.keys() array with one element treated as string? - javascript

What kind of magic is that, when you Object.keys() on an object with one key only, it will be treated as a string when you reference later using square brackets?
See below for example:
let chosenProducts = [
{
toys: 20
}
];
let toys = "toys";
chosenProducts.forEach(product => {
let name = Object.keys(product);
console.log(Array.isArray(name)); // true
console.log(name) // ['toys']
console.log(toys) // "toys" - string
console.log(product[name]); // 20 - array used = wtf?
console.log(product[toys]); // 20 - string used
});

Object.keys returns an array.
All objects have a toString method.
If you use an object in string context, toString is called implicitly.
Square-bracket property accessor notation is, essentially, string context (unless you pass a Symbol)
The default toString method on an array looks something like: function () { return this.join(","); }
const example1 = ["foo", "bar"];
console.log("" + example1);
const example2 = ["baz"];
console.log("" + example2);

Object.keys returns an array of keys. Your name variable is an array with one element ['toys']. When you access product[name] implicit coercion of name array to string happens which results in product["toys"]
In javascript there are two types of coercions implicit and explicit coercion
When you access with Object's properties they will be string, in case of an array they will be indexes (numbers), but array is also an object, when you add any property in array it can be act as object property(string) and will not get counted towards the length of array.
let arr = [1,3];
arr.val = 20;
console.log(arr.length); // 2
In your example you are accessing object's property product[name] here name is an array. What will happen here is implicit coercion of array to string.
Javascript will automatically call toString method on your object which is an array in this case. There is toString method for Array which is linked to their prototype Array.prototype.toString. You can override that if you want to get different result.
In simple terms when you try to access variable in if conditional expression javascript automatically coerce to Boolean value.
let a = 10;
if(a){
console.log("implicit coercion");
}
if(Boolean(a)){
console.log("explicit coercion");
}
To learn more about Javascript grammar and types.

Related

Why does a JavaScript array item created via dot notation exist but not get counted in array.length?

Although counterintuitive, it is possible to create a JavaScript array "property" using dot notation:
const arr = [];
arr.dot = "notation";
console.log(arr.dot); // "notation"
Weird, but ok. However, the array's length still registers as 0:
const arr = [];
arr.dot = "notation";
console.log(arr.dot); // "notation"
console.log(arr.length); // 0
Two questions:
Why is the array's length not impacted by the property assigned via dot notation?
Why is it possible to assign a property to an array via dot notation?
A JavaScript array is just an object. You're setting the property dot on the object.
You can confirm that an array is an object by doing:
typeof arr.
The length property is computed based on the number of numeric entries in the array.
Here's an excerpt taken from developer.mozilla.org:
Arrays cannot use strings as element indexes (as in an associative array) but must use integers. Setting or accessing via non-integers using bracket notation (or dot notation) will not set or retrieve an element from the array list itself, but will set or access a variable associated with that array's object property collection. The array's object properties and list of array elements are separate, and the array's traversal and mutation operations cannot be applied to these named properties.
That dot notation is actually assigning a property to the array, not pushing a new value!
const myArray = ['apples', 'bananas'];
console.log(myArray);
myArray[2] = 'peach';
console.log(myArray);
I assume this is what made you look towards objects, which do this for assignment:
const myObject = {
id: 1,
name: 'Pig',
type: 'animal',
}
// Standard dot-notation assignment
myObject.sound = 'Oink!';
console.log(myObject);
// "Computed" assignment
myObject['emoji'] = '🐷';
console.log(myObject);
Here's a good read on the topic above https://ui.dev/computed-property-names/.
Back to the question at hand, though: why can't I do this:
const myArray = ['choclate', 'strawberry'];
myArray.pinapple = 'Tasty';
Arrays are essentially lists. It doesn't make sense to add an attribute (i.e. "describer") to a list.
Don't get me wrong - it is perfectly alright to set properties of an Array (as it is based off JavaScript objects), but it isn't used in the way that you're thinking of.
Here's an example of when I might use the "dot notation" assignment for an Array:
let zoo = ['Dolphin', 'Lion', 'Monkey'];
console.log(zoo);
// In-built property
console.log('I have', zoo.length, 'animals in my zoo!');
// Now, let's add a property "income"
zoo.income = 500;
console.log('My zoo has £%s', zoo.income);
// We can use this like a normal object property
zoo.income += 50;
console.log('My zoo has £%s', zoo.income);
// Let's create a method for when the zoo goes out of business
zoo.closeDown = () => {
zoo = [];
zoo.income = 0;
return true;
}
zoo.closeDown();
console.log(zoo);
console.log('My zoo has £%s', zoo.income);
Why would I want to do this? In this example, I could've used an object. But it's possible that it makes more sense to keep the animals in my zoo as an array, and then build up the methods and properties from there.
Okay, but how do I get a list of these properties/methods then?
const myArray = ['Foo', 'Bar'];
myArray.isCool = true;
console.log(myArray);
console.log(myArray.length);
let i;
for (i in myArray) {
console.log(i, myArray[i]);
}
The for (i in ...) syntax can be used here, as we are iterating through the properties of the array as an object. As we know from before, Arrays extend the Object class (kind of).
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in
length only counts the numeric properties.
Arrays are objects, so you can add other properties to them.
You don't even have to use dot notation, you can write arr["dot"] = "notation";

Why do String behaves like an Array ? - JS

We all know Strings somewhat behaves like an Array.You can even apply some of the array methods to it and take benefits, like the below example.
[].filter.call('abcdef',function(val){
return val<'e';
});
Also,
var a='xyz';
I can access first element using a[0] or I can call a.length like an Array
My question is why String behaves like an Array. and if it does, why do I get false below when I check if it is an instance of Array. Is String Array-like?
'a' instanceof Array
All that Array.prototype.filter really requires is that the variable being iterated over has a length property, and that the variable has numeric-indexed values. See (part of) the polyfill:
var len = this.length >>> 0,
res = new Array(len), // preallocate array
t = this, c = 0, i = -1;
if (thisArg === undefined){
while (++i !== len){
// checks to see if the key was set
if (i in this){
if (func(t[i], i, t)){
res[c++] = t[i];
}
}
}
}
Strings fulfill this condition - strings have a length property, and numeric indicies accessed on the string resolve to individual characters.
But you can do the same thing with arbitrary objects:
const obj = {
0: 'foo',
1: 'bar',
length: 2
};
const result = [].filter.call(obj, val => val.startsWith('f'));
console.log(result);
You could say that obj is array-like as well, since it has a length property and numeric indicies. Most array methods like .filter, .reduce, etc can be .called on array-like objects, even if those objects aren't actual arrays.
(Technically, you can also call array methods on non-array-like objects too, it just won't do anything useful - no iterations may be performed)
To use instanceof you need to create an instance of an Object and a is not an instance of one. It is a primitive or also known as string literal:
String literals (denoted by double or single quotes) and strings
returned from String calls in a non-constructor context (i.e., without
using the new keyword) are primitive strings. JavaScript automatically
converts primitives to String objects, so that it's possible to use
String object methods for primitive strings. In contexts where a
method is to be invoked on a primitive string or a property lookup
occurs, JavaScript will automatically wrap the string primitive and
call the method or perform the property lookup.
For example:
let foo = 'abc' // primitive
let boo = new String() // object
console.log(foo instanceof String) // false
console.log(boo instanceof String) // true
console.log(typeof foo) // 'string' <-- notice not String
console.log(typeof boo) // object
This is simply due to:
The instanceof operator tests the presence of constructor.prototype in
object's prototype chain
But as we explained above we are dealing with string literals which are created without a constructor call (new keyword) and only boxed for our convenience when operating on them. They are not actual instances of String and thus instanceof returns false.
The reason you can use Array.filter on the string primitive is simply due to the fact that it was boxed for you to a String from where it got the length property at the time of execution.
For example in the case of V8 engine string primitive is parsed/boxed to String and a String to a StringObject. Notice they are actually different instances.
Array.filter only cares about that length property and numeric indicies as pointed nicely by CertainPerformance which are provided by the boxing to String. Example:
console.log(Object.getOwnPropertyNames('a')) // ["0", "length"]
However String is not StringObject and thus instanceof would return false.

Why can I access object property with an array?

Can somebody explain the behaviour of the following code?
let obj = {a:1, b:2}
let i = ['a']
console.log(obj[i])
>> 1
Why is it that even an array can be used to access a property inside an object? As a side note this only works with an array of length 1. I have tried researching this but there's no documentation as far as I know that explains why this should work.
Property names are always strings or symbols.
If you pass something which isn't a string or symbol, it gets converted to a string.
The default toString() method on an array is roughly:
String.prototype.toString = function () { return this.join(","); }
So ['a'] gets converted to 'a'.
As a side note this only works with an array of length 1.
It works fine with arrays that are longer. You just need a matching value:
const o = {
"a,b": "Hello"
}
const a = ["a", "b"];
console.log("" + a);
console.log(o[a]);
And since any object can be converted to a string, and you can customise the toString method, you can do really weird things:
const data = {
"42": "Hello"
}
class Weird {
constructor(x) {
this.x = x;
}
toString() {
return this.x + 40;
}
}
const w = new Weird(2);
console.log(data[w]);
(Note that doing really weird things is usually a stupid idea that makes it hard to debug your own code two weeks later).
let obj = {a:1, b:2}
First you declare an object with 2 properties, a and b with values 1 and 2, respectively.
let i = ['a']
Then a variable i is declared, with it's value set to a string array with a single element, 'a'.
console.log(obj[i])
In this statement, the value of i is resolved to a string because the array only contains one element, as you mentioned. Due to this, 'a' is a valid property name for obj because all object properties are either strings, and if you pass something such as an array, it's converted to a string.
If you reference the variable icontaining the array, and it has multiple elements, it cannot resolve to a single property name without you being more explicit such as obj[i[0]]. If you only have one value, it'll resolves to that value.
obj['a'] is a valid property
obj['a,b'] is not.

Are array keys implicitly coerced to numbers?

I'm in the book, This & Object Prototypes, Chapter 3: Objects.
In the arrays section, the author says if you add a property to an array, but the array looks like a number, it will end up as part of the array index:
myArray["3"] = "baz";
console.log(myArray.length);
console.log(myArray[3]);
It looks like JavaScript is implicitly coercing the "3" string into the number 3, then it's saying place "baz" in index 3 of the array.
Actually, it's the other way around. All keys in JavaScript objects are strings, and arrays are objects. That means myArray[3] is the same as myArray["3"] because all keys, if not already strings, are coerced into strings because all JavaScript object keys are strings. Per MDN:
An object property name can be any valid JavaScript string, or anything that can be converted to a string, including the empty string.
And:
Please note that all keys in the square bracket notation are converted to String type, since objects in JavaScript can only have String type as key type. For example, in the above code, when the key obj is added to the myObj, JavaScript will call the obj.toString() method, and use this result string as the new key.
For example:
const obj = {
toString() {
return 'foobar';
}
};
const anotherObj = {};
anotherObj[obj] = 'baz';
//anotherObj.foobar is now 'baz'
Since obj is converted to a string implicitly, obj.toString is called which returns 'foobar', which is used as the key value. The same applies for arrays -- when using bracket notation. All keys are strings, and accessing using bracket notation coerces the expression in the brackets into a string.

JavaScript: Use Object As Array

Are there any pitfalls to code like this?
var Foo = function() {
this.bar = function() { return 'bar'; };
};
var f = new Foo();
f[0] = 'hi';
f[1] = 'there';
Note that I'm creating a new function object with some misc properties, and then I'm treating the object like an array. Also how are the array values being stored in the object? Are 0 and 1 treated like property names?
Well, yes, 0, and 1 will be just two property names.
When you assign a property with the bracket notation, the expression between the brackets will be converted to String, and that string will be used as the property name.
In fact, even the indexes for real arrays are just that, properties:
var realArray = ['a'];
realArray.hasOwnProperty('0'); // true
The difference is that real array objects on every property assignment that correspond to a valid index[1], track internally the value of their length property.
That's one of the reasons why "subclassing" array objects is difficult, even with the new ECMAScript 5 extensions, or at the moment also with the proposed ECMAScript-Harmony Proxies 2 3, can't be completely done, in a stanard way.
That can be a problem, depending on how you plan to iterate the numeric properties.
If you enumerate the properties with the for-in statement, other members will appear, not just the numeric properties.
[1] A valid array index is any unsigned 32-bit integer in the range of 0 to (2^32)-1.
I think you might have problems if you try to loop through that with a for in loop; the loop will also get bar. There are ways around this, so you just have to be careful.
If you want to extend Array behavious please use : Array.prototype.yourfunc = function()....

Categories