[JS] 동등 연산자 vs 일치 연산자

이충희·2021년 9월 25일
0

JavaScript

목록 보기
1/1
post-thumbnail

누군가 동등 연산자(==, Equality Operator)와 일치 연산자(===, Strict Equality Operator)에 대해 질문한 것을 커뮤니티 게시판에 올려두었다. 갑자기 호기심이 동하여 어떻게 동작하는지 찾아보았다.

모두가 아는 '그 내용'

자바스크립트를 처음 공부할 때, 잘 모르겠으면 === 를 쓰라고 배우는 '그 내용'에 대해 간단히 짚어보고 가자. 그냥 없으면 허전하니까... 😙

  • == 동등 비교는 LHS의 값과 RHS의 값이 같은지 비교한다. 중요한 것은 값을 비교한다는 것이다.
  • === 일치 비교는 LHS의 값, 타입과 RHS의 값, 타입이 같은지 비교한다. 중요한 것은 값과 타입 둘다 비교한다는 것이다.

[Optional] LHS / RHS?

내가 평소에 자주 쓰는 용어가 아니지만 공부 중에 등장해서 뜻을 적어본다.

  • LHS: Left Hand Side
  • RHS: Right Hand Side

V8에서는 어떻게 동작할까? 🤔

사실 오늘 알아보고싶은 내용은 V8엔진에서 두 연산자들이 어떻게 동작하는지이다. 두 연산자들의 실행 코드를 보기전에 Runtime Functions에 대해 알아본다.

Runtime Functions

V8 엔진의 빌트인 함수 중 일부는 곧장 자바스크립트로 구현되어 있고, 런타임에 실행가능한(executable) 코드로 컴파일됩니다. 이들 중 일부는 runtime functions 라고 분류되는 것들이 있습니다. C++로 작성되어 있고 자바스크립트에서 %-접두사를 붙여 호출합니다. 이러한 runtime functions은 V8 내부의 자바스크립트 코드에서만 사용됩니다. <생략> 일부 runtime functions는 컴파일러에 의해 생성된 코드에 삽입됩니다.

Built-in functions, V8 doc

내가 이해한 바로는, V8 내부의 자바스크립트 코드를 통해 실행되는 함수 중 그 기능을 온전히 하기 위해 런타임에 C++로 작성된 runtime functions를 삽입해준다는 것이다. 고마운 분께서 V8 런타임 함수의 리스트를 만들어 주셨다. 여기에서 StrictEqualityEquals, 두 개의 런타임 함수를 찾을 수 있었다.

v8-RuntimeFunctions-list

일치 연산자 (===, Strict Equality)

일단, 코드가 쉬워보이는(짧아서) 이유로 일치 연산자 부터 알아본다.

런타임 함수

일치 연산자에서는 어떤 런타임 함수가 사용되는지 코드를 찾아보았다. 코드는 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));
}

코드의 내용을 해석해본다.

  • Isolate::Scope 클래스 문서를 참조해보면, 지역 스코프 내에서 실행되는 모든 연산(operation)에 대해 isolate를 설정하는 스택처럼 할당되는 클래스라고 나와있다. 다른 스레드가 함부로 끼어들지 못하도록 락을 거는 것과 같은 느낌이라고 생각을 했다. 단지 내 생각일 뿐이다!
  • DCHECK_EQ는 인수의 개수가 2개인지 확인한다. 일치 연산자는 두 가지를 가지고 비교하는 것이기 때문에 확인한다.
  • 첫 번째 CONVERT_ARG_CHECKED를 해석해보자면, 0번째 인덱스의 인수를 x라는 이름의 변수에 Object 타입으로 캐스팅한다는 얘기이다.
  • StrictEquals 함수를 통해 일치 비교를 실행한다.

비교 코드

여기까지 일치 연산자에 대한 런타임 함수를 살펴보았고, 실제 비교하는 코드를 살펴본다. 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)은 다음과 같다. 그냥 한 번 읽어보는게 좋을 것 같아서 소개해본다.

  1. x와 y의 타입이 다르면 false를 리턴한다.
  2. x와 y의 타입이 Undefinedtrue를 리턴한다.
  3. x와 y의 타입이 Null이면 true를 리턴한다.
  4. x의 타입이 Number 일 때,
    a. x의 값이 NaN이면 false를 리턴한다.
    b. y의 값이 NaN이면 false를 리턴한다.
    c. x와 y의 값이 같은 Number 값이면 true를 리턴한다.
    d. x가 +0이고 y가 -0일 때, true를 리턴한다.
    e. x가 -0이고 y가 +0일 때, true를 리턴한다.
    f. false를 리턴한다.
  5. x의 타입이 String일 때, x와 y의 문자 순서와 그 값이 정확이 일치하면 true를 리턴하고 아니라면 false를 리턴한다.
  6. x의 타입이 Boolean일 때, x와 y가 모두 true이거나 false이면 true를 리턴하고 아니라면 false를 리턴한다.
  7. x와 y가 같은 객체를 참조하면 true를 리턴하고, 아니라면 false를 리턴한다.

동등 연산자 연산자 (==, Equality)

이번엔 코드가 길어서 미뤄왔던 동등 연산자에 대해서 알아본다.

런타임 함수

동등 연산자에서도 런타임 함수를 찾아 보았다. 이 코드 또한 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());
}

=== 연산자와의 차이와 특징을 살펴본다.

  • StrictEquals 대신 Equals 함수를 호출한다.
  • Maybe는 값이 있을 수도 없을 수도 있는 객체를 표현한다.
  • ReadOnlyRoots 에서 Roots 라는 단어를 정확히 어떤 의미로 사용했는지 모르겠다. 해당 클래스에 주석이 안 달려있다. ReadOnlyHeap 이라는 읽기 전용 공간, 루트, 캐쉬 생성과 파괴를 관리하는 클래스가 있는데, Heap이라는 자료 구조의 Root를 이야기하는 것이 아닐까 추측해본다... 도와주세요 🙇🏾‍♀️

비교 코드

동등 연산자의 실제 비교 코드를 살펴본다. 역시 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)은 다음과 같다.

  1. x와 y의 타입이 같다면,
    a. x의 타입이 Undefined면 true를 리턴한다.
    b. x의 타입이 Null이면 true를 리턴한다.
    c. x의 타입이 Number 라면,
     i.   x가 NaN 라면, false를 리턴한다.
     ii.  y가 NaN 라면, false를 리턴한다.
     iii. x와 y값이 같으면 true를 리턴한다.
     iv. x가 +0이고 y가 -0일 때, true를 리턴한다.
     v.  x가 -0이고 y가 +0일 때, true를 리턴한다.
     vi. false를 리턴한다.
    d. x의 타입이 String일 때, x와 y의 문자 순서와 그 값이 정확이 일치하면 true를 리턴하고 아니라면 false를 리턴한다.
    e. x의 타입이 Boolean일 때, x와 y가 모두 true이거나 false이면 true를 리턴하고 아니라면 false를 리턴한다.
  2. x가 null이고 y가 undefinedtrue를 리턴한다.
  3. x가 undefined이고 y가 null이면 true를 리턴한다.
  4. x가 Number 타입이고 y가 String 타입일 때, x == ToNumber(y)의 결과를 리턴한다.
  5. x가 String 타입이고 y가 Number 타입일 때, ToNumber(x) == y의 결과를 리턴한다.
  6. x의 타입이 Boolean 이면, ToNumber(x) == y의 결과를 리턴한다.
  7. y의 타입이 Boolean 이면, x == ToNumber(y)의 결과를 리턴한다.
  8. x가 String 이나 Number 타입이고 y가 Object 타입일 때, x == ToPrimitive(y)의 결과를 리턴한다.
  9. x가 Object 타입이고 y가 String 이나 Number 타입일 때, ToPrimitive(x) == y의 결과를 리턴한다.
  10. false를 리턴한다.

[Optional] 재미있는(?) 사이트

내용이 너무 딱딱한 것 같아서 동등, 비교 연산자와 관련한 재미있는(?) 사이트를 소개한다. 비교 연산자를 한 눈에 볼 수 있는 척 하게 해주는 사이트이다 ㅋㅋㅋ.
JavaScript Equality Table

소감

지금까지 자바스크립트를 가장한 C++ 코드 블로깅이었다. 자바와 JS의 간단하게 하면 간단하게 끝나는 내용을 깊게 알아보기 위해 C++와 자료구조를 잘 알고 있어야 한다는 느낌을 많이 받았다. 이게 무슨 소용인지 가치가 있는 일인지 헷갈리지만, 그냥 엉덩이 붙히고 앉아서 오랜 시간 하나의 내용을 찾아보는 것을 해내고 있다는 것으로 위안을 삼고있다. 낄낄 😏

참고한 자료

profile
팔로우 기능 생기면 팔로우 당하고 싶다

0개의 댓글