jseq
v0.2.0
Published
test suite for testing shallow & deep, strict equality as provided by various libraries
Downloads
3
Readme
On Equality (in JavaScript)
Table of Contents generated with DocToc
- Breaking News
- Deep Equality acc to
deep-equal-ident
- jsEq
- Language Choice and Motivation
- Test Module Setup
- Implementations Module Setup
- Equality, Identity, and Equivalence
- First Axiom: Value Equality Entails Type Equality
- Equality of Sub-Types
- Equality of Numerical Values in Python
- Second Axiom: Equality of Program Behavior
- Infinity, Positive and Negative Zero
- Not-A-Number
- Object Property Ordering
- Properties on 'Non-Objects'
- Primitive Values vs Objects
- Undefined Properties
- Functions (and Regular Expressions)
- How Many Methods for Equality Testing?
- Plus and Minus Points
- Benchmarks
- Libraries Tested
- Caveats and Rants
Breaking News
Should Key Ordering Matter in Objects?
- key ordering used to be an accidental property of JS objects
- constance of key ordering (key ordering by definition order) always used to be silently implemented by most popular engines because reasons
- now it's in ES XXX so it's official
- question is, should
x = { a: 1, b: 1, }
,y = { b: 1, a: 1, }
be equal or not equal? - conventionally key ordering is considered irrelevant for equality, so x eq y should hold
- but sometimes key ordering is used, so x eq y should fail
- propose to agree upon setting a symbol (
Symbol.x_orderedkeys
) to signal objects that should be keeping key ordering (tested asx[Symbol.x_orderedkeys]?
) - deriving from specific class (
class OrderedKeysObject extends Object
) possible but better not done as it would conflict with other more important uses of custom classes - what to do if
x
has that property buty
lacks it?- always reject equality because '
x
has something thaty
is lacking', or else - assume that
x_orderedkeys
is 'contagious' but not 'dominant', i.e. presence in any one value mandates checking for ordered keys
- always reject equality because '
- should also be possible to configure test whether to check for this, or use special method
(
eq_with_ordered_keys()
)
ECMAScript6 Classes and Type Checking
UPDATE this topic is dealt with in more detail in InterType.
Whenever one thinks one has tamed the utter madness that is JavaScript's type system, one can be reasonably sure another one of the Hydra's ugly heads is waiting right behind the corner. This happens with ECMAScript6 Classes.
Let us go on a Journey in Five Parts where I'd like to define a class that extends JS Array
; I then
instantiate it and poke at it with all the sonic screwdrivers I have. This looks good until I use either
typeof
or the old trusty (but, by now, a bit rusty) Miller Device to ascertain the class name of that
thing:
# Preface. Packing for the Journey.
# ---------------------------------
types = new ( require 'intertype' ).Intertype() # https://github.com/loveencounterflow/intertype
class Myclass extends Array
# Chapter I. Embarking on the Boat.
# ---------------------------------
d = new Myclass() # in REPL, correctly echoes `Myclass(0) []`, `0` being array length
# Chapter II. No Problems (So Far.)
# ---------------------------------
Array.isArray d # `true`, no problem
d instanceof Array # `true`, no problem
d instanceof Myclass # `true`, no problem
types.isa.list d # `true`, no problem
# Chapter III. OMG It's the Titanic
# ---------------------------------
typeof d # 'object'; NB that `( typeof [] ) == 'object'`
types.type_of d # 'list' (our name for JS `Array` instances)
Object::toString.call d # 'Miller Device', gives '[object Array]'
# Chapter IV. One Single Raft Left.
# ---------------------------------
d.constructor.name # 'Myclass'! Yay!
Turns out only d.constructor.name
does the trick—let's call it the Dominic Denicola
Device since he wrote the top-rated SO
answer to this pressing question back in 2015.
So let's try and see what the DDDevice can do for us.
In essence, we just need to set up a function ddd = ( x ) -> x.constructor.name
; the only problem with
that is of course that checking attributes on null
and undefined
will fail loudly (as if JS ever cared
but whatever), so we have to safeguard against that; these two definitions are equivalent:
ddd = ( x ) -> if x? then x.constructor.name else ( if x is null then 'null' else 'undefined' )
ddd = ( x ) -> x?.constructor.name ? ( if x is null then 'null' else 'undefined' )
Our ddd()
method does give reasonable answers (for a JS type detecting method):
ddd {} # 'Object'
ddd [] # 'Array'
ddd null # 'null'
ddd true # 'Boolean'
ddd 42 # 'Number'
ddd NaN # 'Number'
ddd Infinity # 'Number'
ddd ( new Myclass() ) # 'Myclass'
Segue on The Miller Device
A code comment from 2010 (CND Types module):
It is outright incredible, some would think frightening, how much manpower has gone into reliable JavaScript type checking. Here is the latest and greatest for a language that can claim to be second to none when it comes to things that should be easy but aren’t: the ‘Miller Device’ by Mark Miller of Google (http://www.caplet.com), popularized by James Crockford of Yahoo!.*
As per https://groups.google.com/d/msg/nodejs/P_RzSyPkjkI/NvP28SXvf24J, now also called the 'Flanagan Device'
- http://ajaxian.com/archives/isarray-why-is-it-so-bloody-hard-to-get-right
- http://blog.360.yahoo.com/blog-TBPekxc1dLNy5DOloPfzVvFIVOWMB0li?p=916 # page gone
- http://zaa.ch/past/2009/1/31/the_miller_device_on_null_and_other_lowly_unvalues/ # moved to:
- http://zaa.ch/post/918977126/the-miller-device-on-null-and-other-lowly-unvalues
Deep Equality acc to deep-equal-ident
GitHub user fkling
has recently published
deep-equal-ident, where he wants to "track[...] the identity
of nested objects". That may sound a bit cryptic at first, but should become clear when considering two
example test cases:
# #1
a = [ 1, 2, 3, ]
b = [ 1, 2, 3, ]
foo = [ a, a, ]
bar = [ b, b, ]
deepEqualIdent foo, bar # true
# #2
a = [ 1, 2, 3, ]
b = [ 1, 2, 3, ]
foo = [ a, a, ]
bar = [ a, b, ]
deepEqualIdent foo, bar # false
The gist of the comparison policy of deep-equal-ident
is this: for two objects foo
, bar
to be deeply
equal, we should expect that they contain deeply equal values at the same indices. now, if we inspect foo
and bar
, we find that both objects in both cases do have x[ 0 ][ 0 ] == 1
, x[ 0 ][ 1 ] == 2
, x[ 1 ][
0 ] == 1
, and so on, so at a glance, foo
and bar
should be considered deeply equal.
However, we've all learned in school that when you have an equation like x == y * 4
, that equation must
still hold when transformed in a way that does the same to the left and the right hand term; for example,
you might want to divide both sides by four, which gives you
x y * 4
x = y * 4 => ——— == ——————— => x / 4 == y
4 4
So far so good. Now consider what happens in test case #2 when we manipulate a
or b
. We start out with
assuming foo == bar
. Then we perform the same operation on the left and the right hand side, say
| foo[ 1 ][ 0 ] += 1
foo == bar => | => ?
| bar[ 1 ][ 0 ] += 1
Does equality still hold or has it been violated? Indeed, the latter is the case: when we say foo[ 1 ][ 0 ]
+= 1
, we're really doing a[ 0 ] += 1
, since foo[ 1 ] is a
(foo[ 1 ]
is identical to a
). But a
is
also referenced as the first element of foo
, so foo[ 0 ]
changes along with foo[ 1 ]
, which means
foo
is now [ [ 2, 2, 3, ], [ 2, 2, 3, ], ]
.
OK great, what with bar
? Doing bar[ 1 ][ 0 ] += 1
gives [ 2, 2, 3, ]
alright, but in this
case, bar[ 0 ]
remains unaffected—meaning when we print out foo
and bar
, what we see is
[ [ 2, 2, 3, ], [ 2, 2, 3, ], ] # foo
[ [ 1, 2, 3, ], [ 2, 2, 3, ], ] # bar
which, obviously, does not describe two deeply equal objects.
jsEq
There are a couple of related, recurrent and, well, relatively 'deep' problems that vex many people who program in JavaScript on a daily basis, and those are sane (deep) equality testing, sane deep copying, and sane type checking.
jsEq attempts to answer the first of these questions—how to do sane testing for deep equality in JavaScript
(specifically in NodeJS)—by providing an easy to use test bed that compares a number of libraries that
purport to deliver solutions for deep equality. It turns out that there are surprising differences in detail
between the libraries tested, as the screen shot below readily shows (don't take the qunitjs
test
seriously, those are currently broken due to the (to me at least) strange API of that library).
Here is a sample output of jsEq running node jseq/lib/main.js
:
Implementations whose key starts with a
*
are 'hobbyists solutions' that i have gleaned from blogs and answers on StackOverflow.com; they're included mainly to show how well one can expect an ad-hoc solution can be expected to work (quite well in some cases, it turns out). See the list of tested libraries and the caveats section.
The lodash
and underscore
results are probably identical because lodash
strives to be a 'better
underscore
'.
At first it may be hard to see what
ne +0, -0
could be useful for as in JavaScript,+0 == -0
holds, but see below for Infinity, Positive and Negative Zero
The jkroso equals
and CoffeeNode Bits'N'Pieces
results are identical since the former is really the
implementation of the latter; based on the results shown i'll try to devise a solution that combines
different libraries and passes all tests.
It has to be said that while—as it stands—jsEq will run no less than 12 * 212 == 2544
tests, most tests
are between primitive values, which explains why bot JS ==
and ===
turn in with around 9 out of 10 tests
passed.
Language Choice and Motivation
The present module has been implemented in CoffeeScript, my favorite language
these days. Most of the examples in the present ReadMe are in CoffeeScript, too, so whenever you see
a construct like f x, y
, you'll have to
mentally translate that into f( x, y )
. What's more, CoffeeScript's ==
operator translates to
JavaScript's ===
, while JS ==
has (rightly) no equivalent in CS. I agree that this can be
confusing, especially in a text like this where different concepts of equality play a crucial role. I
strive for clarity in this point by making sure that whenever an isolated ==
or ===
appears, it is
annotated from which language it has been taken.
I for one prefer to use
:
for assignment (as it is already done inside object literals) and=
for equality testing, which is one of the reasons i started Arabika, an as yet incipient and experimental language where i try out parametrized, modular grammars (so that if you like the language but can't live with my particular choice for the equals operator, you can instantiate your own grammar with your own choice for that one).
Incidentally, i'm writing lots of tests for Arabika, and one day i was struck
by a false positive when a parsing result à la [ 3 ]
passed the comparison to [ '3' ]
.
Research quickly showed NodeJS' assert.deepEqual
to be the culprit, so i chimed in to the
discussion on bug #7161. I felt i was not completely alone
in my quest for sound equality testing in JavaScript, and the subject being too complex to grasp with
haphazard, isolated ad-hoc tests issued via the NodeJS REPL, i came up with jsEq: it is not a new
implementation of JS (deep) equality, but rather an extensible framework to test available software that
purports to have an answer to the vexing problem whether two given values are or are not equal.
Test Module Setup
Test cases are set up in the src/implementations.coffee
modules, inside a function that accepts two
functions eq
, ne
and returns an object of test cases. Test case names are short descriptions of what
they test for and are used to produce a legible output.
Tests that run a single test should return the result of applying the provided eq
or ne
to test for the
current implementation's concept of equality with eq
or inequality with ne
aginst a pair of values.
Tests that run multiple subtests should return a pair [ n, errors, ]
where n
is the subtest count and
errors
is a list with a meaningful message for each failed subtest (the individual messages from such
'mass testing facilities' are currently not shown, as they produce a lot of output, but that will probably
be made a matter of configuration). Here's what src/implementations.coffee
currently looks like:
#-----------------------------------------------------------------------------------------------------------
module.exports = ( eq, ne ) ->
R = {}
### 1. simple tests ###
#---------------------------------------------------------------------------------------------------------
### 1.1. positive ###
R[ "NaN equals NaN" ] = -> eq NaN, NaN
R[ "finite integer n equals n" ] = -> eq 1234, 1234
R[ "emtpy array equals empty array" ] = -> eq [], []
R[ "emtpy object equals empty object" ] = -> eq {}, {}
#---------------------------------------------------------------------------------------------------------
### 1.2. negative ###
R[ "object doesn't equal array" ] = -> ne {}, []
R[ "object in a list doesn't equal array in array" ] = -> ne [{}], [[]]
R[ "integer n doesn't equal rpr n" ] = -> ne 1234, '1234'
R[ "empty array doesn't equal false" ] = -> ne [], false
R[ "array with an integer doesnt equal one with rpr n" ] = -> ne [ 3 ], [ '3' ]
#=========================================================================================================
### 2. complex tests ###
#---------------------------------------------------------------------------------------------------------
R[ "circular arrays with same layout and same values are equal (1)" ] = ->
d = [ 1, 2, 3, ]; d.push d
e = [ 1, 2, 3, ]; e.push d
return eq d, e
#---------------------------------------------------------------------------------------------------------
R[ "circular arrays with same layout and same values are equal (2)" ] = ->
d = [ 1, 2, 3, ]; d.push d
e = [ 1, 2, 3, ]; e.push e
return eq d, e
#---------------------------------------------------------------------------------------------------------
### joshwilsdon's test (https://github.com/joyent/node/issues/7161) ###
R[ "joshwilsdon" ] = ->
d1 = [ NaN, undefined, null, true, false, Infinity, 0, 1, "a", "b", {a: 1}, {a: "a"},
[{a: 1}], [{a: true}], {a: 1, b: 2}, [1, 2], [1, 2, 3], {a: "1"}, {a: "1", b: "2"} ]
d2 = [ NaN, undefined, null, true, false, Infinity, 0, 1, "a", "b", {a: 1}, {a: "a"},
[{a: 1}], [{a: true}], {a: 1, b: 2}, [1, 2], [1, 2, 3], {a: "1"}, {a: "1", b: "2"} ]
errors = []
for v1, idx1 in d1
for idx2 in [ idx1 ... d2.length ]
v2 = d2[ idx2 ]
if idx1 == idx2
unless eq v1, v2
errors.push "eq #{rpr v1}, #{rpr v2}"
else
unless ne v1, v2
errors.push "ne #{rpr v1}, #{rpr v2}"
#.......................................................................................................
return [ d1.length, errors, ]
#---------------------------------------------------------------------------------------------------------
return R
Implementations Module Setup
Like the test cases, the src/implementations.coffee
module is of rather light structure:
#-----------------------------------------------------------------------------------------------------------
module.exports =
# Ex.: how to use JS syntax
#.........................................................................................................
"==: native ==":
#.......................................................................................................
eq: ( a, b ) -> `a == b`
ne: ( a, b ) -> `a != b`
# Ex.: how to adapt methods of assertion frameworks
#.........................................................................................................
"NDE: NodeJS assert.deepEqual":
#.......................................................................................................
eq: ( a, b ) ->
try
ASSERT.deepEqual a, b
catch error
return false
return true
#.......................................................................................................
ne: ( a, b ) ->
try
ASSERT.notDeepEqual a, b
catch error
return false
return true
# Ex.: how to adapt other methods
#.........................................................................................................
"LDS: lodash _.isEqual":
#.......................................................................................................
eq: ( a, b ) -> LODASH.isEqual a, b
ne: ( a, b ) -> not LODASH.isEqual a, b
The setup is very simple: Each implementation is an object with two members, eq
to test for 'is equal to'
and ne
to test for 'is not equal to'. Each name starts with a one to three-letter (unique) key which is
used for reference in the report display (see above), followed by a :
(colon) and the name proper (which
should be descriptive and unique). The name may be prepended with an !
(exclamation sign) in case the way
it has been adapted is suspected to be faulty (happened to me with the QUnit framework, which has a weird
API; i will probably make the relevant test results appear in grey and not include them in the grand
totals). Each function eq
, ne
must accept two arguments and return true
or false
, indicating success
or failure.
Equality, Identity, and Equivalence
There will be a lot of talk about equality and related topics in this text, so it behooves us to shortly if not strictly define, then at least make sufficiently clear some pertinent terms. Fear not, this formal discussion will be short, and save for one more stretch of (rather shallow) theoretical discussion, this ReadMe will remain fairly pragmatic; it may even be said that it is a pronouncedly pragmatic text that aims to deliver arguments against an ill-conceived standard fraught with artificial rules that serve no practical purpose (readers who bear with me will not be left in doubt which standard i'm talking about).
Three vocables will have to be used in any discussion of 'what equals what' in programming languages: these are equality, identity, and equivalence.
First off, to me, equality and identity are extensional, formal qualities, but equivalence is an intentional, informal quality. With 'extensional' i mean 'inherent to the material qualities of a given value', while 'intentional' pertains to values only in so far as they are put to some specific use, with a certain functionality or result to be achieved.
Put simply, the physical weight of a given nail is an extensional quality of that nail; that it is used, in
a given case, to hang some framed picture onto some wall is an incidental property of it that arises from a
willful decision of some agent who arranged for this configuration. Likewise, the property that, in
JavaScript, i can say both console.log( 42 )
and console.log( '42' )
to achieve a display of a digit 4
is an intentional property; it could be different. It surely strikes us as natural, but that is mainly
because we are so accustomed to write out numbers in the decimal system that we are prone to think of
'number forty-two' as 'sequence of digit 4, digit 2'. This analogy breaks down quickly as soon as one
modifies the setup: when i write console.log( 042 )
(or console.log( 0o42 )
in more recent editions of
JS), what i get is a sequence of 'digit 3, digit 4', which is different from the sequence 0
, 4
, 2
as
used in the source that caused this behavior. While it is acceptable to prefer the decimal system for
producing human-readable outputs, JavaScript's handling of strings-that-look-like-numbers is plain nutty.
Consider (using CoffeeScript and log
for console.log
):
log '42' + 8 # prints 428
log 42 + '8' # prints 428
log 42 * '8' # prints 336
log '42' * 8 # prints 336
Here, we see that when we use the +
(plus operator) to 'add' a string and a number, the output will be a
string that concatenates the string with the decimal representation of that number. BUT if we use the *
(times) operator, we get a number that is the result of the multiplication of the two arguments, the string
being interpreted as a decimal number, where possible. This is so confusing and leads to so many surprising
ramifications that there is, in the community, an expletive to describe such phenomena, and that expletive
is WAT!
I'm discussing these well-known JS WATs in the present context because JavaScript programmers (much like
users of PHP, and certainly more than users of Python) are very much inclined to have a rather muddled
view on data types, and, hence, of equality at large. This is borne out by the refusal of some people
to acknowledge that a method called deepEqual
that considers [ 42 ]
to 'equal' [ '42' ]
is pretty
much useless; more on that topic below.
It can be said that JavaScript's ==
'non-strict equals operator' never tested value equality at all,
rather, it tested value equivalence. Now we have seen that equivalence is a highly subjective concept that
is susceptible to the conditions of specific use cases. As such, it is a bad idea to implement it in the
language proper. The very concept that 3 == '3'
('number three is equivalent to a string with the ASCII
digit three, U+0033') does hold in some common contexts (like console.log( x )
), but it breaks down in
many other, also very common contexts (like x.length
, which is undefined for numbers).
Further, it can be said that JavaScript's ===
'strict equals operator' never tested value equality at
all, but rather object identity (alas, with a few lacunae, as we shall see), with the understanding that
all the primitive values have one single identity per value (something that e.g. seems to hold in Python for
all integers, but not necessarily all strings).
First Axiom: Value Equality Entails Type Equality
An important axiom in computing is that
Axiom 1 Two values x
and y
can only ever be equal when they both have the same type; conversely,
when two values are equal, they must be of equal type, too.
More formally, let L denote the language under inspection (JS or CS), and be M the meta-language to
discuss and / or to implement L. Then, saying that (in CS) eq x, y
results in true
implies that
eq ( type_of x ), ( type_of y )
must also be true
.
We can capture that by saying that in M, all values x
of L are represented by tuples ⟨t, v⟩
where t is the type of x
and v is its 'magnitude', or call it 'its underlying data proper', i.e. its
'value without its type'—which does sound strange but is technically feasible, since all unique values that
may occur within a real-world program at any given point in time are enumerable and, hence, reducible to
⟨t, n⟩, where n is a natural number. Since all n are of the same type, they can be said to be
typeless.
When we are comparing two values for equality in L, then, we are really comparing the two elements of
two tuples ⟨t1, v1⟩, ⟨t2, v2⟩ that represent the values
in M, and since we have reduced all values to integers, and since types are values, too, we have reduced
the problem of computing eq x, y
to doing the equivalent of eq [ 123, 5432, ], [ 887, 81673, ]
which has
an obvious solution: the result can only be true
if the two elements of each tuple are pairwise identical.
The above is not so abstruse as it may sound; in Python,
id( value )
will give you an integer that basically returns a number that represents a memory location, and in JavaScript, types are commonly represented as texts. Therefore, finding the ID of a type entails searching through memory whether a given string is already on record and where, and if not, to create such a record and return its memory address. Further, i would assume that most of the time, maybe always when you do'foo' === 'foo'
in JavaScript, what you really do is comparing IDs, not strings of characters.To make it very clear: i am not proposing here that the shown implementation of L in M is actually used in practical programming languages, or that it would be overall a good design at all; rather, it is teaching device like BASIC (intended to be an easy language for beginners) and a thought-experiment like Turing machines (intended to make a proof by way of reduction).
I hope this short discussion will have eliminated almost any remaining doubt whether two values of different types can ever be equal. However, there are two questions i assume the astute reader will be inclined to ask. These are: what about sub-typed values? and, what about numbers?
Equality of Sub-Types
As for the first question—what about sub-typed values?—i think we can safely give it short shrift. A
type is a type, irregardless of how it is derived. That an instance of a given type shares methods or data
fields with some other type doesn't change the fact that somewhere it must have—explicitly or implicitly,
accessible from L or only from M—a data field where its type is noted, and if the contents of that
field do not equal the equivalent field of the other instance, they cannot be equal if our above
considerations make any sense. True, some instances of some sub-types may stand in for some instances of
their super-type in some setups, but that is the same as saying that a nail can often do the work of a
screw—in other words, this consideration is about fitness for a purpose a.k.a. equivalence, not about
equality as understood here. Also, that a nail can often do the work of a screw does crucially not hinge on
a screw being conceptualized as 'a nail with a screw thread' or a nail reified as 'a screw with a zero-depth
thread'. In other words, just because, in some languages, both print 3
and print '3'
effect the
appearance of a digit three in the output medium doesn't mean that 3
and '3'
are 'the same'.
Equality of Numerical Values in Python
As for the second question—what about numbers?—it is in theory somewhat harder than the first, but, fortunately, there is an easy solution.
JavaScript may be said to be simpler than many other languages, since it has only a single numerical type, which implements the well-known IEEE 754 floating point standard with all its peculiarities.
Many languages do have more than a single numerical type. For instance, Java has no less than six: byte
,
short
, int
, long
, float
, double
, which users do have to deal consciously with.
Python before version 3 had four types: int
, float
, long
, complex
; in version 3, the int
and
long
types have been unified. Moreover, Python users have to worry much less about numerical types than
Java users, as Python tries very hard—and manages very well—to hide that fact; for most cases, numerical
types are more of a largely hidden implementation detail than a language feature. This even extends to
numerical types that are provided by the Standard Library, like the arbitrary-precision Decimal
class.
Python has the best thought-out numerical system of any programming language i had ever contact with, so my rule of thumb is that whatever Python does in the field of numbers is worthy of emulation.
It turns out that in Python, numbers of different types do compare equal when the signs and magnitudes of
their real and complex parts are equal; therefore, 1 == 1.0 == 1 + 0j == Decimal( 1 )
does hold. This
would appear to be in conflict with our theory (since we're comparing values of four different types here),
so either Python gets it wrong or the theory is incorrect.
One way to resolve the conflict is to say that the t in the tuples ⟨t, v⟩ of M do simply record an
abstract type number
instead of any subclass of numbers, this being an exception that is made for
practical reasons. Another solution would be to state that our theory is only applicable to languages which
have only a single numerical type, so it may be valid for JavaScript, but certainly not Java or Python.
A third way, and i believe the right one, is to assert that what Python does with its 1 == 1.0 == 1 + 0j
== Decimal( 1 )
comparison is really not doing equality, but rather equivalence testing, tailored to the
specific use-case of comparing numerical values for arithmetic purposes. Indeed, it turns out that Python
allows you to overload the behavior of the ==
operator by defining a special method __eq__
on a class,
and, if you are so inclined, you can make Python say yes to x == y
even though x.foo == y.foo
does not
hold! It is in fact very simple:
class X:
def __init__( self, foo ):
self.foo = foo
def __eq__( self, other ):
return ( self is not other ) and self.foo % other.foo == 0
x = X( 12 )
y = X( 6 )
z = X( 7 )
print( x == x ) # False
print( x == y ) # True
print( x == z ) # False
print( y == z ) # False
This example is more evidence in favor of the above assertion. If Python's ==
operator had been intended
to comply with our strict version of equality, there would have been little need to encourage overloading
the ==
operator, as the answer to that question can be given without implementing any class-specific
methods, from an abstract point of view. It is not immediately clear what use could be made of a value that
blatantly violates x == x
, but the fact that Python has no qualms in allowing the programmer such utterly
subversive code corroborates the notion that what we're dealing with here is open-minded equivalence rather
than principled equality.
Since there is, anyways, only a single numerical type in JavaScript, i believe we should stick with the unadultered version of the First Axiom which forbids cross-type equality even for numerical types.
Second Axiom: Equality of Program Behavior
The above treatment of numerical types has shown that Python prefers to consider 1 == 1.0
true because it
may be said that for most practical cases, there will be no difference between results whatever numerical
type you use (although it should be pointed out that already division in older Pythons used to act very
differently whether used with integers or floating-point numbers).
But that, of course, is not quite right; the whole reason for using, say, Decimal
instead of Float
is
to make it so that arithmetic operations do turn out differently, e.g. in order to deal with precise
monetary amounts and avoid rounding errors (you never calculate prices using floating-point numbers in
JavaScript, right?).
Now, the reason for programmers to write test suites is to ensure that a program behaves the expected way,
and that it continues to return the expected values even when some part of it gets modified. It is clear
that using some BigNum
class in place of ordinary numbers will likely make the program change behavior,
for the better or the worse, and in case you're writing an online shopping software, you want to catch all
those changes, which is tantamount to say you do not want any kind of eq ( new X 0 ), 0
tests to
return true
, precisely because 0.00
is your naive old way and new X 0.00
is your fool-proof new way of
saying 'zero dollars', and you want to avoid missing out on any regression in this important detail.
Thus our second axiom becomes:
Axiom 2 Even two values x
, y
of the same type that can be regarded as equal for most use cases, they
must not pass the test eq x, y
in case it can be shown that there is at least one program that has different
outputs when run with y
instead of with x
.
The second axiom helps us to see very clearly that Python's concept of equality isn't ours, for there is a
very simple program def f ( x ): print( type( x ) )
that will behave differently for each of 1
, 1.0
,
1 + 0j
, Decimal( 1 )
. As for JavaScript, the next section will discuss a relevant case.
Infinity, Positive and Negative Zero
One of the (many) surprises / gotchas / peculiarities that JavaScript has in store for the n00be programmer
is the existence of two zeroes, one positive and one negative. What, i hear you say, and no sooner said
than done have you typed +0 === -0
, return, into the NodeJS REPL, to be rewarded with a satisfyingly
reassuring true
. That should do it, right?—for haven't we all learned that when a x === y
test returns
true
it 'is True', and only when that fails do we have to do more checking? Sadly, this belief is
mistaken, as the below code demonstrates:
signed_rpr = ( x ) ->
return ( if is_negative_zero x then '-0' else '+0' ) if x is 0
return Number.prototype.toString.call x
is_negative_zero = ( x ) -> x is 0 and 1 / x < 0
test_signed_zero = ->
log +0 == -0 # true
log +1 / +0 # Infinity
log +1 / -0 # -Infinity
log 1 / +0 * 7 # Infinity
log 1 / -0 * 7 # -Infinity
log +0 < 0 # false
log -0 < 0 # false
log +0 * 7 < 0 # false
log -0 * 7 < 0 # false
log Infinity * 0 # NaN
log Infinity / +0 # Infinity
log Infinity / -0 # -Infinity
log signed_rpr +0 ** +1 # +0
log signed_rpr -0 ** +1 # -0
log signed_rpr +0 ** -1 # Infinity
log signed_rpr -0 ** -1 # -Infinity
test_signed_zero()
When i first became aware of there being a +0
and a -0
in JS, i immediately wrote a test case: R[ "+0
should eq -0" ] = -> eq +0, -0
. I then proceeded adding libraries to jsEq and felt happy that the work i
put into delivering pretty detailed test reports was not for naught as more and more small differences
between the libraries popped up: this library misses that test case, the next passes the other test, and so
on. I sorted the results, and seeing that underscore
got the highscore (pun intended), it surprised me to
see that it insisted on treating +0
and -0
as not equal. Ultimately, this led to the discovery of the
second Axiom, and with that in my hands, it became clear that underscore
got this one right and my test
case got it wrong: Since there are known programs that behave differently with positive and negative zero,
these two values must not be considered equal.
Not-A-Number
Yet another one of that rich collection of JavaScript easter eggs (and, like +0
vs -0
, one that is
mandated by IEEE 754), is the existence of a NaN
(read: Not A Number) value. In my opinion, this value
shouldn't exist at all. JS does consistently the right thing when it throws an exception on undefined.x
(unable to access property of undefined
) and on f = 42; f 'helo'
(f
is not a function), and, as
consistently, fails silently when you access undefined object properties or do numerical nonsense. In the
latter case, it resorts to returning sometimes Infinity
, and sometimes NaN
, both of which make little
sense in most cases.
Now, 'infinity' can be a useful concept in some cases, but there is hardly any use case for NaN
, except
of course for Array( 16 ).join( 'wat' - 1 ) + ' Batman!'
to get, you know that one,
NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN Batman!
Worse, while NaN
is short for 'not a number', typeof NaN
returns... 'number'
! WAT!
This is not the end to the weirdness: as mandated by the standard, NaN
does not equal itself. Now try
and tack attributes unto a NaN
, and it will silently fail to accept any named members. There's no
constructor for this singleton value, so you can not produce a copy of it. You cannot delete it from the
language; it is always there, a solitary value with an identity crisis. Throw it into an arithmetic
expression and it will taint all output, turning everything into NaN
.
The sheer existence of NaN
in a language that knows how to throw and catch exceptions is an oxymoron, as
all expressions that currently return it should really throw an error instead.
Having read several discussion threads about the merits and demerits of JS NaN !== NaN
, i'm fully
convinced by now that what we have seen concerning Python's concept of numerical equality (which turned out
to be equivalence) applies to NaN !== NaN
as well: it was stipulated because any of a large class of
arithmetic expressions could have caused a given occurrence of NaN
, and claiming that those results are
'equal' would be tantamount to claiming that 'wat' - 1
equals Infinity * 0
, which is obviously wrong
(although it must be said that Infinity * 0 !== Infinity * 0
is not very intuitive, either). Still, NaN
!== NaN
is a purpose-oriented stipulation for defining equivalence, not the result of a principled approach
to define strict equality in our sense.
I conclude that according to the First and Second Axioms, eq NaN, NaN
must hold, on the grounds
that no program using NaN
values from different sources can make a difference on the base of manipulating
these values or passing them as arguments to the same functions.
A collateral result of these considerations is that while JavaScript's ===
so-called strict equality
operator (which is really an object identity operator) functions quite well in most cases, it fails with
NaN
. Specifically, it violates the
Fundamental Axiom: value identity implies value equality. When a given test f
purports to test for
equality, but f x, x
fails to yield true
for any given x
, then that test must be considered broken.
Update: i just came across
Object.is
,
which, according to MDN,
determines whether two values are the same value. Two values are the same if one of the following holds:
- both undefined
- both null
- both true or both false
- both strings of the same length with the same characters
- both the same object
- both numbers and
- both +0
- both -0
- both NaN
- or both non-zero and both not NaN and both have the same value
Evidently, the 'both NaN' and 'both +0' / 'both –0' clauses corroborates our findings in the present and the previous sections.
Incidentally, this also shows that regulation 7.1 of the CommonJS Unit Testing specs is ever so slightly off the mark when they say:
All identical values are equivalent, as determined by ===.
(OBS that their use of 'equivalent' doesn't match my definition; the point is that the JS community has
already taken steps to provide for a more precise value identity metric than ===
can deliver).
Aside: don't use the global function isNaN
in your code unless you know what you're doing, as
isNaN
is broken.
Instead, do (JS) x !== x
(x != x
in CS).
Object Property Ordering
Many people in the JS programming community are aware of the issues around ordering of object properties ever since Chrome (and, because of that, NodeJS) broke customary behavior with regard to the ordering of object properties. To wit, in NodeJS 0.10.28:
obj = {}
obj[ 4 ] = 'first'
obj[ 2 ] = 'second'
obj[ 1 ] = 'third'
for name, value of obj
log name, value
gives 1 third
, 2 second
, 4 first
, which reflects the keys re-ordered by their numerical values, not
their order of insertion. Confusingly, this behavior lingers on when we use '4'
, '2'
, '1'
as
keys, and magically vanishes as soon as we use keys that can not be interpreted as (32-bit) integers.
Now, on the one hand it is evident that the ECMA specs do state that objects are unordered collections of keys and values, but on the other hand, the agreement among browsers has—from the beginning, it would seem—been that objects should preserve the order of inserted properties. As a commenter in a relevant thread on esdicuss.org put it:
[Object property ordering] behavior was perfectly consistent across all browsers until Chrome 6. I think it's more appropriate to say that Chrome is not interoperable with thousands of sites than to define interoperable behavior based on a minority browser's very very recent break from a de-facto standard that stood for 15 years.
The real problem here lies with the Chrome folks. It is not the only occasion that they completely and stubbornly shut up when anyone is so impertinent as to criticize their specific readings of their Holy Book. They surely deserve praise for the general exactness of their work and the swiftness of JavaScript running inside of V8, but their insistence on even the most marginal of performance gains at the expense of long-standing but non-standardized expected behavior are nothing short of asinine. Sure, the Specs do not mandate that any ordering of properties be kept, but does that mean it's a good idea to not keep ordering when most people expect it, most JS engines keep it, and it can convincingly shown to be a very useful behavior? But this is idle talk as the Chrome folks will only swear by the millisecond gained in some synthetic test case. (They are likewise quite indifferent towards the merits of a stable sort and in all earnesty expect the general public to accept an algorithm that shows one behavior for short and another behavior for long lists, the threshold being set at an arbitrary limit of ten elements.)
It may then be asked whether our version of strict equality (A) should or (B) should not treat two objects as equal when their only difference lies in the ordering of properties. First off, there would appear to be little support from the tested libraries for (B) (i.e. most libraries discard ordering information). Next, the Specs do not mandate any ordering behavior, so maybe equality tests shouldn't require it, either. Then again, there is a strawman proposal so there's a chance a future version of the language will indeed mandate preservation of object key insertion. Moreover, our Second Axiom makes it quite clear that, since otherwise identical programs can deliver different outputs for different order of key insertion, and people have come to rely on consistent ordering for many years, there is something to be said in in favor of solution (B).
I guess that a good pragmatic solution is to go with crowd and use object property ordering where supported, but not make that factor count in equality tests: two objects that only differ in the order of key insertion shall be regarded equal. Where object key ordering is an important factor, it can and should be tested separately.
Properties on 'Non-Objects'
should be tested, also on functions and arrays
should we consider property descriptors? guess that's an opt-in
Primitive Values vs Objects
The difference that exists in many object-oriented languages between primitive values (i.e. values without
properties) and objects (i.e. values that are composed of properties) is puzzling to many people. To make it
clear from the outstart: i believe that wherever and whenever a distinction has been or should have been
made in a program between, say, 5
plain and simple, and new Number 5
, then that language is at fault for
not shielding the programmer from such an abomination.
I do get the feeling that the smart people who came up with JavaScript thought along the same lines, and
that the fact that you can sometimes make a difference between 5
and new Number 5
is actually an
oversight where the intention was that programmers should never have to worry about that detail. Thus, in
JavaScript, when you access a property of a primitive value, that primitive is (at least conceptually)
temporarily cast as an object, and suddenly you can access properties on a primitive.
As for our inquiry, we have to ask: should eq 5, new Number 5
hold or not? In the light of the foregoing
discussion, we can give a quick answer: a primitive value and an equivocal object instance must be
regarded as different. It follows from our Second Axiom in conjunction with the fact that trying to attach
a property to a primitive or an object will show a different outcome depending on the receiver.
One could also assert that the need for
ne 5, new Number 5
follow from the First Axiom, as5
andnew Number 5
are not of the same type. However, it is not quite as clear. After all, checking types can be done in two ways: one way is to submit a given value to a series of tests—how does it behave when passed toMath.sin()
, what is the result of doingx + ''
, and so on; the other way is to employ JavaScript language constructs like thetypeof
statement (there's a number of these devices, and usually a judiciously selected combination of several is needed to arrive at a sane result). Now let us imagine that alltypeof
-like devices were not implemented in a language K that compiles to JavaScript. Let it further be a known fact that no language construct of K results in an accidental use of atypeof
-like device in the targetted JavaScript; still, we realize, on perusing the generated JS target code resulting from an input written in K, that in some cases,5
appears in the target code, andnew Number 5
in others. The question is then: can we make it so that a program written in K behaves differently for two valuesx
,y
, where one compiles to a primitive, the other to an object? The answer will be 'yes' in case our probing method (as demonstrated below) can somehow be expressed within K. Thus, even in some more restricted dialects of JS, the equivalent ofne 5, new Number 5
should hold; otherwise, our equality testing would be flawed.The reason i'm going to these lengths here lies in the observation that JavaScript's type system is rather deeply broken. It is for this reason that i've written the CoffeeNode Types package quite a while ago, and the present discussion is reason enough for me to work on a 2.0.0 release for that module which will introduce some breaking changes. Long story short: in the absence of a clear-cut typing system, using the First Axiom to decide on equality can only be done when type difference is more than obvious: a number is not a text, a boolean is not 'null', period. But whether or not both a primitive number and an objectified number are of the same type is a much harder question.
In JavaScript, there are the primitive types undefined
, null
, boolean
, string
and number
;
undefined
and null
are singletons and do not have a constructor, there's only Boolean
, Number
and String
. When you try to attach a property to a primitive value, JavaScript will either complain
loudly (in the case of undefined
and null
), or fail silently (in the case of booleans, numbers and
strings). So one might say that there are really 'primeval primitives' and 'advanced(?) primitives' (rather
than just primitives) in JavaScript.
It gets even a little worse. Consider this:
test = ( value, object ) ->
value.foo = 42
object.foo = 42
# == === p o
return [ `value == object`, `value === object`, value.foo is 42, object.foo is 42 ]
# == === p o
log test 5, new Number 5 # O X X O
log test 'x', new String 'x' # O X X O
log test true, new Boolean true # X X X O
log test /x/, new RegExp /x/ # X X O O
log test [], new Array() # X X O O
For readability, i've here rendered true
as O
and false
as X
. We can readily discern three
patterns of output values, the OXXO
kind, the XXXO
kind, and the XXOO
kind. When i say 'kind', i mean
'types of types', and it is plausible that longer series of like tests will result in 'fingerprint patterns'
that will allow us to sort out not only types of types, but also the types themselves.
The sobering factoid that is provided by the above program is that there are at least three kinds of primitives.
Worse: since NaN
is a primitive, too, but singularly fails to satisfy JS x === x
, there are at least
four kinds.
Worster still: undefined
can be re-defined in plain JS, something you can't do with NaN
, so there
are at least five kinds of primitive values in JavaScript.
I think i'll leave it at that.
Undefined Properties
Undefined properties are quite a nuisance. One might want to think that an 'undefined' property is just a
property that doesn't exit, but in the wonderful world of JavaScript, where there is an undefined
value
that is actually used as a stand-in return value for cases like {}[ 'foo' ]
and [][ 87 ]
(instead of
throwing an exception), that is not so clear. To wit:
d = { x: undefined }
log Object.keys d # [ 'x' ]
d = [ 'a', 'b', 'c' ]
delete d[ 2 ]
log d.length, Object.keys d # 3 [ '0', '1' ]
d[ 3 ] = undefined
log d.length, Object.keys d # 4 [ '0', '1', '3' ]
What this experiment shows is that according whether you base your judgement on (CS) d[ 'x' ] != undefined
or on (CS) ( ( Object.keys d ).indexOf 'x' ) != 1
, d
has or has not a key x
. Sometimes the one test
makes sense, sometimes the other; generally, it's probably best to avoid properties whose value has been set
to undefined
.
The experiment further shows that delete
just 'pokes a hole' into a list (instead of making all subsequent
entries move forward one position, as done in Python), but doesn't adjust the length
property, therefore
causing the same trouble as with other objects (the one thing that can be said in favor of this mode of
operation is that it allows to make sparse lists with arbitrarily large indices on elements).
It may be said without hesitation that ne { x: undefined }, {}
should hold without further qualification,
and in fact, there is very broad agreement across implementations about this.
Functions (and Regular Expressions)
In this section, i want to discuss the tricky question whether two functions f
, g
can or cannot be
considered equal. First off, it should be clear that whenever (JS) f === g
holds, f
and g
are merely
two names for the same object, so they are trivially equal in our sense. The troubles start when f
and g
are two distinct callables, and this has to do with a couple of topics whose discovery and treatmeant must
be counted among the great intellectual achievements of the 20th century. You know all of these names:
Gödel's incompleteness theorems,
Turing machines,
Halting problem,
Rice's theorem.
I will not iterate any details here, but what the programmer should understand is that there is no, and cannot be for logical reasons, any general algorithm that is able to test whether two given programs will behave equally for all inputs.
The emphasis is on general, because, of course, there are cases where one can say with confidence that
two given functions behave equally. For example, when i have two f
, g
that are explicitly limited to a
certain finite set of inputs (say, positive integer numbers less than ten), i can repeatedly call both
functions with each legal input and compare the results. But even that is not strictly true, because it is
simple to define a function that will sometimes deliver a different result (say, based on a random number
generator, or the time of the day). Furthermore, the test will break down when the returned value should be
a function itself, as we are then back to square one then and possibly caught in an infinite regress.
By inspecting source code, there are some cases where a decision can be made more confidently. For example, if we have
var f = function( a, b, c ){ return a * b * c; };
var g = function( a, b, c ){ return a * b * c };
then we can tell with certainty that f and g will return the same value, as the only difference is in the
use of the ;
(semicolon) which in JavaScript in this case does not cause any behavioral difference. The
same goes when i reorder the factors of the product as c * a * b
.
The question is: how to decide whether two functions have only minor differences in their sources? and the answer is: we shouldn't even try. The reason is simple: A JavaScript program has access to the source code of (most) functions; as such, we can always inspect that code and cause behavioral differences:
log f.toString().indexOf ';' # 38
log g.toString().indexOf ';' # -1
We have now reduced our field of candidates for equality to one remaining special case: how about two
functions for which eq f.toString(), g.toString()
holds?
I want to suggest that two functions may be considered equal when their source code (as returned by
their x.toString()
methods) are equal. However, because of some limitations to this, that should be made
optional.
This i believe should be done for pragmatic reasons as, sometimes, the objects you want to test will contain
functions, and it can be a nuisance to first having to remove them and then be left without any way to
test whether the objects have the expected shapes (i'm not the only one to think so; the generally quite
good equals
method by jkroso does essentially the same).
However, there's a hitch here. As i said, JavaScript can access the source of most functions. It cannot
show the source of all functions, because built-ins are typically not written in JS, but compiled (from
C). All that you get to see when you ask for, say, [].toString.toString()
will be (at least in NodeJS and
Firefox)
function toString() { [native code] }
and since all objects have that method, it's easy to come up with a litmus test that shows our
considerations are not quite watertight. I use the aformentioned equals
implementation here:
eq = require 'equals' # https://github.com/jkroso/equals
f = ( [] ).toString
g = ( 42 ).toString
h = -> 'test method'
log 'does method distinguish functions?', eq ( eq f, g ), ( eq f, h ) # false (1)
log 'are `f` and `g`equal? ', eq f, g # true (2)
log 'do they show the same behavior? ', eq ( f.call 88 ), ( g.call 88 ) # false (3)
On line (1), we demonstrate that the eq
implementation chosen does indeed considers some functions to be
different (eq f, h
returns false
) and others as equal (eq f, g
returns true
, as the result from line
(2) shows). According to our reasoning, f
and g
then should show equivalent behaviors for equivalent
inputs (in case they are deterministic functions that base their behavior solely on their explicit
arguments, which they are). However, as evidenced by the output of line (3), they return different outputs
(namely [object Number]
and 88
) when called with the same argument, 88
(which acts as this
in this
case, but that is beside the point).
Actually, i feel a bit stoopid, because, as i'm writing this, another, less contrived, conceptually simpler, more transparent and probably more relevant counter example comes to my mind, viz.:
get_function = ( x ) ->
return ( n ) -> n * x
f = get_function 2
g = get_function 3
log eq f, g # true
log eq ( f 18 ), ( g 18 ) # false
These functions are the same in the sense that they always execute the same code; they are different in the way that they see different values in their respective closures. I doubt it will make sense to go much further than this; to me, the adduced evidence leaves me at 50/50 whether function equivalence makes sense or not, which is why i think this feature should be made an opt-in.
How Many Methods for Equality Testing?
It is a recurrent feature of many assertion libraries that they provide one method for doing shallow
equality testing and another for deep equality testing. A case in point is NodeJS' assert
module with no
less than six equality-testing methods: equal
, notEqual
, deepEqual
, notDeepEqual
, strictEqual
,
notStrictEqual
. Given this state of affairs, it is perhaps not so surprising that
issue #7161: assert.deepEqual doing inadequate comparison
prompted the suggestion to add deepStrictEqual
(and, to keep the tune, notDeepStrictEqual
as well)
to the API, which ups the tally to eight.
The reader will not have failed to notice that i make do, in the present discussion and the implementation
of the jsEq package, with a mere two API items, eq
and ne
, a fourth of what NodeJS offers. One gets
the impression the CommonJS folks who wrote
the unit testing specs
must have started out, like, "wah equality, that's JS
==
", and then at some point realized "cool, there's JS ===
, let's add it, too". A little later someone
may have pointed out that JS [] === []
fails, and those folks went, like, "oh noes, we need deepEqual
, let's
add it already". Of course, since they started out with the broken JS ==
operator, they recycled that
experience when implementing deepEqual
, so now their deepEqual
is as broken as their equal
. But, hey,
at least its consistently broken, and what's more, we have a standard! Yay!
So now we have a widely-deployed assertion framework which seriously claims that assert.deepEqual [[]], [{}]
and
assert.deepEqual [ 3 ], [ '3' ]
should both hold and not throw exceptions like crazy. One wonders what
the intended use cases for such tests are; i can't think of any.
It looks like it never came to the minds of these folks that JS ==
is, overall, a pretty much useless
operator (===
was added to JavaScript specifically to remedy the pitfalls of ==
; the only reason it did
not replace ==
was a perceived concern about backwards compatibility). Likewise, it escaped their attention
that APIs do not get better just by adding more and more methods to them.
I believe it can be made unequivocally clear that separating deep and shallow equality has no place in an orderly API, especially not in an assertion framework.
The reasoning is simple: when i test for
equality, i want to test two (or more) values x
, y
. If i knew for sure that x
and y
are equal,
there wasn't a need to test them. Given that i'm unsure about the value of at least one of x
, y
, which
method—shallow equality for testing primitive values (Booleans, numbers, strings, ...) or deep equality
for testing 'objects' (lists, dates, ...)—should i take? In the absence of more precise knowledge of my
values, i cannot choose. So maybe i do some type checking (notoriously hard to get right in JS), or i
play some try ... catch
games to find out. It is clear that if shallow_equals [], 42
should fail (or return false
) because one of the arguments is not a primitive value, i have to try the other method,
deep_equals [], 42
. Since the first failed, the second should fail in the same way, so now i
know that the two values are not equal according to my library, since i have run out of methods. It is then
easy enough to come up with a method equals x, y
that does exactly that: try one way and, should that fail,
try the other way, catch all the errors and reduce the output to true
and false
.
There is no reason why the burden of implementing an all-embracing equals
method should be put on the
user; rather, it is a failure on part of the library authors to export anything but an equals
method
(and maybe a not_equals
method, especially in the context of an assertion library), which is one more reason
i consider NodeJS' assert
broken: instead of two methods, it exports six (and maybe eight at some point in
the future). This is also revealed by the jsEq tests: for instance, assert.deepEqual 1234, 1234
and assert.notDeepEqual 1234, 1235
as such work correctly, obviating any need for both assert.equal
and
assert.notEqual
if there ever was one. Their presence is an implementation detail that happened to get
exposed to the general public.
Plus and Minus Points
- +1 if method allows to configure whether
eq NaN, NaN
should hold. - +1 if method allows to configure whether object key ordering should be honored.
- +1 if method allows to configure whether function equality should be tested.
- +1 if method allows to test arbitrary number of arguments for pairwise equality.
- –1 if a (non-assertive) method throws an error on any comparison.
- –1 if a method for deep equality testing fails on primitive values.
- –1 where a method
eq
fails oneq x, x
for any givenx
(except forNaN
which is a hairy case). - –1 where a library provides both an
eq
and ane
method buteq ( not eq x, y ), ( ne x, y )
fails for any givenx
andy
. - –1 where a pair
x
,y
can be found that causeseq ( eq x, y ), ( eq y, x )
to fail. - –1000 where anyone dares to pollute the global namespace.
- –1000 where anyone dares to monkey-patch built-ins like
String.prototype
except for doing a well-documented, well-motivated (by existing future standards), well-tested polyfill.
Benchmarks
A through comparison of equality-testing implementations whould also consider performance (and maybe memory consumption). This task has been left for a future day to be written.
Libraries Tested
==
: native JavaScript comparison with==
===
: native JavaScript comparison with===
OIS
: native JavaScript comparisonObject.is
LDS
: https://github.com/lodash/lodashUDS
: https://github