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']
기억할 점
프로토타입의 오염을 막기위한 방법으로 생성자의 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사용을 권장한다.
기억할 점
{ __proto__: null }
을 이용해라__proto__
는 비표준이다. 사용에 유의하라우리가 만든 딕셔너리를 처리하기 위해 객체를 조작하는 자바스크립트의 기본 문법을 사용하려 할 수도 있다.
'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
(추가 내용필요)
기억할 점
(정확히 이해가 잘안됨) for...in 반복문의 정확한 순서에 의해 프로그램의 동작이 변경될 수 있다는 것을 알아차리지도 못할 수 있다.
데이터 구조 내의 항목들의 순서에 의존할 필요가 있다면, 딕셔너리 대신 배열을 사용하라
기억할 점
만약 객체의 프로퍼티 이름을 배열로 생성해주는 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 반복문을 오염시키지 않는다는 확실한 이점이 있다.
기억할 점
기억할 점
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 (var i = 0; i <= names; i++) {} // 부가적인 마지막 반복
for (var i = 1; i < n; i++) {} // 첫번째 반복을 빠뜨림
for (var i = n; i >= n; i--) {} // 부가적인 첫번째 반복
기억할 점
function highlight () {
[].forEach(arguments, function(widget){
widget.setBackground('yellow')
})
}
위 두가지 규칙이 객체가 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는 리터럴 배열, 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) // 다름
배열 리터럴을 사용하는 쪽이 훨씬 더 명확하고, 버그를 줄일 수 있다
기억할 점