Questions about deep comparison of objects have been asked, and I have the solution. But there is a line in the solution that I don't completely understand.
This is the solution, it is a question in Ch 4 of Eloquent JS to compare objects. I get all of it except the line:
if (!(prop in a) || !deepEqual(a[prop], b[prop]))
It is found toward the bottom. Here is full function:
function deepEqual(a, b) {
if (a === b) return true;
if (a == null || typeof a != "object" ||
b == null || typeof b != "object")
return false;
var propsInA = 0, propsInB = 0;
for (var prop in a)
propsInA += 1;
for (var prop in b) {
propsInB += 1;
if (!(prop in a) || !deepEqual(a[prop], b[prop]))
return false;
}
return propsInA == propsInB;
}
Is if (!(prop in a) comparing the existence of a property in 'b' at that index, or is it comparing values?
Really the same q. about the second half, but I know it's a different answer: what type of comparison is !deepEqual(a[prop], b[prop]) making (e.g., true or false)? I understand the recursion, but as in my previous question, is this making an 'it exists' comparison, or a comparison of the information in the values?
Thank you in advance.
According to MDN:
The in operator returns true if the specified property is in the specified object.
Also:
The for...in statement iterates over the enumerable properties of an object, in arbitrary order. For each distinct property, statements can be executed.
So to answer your first question, prop in a is checking whether prop, a field from object b, exists in object a.
To answer your second question, deepEqual(a[prop], b[prop]) is checking whether the object a[prop] and b[prop] are equal including all its children and contents.
The in operator returns true if the object on the right of the expression contains a key with the value of the string on the left of the expression.
Eg: prop in a is true if a contains a key with the same name as the string value of prop. Eg:
var prop = "age";
var obj1 = {
name: "Dave",
age: 21,
record: { ... }
};
var obj2 = {
name: "John",
record: { ... }
};
console.log(prop in obj1); // true
console.log(prop in obj2); // false
If prop was set to "record" then deepEqual(a[prop], b[prop]) recursively compares the values in a.record and b.record.
It's checking for existence. It says: "For each property in B, examine A. If A doesn't have the property, or if the value of the property on A doesn't deeply equal the value of the property on B, return false".
An alternate implementation would avoid the existence check on that line, and instead use if (typeof A == 'undefined') at the top of the function to validate the input parameters at the beginning of each round of recursion...at a glance, I think that'd be equivalent, but less efficient. The current implementation avoids the invocation on the undefined property.
Related
This question already has answers here:
How do I test for an empty JavaScript object?
(48 answers)
Closed 7 months ago.
I'm learing JavaScript. I cannot grasp the idea of an empty object. As I understand, there are situations when I need to check a variable whether it holds an object and has a value.
So far, I know that a variable can be undefined.
var car; // the value is undefined, as well as the type (which is it? number, array, etc.)
I also know, that everything that has a value, is true:
var car = "Opel";
Boolean(car); // true
And anything without a value is false:
var car = ""; // typeof string, but it is empty, so
Boolean(car); // false
Boolean(null); // false
So why doesn't it work the same way with arrays and objects? Why is all of this true? Shouldn't empty arrays and objects return false?
var car = {make: "Opel", year: 2015};
Boolean(car); // true
car = {};
Boolean(car); // true
car = ["BMW", "Opel"];
Boolean(car); // true
car = [];
Boolean(car); // true
Now I see that there are methods, that can be applied to check an object's length, I just haven't reached that part yet.
I'm learning at W3Schools website and this bit just got me puzzled:
But you cannot test if an object is null, because this will throw an error if the object is undefined:
Incorrect:
if (myObj === null)
To solve this problem, you must test if an object is not null, and not undefined.
But this can still throw an error:
Incorrect:
if (myObj !== null && typeof myObj !== "undefined")
Because of this, you must test for not undefined before you can test for not null:
Correct:
if (typeof myObj !== "undefined" && myObj !== null)
I still cannot understand the last line here.
Checking if an object is empty:
Object.keys(yourObject).length
var car = {};
var isEmpty = Object.entries(car).length > 0; //false
var car = {make: "Opel", year: 2015};
var isEmpty = Object.entries(car).length > 0; //true
This should solve your problem, if your curious about the utility function Object.entries you can look on mdn
I also know, that everything that has a value, is true
I wouldn't say that. All the examples given are values, even undefined and null are predefined values in JavaScript.
Shouldn't empty arrays and objects return false?
It is like that by specification. And that is really the answer to your question. It is a choice made by the designers of the language.
You could find some logic in it though. In JavaScript, values representing objects (and so also arrays) are references. These references are memory addresses/pointers, and although you cannot find out what exactly those addresses are, these are non-zero, even when an object has no properties.
Note that your example objects ({} and []) still have some non-enumerable properties, such a __proto__ and (for array) length, so they are not really as empty as you think.
The most empty object you can create, is: Object.create(null)
You can check wheather a object is empty or not in this simple way.
function objectLength( object ) {
return Object.keys(object).length;
}
if(objectLength(yourObject) == 0){
//object is empty
}else{
//object not empty
}
Checking if an object is empty:
Reflect.ownKeys(car).length
Returns an array with one element when a Symbol is used as the key:
let key = Symbol('vin')
let car = { [key]: 'honda' }
Reflect.ownKeys(car).length // => 1
Whereas Object.keys returns an array with zero elements in this case:
let key = Symbol('vin')
let car = { [key]: 'honda' }
Object.keys(car).length // => 0
I would always use typeof myvar !== 'undefined' and checks for content myvar !== ''
This would always would have a clear result.
There are concepts which are Truthy and Falsy values in JavaScript.
I highly recommend you to read MDN documents about this issue.
All values are truthy unless they are defined as falsy (i.e., except for false, 0, -0, 0n, "", null, undefined, and NaN).
Truthy values
Falsy values
No, they should not be false.
All objects are 'truthy', in that they return true when evaluated as a Boolean. In Javascript, all arrays are objects (try console.log(typeof [])), so they also return true, regardless of whether or not they are empty.
To check if any array is empty:
if (MYARRAY.length === 0) {
// console.log(true);
}
To check if an object is empty:
if (Object.keys(MYOBJECT).length === 0) {
// console.log(true);
}
You can use
if ($('#element').is(':empty')){
//do something
}
OR
function isEmpty( el ){
return !$.trim(el.html())
}
if (isEmpty($('#element'))) {
// do something
}
Here are some examples: http://api.jquery.com/is/
But if you need it in JavaScript:
if( typeof foo !== 'undefined' ) {
// foo could get resolved and it's defined
}
You can simply use typeof
I cannot get my for in loop to keep working after the first property in my object. This is a question from Eloquent JavaScript in Chapter 4:
Write a function, deepEqual, that takes two values and returns true
only if they are the same value or are objects with the same
properties whose values are also equal when compared with a recursive
call to deepEqual.
To find out whether to compare two things by identity (use the ===
operator for that) or by looking at their properties, you can use the
typeof operator. If it produces "object" for both values, you should
do a deep comparison. But you have to take one silly exception into
account: by a historical accident, typeof null also produces "object".
Here is my code:
function deepEqual(obj1, obj2) {
if ((typeof obj1 === 'object' && obj1 != null) && (typeof obj2 === 'object' && obj2 != null)) {
for (var property in obj1) {
if (property in obj2) {
return deepEqual(obj1[property], obj2[property])
} else {
return false;
}
}
} else if (obj1 !== obj2) {
return false;
} else {
return true;
}
}
var obj = {object: 3, here: 1};
var obj2 = {object: 3, here: 2};
console.log(deepEqual(obj, obj2));
The console returns true, when it should say false because the 'here' properties are not equal. When looking into the output, it's because the 'for in loop' in the function quits after the first property. Please help me as to why it's not continuing to loop.
your for loop can't move beyond the first property because you return out of the function when you call deepEqual
for (var property in obj1) {
if (property in obj2) {
// returning means no more looping....
return deepEqual(obj1[property], obj2[property])
}
you want to carry on looping if deepEqual returns that its equal, or return if its false.
I'm trying to teach myself some JavaScript via Eloquent Javascript.
I'm on chapter 4 practice 4.4 Deep comparison.
I've written my own code, passed all the tests given, even some tests I've made myself, but when I checked the solution, it was something totally different than what I had.
Could anyone tell me whether my code produces the same result as the solution, or some ideas of how to figure out whether if my code is right even when mine is something totally different than what the solution is, not just for this practice problem but also in future problems?
I'm just afraid of having a wrong idea and not realizing since my code works.
The question is:
Write a function, deepEqual, that takes two values and returns true only if they are the same value or are objects with the same properties whose values are also equal when compared with a recursive call to deepEqual.
To find out whether to compare two things by identity (use the === operator for that) or by looking at their properties, you can use the typeof operator. If it produces "object" for both values, you should do a deep comparison. But you have to take one silly exception into account: by a historical accident, typeof null also produces "object".
My code is:
function deepEqual(val1, val2) {
if(typeof val1 !== typeof val2) return false;
else if(typeof val1 !== "object") return val1 === val2;
for(var event in val1){
return deepEqual(val1[event], val2[event]);
}
}
and the solution is:
function deepEqual2(a, b) {
if (a === b) return true;
if (a == null || typeof a != "object" ||
b == null || typeof b != "object")
return false;
var propsInA = 0, propsInB = 0;
for (var prop in a)
propsInA += 1;
for (var prop in b) {
propsInB += 1;
if (!(prop in a) || !deepEqual(a[prop], b[prop]))
return false;
}
return propsInA == propsInB;
}
Thanks in advance!
There are a couple of problems with your code:
You only test the first property in an object, because you have a return in your for-in loop. So when comparing objects, all your code does is check that first property.
If you kept a flag and returned the result after the loop, it would still have the issue that it only checks the properties in a; what if b has a property a doesn't have?
Is there a way to check if an object is "array-like", like for these types of objects:
Arrays (duh)
Typed Arrays (Uint8Array, etc), these will return false when Array.isArray is used
arguments object
NodeLists*
There are a few other ones that I can't think of off-hand
I suppose you could check for the presence of a .length property, but non-array-like objects can contain the .length property. I guess the thing these all share in common is the array accessor.
As best I've found in my research on this topic, you have only a couple choices:
You can look only at the .length property and accept any object that seems to have an appropriate .length property that isn't any other things you know you should eliminate (like a function).
You can check for specific array-like objects (HTMLCollection, nodeList) and bias in favor of them.
Here are two options for the first method - one that doesn't accept a zero length and one that does (these incorporate suggestions by gilly3 and things we see in jQuery's similar function):
// see if it looks and smells like an iterable object, but don't accept length === 0
function isArrayLike(item) {
return (
Array.isArray(item) ||
(!!item &&
typeof item === "object" &&
item.hasOwnProperty("length") &&
typeof item.length === "number" &&
item.length > 0 &&
(item.length - 1) in item
)
);
}
This, of course, reports false for items with .length === 0, If you want to allow .length === 0, then the logic can be made to include that case too.
// see if it looks and smells like an iterable object, and do accept length === 0
function isArrayLike(item) {
return (
Array.isArray(item) ||
(!!item &&
typeof item === "object" &&
typeof (item.length) === "number" &&
(item.length === 0 ||
(item.length > 0 &&
(item.length - 1) in item)
)
)
);
}
Some test cases: http://jsfiddle.net/jfriend00/3brjc/
2) After checking to see that it's not an actual array, you can code to check for specific kinds of array-like objects (e.g. nodeList, HTMLCollection).
For example, here's a method I use when I want to make sure I include nodeList and HTMLCollection array-like objects:
// assumes Array.isArray or a polyfill is available
function canAccessAsArray(item) {
if (Array.isArray(item)) {
return true;
}
// modern browser such as IE9 / firefox / chrome etc.
var result = Object.prototype.toString.call(item);
if (result === "[object HTMLCollection]" || result === "[object NodeList]") {
return true;
}
//ie 6/7/8
if (typeof item !== "object" || !item.hasOwnProperty("length") || item.length < 0) {
return false;
}
// a false positive on an empty pseudo-array is OK because there won't be anything
// to iterate so we allow anything with .length === 0 to pass the test
if (item.length === 0) {
return true;
} else if (item[0] && item[0].nodeType) {
return true;
}
return false;
}
You can check if the object is iterable :
function isIterable(o){
return (o!=null && typeof(o[Symbol.iterator])==='function');
}
Careful though, returns true for strings. If that's a problem, exclude them :
function isIterable(o){
return (o!=null && typeof(o[Symbol.iterator])==='function' && typeof(o)!=='string');
}
Then either access the elements using the iterator or if you want to use the regular array[0] way, just add a check for length property.
Complete isArrayLike function :
function isArrayLike(a){
return (
a!=null &&
typeof(a[Symbol.iterator])==='function' &&
typeof(a.length)==='number' &&
typeof(a)!=='string'
);
}
In JavaScript an array-like object is an object containing a length property typically of type number with a non-negative whole value no greater than 2^53-1. Array-like objects with lengths greater than zero also typically contain additional properties beginning at [0] and going up to [n-1] where n = the value of length.
From MDN:
The term array-like object refers to any object that doesn't throw during the length conversion process described [below]. In practice, such object is expected to actually have a length property and to have indexed elements in the range 0 to length - 1. (If it doesn't have all indices, it will be functionally equivalent to a sparse array.)
Normalization of the length property
The length property is converted to an integer and then clamped to the range between 0 and 2^53 - 1.
Array-like objects also differ from other objects in that Array methods such as Array.prototype.push can be called indirectly on them using Function.prototype.call().
Also from MDN:
Array methods cannot be called directly on array-like objects. But you
can call them indirectly using Function.prototype.call().
Calling Array functions on an object without a normalized length property changes the value of length. Therefore I thought it best that my function return false if the length property is not already an integer between 0 and 2^53 - 1. The length property cannot be a BigInt as that would throw a TypeError.
This function also checks for existence of the last element to help rule out objects that have a length property but aren't intended to be array-like.
Tested and works in old browsers (ES3 and up).
function isArrayLike(obj) {
var i;
return /( Arguments|NodeList|Collection|Array)\b/.test(({}).toString.call(obj))// Always array-like
// Confirm obj is an object but not a function or null
|| !!obj && typeof obj=="object"
// Confirm that Array methods can be called indirectly on obj
// without throwing or changing the value of obj.length
// by confirming obj.length (i) is a normalized integer
// Usually this means i is in the range 0 - 9007199254740991
&& [].push.call({length:Number(i=obj.length)})===i
// And when non-zero confirm i>0 and i-1 is a property in obj
&& (!i || i>0 && i-1 in obj);
}
Well, it depends what you mean by array-like. Probably something you could iterate with a for loop like this:
for (var i=0, l=obj.length; i<l; ++i) {
var item = obj[i];
}
So then the test is simple:
function isArrayLike(obj) {
if (!obj) return false;
var l = obj.length;
if (typeof l != 'number' || l < 0) return false;
if (Math.floor(l) != l) return false;
// fast check
if (l>0 && !((l-1) in obj)) return false;
// more complete check (optional)
for (var i=0; i<l; ++i) {
if (!(i in obj)) return false;
}
return true;
}
Of course, this won't catch arrays which are sparsely populated, but then again, are they really being used as arrays? NodeLists and the like won't be sparsely populated.
Enjoy!
There is a way to check if an object is array-like or not, even if there are no elements in it, using this function here:
isArrayLike = function (_) {
_[0] = 0; return [].slice.call(_).length >= Object.values(_).length;
}
This uses a little hack I accidentally discovered that allows you to determine if an object is (1) an array, (2) array-like, or (3) object / object-like.
The only downside is that it does not work correctly for array-likes that have object-like properties added, such as arguments
Technically, (pretty much) every object is "array-like" (because of type coercion of undefined) according to the standard (ECMAScript 2015 Language Specification §7.3.17, CreateListFromArrayLike (obj [, elementTypes] )):
7.3.17 CreateListFromArrayLike (obj [, elementTypes] )
The abstract operation CreateListFromArrayLike is used to create a List value whose elements are provided by the indexed properties of an array-like object, obj. The optional argument elementTypes is a List containing the names of ECMAScript Language Types that are allowed for element values of the List that is created. This abstract operation performs the following steps:
ReturnIfAbrupt(obj).
If elementTypes was not passed, let elementTypes be (Undefined, Null, Boolean, String, Symbol, Number, Object).
If Type(obj)
is not Object, throw a TypeError exception.
Let len be ToLength(Get(obj, "length")).
ReturnIfAbrupt(len).
Let list be an empty List.
Let index be 0.
Repeat while index < len
Let indexName be ToString(index).
Let next be Get(obj, indexName).
ReturnIfAbrupt(next).
If Type(next)
is not an element of elementTypes, throw a TypeError exception.
Append next as the last element of list.
Set index to index + 1.
Return list.
Generated via https://www.browserling.com/tools/html-to-markdown
An Array is a value that has following properties:
Its of type object
It has length property which is equal or greater than 0.
All the value based keys are numeric and is greater than or equal to 0.
Exceptions:
length
Value is a function.
function isArrayLike(value) {
if (typeof value === "object" && !!value) {
const isArray = Array.isArray(value);
const allNumericKeys = Object.keys(value).every((k) =>
(!isNaN(k) && +k >= 0) ||
k === "length" ||
typeof value[k] === "function"
)
const hasLength = value.length > 0
return isArray || (allNumericKeys && hasLength)
}
return false
}
console.log('Array: ', isArrayLike([]))
console.log('Array Like: ', isArrayLike({1: 'a', 2: 'b', length: 3}))
console.log('Array Like with function: ', isArrayLike({1: 'a', 2: 'b', length: 3, foo: () => {} }))
console.log('Array Like with negative keys: ', isArrayLike({ "-1": 'a', "-2": 'b', length: 1}))
console.log('Array Like without length:', isArrayLike({1: 'a', 2: 'b' }))
console.log('Node List: ', isArrayLike(document.querySelectorAll('p')))
console.log('null: ', isArrayLike(null))
console.log('String: ', isArrayLike('Test'))
console.log('Number: ', isArrayLike(123))
<div>
<p></p>
<p></p>
<p></p>
<p></p>
</div>
I say nothing beats the simplicity and expressiveness of extending native objects:
Object.prototype.isArrayLike = function(){ return false; };
Array.prototype.isArrayLike = function(){ return true; };
NodeList.prototype.isArrayLike = function(){ return true; };
HTMLCollection.prototype.isArrayLike = function(){ return true; };
This approach could cause conflicts between frameworks, however, I recommend keeping your distance from a framework whose isArrayLike function does not what the name suggests.
I need a function:
function isSame(a, b){
}
In which, if a and b are the same, it returns true.
, I tried return a === b, but I found that [] === [] will return false.
Some results that I expect this function can gave:
isSame(3.14, 3.14); // true
isSame("hello", "hello"); // true
isSame([], []); // true
isSame([1, 2], [1, 2]); // true
isSame({ a : 1, b : 2}, {a : 1, b : 2}); //true
isSame([1, {a:1}], [1, {a:1}]); //true
You could embed Underscore.js and use _.isEqual(obj1, obj2).
The function works for arbitrary objects and uses whatever is the most efficient way to test the given objects for equality.
the best way to do that is to use a JSON serializer. serialize both to string and compare the string.
There are some examples, adapted from scheme, on Crockford's site. Specifically, check out:
function isEqual(s1, s2) {
return isAtom(s1) && isAtom(s2) ? isEqan(s1, s2) :
isAtom(s1) || isAtom(s2) ? false :
isEqlist(s1, s2);
}
It can all be found here:
http://javascript.crockford.com/little.js
Here is a working example:
http://jsfiddle.net/FhGpd/
Update:
Just wrote some test cases based on the OP. Turns out I needed to modify the sub1 function to check <= 0 not === 0 otherwise isEqual(3.14, 3.14) blew the stack. Also, isEqual does not work for object comparison, so you are on your own there. However, if you follow the examples on Crockford's site you will see how easy and fun it is to write recursive methods that could be used to check for object equality.
Here is something that can work:
function isSame(obj1, obj2, prefer){
// Optional parameter prefer allows to only check for object keys and not both keys and values of an object
var obj_prefer = prefer || "both";
function checkArray(arr1, arr2){
for(var i = 0, j = obj1.length; i<j; i++){
if(obj1[i] !== obj2[i]){return false;}
}
return true;
}
function checkValues(obj_1, obj_2){
for(var prop in obj_1){
if(typeof obj_1[prop] === "function"){ // converting functions to string so that they can be matched
obj_1[prop] = String(obj_1[prop]);
obj_2[prop] = String(obj_2[prop]);
}
if(obj_1[prop] !== obj_2[prop]){ return false;}
}
return true;
}
// The built in === will check everything except when typeof object is "object"
if ( typeof obj1 === "object"){
// typeof Array is object so this is an alternative
if((typeof obj1.push === "function") && (!obj1.hasOwnProperty('push'))){
return checkArray(obj1, obj2);
}
else{
if( obj_prefer !== "keys"){ // do not check for values if obj_prefer is "keys"
return checkValues(obj1, obj2);
}
var keys_1 = Object.keys(obj1);
var keys_2 = Object.keys(obj2);
if(!checkArray(keys_1, keys_2)){return false;}
return true;
}
}
// I thought undefined === undefined will give false but it isn't so you can remove it
if( typeof obj1 === "undefined" && typeof obj2 === "undefined" ){return true}
if(typeof obj1 === "function"){
return String(obj1) === String(obj2);
}
return obj1 === obj2;
}
console.log(isSame(2, 2)); //true
console.log(isSame([1], [1])); // true
Since it converts Functions into Strings to compare them, check out for spaces as that can break things:
var func1 = function(){},
func2 = function(){ }; // function with extra space
isSame(func1, func2); // false
You can check out http://jsfiddle.net/webholik/dwaLN/4/ to try it yourself.
If anyone reading this answer is using Angular.js, you can use angular.equals(obj1,obj2);
According to the docs:
Determines if two objects or two values are equivalent. Supports value
types, regular expressions, arrays and objects.
Two objects or values are considered equivalent if at least one of the
following is true:
Both objects or values pass === comparison.
Both objects or values are of the same type and all of their properties are equal by comparing them with angular.equals.
Both values are NaN. (In JavaScript, NaN == NaN => false. But we consider two NaN as equal).
Both values represent the same regular expression (In JavaScript, /abc/ == /abc/ => false. But we consider two regular expressions as
equal when their textual representation matches).
During a property comparison, properties of function type and properties with names that begin with $ are ignored.
Scope and DOMWindow objects are being compared only by identify (===).