ES2015에는 4가지 같음(equality) 알고리즘이 있습니다:
- 추상적(abstract) 같음 비교 (
==
) - 엄격한(strict) 같음 비교 (
===
):Array.prototype.indexOf
,Array.prototype.lastIndexOf
및case
절 매칭에 쓰임 - 등가0(SameValueZero):
Map
및Set
연산뿐만 아니라%TypedArray%
및ArrayBuffer
생성자, 그리고 ES2016에 예정된String.prototype.includes
에 쓰임 - 등가(SameValue): 그 외 모든 곳에 쓰임
JavaScript는 3가지 서로 다른 값 비교 연산을 제공합니다:
- ===를 사용하는 엄격한 같음 (또는 "삼중 등호" 또는 "항등(identity)"),
- ==를 사용하는 느슨한(loose) 같음 ("이중 등호"),
- 그리고
Object.is
(ECMAScript 2015에 새로 들임).
어느 연산을 쓸 지 그 선택은 당신이 어떤 종류의 비교를 수행하기 위해 찾고 있는 지에 달렸습니다.
간단히 말해서, 이중 등호는 두 값을 비교할 때 형 변환(type conversion)을 수행합니다. 반면에 삼중 등호는 형 변환 없이 같은 비교를 행합니다 (형이 다른 경우 단순하게 항상 false를 반환하여); 그리고 Object.is
는 삼중 등호와 같은 식으로 동작하지만, NaN
및 -0
과 +0
(중 마지막 둘은 같다고 하지 않기 위한)에 대한 특별 처리로 인해, Object.is(NaN, NaN)
는 true
가 됩니다. (보통 NaN
과 NaN
을 비교하면—즉, 이중 등호 또는 삼중 등호를 사용하여—false
로 평가합니다, IEEE 754 표준이 그렇다 하니까.) 이들 사이의 구별은 모두 원시형(primitive) 자신의 처리로 해야 함을 주의하세요. 즉 그들 중 아무도 매개변수(parameter)가 개념상 구조가 비슷한 지 여부를 비교하지 않습니다. 구조가 같지만 그들 스스로 별개 객체인 비원시형 객체 x 및 y에 대해, 위 형태(form) 모두 false로 평가합니다.
===
를 사용하는 엄격한 같음
엄격한 같음(strict equality)은 두 값이 같은 지 비교합니다. 어느 값도 비교되기 전에 어떤 다른 값으로 남몰래 변환되지 않습니다. 둘이 서로 다른 형이면, 둘은 같지 않다고 여깁니다. 그렇지 않고 둘이 같은 형이고 숫자가 아닌 경우, 같은 값이면 같다고 여깁니다. 끝으로, 둘이 숫자인 경우, 둘 다 NaN
이 아닌 같은 값이거나 하나는 +0
또 하나는 -0
인 경우 같다고 여깁니다.
var num = 0; var obj = new String("0"); var str = "0"; var b = false; console.log(num === num); // true console.log(obj === obj); // true console.log(str === str); // true console.log(num === obj); // false console.log(num === str); // false console.log(obj === str); // false console.log(null === undefined); // false console.log(obj === null); // false console.log(obj === undefined); // false
엄격한 같음은 거의 항상 사용하는 올바른 비교 연산입니다. 숫자를 뺀 모든 값에 대해, 분명한 의미(semantics)를 사용합니다: 값은 그 자체와만 같습니다(/ 단지 그 자체입니다). 숫자는 서로 다른 두 극단 상황(edge case)을 얼버무리기(gloss over) 위해 약간 다른 의미를 사용합니다. 첫째는 부동 소수점 0은 양이든 음이든 하나의 부호를 지닙니다. 이는 특정 수학상의 해결책을 나타내는 데 유용하지만, 대부분의 상황에 +0
과 -0
의 차이에 신경쓰지 않기에, 엄격한 같음은 둘을 같은 값으로 다룹니다. 둘째는 부동 소수점은 not-a-number 값(NaN
) 개념을 포함합니다, 특정 잘못 정의된(ill-defined) 수학 문제의 해결책을 보여주기 위해: 예를 들어, 양의 무한대(infinity)에 추가된 음의 무한대. 엄격한 같음은 NaN
을 다른 모든 값과 같지 않게 다룹니다 -- 자신 포함. ((x !== x)
가 true
인 유일한 경우는 x
가 NaN
일 때입니다.)
==를 사용하는 느슨한 같음
느슨한 같음(loose equality)은 두 값이 같은 지 비교합니다, 두 값을 공통(common) 형으로 변환한 후에. 변환 후 (하나 또는 양쪽이 변환을 거칠 수 있음), 최종 같음 비교는 꼭 ===
처럼 수행됩니다. 느슨한 같음은 대칭(symmetric)입니다: A == B
는 A
및 B
가 어떤 값이든 항상 B == A
와 같은 의미를 갖습니다 (적용된 변환의 순서 말고는).
같음 비교는 다양한 형의 피연산자에 대해 다음과 같이 수행됩니다:
피연산자 B | |||||||
---|---|---|---|---|---|---|---|
Undefined | Null | Number | String | Boolean | Object | ||
피연산자 A | Undefined | true |
true |
false |
false |
false |
false |
Null | true |
true |
false |
false |
false |
false |
|
Number | false |
false |
A === B |
A === ToNumber(B) |
A === ToNumber(B) |
A == ToPrimitive(B) |
|
String | false |
false |
ToNumber(A) === B |
A === B |
ToNumber(A) === ToNumber(B) |
A == ToPrimitive(B) |
|
Boolean | false |
false |
ToNumber(A) === B |
ToNumber(A) === ToNumber(B) |
A === B |
ToNumber(A) == ToPrimitive(B) |
|
Object | false |
false |
ToPrimitive(A) == B |
ToPrimitive(A) == B |
ToPrimitive(A) == ToNumber(B) |
A === B |
위 표에서, ToNumber(A)
는 비교 전에 그 인수를 숫자로 변환하려고 시도합니다. 그 동작(behavior)은 +A
(단항 + 연산자)에 해당합니다. ToPrimitive(A)
는 그 객체 인수를 원시형 값으로 변환하려고 시도합니다, 다양한 순서로 A
의 A.toString
및 A.valueOf
메서드 호출을 시도하여.
전통 및 ECMAScript에 따르면, 모든 객체는 undefined
및 null
과 느슨하게 같지 않습니다. 그러나 대부분의 브라우저는 일부 문맥(context)에서 undefined
값을 모방하는(emulate) 것처럼 행동하기 위해 매우 좁은 부류의 객체(특히, 모든 페이지에 대한 document.all
객체)에 허용합니다. 느슨한 같음이 그러한 문맥 중 하나입니다: null == A
및 undefined == A
는 A가 undefined
를 모방하는 객체인 경우, 그리고 그 경우에만 true로 평가합니다. 다른 모든 경우에 객체는 결코 undefined
또는 null
과 느슨하게 같지 않습니다.
var num = 0; var obj = new String("0"); var str = "0"; var b = false; console.log(num == num); // true console.log(obj == obj); // true console.log(str == str); // true console.log(num == obj); // true console.log(num == str); // true console.log(obj == str); // true console.log(null == undefined); // true // 둘 다 false, 드문 경우를 제외하고는 console.log(obj == null); console.log(obj == undefined);
일부 개발자는 느슨한 같음을 사용하는 게 좋은 생각이 거의 아니라고 생각합니다. 엄격한 같음을 사용한 비교 결과는 예측이 쉽고 형 강제(coercion)가 일어나지 않기에 평가가 빠를 수 있습니다.
등가 같음
등가(same-value) 같음은 최종 사용 사례(use case)를 다룹니다: 두 값이 모든 문맥에서 기능상 같은지 여부를 결정하는. (이 사용 사례는 리스코프 치환 원칙의 실례를 보입니다.) 다음은 불변 속성(property)을 변화시키려 시도할 때 일어나는 한 사례입니다:
// 불변(immutable) NEGATIVE_ZERO 속성을 Number 생성자에 추가. Object.defineProperty(Number, "NEGATIVE_ZERO", { value: -0, writable: false, configurable: false, enumerable: false }); function attemptMutation(v) { Object.defineProperty(Number, "NEGATIVE_ZERO", { value: v }); }
Object.defineProperty
는 불변 속성을 실제로 바꾸려 하는 변경을 시도할 때 예외를 발생시키지만, 실제 변경이 요청되지 않은 경우 아무것도 하지 않습니다. v
가 -0
이면, 어떤 변화도 요청되지 않고 아무 오류도 발생되지 않습니다. 하지만 v
가 +0
이면, Number.NEGATIVE_ZERO
는 더 이상 불변 값을 갖지 않습니다. 내부에서 불변 속성이 재정의되면, 새로 지정된 값은 등가 같음을 사용하여 현재값과 비교됩니다.
등가 같음은 Object.is
메서드로 제공됩니다.
등가0 같음
등가 같음과 비슷하지만 +0과 -0이 같다고 여깁니다.
스펙 내 추상적 같음, 엄격한 같음 및 등가
ES5에서, ==
로 수행되는 비교는 11.9.3절, 추상적 같음 알고리즘에서 설명됩니다. ===
비교는 11.9.6절, 엄격한 같음 알고리즘입니다. (이를 보러 가세요. 짧고 읽기 쉽습니다. 귀띔: 엄격한 같음 알고리즘을 먼저 보세요.) ES5는 JS 엔진 내부에서 사용하기 위한 9.12절, 등가 알고리즘도 설명합니다. 이는 엄격한 같음 알고리즘과 대부분 같습니다, 11.9.6.4 및 9.12.4절에 Number
처리가 다르다는 것 빼고는. ES2015는 단순히 Object.is
를 통해 이 알고리즘의 노출을 제안합니다.
11.9.6.1에 앞서 형 검사를 수행함을 제외하고, 이중 및 삼중 등호를 사용한 엄격한 같음 알고리즘은 11.9.6.2–7항이 11.9.3.1.a–f항에 해당하기에 추상적 같음 알고리즘의 부분 집합임을 볼 수 있습니다.
같음 비교를 이해하기 위한 모델은?
ES2015 이전에, 이중 등호 및 삼중 등호에 대해 하나가 다른 하나의 "확장"판이라고 (말)했을 지 모릅니다. 예를 들어, 누군가는 이중 등호는 삼중 등호의 확장판이라고 합니다, 전자는 후자가 하는 모든 것을 하지만 그 피연산자에 형 변환을 하기에. 가령, 6 == "6"
. (대신에, 이중 등호는 기준선이고 삼중 등호는 향상판이라고 하는 이도 있습니다, 두 피연산자가 같은 형이길 요구하고 그래서 별도 제약을 추가하기에. 어느 게 더 이해하기 좋은 모델인지는 당신이 상태(things)를 보기 위해 선택한 방법에 달렸습니다.)
그러나, 내장 똑같음(sameness) 연산자에 대한 이런 식의 생각은 이 "스펙트럼"에 ES2015의 Object.is
를 위한 자리를 허용토록 늘어날(stretch) 수 있는 모델이 아닙니다. Object.is
는 단순히 이중 등호보다 "더 느슨"하거나 삼중 등호보다 "더 엄격"하지 않고, 그 사이 어딘가에 맞지도 않습니다 (즉, 이중 등호보다 엄격하거나 삼중 등호보다 느슨한 둘 다인). 이는 Object.is
가 NaN
을 처리하는 방식 때문임을 아래 똑같음 비교 표에서 볼 수 있습니다. Object.is(NaN, NaN)
이 false
로 평가되는 경우, 우리는 그것이 -0
과 +0
을 구분하는 삼중 등호의 더 엄격한 형태로서 느슨한/엄격한 스펙트럼에 맞다고 말할 수 있음을 알아차리세요. 그러나, NaN
처리는 이게 사실이 아님을 뜻합니다. 불행하게도, Object.is
는 단순히 그 특정 특성의 관점에서 생각되어야 합니다, 같음 연산자에 관해서는 그 느슨함이나 엄격함보다는.
x | y | == |
=== |
Object.is |
---|---|---|---|---|
undefined |
undefined |
true |
true |
true |
null |
null |
true |
true |
true |
true |
true |
true |
true |
true |
false |
false |
true |
true |
true |
"foo" |
"foo" |
true |
true |
true |
{ foo: "bar" } |
x |
true |
true |
true |
0 |
0 |
true |
true |
true |
+0 |
-0 |
true |
true |
false |
0 |
false |
true |
false |
false |
"" |
false |
true |
false |
false |
"" |
0 |
true |
false |
false |
"0" |
0 |
true |
false |
false |
"17" |
17 |
true |
false |
false |
[1,2] |
"1,2" |
true |
false |
false |
new String("foo") |
"foo" |
true |
false |
false |
null |
undefined |
true |
false |
false |
null |
false |
false |
false |
false |
undefined |
false |
false |
false |
false |
{ foo: "bar" } |
{ foo: "bar" } |
false |
false |
false |
new String("foo") |
new String("foo") |
false |
false |
false |
0 |
null |
false |
false |
false |
0 |
NaN |
false |
false |
false |
"foo" |
NaN |
false |
false |
false |
NaN |
NaN |
false |
false |
true |
Object.is
대신 삼중 등호를 사용하는 경우
보통 NaN
을 다루는 방식 말고도, 0에 대한 Object.is
의 특별 동작이 관심이 되기 쉬운 유일한 시간은 특정 메타 프로그래밍 체계(scheme)의 추구에 있습니다, 특히 속성 설명자에 대해 당신 작업에 Object.defineProperty
의 특성 중 일부를 반영하기 바람직한 경우. 사용 사례에 이게 필요하지 않은 경우, Object.is
를 피하고 대신 ===
를 사용하기를 추천합니다. 당신의 요구사항이 두 NaN
값 사이의 비교를 true
로 평가함을 포함하더라도, 보통 특별한 경우 NaN
검사가 (이전 버전 ECMAScript에서 이용 가능한 isNaN
메서드를 사용하는) 쉬워집니다, 어떻게 주변 계산이 비교에서 마주치는 모든 0의 부호에 영향을 줄 수 있는지 알아내는 것보다.
여기 당신 코드에서 그 자체를 드러내기 위해 -0
과 +0
사이의 구별을 일으킬 수도 있는 철저하지 않은(in-exhaustive) 내장 메서드 및 연산자 목록이 있습니다:
-
0
을 부정하는 것이-0
을 만듦은 분명합니다. 그러나 식의 추상화는 당신이 깨닫지 못할 때에-0
을 서서히 악화(creep, 파행)시킬 수 있습니다. 예를 들어, 다음을 보세요:let stoppingForce = obj.mass * -obj.velocity
obj.velocity
가0
인 (또는0
으로 계산하는) 경우,-0
이 그 자리에 소개되고stoppingForce
로 전해집니다.
- 일부 경우에
-0
이 이들 메서드의 반환값으로 식에 도입되는 게 가능합니다,-0
이 매개변수 중 하나로 존재하지 않는 경우에도. 가령, 어떤 음수의 거듭제곱(power)에-무한대
를 제곱하기(raise) 위해Math.pow
를 사용하면, 홀수 지수(exponent)는-0
으로 평가합니다. 개별 메서드에 대한 문서를 참조하세요.
-0
이 매개변수 중 하나로 존재하는 일부 경우에 이들 메서드 중에-0
반환값을 얻을 수 있습니다. 가령,Math.min(-0, +0)
은-0
으로 평가합니다. 개별 메서드에 대한 문서를 참조하세요.
~
<<
>>
- 이러한 연산자 각각은 내부에서 ToInt32 알고리즘을 사용합니다. 내부 32-bit 정수형에는 0에 대해 한 표현만 있기에,
-0
은 역(inverse) 연산 후 왕복 여행(round trip, 이중 역 연산)에 살아남지 못합니다. 가령,Object.is(~~(-0), -0)
와Object.is(-0 << 2 >> 2, -0)
는false
로 평가합니다.
0의 부호지님(signedness)이 고려되지 않을 때 Object.is
에 의존하는 것은 위험할 수 있습니다. 물론, 의도가 -0
과 +0
사이를 구분하는 것이면, 이는 정확히 원했던 것을 행합니다.