top of page

What is the Real Order of the Return Value of Object.keys()?

Our company’s App has a sharing function. When the user intends to share his/her personal dynamic, the app can automatically generate a picture, so that the user can share this picture on other social media.


This function has been normal before. But recently we’ve found that when certain types of feeds are shared on certain devices, the resulting images can get messy.


After some troubleshooting, we found that the problem occurred in a module that generates images. The author of this module misunderstood the order of the return values of Object.keys(), which led to some special cases where the order of the return values did not meet his expectations, which in turn led to subsequent code errors and finally generated wrong pictures.


In this article, I will share this story with you, and then discuss the order of the return values of Object.keys().


The Story

After the problem appeared, I started debugging. Since the cause of the problem is that the positions of different elements in the generated image are messy, I focus on the code related to drawing images. Then during debugging, I found this piece of code:

_drawWxmlBlock(item, sorted, all, progress, results)
{
    let self = this;
    let limitLeft = (results ? results.left : 0);
    let limitTop = (results ? results.top : 0);
    Object.keys(sorted).forEach((top,topIndex)=>{
        // from left to right
        let list = sorted[top].sort((a,b)=>{
            return(a.left-b.left);
        });

This is a piece of business-related code that you don’t need to fully understand. Here we focus on line 5: Object.keys(sorted).


In this module, sorted is a plain object, which roughly looks like this:

let sorted={
    '12.4': [],
    '23.1': [],
    '43.2': [],
    '32.2': [],
}

(By the way, I think the name of this variable is terrible.)


Its keys are strings that can be converted to a number, and its values are arrays.


Well, the above is the background. Now I ask a question: what do you think will be the result printed on the console after the following code is executed?

let sorted={}

sorted['12.4']=[];
sorted['23.1']=[];
sorted['43.2']=[];
sorted['32.2']=[];

console.log(Object.keys(sorted))
  • Option 1: The keys in the array will be arranged in ascending order, so the output result is: [ '12.4', '23.1', '32.2', '43.2']

  • Option 2: The keys in the array will be arranged in the order of creation, so the output result is: [ '12.4', '23.1', '43.2', '32.2' ]

Which is the correct answer?

You can think about your answer before reading on.

…

If we execute this code, the result will be:



From the results of running this code above, option 2 seems to be correct, it looks like the keys in the array will be in the order they were created.


If you think so, then you have made the same mistake as my colleague. Although the final execution result is consistent with the predicted result in option 2, this is actually just a coincidence. The truth of the matter is not that.


Let’s take a look at another piece of code, what do you think the result of its execution is?

let sorted={}

sorted['12.4']=[];
sorted['43.2']=[];
sorted['35']=[];
sorted['32.2']=[];
sorted['10']=[];

console.log(Object.keys(sorted))

When an integer is used as the key of an object, the results returned by Object.keys() are not in the order of creation. (To be precise, the key of the object should be a string like ‘10’, but for the convenience of our expression, I will treat it as an integer.)



Since keys of sorted are not integers on most people’s phones, the results returned by Object.keys() appear to be arranged in the order they were created, and the code just happens to work correctly. So this bug was not detected by the testing team during the testing phase.


When users are using the App, because the users’ mobile phones have too many types, in some users’ mobile phones, some keys of the sorted are just integers, so the result after the execution of Object.keys() is different from the original author’s expectation, which eventually led to a bug.


How Does Object.keys() work?

I shared a story earlier, so what is the real order of the return values of Object.keys()?

If we check MDN documentation, it writes:

The Object.keys() method returns an array of a given object's own enumerable property names, iterated in the same order that a normal loop would.

MDN doesn’t directly state the order of the array, it just says: iterated in the same order that a normal loop would.


So what is the normal loop order?


If the MDN document does not clarify a concept, then we can only check it through the ECMAScript Specification.


The specification defines Object.keys() like this:



The nameList was get from EnumerableOwnPropertyNames , so we check it again:



Then we found that ownKeys are obtained through O.[[OwnPropertyKeys]]. So we continue to investigate O.[[OwnPropertyKeys]] .



Then we need to continue to check OrdinaryOwnPropertyKeys() :



At this point, we have the answer we want. The above specification details the order of the return values of OrdinaryOwnPropertyKeys().


Here’s a summary of what’s in the specification:

  • Create an empty list to store keys

  • Store all valid array indices in ascending order

  • Store all string type indexes in ascending order by property creation time

  • Store all Symbol type indexes in ascending order by property creation time

  • return keys

We can also get a conclusion: the indices of positive integers always come before the indices of strings, and in ascending order.


This is another example:

const testObj = {}

testObj[-1] = ''
testObj[1] = ''
testObj[1.1] = ''
testObj['2'] = ''
testObj['c'] = ''
testObj['b'] = ''
testObj['a'] = ''
testObj[Symbol(1)] = ''
testObj[Symbol('a')] = ''
testObj[Symbol('b')] = ''
testObj['d'] = ''

console.log(Object.keys(testObj))


Then result:


Seeing this result, you may still have a question: where are the Symbol keys? Because the specification of EnumerableOwnPropertyNames stipulates that the return value should only contain string properties, Symbols are ignored.


V8 Engine

We can see that in the ECMA specification, a distinction is made between keys of positive integers and other keys. In the V8 engine, there is a similar action.


When the V8 engine stores the properties of an object, properties are not simply stored by the key-value structure. In order to improve the access efficiency of object properties, the V8 engine divides properties into two types:

  • Array-indexed properties, more commonly known as elements

  • properties in general


They look like this:



Conclusion

Well, we started with a bug and shared an interesting story about my company.

Then we explored the real order of the return value of Object.keys() by reading ECMAScript Specification, hope this article is useful to yo




Source: Medium - bytefish


The Tech Platform

0 comments
bottom of page