[이펙티브 자바스크립트] 5장 배열과 딕셔너리

Bewell·2023년 8월 4일
1
post-thumbnail

들어가며

  • 객체는 다재다능한 데이터 구조
  • 객체는 key-value로 연관된 기록이나, 상속된 메서드, 추상, 밀집을 표현할 수 있다
  • 이전 장에서는 구조화된 객체와 상속에 대해 익혔다면, 이 장에서는 객체를 컬렉션으로써, 즉 여러개의 요소로 구성된 종합 데이터 구조를 사용하는 방법에 대해 살펴보자

[아이템 43] 직접적인 객체의 인스턴스로 가벼운 딕셔너리를 만들어라

  • 객체는 문자열을 key로 매핑하는 다양한 모음(Collections)을 구현하는데 매우 간편하게 사용 가능
  • 객체를 순회하기 위해 for...in반복문 사용
var dict = {a: 1, b: 2}
var arr = []
for (var key in dict) {
	arr.push(key + ' : ' +dict[key])
}


하지만 모든 객체는 그 prototype 객체의 프로퍼티들을 상속하고, for...in문은 객체의 상속된 프로퍼티 또한 자신이 소유한 프로퍼티로 열거한다.
만약 요소를 딕셔너리 객체 자신의 프로퍼티로 저장하는 사용자 정의 딕셔너리 클래스를 만든다면??

function NaiveDict() {}
NaiveDict.prototype.count = function (key) {
  var i = 0
  for (var name in this) {
    i++
  }

  return i
}
NaiveDict.prototype.toString = function () {
  return '[object NaiveDict]'
}
var dict = new NaiveDict()
dict.alice = 34
dict.bob = 24
dict.chris = 62
dict.count() // 5

문제는 NaiveDict 데이터 구조의 고정된 프로퍼티(count, toString)와 특정 딕셔너리(alice, bob, chris)의 변수 항목을 모두 저장하기 위해 동일한 객체를 사용한다는 점.
따라서 count가 딕셔너리의 프로퍼티를 열거할 때, 우리가 신경 쓰는 항목 대신, 모든 프로퍼티들 (count, toString, alice, bob, chris)의 수를 세게된다.


비슷한 실수로 딕셔너리를 표현하기 위해 Array 타입을 사용하는 경우가 있다.

var dict = new Array()
dict.alice = 34
dict.bob = 24
dict.chris = 62
dict.bob  // 24

불행하게도 이 코드는 프로토타입을 오염시킬 수 있다.
딕셔너리 항목을 열거할 때 프로토타입 객체의 프로퍼티가 예상치 않게 나타날 수 있기 때문

아래와 같이 Array.prototype에 메서드를 추가하고, 배열의 요소를 열거하려고 하면 어떤 일이 벌어질까?

Array.prototype.first = function () {}
Array.prototype.last = function () {}

var names = []
for (var name in dict) {
  names.push(name)
}
names // ['alice', 'bob', 'chris', 'first', 'last']

이는 객체를 가벼운 딕셔너리로써 사용해야 한다는 기본 규칙을 다시 일깨워 준다

오직 Object의 직접적인 인스턴스를 딕셔너리로 사용하고 , NaiveDict와 같은 하위 클래스나, 배열을 사용하지 말아야 한다.
new Object()나 빈객체 리터럴로 교체할 경우 프로토타입 오염에 훨씬 덜 민감해진다.

var dict = {}
dict.alice = 34
dict.bob = 24
dict.chris = 62

var names = []
for (var name in dict) {
  names.push(name)
}
names // ['alice', 'bob', 'chris']

기억할 점

  • 객체 리터럴을 사용해 가벼운 딕셔너리를 만들어라
  • 가벼운 딕셔너리는 for...in 반목문 내에서의 프로토타입 오염을 막기 위해 Object.prototype의 직접적인 자손이여야만 한다 (?)





[아이템44] 프로토타입 오염을 막기 위해 null 프로토타입을 사용하라

프로토타입의 오염을 막기위한 방법으로 생성자의 prototype 프로퍼티를 null이나 undefined로 설정하려고 할 것이다.

function C () {}
C.prototype = null

하지만 이 생성자로 인스턴스를 만들면 여전히 Object의 인스턴스를 갖게 된다.

var o = new C()
Object.getPrototypeOf(o) === null // false
Object.getPrototypeOf(o) === Object.prototype // true

ES5는 처음으로 Object.create를 통해 프로토타입이 없는 객체를 만드는 표준적인 방법을 제공한다

var x = Object.create(null)
Object.getPrototypeOf(o) === null // true

이런 객체에는 포로토타입 오염이 어떠한 영향도 미칠 수 없을 것이다.


Object.create가 지원되지 않는 오래된 자바스크립트 실행환경에서는 __proto__을 쓸 수 있다.

var x = { __proto__: null }
x instanceof Object // false (비표준)

__proto__는 비표준이기 때문에 Object.create사용을 권장한다.

기억할 점

  • ES5 Object.create를 사용해서 프로토타입이 없고, 오염에 덜 민감한 빈 객체를 사용해라
  • 오래된 실행환경에서는 { __proto__: null }을 이용해라
  • __proto__는 비표준이다. 사용에 유의하라





[아이템 45] 프로토타입 오염을 막기 위해 hasOwnProperty를 사용하라

우리가 만든 딕셔너리를 처리하기 위해 객체를 조작하는 자바스크립트의 기본 문법을 사용하려 할 수도 있다.

'alice' in dict // 딕셔너리에 존재한느지 확인
dict.alice // 가져오기
dict.alice = 24 // 갱신

하지만 자바스크립트 객체의 처리는 항상 상속으로 이뤄진다. 빈 객체 리터럴 조차도 Object.prototype의 수많은 프로퍼티들을 상속한다

var dict = {}
'alice' in dict // false
'bob' in dict // false
'chris' in dict // false
'toString' in dict // true
'valueOf' in dict // true

Object.prototype은 hasOwnProperty메소드를 제공하여 딕셔너리 항목들을 테스트할 수 있다

dict.hasOwnProperty('alice') // false
dict.hasOwnProperty('toString') // false
dict.hasOwnProperty('valueOf') // false

Object.prototype에 의해 상속된 hasOwnProperty메서드를 다른 항목으로 저장한다면, 프로토타입의 hasOwnProperty메서드에는 더이상 접근할 수 없다.

dict.hasOwnProperty = 10
dict.hasOwnProperty('alice')
// 오류 : dict.hasOwnProperty는 함수가 아님

이런 경우 hasOwnProperty를 딕셔너리의 메서드로 호출하는 대신에, call을 사용할 수 있다

var hasOwn = Object.prototype.hasOwnProperty
var hasOwn = {}.hasOwnProperty

hasOwn.call(dict, 'alce')

이 방법은 수신자 객체의 hasOwnProperty메서드가 오버라이딩되었는지 상관없이 잘 작동한다



견고한 딕셔너리를 작성하기 위한 모든 기법을 캡슐화하여, 다음과 같은 패턴으로 Dict생성자에 추상화시킬 수 있다

function Dict(elements) {
  // 부가적인 초기 테이블을 허용
  this.elements = elements || {} // 단순한 객체
}
Dict.prototype.has = function (key) {
  //자신이 소유한 프로퍼티만
  return {}.hasOwnProperty.call(this, elements, key)
}
Dict.prototype.get = function (key) {
  //자신이 소유한 프로퍼티만
  return this.has(key) ? this.elements[key] : undefined
}
Dict.prototype.set = function (key, val) {
  this.elements[key] = val
}
Dict.prototype.remove = function (key) {
  delete this.elements[key]
}


var dict = new Dict({
  alice: 34,
  bob: 24,
  chris: 62
})

dict.has('alice') // true
dict.get('bob') // 24
dict.has('valueOf') // false

(추가 내용필요)

기억할 점

  • 프로토타입 오염을 막기 위해 hasOwnProperty를 사용하라
  • hasOwnProperty메서드의 오버라이딩을 막기 위해 어휘적인 스코프오 call메서드를 사용하라





[아이템 46] 순서가 정해진 컬렉션에는 딕셔너리 대신 배열을 사용하라

  • 자바스크립트 객체는 순서없는 프로퍼티의 모음이다
  • ECMAScript 표준은 프로퍼티를 저장하는 특별한 순서에 어떠한 언급도 없다
  • 하지만 문제는, for...in 반복문은 객체의 프로퍼티를 열거하기 위해서는 어떤 순서가 정해져야만 한다는 점이다.
  • (정확히 이해가 잘안됨) for...in 반복문의 정확한 순서에 의해 프로그램의 동작이 변경될 수 있다는 것을 알아차리지도 못할 수 있다.

  • 데이터 구조 내의 항목들의 순서에 의존할 필요가 있다면, 딕셔너리 대신 배열을 사용하라

기억할 점

  • for..in 반복문이 객체의 프로퍼티를 열거할 때 순서에 의존하지 않도록 하라
  • 딕셔너리 안의 데이터를 합한다면, 그 연산이 순서에 민감하지 않은지 확인하라
  • 순서가 정해진 컬렉션을 위해서는 딕셔너리 객체 대신 배열을 사용하라





[아이템 47] Object.prototype에 열거 가능한 프로퍼티를 절대 추가하지 마라

  • for...in 반복문은 굉장히 편리하지만, prototype을 오염시키기 쉽다
  • 만약 딕셔너리 객체에 for...in의 사용을 허용하기 원한다면, 공유된 Object.prototype에 열거할 수 잇는 프로퍼티를 절대 추가하지 마라

만약 객체의 프로퍼티 이름을 배열로 생성해주는 allKey메서드를 추가한다면, 이 메서드는 그 자신의 결과마저 오염시킨다.

Object.prototype.allKeys = function () {
  var result = []
  for (var key in this) {
    result.push(key)
  }

  return result
}

({ a: 1, b: 2, c: 3 }).allKeys() // ['allKeys','a', 'b', 'c']

위의 문제를 해결할 수 있는 방법은 함수로 정의하는 것이다

allKeys(obj) {
  var result = []
  for (var key in obj) {
    result.push(key)
  }
  return result
}

하지만 Object.prototype에 프로퍼티를 추가하기 원한다면 ES5의 Object.defineProperty 메서드를 통해 객체 프로퍼티를 그 속성에 대한 메타데이터와 함께 정의할 수 있다.

Object.defineProperty(Object.prototype, 'allKeys', {
  value() {
    var result = []
    for (var key in this) {
      result.push(key)
    }

    return result
  },
  writable: true,
  enumerable: false,
  configurable: true
})

위 코드는 길고 복잡하지만 Object인스턴스에 대한 for...in 반복문을 오염시키지 않는다는 확실한 이점이 있다.

기억할 점

  • Object.prototype에 프로퍼티를 추가하지 마라
  • Object.prototype에 메서드를 작성하는 대신 함수를 고려하라
  • Object.prototype에 프로퍼티를 추가한다면, ES5의 Object.defineProperty를 사용해서 열거할 수 없는 프로퍼티로 정의하라





[아이템 48] 열거하는 동안 객체를 수정하지 마라

  • for...in 반복문은 열거되었을 때 현재의 수정사항을 유지하지 않는다
  • ECMAScript 표준은 다른 자바스크립트 실행 환경이 동시에 발생하는 수정 사항을 인정하여 서로 다르게 동작할 수 있는 여지를 남겨 두었다.
    • 객체를 열거하는 도중 새로운 프로퍼티가 이 객체에 추가된다면, 새롭게 추가된 프로퍼티는 현재 열거에 포함됨을 보장하지 않는다.

기억할 점

  • for...in 반복문으로 객체의 프로퍼티를 열거하는 동안 객체를 수정하지 마라
  • 반복문 내에서 내용이 변경될 수 있는 객체를 반복할 때는 for...in 대신 while이나 for 반복문을 사용하자
  • 데이터 구조가 변경될 수도 있는 열거에는, 딕셔너리 객체 대신에 배열같은 순차적인 데이터 구조의 사용을 고려하라





[아이템 49] 배열을 반복할 때 for...in 대신 for 반복문을 사용하라

var scores = [98, 74, 85, 77, 93, 100, 89]
var total = 0
for (var score in scores) {
  total += score
}
var mean = total / scores.length
total	//	'00123456'
mean //   17636.571428571428
  • 객체의 프로퍼티 키는 항상 문자열이고 += 병합이 되었다
  • 예측하기 어려운 결과값
  • 배열의 내용을 반복하기 위한 적절한 방법은 for 반복문을 사용하는 것이다
    • 정수형 인덱스와 배열 요소를 얻을 수 있다
    • 강제 형변환도 발생시키지 않음을 보장해준다
    • 적절한 순서로 반복이 진행됨
    • 배열 객체나 프로토타입에 저장된 정수가 아닌 프로퍼티를 우연히 포함하지 않음을 보장한다

기억할 점

  • 인덱스가 지정된 배열의 프로퍼티를 반복할 때는 항상 for...in대신 for반복문을 사용하자
  • 프로퍼티 검색을 재계산하지 않기 위해 배열의 length 프로퍼티를 반복문 앞에 지역변수로 저장하는것을 고려하라





[아이템 50] 반복문 대신 반복 메서드를 사용하라

반복문의 종료 조건을 결정하는 작은 실수들

for (var i = 0; i <= names; i++) {}	//	부가적인 마지막 반복
for (var i = 1; i < n; i++) {}	//	첫번째 반복을 빠뜨림
for (var i = n; i >= n; i--) {}	//	부가적인 첫번째 반복

기억할 점

  • 코드 가독성을 높게 만들고 loop 제어 로직의 복제를 막기 위해 for 반복문 대신에 Array.prototype.forEach와 Array.prototype.map과 같은 반복 메서드를 사용하라
  • 표준 라이브러리가 제공하지 않는 공통적인 반복 패턴을 추상화하기 위해 사용자정의 반복함수를 사용하라
  • 이른 종료가 필요한 경우에는 전통적인 반복문이 여전히 유용한데, 이를 위해 some, every 메서드를 사용할 수 있다





[아이템 51] 유사배역 객체에 범용적인 배열 메서드를 재사용하라

  • Array.prototype의 표준 메서드들은 Array로부터 상속되지 않은 객체더라도 다른 객체들의 메서드처럼 재사용 가능하도록 설계되었다.
  • arguments 객체는 Array.prototype을 상속하지 않기 때문에, forEach를 호출할 수 없지만, call메서드를 사용해서 쓸 수 있다.
function highlight () {
  [].forEach(arguments, function(widget){
      widget.setBackground('yellow')
  })
}


  • 유사 배열 객체의 또다른 예로 DOM의 NodeList클래스를 들 수 있다.
  • arguments 객체와 비슷하게, NodeList는 배열처럼 동작하지만 Array.prototype을 상속하지 않는다
  • 객체를 '배열과 비슷하게' 만드는 조건
    1. 배열은 0부터 2(32승)-1 까지 범위의 정수형 length프로퍼티를 가진다
    2. length 프로퍼티는 객체의 가장 큰 인텍스보다 더 큰 값을 가진다. 인덱서는 정수형이고 0부터 2(23승)-1까지 범위의 값이며 그 문자열 표현이 객체의 키 프로퍼티이다

위 두가지 규칙이 객체가 Array.prototype의 모든 메서드와 호환되도록 구현하기 위한 전부이다

var arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 }
var result = Arra.prototype.map.call(arrayLike, function (s){
  return s.toUpperCase()
})  // [ 'A', 'B', 'C' ]

문자열은 인덱싱될 수 있기 때문에 수정할 수 없는 배열처럼 동작하며, length프로퍼티로 접근할 수 있다. 따라서 Array.prototype으로 사용할 수 있다.

var result = Arra.prototype.map.call('abc', function (s){
  return s.toUpperCase()
})  // [ 'A', 'B', 'C' ]

완전히 범용적으로 사용할 수 없는 Array메서드는 배열 병합 메서드인 concat뿐이다
인자가 진짜 배열이라면 그 내용이 결과로 병합되지만, 그렇지 않으면 단일 요소로 추가된다.
이는 arguments객체의 내용과 배열을 단순히 병합할 수 없다는 의미

function namesColumn() {
  return ['Names'].concat(arguments)
}
namesColumn('Alice', 'Bob', 'Chris')
// [ 'Names', { '0': 'Alice', '1': 'Bob', '2': 'Chris' }]

concat메서드가 유사배열객체를 진짜 배열처럼 처리하기 위해서는 직접 변환해줘야 한다


function namesColumn() {
  return ['Names'].concat0([].slice.call(arguments))
}
namesColumn('Alice', 'Bob', 'Chris')
// [ 'Names', 'Alice', 'Bob', 'Chris' ]

기억할 점

  • 범용적인 Array 메서드들을 메서드 객체로 추출하고 call메서드를 사용하여 유사 배열 객체에 재사용하라
  • 어떤 객체든 인덱싱된 프로퍼티와 적절한 length 프로퍼티를 가진다면 범용적인 Array 메서드를 사용할 수 있다





[아이템 52] Array 생성자 대신 배열 리터럴을 사용하라

Array는 리터럴 배열, Array 생성자로 사용할 수 있다.

var a = [1, 2, 3, 4, 5, 6]
var a = new Array(1, 2, 3, 4, 5, 6)

하지만 Array변수를 다시 바인딩하지 않는지 확인해야 한다

function f(Array) {
  return new Array(1, 2, 3, 4, 5, 6)
}
f(String) // new String(1)

Array = String
new Array(1, 2, 3, 4, 5, 6) // new String(1)

['hello']
new Array('hello')	// 동일

[17]
new Array(17)	//	다름

배열 리터럴을 사용하는 쪽이 훨씬 더 명확하고, 버그를 줄일 수 있다

기억할 점

  • Array 생성자는 첫번째 인자가 숫자일때 다르게 동작한다
  • Array 생성자 대신에 배열 리터럴을 사용하라

0개의 댓글