누군가 동등 연산자(==, Equality Operator)와 일치 연산자(===, Strict Equality Operator)에 대해 질문한 것을 커뮤니티 게시판에 올려두었다. 갑자기 호기심이 동하여 어떻게 동작하는지 찾아보았다.
자바스크립트를 처음 공부할 때, 잘 모르겠으면 ===
를 쓰라고 배우는 '그 내용'에 대해 간단히 짚어보고 가자. 그냥 없으면 허전하니까... 😙
==
동등 비교는 LHS의 값과 RHS의 값이 같은지 비교한다. 중요한 것은 값을 비교한다는 것이다.===
일치 비교는 LHS의 값, 타입과 RHS의 값, 타입이 같은지 비교한다. 중요한 것은 값과 타입 둘다 비교한다는 것이다.내가 평소에 자주 쓰는 용어가 아니지만 공부 중에 등장해서 뜻을 적어본다.
사실 오늘 알아보고싶은 내용은 V8엔진에서 두 연산자들이 어떻게 동작하는지이다. 두 연산자들의 실행 코드를 보기전에 Runtime Functions에 대해 알아본다.
V8 엔진의 빌트인 함수 중 일부는 곧장 자바스크립트로 구현되어 있고, 런타임에 실행가능한(executable) 코드로 컴파일됩니다. 이들 중 일부는 runtime functions 라고 분류되는 것들이 있습니다. C++로 작성되어 있고 자바스크립트에서
%
-접두사를 붙여 호출합니다. 이러한 runtime functions은 V8 내부의 자바스크립트 코드에서만 사용됩니다. <생략> 일부 runtime functions는 컴파일러에 의해 생성된 코드에 삽입됩니다.
Built-in functions, V8 doc
내가 이해한 바로는, V8 내부의 자바스크립트 코드를 통해 실행되는 함수 중 그 기능을 온전히 하기 위해 런타임에 C++로 작성된 runtime functions를 삽입해준다는 것이다. 고마운 분께서 V8 런타임 함수의 리스트를 만들어 주셨다. 여기에서 StrictEquality
와 Equals
, 두 개의 런타임 함수를 찾을 수 있었다.
일단, 코드가 쉬워보이는(짧아서) 이유로 일치 연산자 부터 알아본다.
일치 연산자에서는 어떤 런타임 함수가 사용되는지 코드를 찾아보았다. 코드는 runtime-operator.cc 파일에서 찾을 수 있다.
RUNTIME_FUNCTION(Runtime_StrictEqual) {
SealHandleScope scope(isolate);
DCHECK_EQ(2, args.length());
CONVERT_ARG_CHECKED(Object, x, 0);
CONVERT_ARG_CHECKED(Object, y, 1);
return isolate->heap()->ToBoolean(x.StrictEquals(y));
}
코드의 내용을 해석해본다.
여기까지 일치 연산자에 대한 런타임 함수를 살펴보았고, 실제 비교하는 코드를 살펴본다. objects.cc 파일에서 찾을 수 있다.
bool Object::StrictEquals(Object that) {
if (this->IsNumber()) {
if (!that.IsNumber()) return false;
return StrictNumberEquals(*this, that);
} else if (this->IsString()) {
if (!that.IsString()) return false;
return String::cast(*this).Equals(String::cast(that));
} else if (this->IsBigInt()) {
if (!that.IsBigInt()) return false;
return BigInt::EqualToBigInt(BigInt::cast(*this), BigInt::cast(that));
}
return *this == that;
}
위 코드의 주요점은 타입이 다르면 return false;
를 한다는 것이다. 그 후에 값을 비교한다.
ECMA가 정의한 일치 연산자 알고리즘(Strict Equality Comparison Algorithm)은 다음과 같다. 그냥 한 번 읽어보는게 좋을 것 같아서 소개해본다.
이번엔 코드가 길어서 미뤄왔던 동등 연산자에 대해서 알아본다.
동등 연산자에서도 런타임 함수를 찾아 보았다. 이 코드 또한 runtime-operator.cc 파일에서 찾을 수 있다.
RUNTIME_FUNCTION(Runtime_Equal) {
HandleScope scope(isolate);
DCHECK_EQ(2, args.length());
CONVERT_ARG_HANDLE_CHECKED(Object, x, 0);
CONVERT_ARG_HANDLE_CHECKED(Object, y, 1);
Maybe<bool> result = Object::Equals(isolate, x, y);
if (result.IsNothing()) return ReadOnlyRoots(isolate).exception();
return isolate->heap()->ToBoolean(result.FromJust());
}
===
연산자와의 차이와 특징을 살펴본다.
동등 연산자의 실제 비교 코드를 살펴본다. 역시 objects.cc 파일에서 찾을 수 있었다. 꽤 길다 ^^;
Maybe<bool> Object::Equals(Isolate* isolate, Handle<Object> x,
Handle<Object> y) {
while (true) {
if (x->IsNumber()) {
if (y->IsNumber()) {
return Just(StrictNumberEquals(x, y));
} else if (y->IsBoolean()) {
return Just(
StrictNumberEquals(*x, Handle<Oddball>::cast(y)->to_number()));
} else if (y->IsString()) {
return Just(StrictNumberEquals(
x, String::ToNumber(isolate, Handle<String>::cast(y))));
} else if (y->IsBigInt()) {
return Just(BigInt::EqualToNumber(Handle<BigInt>::cast(y), x));
} else if (y->IsJSReceiver()) {
if (!JSReceiver::ToPrimitive(Handle<JSReceiver>::cast(y))
.ToHandle(&y)) {
return Nothing<bool>();
}
} else {
return Just(false);
}
} else if (x->IsString()) {
if (y->IsString()) {
return Just(String::Equals(isolate, Handle<String>::cast(x),
Handle<String>::cast(y)));
} else if (y->IsNumber()) {
x = String::ToNumber(isolate, Handle<String>::cast(x));
return Just(StrictNumberEquals(x, y));
} else if (y->IsBoolean()) {
x = String::ToNumber(isolate, Handle<String>::cast(x));
return Just(
StrictNumberEquals(*x, Handle<Oddball>::cast(y)->to_number()));
} else if (y->IsBigInt()) {
return BigInt::EqualToString(isolate, Handle<BigInt>::cast(y),
Handle<String>::cast(x));
} else if (y->IsJSReceiver()) {
if (!JSReceiver::ToPrimitive(Handle<JSReceiver>::cast(y))
.ToHandle(&y)) {
return Nothing<bool>();
}
} else {
return Just(false);
}
} else if (x->IsBoolean()) {
if (y->IsOddball()) {
return Just(x.is_identical_to(y));
} else if (y->IsNumber()) {
return Just(
StrictNumberEquals(Handle<Oddball>::cast(x)->to_number(), *y));
} else if (y->IsString()) {
y = String::ToNumber(isolate, Handle<String>::cast(y));
return Just(
StrictNumberEquals(Handle<Oddball>::cast(x)->to_number(), *y));
} else if (y->IsBigInt()) {
x = Oddball::ToNumber(isolate, Handle<Oddball>::cast(x));
return Just(BigInt::EqualToNumber(Handle<BigInt>::cast(y), x));
} else if (y->IsJSReceiver()) {
if (!JSReceiver::ToPrimitive(Handle<JSReceiver>::cast(y))
.ToHandle(&y)) {
return Nothing<bool>();
}
x = Oddball::ToNumber(isolate, Handle<Oddball>::cast(x));
} else {
return Just(false);
}
} else if (x->IsSymbol()) {
if (y->IsSymbol()) {
return Just(x.is_identical_to(y));
} else if (y->IsJSReceiver()) {
if (!JSReceiver::ToPrimitive(Handle<JSReceiver>::cast(y))
.ToHandle(&y)) {
return Nothing<bool>();
}
} else {
return Just(false);
}
} else if (x->IsBigInt()) {
if (y->IsBigInt()) {
return Just(BigInt::EqualToBigInt(BigInt::cast(*x), BigInt::cast(*y)));
}
return Equals(isolate, y, x);
} else if (x->IsJSReceiver()) {
if (y->IsJSReceiver()) {
return Just(x.is_identical_to(y));
} else if (y->IsUndetectable()) {
return Just(x->IsUndetectable());
} else if (y->IsBoolean()) {
y = Oddball::ToNumber(isolate, Handle<Oddball>::cast(y));
} else if (!JSReceiver::ToPrimitive(Handle<JSReceiver>::cast(x))
.ToHandle(&x)) {
return Nothing<bool>();
}
} else {
return Just(x->IsUndetectable() && y->IsUndetectable());
}
}
}
길어서 좀 압박이 있었을 수도 있겠지만, 결국 필요할 때 캐스팅해서 값을 비교하는 것이다. 코드보다는 아래의 알고리즘을 읽어보는게 훨씬 나을 것 같다 😌.
ECMA의 동등 연산자 알고리즘(Abstract Equality Comaprison Algorithm)은 다음과 같다.
내용이 너무 딱딱한 것 같아서 동등, 비교 연산자와 관련한 재미있는(?) 사이트를 소개한다. 비교 연산자를 한 눈에 볼 수 있는 척 하게 해주는 사이트이다 ㅋㅋㅋ.
JavaScript Equality Table
지금까지 자바스크립트를 가장한 C++ 코드 블로깅이었다. 자바와 JS의 간단하게 하면 간단하게 끝나는 내용을 깊게 알아보기 위해 C++와 자료구조를 잘 알고 있어야 한다는 느낌을 많이 받았다. 이게 무슨 소용인지 가치가 있는 일인지 헷갈리지만, 그냥 엉덩이 붙히고 앉아서 오랜 시간 하나의 내용을 찾아보는 것을 해내고 있다는 것으로 위안을 삼고있다. 낄낄 😏