20210706 JavaSciprt DeepDive 04 : 프로토타입 객체, 생성시점, 생성방식, 교체, 체인, 프로퍼티 섀도잉, 교체, instanceof 연산자, 직접상속, 정적 프로퍼티/메서드, 프로퍼티 존재 확인(in 연산자, hasOwnProperty메서드), 프로퍼티 열거(for…in, Object.keys/values/entries 메서드)

8 분 소요

JavaScript Deep Dive 04


용어 및 중요사항 정리


프로토타입


  • 멀티 패러다임 프로그래밍 언어 : 자바스크립트는 명령형, 함수형, 프로토 타입기반 객체지향 프로그래밍을 지원하는 언어임
  • 객체지향 프로그래밍 : 독립적인 객체의 집합으로 객체의 상태 데이터(프로퍼티), 상태를 조작하는 동작(메서드)을 하나의 논리적 단위로 묶어 프로그램을 표현하려는 프로그래밍 패러다임
    • 추상화 : 필요한 속성만 간추려 내어 표현하는 것
    • 객체 : 속성을 통해 여러개의 값을 하나의 단위로 구성한 복합적인 자료구조
  • 상속 : 어떤 객체의 프로퍼터, 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것
    • 자바스크립트는 프로토 타입을 기반으로 상속을 구현
    • 생성자 함수의 prototype이 인스턴스들의 부모 객체 역할을 하게 하여 메서드를 넣고 상속받게 하여 다른 인스턴스가 사용할 수 있게함
    • 불필요한 중복 제거 -> 코드 재사용
  • 메서드 중복 생성 : 생성자 함수를 통해서 인스턴스를 찍어 낼 때마다 같은 메서드를 생성하게됨 -> 메모리 낭비 (한번 생성해서 공유하여 사용하는게 바람직함)
    • 프로퍼티의 경우에는 값이 달라지니까 인스턴스마다 다르지만, 메서드는 동작방식이 같기 때문에 중복이 발생함




프로토타입 객체


  • 생성자 함수의 prototype 프로퍼티 : 생성자 함수는 자신의 prototype 프로퍼티로 프로토타입 객체에 접근 가능


  • 프로토타입(프로토타입 객체) : 어떤 객체의 부모 객체 역할을 하는 객체로 다른 객체에 공유 프로퍼티,메서드를 제공함으로서 객체간 상속을 구현하기 위해 사용됨
    • 프로토타입은 생성자 함수와 연결되어 있음
    • constructor 프로퍼티 : 프로토타입은 자신의 constructor를 통해서 생성자 함수에 접근 가능


  • 객체(인스턴스)
    • [[Prototype]] 내부 슬롯 : 모든 객체에 있는 내부 슬롯으로 객체생성방식에 따라 프로토타입이 결정되면서 이 슬롯에 프로토타입이 저장됨
    • __proto__ 접근자 프로퍼티 : [[Prototype]] 내부 슬롯이 가리키는 프로토타입에 간접적으로 접근할수 있는 접근자
      • 모든 객체가 직접 소유하는게 아니라 Object.prototype의 프로퍼티에게 상속 받아 사용함
      • __proto__ 접근자 프로퍼티로 프로토타입 접근시 get __proto__ 함수 가 호출
      • __proto__ 접근자 프로퍼티에 새로운 프로토타입 할당시 set __proto__ 함수 가 호출
      • __proto__ 접근자 프로퍼티 사용은 상호참조에 의해 프로토타입 체인이 생성되는 것을 방지 하기 위함 (프로토타입 체인은 단방향 링크드 리스트로 구현되어야 함, 순환참조로 되면 프로퍼티 검색시 무한 루프에 빠짐)
      • 프로토타입의 참조 얻기, 교체하기 :
        • __proto__ 접근자 프로퍼티 사용은 권장하지 않음(사용불가한 객체도 있기 때문)
        • 대신, Object.getPrototypeOf(), Object.setPrototypeOf() 사용 권장


  • 함수 객체(생성자 함수)의 prototype 프로퍼티 :
    • 함수객체만 가진 프로퍼티로 생성자 함수가 생성할 인스턴스의 프로토타입을 가르킴
    • non-constructor인 화살표 함수와 메서드 축약 표현으로정의한 메서드는 prototype을 소유하지 않고, 프로토타입도 없다.
    • 모든 객체가 가진 __proto__ 접근자 프로퍼티와 함수 객체만이 가지고 있는 prototype 프로퍼티는 사용주체만 다르고 동일한 프로토타입을 가르킴
    • 생성자함수의 프로토타입 = 프로토타입(객체) = 인스턴스의 __proto__ 접근자 프로퍼티


  • 프로토타입의 constructor 프로퍼티 : 모든 프로토 타입은 constructor 프로퍼티를 갖고, constructor 프로퍼티는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가르킴
    • 프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재함




리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토 타입


  • new 연산자와 함께 생성자 함수를 호출하지 않고 객체 인스턴스를 생성하는 리터럴 표기법에 의해 생성된 객체도 프로토타입이 존재함
  • 리터럴 표기법에 의해 생성된 객체의 프로토타입의 constructor는 Object 생성자 함수를 가르킴
  • 하지만, 리터럴 표기법으로 객체를 생성하는게 Object 생성자로 객체가 생성되는 것은 아니고 추산연산 OrdinaryObjectCreate()를 호출하여 Object.prototype을 프로토 타입으로 갖는 빈객체를 생성함
  • Object 생성자 또한 인자를 갖지 않고 생성하는 경우에는 추산연산을 사용해서 빈객체를 생성함
  • Function 생성자 함수도 똑같은 매커니즘으로 함수 선언문과 함수 표현식을 통해 만든 함수객체도 Function 생성자로 만들지 않았지만, contructor는 Function 생성자 함수를 가르킴
  • Function 생성자 함수의 함수객체 vs 함수 선언문과 함수표현식의 함수객체 -> 생성과정과 스코프, 클로저 등의 차이를 갖게됨




프로토타입의 생성시점


  • 프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성됨(언제나 쌍으로 존재하기 때문)
  • 사용자 정의 생성자함수의 프로토타입 생성 시점:
    • 생성자 함수가 정의되어 런타임 전에 평가되어 함수객체가 생성 -> 프로토타입 생성됨(consturctor에 생성자 함수 바인딩) -> 함수객체의 prototype 프로퍼티에 프로토타입 바인딩
    • non-constructor는 프로토타입이 생성되지 않음 (화살표 함수)
    • 생성된 프로토타입의 프로토타입은 언제나 Object.protype
  • 빌트인 생성자 함수의 프로토타입 생성 시점:
    • 전역객체 : 코드가 실행되기 이전 단계에 생성되는 특수 객체 (window, global)
    • 전역객체가 생성되는 시점에 빌트인 생성자 생성 -> 빌트인 생성자의 프로토타입이 생성 -> 빌트인 생성자 함수의 prototype 프로퍼티에 프로토타입이 바인딩
  • 공통적으로, 해당 객체가 생성되기전에 생성자함수, 프로토타입은 객체화 되어 생성됨
    • 그리고 이 프로토타입은 생성자함수 또는 리터럴 표기법으로 인스턴스를 생성하면, 생성된 인스턴스 객체의 [[Prototype]] 슬롯에 할당됨




객체 생성 방식과 프로토타입의 결정


  • 객체 생성 방식
    • 객체리터럴, Object 생성자함수, 사용자 생성자 함수, Object.create메서드, 클래스
    • 해당 방식 모두 OrdinaryObjectCreate에 의해 생성됨
    • OrdinaryObjectCreate는 두가지 인수를 받아 처리하여 객체를 생성함
      • 생성할 객체의 프로토타입을 인수로 받음 -> 생성한 객체의 [[Prototype]]슬롯에 할당
      • 생성할 객체에 추가할 프로퍼티 목록을 인수로 받음 -> 객체에 프로퍼티를 추가함
      • OrdinaryObjectCreate가 생성자함수, 프로토타입 - 객체 사이의 연결을 담당


  • 객체리터럴에 의해 생성된 객체의 프로토타입
    • JS엔진이 객체리터럴 평가하여 객체 생성시 OrdinaryObjectCreate를 호출
    • 인수로 Object.prototype을 프로토타입으로 전달
    • Object 생성자 함수 - Object.prototype - 객체
    • 프로퍼티 추가시 객체 리터럴 내부에서 추가(빈 객체인 상태 없이)


  • Object 생성자 함수에 의해 생성된 객체의 프로토타입
    • Object 생성자 함수 호출시 OrdinaryObjectCrate를 호출
    • Object.prototype을 프로토타입으로 전달
    • Object 생성자 함수 - Object.prototype - 객체
    • 객체리터럴에 의해 생성된 객체와 동일한 구조
    • 프로퍼티 추가시 빈객체인 상태에서 프로퍼티를 추가


  • 사용자 생성자 함수에 의해 생성된 객체의 프로토타입
    • new 연산자와 함께 생성자 함수를 호출하여 인스턴스 생성시 OrdinaryObjectCrate를 호출
    • 생성자 함수의 prototype 프로퍼티에 바인딩 되어 있는 객체가 프로토타입으로 전달됨
    • 사용자 생성자 함수 - 사용자 생성자 함수.prototype - 인스턴스 객체
    • 해당 사용자 생성자 함수.prototype은 constructor 프로토타입만 가지고, 빌트인 메서드는 없음 (하지만, 프로토타입 체인으로 사용할 수 있음)
    • 그래서 사용자 생성자 함수.prototype에 프로퍼티를 추가하여 하위 인스턴스가 상속받아 사용할 수 있는 프로퍼티, 메소드를 구현할 수 있고 구현시 프로토타입 체인에 즉각 반영됨




프로토타입 체인


  • 프로토타입 체인 : 객체의 프로퍼티(메서드)에 접근시 해당 객체에 접근하려는 프로퍼티가 없으면 [[Prototype]] 내부 슬롯의 참조를 따라서 자신의 부모역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색해 올라가는 것을 말함 (상속과 프로퍼티 검색을 위한 매커니즘)
    • 프로토타입 체인의 종점: 프로토타입 체인의 최상위의 객체는 언제나 Object.prototype이기 때문에 모든 객체는 상속받아 사용이 가능하고 이를 종점이라고 함
    • Object.prototype의 [[Prototype]] 내부슬롯은 null이고 종점까지 프로퍼티 검색을 못하면 undefined를 반환 받음 (에러가 없음에 주의)
    • 스코프 체인은 식별자를 찾는 매커니즘으로, 스코프체인과 프로토타입체인이 협력하여 식별자와 프로퍼티를 검색하는데 사용됨
  • call 메서드 : this로 사용할 객체를 전달하면서 함수를 호출함(사용못하는 함수를 연결해주는 느낌)




오버라이딩과 프로퍼티 섀도잉


  • 프로토타입 프로퍼티 : 프로토타입이 가진 프로퍼티
  • 인스턴스 프로퍼티 : 인스턴스가 가진 프로퍼티
  • 오버라이딩 : 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용하는 방식
    • 프로토타입 프로퍼티와 같은 이름의 프로퍼티를 인스턴스에 추가하는 경우 프로토타입 프로퍼티를 덮어쓰지 않고, 인스턴스 프로퍼티에 추가되어 검색시 인스턴스 프로퍼티를 먼저 참조하는 현상
    • 다만, 하위 객체에서 프로토타입의 프로퍼티 변경 또는 삭제는 불가능함(get은 가능하나, set은 불가능함)
      • 변경,삭제하고 싶다면, 직접 프로토타입에 접근해야함
  • 프로퍼티 섀도잉 : 오버라딩이에 의해서 프로토타입 프로퍼티가 가려지는 현상
  • 오버로딩 : 함수의 이름은 동일하지만 매개변수의 타입 또는 개수가 다른 메서드를 구현하고 매개변수에 의해 메서드를 구별하여 호출하는 방식(자바스크립트에서는 arguments 객체를 사용하여 구현가능)




프로토타입 교체


  • 프로토타입 교체 : 프로토타입(부모 객체)을 동적으로 임의의 다른 객체로 변경가능함
    • 생성자함수 또는 인스턴스에 의해 교체 할수 있음
    • 생성자 함수에 의한 교체 : 미래에 생성할 인스턴스의 프로토타입 교체
      • 생성자함수로 prototype 프로퍼티에 접근해서 객체리터럴을 할당
      • constructor 프로퍼티 없는 객체 리터럴을 할당하여 교체하면, 해당 객체리터럴이 암묵적으로 가진 프로토타입인 Object.prototype이 상위 프로토타입이 되어 생성자함수와의 연결이 끊어지고, Object.prototype의 contructor인 Objcect 생성자가 연결됨
      • 생성자 함수를 가르키도록하는 constructor 프로퍼티를 추가하여 생성자 함수와 연결해주어야 함
    • 인스턴스에 의한 교체 : 이미 생성된 인스턴스의 프로토타입 교체
      • Object.setPrototypeOf(인스턴스. 객체리터럴)를 활용해 인스턴스의 __proto__ 접근자로 접근하여, 교체할 객체 리터럴을 할당함
      • 인스턴스 방식 또한 constructor 프로퍼티가 없는 객체리터럴을 할당시키면, 생성자 함수와의 연결이 끊어짐으로 생성자 함수를 가르키는 constructor 프로퍼티를 추가한 객체리터럴을 할당해야한다.


  • 프로토타입 교체를 통한 상속 관계를 동적으로 변경하는 것은 번거러우니, 직접교체하지 말고 직접 상속 방법을 사용해야 더 편리하고 안전함




instanceof 연산자


  • 객체 instanceof 생성자 함수
    • 우변에 함수가 아니면 typeError 발생
    • 우변에 생성자 함수의 prototype에 바인딩된 객체가 좌변의 객체의 프로토타입 체인상에 존재하면 true, 아니면 false로 평가
    • 사용자 생성자 함수의 프로토타입의 경우 cunstructor로 사용자 생성자함수와 연결되기었기도 하고, 프로토타입도 객체라서 __proto__ 프로퍼티로 Object 생성자 함수에도 연결 되어 있음 -> instnaceof 확인하면 사용자 생성자함수, Object 생성자함수 둘다 true임
    • instanceof는 프로토타입 체인을 확인할 수 있게 도와줌 (양방향 연결을 조건으로 하지 않고 단방향 연결을 조건으로 함)
      • 교체된 프로토타입의 constructor에 사용자 생성자함수가 연결되지 않아도, 사용자 생성자함수에 교체된 프로토타입이 연결되어 있다면, instanceof는 true임




직접 상속


  • Object.create에 의한 직접 상속
    • Object.create 호출시 OrdinaryObjectCreate를 호출함
    • 명시적으로 프로토타입을 지정하여 새로운 객체를 생성함
      • Object.create(상속 받을 프로토타입 객체, {프로퍼티 키: 프로퍼티 디스크립터 객체})
      • 두번째 인수는 옵션으로 생략 가능
      • 상속 받을 프로토타입 객체의 프로토타입 체인에 속하는 객체를 생성함
      • 객체를 생성하면서, 동시제 직접적으로 상속을 구현할 수 있음
    • 상속받을 프로토타입을 null을 넣게 되면 프로토타입의 종점이 되어 Object.prototype의 빌트인 메서드를 사용할 수 없음
    • ESLint에서는 객체가 Object.prototype의 빌트인 메서드를 직접 호출하는 것을 권장하지 않음 -> Object.create를 생성해서 종점 객체를 생성 함으로서 편하게 Object.prototype 빌트인 메서드를 사용할수 없는 상황을 만들기 때문에
    • 그래서 간접 호출로 call을 활용함
  • Object.create의 장점
    • new 연산자 없이 객체 생성
    • 프로토타입 지정하면서 객체생성
    • 객체 리터럴에 의해 생성된 객체도 상속 받을 수 있음


  • 객체 리터럴 내부에서 __proto__에 의한 직접 상속
    • Object.create 메서드를 통해서 두번째 인자로 프로퍼티를 정의하는 것은 번거로움
    • ES6에서는 객체 리터럴 내부에서 __proto__접근자 프로퍼티를 사용해서 직접 상속 구현가능
    • 객체 리터럴 방식으로 만들려는 객체에 직접적으로 __proto__접근자 프로퍼티에 상속받을 프로토타임을 할당하여 객체를 생성하면서 프로토타입을 지정할 수 있음




정적 프로퍼티/메서드


  • 정적(static)프로퍼티/메서드 : 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출 할수 있는 프로퍼티/메서드를 말함
  • 사용자 생성자 함수는 객체이므로 자신의 프로퍼티/메서드를 소유할 수 있는데 이렇게 사용자 생성자 함수가 가진 프로퍼티/메서드를 말함
    • 생성자 함수 정의시 this를 사용한 프로퍼티/메서드는 인스턴스가 가질 프로퍼티/메서드로서 들어가는 것을 정의하는 것임
    • 생성자 함수 정의시 this를 사용하지 않은 프로퍼티/메서드는 사용자 생성자 함수 자신의 프로퍼티/메서드로 정적 프로퍼티/메서드임
  • 사용자 생성자 함수만 참조/호출가능하고 인스턴스는 참조/호출이 불가함
    • 예시) Object.create vs Object.prototype.hasOwnProperty
      • Object.create : Object의 정적 메서드
      • Object.prototype.hasOwnProperty : Object.prototype으로 객체의 종점이므로 모든 객체에서 사용가능
  • 표기법 만으로도 정적메서드인지, 프로토타입 프로퍼티/메서드인지 알수 있음
  • 프로토타입 프로퍼티/메서드 축약 표기 : .prototype. -> #




프로퍼티 존재 확인


  • in 연산자
    • '프로퍼티 키' in 객체 : 객체 내에 특정 프로퍼티가 존재하는지 여부를 확인함
    • 상속받은 모든 프로토타입의 프로퍼티를 확인하므로 주의
  • Reflect.has() 메서드 : in 연산자와 동일하게 작동
    • Reflect.has(객체, '프로퍼티 키')
  • Object.prototype.hasOwnProperty() 메서드 : 고유 프로퍼티 인지 존재 확인 가능
    • 인스턴스.hasOwnProperty('프로퍼티키')




프로퍼티 열거


  • for...in 문 : 객체의 모든 프로퍼티를 순회하여 열거함
    • for (변수선언문 in 객체) {...}
    • 객체의 프로퍼티 개수만큼 순회하며 변수 선언문에서 선언한 변수에 프로퍼티키를 할당
    • 상속받은 프로토타입의 프로퍼티까지 열거 (Object.prototype의 프로퍼티중 Enumerable이 false인것을 제외하고)
    • 즉, for …in 문은 객체의 프로토타입 체인상에 존재하는 모든 프로토타입의 프로퍼티 중에서 프로퍼티 어트리뷰트 [[Enumerable]] 값이 true인 프로퍼티를 순회하며 열거함
    • 열거시 순서를 보장하지 않음 (최신 브라우저의 경우만 보장, 숫자형태의 문자열인 프로퍼티 키만 정렬실시)
    • 프로퍼티 키가 심벌인 프로퍼티는 열거하지 않음
    • 프로토타입 체인이 아닌 자신만 가지고 있는 프로퍼티를 열거하고 싶다면, if 문과 hasOwnProperty를 활용하여 걸러내며 열거하게 하면 됨 (Object.key/value,entries 메서드 권장)
    • 배열인 자료구조인 경우에는 일반적인 for, for…of문, forEach메서드를 권장함(요소인 프로퍼티만 다루기 위함)
      • for ... of: 선언한 변수에 배열에서 가져온 키가 아닌 값을 할당함


  • Object.keys/values/entries 메서드 : 객체 자신의 고유 프로퍼티만 열거하는데 사용하는 메서드로 권장함
    • Object.keys() : 객체 자신의 열거 가능한 프로퍼티 키를 배열로 반환함
    • Object.values() : 객체 자신의 열거 가능한 프로퍼티 값을 배열로 반환함
    • Object.entries() : 객체 자신의 열거 가능한 프로퍼티 키와 프로퍼티 값의 쌍을 배열로 반환함
      • forEach와 사용시 좋음