chapter21. 객체 프로퍼티 설정과 프락시

객체 프로퍼티에는 데이터 프로퍼티접근자 프로퍼티 두 가지가 있다.
접근자 프로퍼티는 메서드와 비슷한데, 1. gettersetter 두 가지 함수로 구성된 점과 2.접근했을때 함수라기보단 데이터 프로퍼티와 비슷하게 동작한다는 점에서 조금 다르다. 이런 점에서 접근자 프로퍼티를 동적 프로퍼티라고 부르기도 한다.

이메일 주소 체크
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// setEmail과 getEmail메서드가 있다.
//@가 들어있는 문자열은 모두 유효한 이메일 주소라고 간주
const USER_EMAIL = Symbol();
class User {
setEmail(value) {
if (!/@/.test(value)) throw new Error(`invalid email ${value}`);
this[USER_EMAIL] = value;
}
getEmail() {
return this[USER_EMAIL];
}
}

//이 클래스는 다음과 같은 방식으로 사용해야함
const u = new User();
u.setEmail("join@dot.com");
console.log(`${u.getEmail()}`);

//그러나 우리는 이 방식을 더 자연스럽게 느낌
const u = new User();
u.email = "join@dot.com";
console.log(`${u.getEmail()}`);
USER_EMAIL 프로퍼티 대신 두 가지 메서드(setEmail,getEmail)을 쓴 이유는 잘못된 이메일 주소가 저장되는 것을 방지하려고함.
 프로퍼티에는 심볼을 서 실수로 직접 접근하는 것을 방지

접근차 프로퍼티를 사용하면 후자의 자연스러운 문법을 사용하면서도, 부주의한 접근을 차단하는 전자의 장점을 누릴수 있다.

접근자 프로퍼티를 사용하여 이메일 체크
1
2
3
4
5
6
7
8
9
10
11
//접근자 프로퍼티를 사용하여 이메일 체크
const USER_EMAIL = Symbol();
class User {
set email(value) {
if (!/@/.test(value)) throw new Error(`invalid email ${value}`);
this[USER_EMAIL] = value;
}
get email(value){
return this[USER_EMAIL];
}
}
  함수 두 개(set,get)을 사용했지만 두 함수는 email 프로퍼티 하나에 묶였다.
  프로퍼티를 할당할때는 setter 가 호출되고, 할당하는 값이 첫 번째 매개변수로 전달된다. 프로퍼티를 평가할 때는 getter가 호출된다.
  setter없이 getter만 만들수도 있다.
1
2
3
4
5
6
7
8
9
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
get perimeter() {
return this.width * 2 + this.height * 2;
}
}

21.2 객체 프로퍼티 속성

프로퍼티에는 항상 키가 있고, 키는 문자열이나 심볼일 수 있다.
프로퍼티의 값은 어떤 타입이든 괜찮다.
배열이나 맵과 달리 객체의 프로퍼티에는 순서가 없다.
객체 프로퍼티에 접근 할 떄는 점(.)연산자나 대괄호 연산자([])를 사용한다.
객체 프로퍼티는 식별자를 키로 사용하는 일반적인 프로퍼티, 심볼이나 표현식을 사용하는 계산된 프로퍼티, 메서드 단축 표기의 세 가지가 있다.
프로퍼티에는 자신이 속한 객체 안에서 어떻게 동작할지 결정하는 속성이 있다.
우리가 아는 방식으로 프로퍼티를 만들고, Object.getOwnPropertyDescriptor을 서서 속성을 알아보자.

1
2
3
const obj = { foo: "bar" };
Object.getOwnPropertyDescriptor(obj, "foo");
//{value: "bar", writable: true, enumerable: true, configurable: true}

프로퍼티 속성 3가지가 모두 나타남

  • 쓰기 가능한지(writable)
    프로퍼티 값을 바꿀 수 있는지 아닌지 판단

  • 나열 가능한지(enumerable)
    for…in문이나 Object.keys, 확산 연산자에서 객체 프로퍼티를 나열할 때 해당 프로퍼티가 포함될지 아닐지 판단

  • 설정 가능한지(configurable)
    프로퍼티를 객체에서 삭제하거나 속성을 수정할 수 있는지

Object.defineProperty로는 프로퍼티 속성을 컨트롤하거나, 새 프로퍼티를 만들 수 있다.

1
2
3
4
5
6
7
8
//읽기 전용 프로퍼티에 값을 할당하려고할 때 에러가 발생하는것은 스트릭트 모드에서 뿐이다.
"use strict";
Object.defineProperty(obj, "foo", { writable: false });

//foo에 값을 할당하려고 하면 오류가 난다.
obj.foo = 3;

//VM121:7 Uncaught TypeError: Cannot assign to read only property 'foo' of object '#<Object>'

Object.defineProperty를 써서 객체에 새 프로퍼티를 추가 할 수도 있다.
일반적인 데이터 프로퍼티와 달리, 객체가 일단 생성된 뒤에는 접근자 프로퍼티를 추가할 다른 방법이 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//obj에 color프로퍼티를 추가해보기
Object.defineProperty(obj, "color", {
get: function() {
return this._color;
},
set: function(value) {
this._color = value;
}
});

obj.color = "red";
console.log(obj);

//{foo: "bar", _color: "red"}

Object.defineProperty로 데이터 프로퍼티를 추가 할 때는 value프로퍼티를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
//obj에 name 과 greet 프로퍼티를 추가해보기
Object.defineProperty(obj, "name", {
value: "cynthia"
});
Object.defineProperty(obj, "greet", {
value: function() {
return `hello my name is ${obj.name}`;
}
});

console.log(obj.name); //cynthia
console.log(obj.greet()); //hello my name is cynthia

Object.defineProperty는 배열 프로퍼티를 나열할 수 없게 만들때 주로 사용한다.
배열은 원래 프로퍼티를 사용하지 않도록 설계되었으므로 문자열이나 심볼 프로퍼티는 사용하지 않는 편이 좋다.
배열에서 for…in이나 Object.keys를 사용하는 것 역시 권장하지 않는다.
따라서 배열에 숫자형 프로퍼티가 아닌 다른 프로퍼티를 추가한다면 , 그 배열에서 for…in이나 Object.keys를 사용했을때 노출되지 않도록 나열할 수 없게 만들어야 함

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//배열에 sum와 avg 메서드를 추가하는 경우
const arr = [3, 1.5, 9, 2, 5.2];
arr.sum = function() {
return this.reduce((a, x) => a + x);
};
arr.avg = function() {
return this.sum() / this.length;
};

// for...in문이나 Object.keys, 확산 연산자에서 객체 프로퍼티를 나열할 때 해당 프로퍼티가 포함될지 아닐지 판단
Object.defineProperty(arr, "sum", { enumerable: false });
Object.defineProperty(arr, "avg", { enumerable: false });

Object.keys(arr);

Object.defineProperties : 객체 프로퍼티 이름과 프로퍼티 정의를 서로 연결

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Object.defineProperties
const arr = [3, 1.5, 9, 2, 5.2];
Object.defineProperties(arr, {
sum: {
value: function() {
return this.reduce((a, x) => a + x);
},
enumerable: false
},
avg: {
function() {
return this.sum() / this.length;
},
enumerable: false
}
});

21.3 객체 보호 : 동결, 봉인, 확장 금지

자바스크립트에는 객체를 보호해서 의도하지 않은 수정을 막고, 의도적인 공격은 더 어렵게 만드는 세 가지 메커니즘이 있다. 동결(freezing),봉인(sealing),확장금지(preventing extension)이다.

객체동결 :
동결된 객체는 수정할 방법이 없다. 일단 객체를 동결하면 다음과 같은 작업이 불가능해진다.
객체를 동결하면 그 객체는 문자열이나 숫자처럼 불변이된다.
Object.freeze를 사용하고, 객체가 동결됐는지 확인 할 때는 Object.isFrozen을 사용한다.

  • 프로퍼티 값 수정 또는 할당
  • 프로퍼티 값을 수정하는 메서드 호출
  • setter 호출
  • 새 프로퍼티 추가
  • 새 메서드 추가
  • 기존 프로퍼티나 메서드의 설정 변경
객체동결
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//## 21.3 객체 보호 : 동결, 봉인, 확장 금지
"use strict";
const appInfo = {
company: "white knight software, Inc",
version: "1.3.5",
buildId: "padddddddddddddddddd",
//이 함수는 getter이므로 동결한 상태에서도 계속 동작한다.
copyright() {
return `${new Date().getFullYear()},${this.company}`;
}
};

Object.freeze(appInfo); //동결
Object.isFrozen(appInfo); //true

appInfo.newPro = "test";
//VM3275:16 Uncaught TypeError: Cannot add property newPro, object is not extensible

delete appInfo.company;

appInfo.company; //"white knight software, Inc"

Object.defineProperty(appInfo, "company", { enumerable: false });
//VM3316:2 Uncaught TypeError: Cannot redefine property: company...

객체 봉인 :
객체를 봉인하면 새 프로퍼티를 추가하거나 기존 프로퍼티를 변경 , 삭제할 수 없습니다. (프로퍼티의 값을 변경하는 것은 가능)
클래스의 인스턴스를 사용하면서, 인스턴스의 프로퍼티를 수정하는 메서드가 동작하도록 할때 봉인을 사용할 수 있습니다.
Object.seal로 객체를 봉인하고, 확인할때는 Object.isSealed를 사용한다.

객체 봉인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//객체 봉인
class Logger {
constructor(name) {
this.name = name;
this.log = [];
}
add(entry) {
this.log.push({
log: entry,
timestamp: Date.now()
});
}
}
const log = new Logger("Logs");
Object.seal(log);
Object.isSealed(log); //true

log.name = "capting boring log"; //ok
log.add("another boring day at sea..."); //ok

log.newPro = "test";
//TypeError: Cannot add property newPro, object is not extensible

log.name = "test";

delete log.name; //VM4096:23 Uncaught TypeError: Cannot delete property 'name' of #<Logger>

Object.defineProperty(log, "log", { enumerable: false });
//Uncaught TypeError: Cannot redefine property: log...

확장 금지 : 객체에 새 프로퍼티를 추가하는 것만 금지된다.
프로퍼티에 값을 할당하거나 , 삭제하거나, 속성을 변경하는 작업은 모두 허용된다.
확장을 금지할땐 Object.preventExtensions, 확장 금지를 확인할때는 Object.isExtensible을 사용한다.

확장금지
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
"use strict";
class Logger {
constructor(name) {
this.name = name;
this.log = [];
}
add(entry) {
this.log.push({
log: entry,
timestamp: Date.now()
});
}
}
const log2 = new Logger("First Mate's log");
Object.preventExtensions(log2);
Object.isExtensible(log2); //false

log2.name = "first.."; //true
log2.add("another..."); //true

log2.newPro = "test"; //VM5050:18 Uncaught TypeError: Cannot add property newPro, object is not extensible

log2.name = "test.."; //true
delete log2.name; //true
Object.defineProperty(log, "log", { enumerable: false });
// Uncaught TypeError: Cannot redefine property: log...

표 21-1 객체 보호 옵션

21.4 프락시

프락시(proxies)는 ES6에서 새로 추가된 메타프로그래밍 기능이다.
메타프로그래밍이란 자기 자신을 수정하는 것을 말한다.
객체 프락시는 객체에 대한 작업을 가로채고, 필요하다면 작업 자체를 수정하는 기능이다.

프락시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const cofficients = {
a: 1,
b: 2,
c: 5
};
//이 객체의 프로퍼티가 수학의 계수라면
function evaluate(x, co) {
return co.a + co.b + co.c * Math.pow(x, 2);
}

evaluate(4, cofficients); //83

//그런데,
//계수 일부가 빠진 객체를 가지고 계산해야한다면?
const cofficients = {
a: 1,
c: 5
};
evaluate(4, cofficients); //NaN

//cofficients.b에 0을 할당하면 문제를 해결할수 있지만 프락시를 쓰는 게 더 낫다.
//프락시는 정의되지 않은 프로퍼티는 항상 0을 반환하게 만들 수 있다.

//cofficients에 객체의 프락시 생성
const betterCofficients = new Proxy(cofficients, {
get(target, key) {
return target[key] || 0;
}
});
evaluate(4, cofficients); //NaN
evaluate(4, betterCofficients); //81

//betterCofficients 객체의 프락시에는 무한한 프로퍼티가 있고,
//직접 정의한 프로퍼티를 제외하면 값이 0인것과 마찬가지이다.
betterCofficients.a; //1
betterCofficients.b; //0
betterCofficients.c; //5
betterCofficients.d; //0

//키로 소문자 한 글자만 받았을때 프락시가 동작하게 할 수도 있다.
const betterCofficients = new Proxy(cofficients, {
get(target, key) {
if (!/^[a-z]&/.text(key)) return target[key];
return target[key] || 0;
}
});

Proxy 생성자에 넘기는 첫 번째 매개변수는 타켓, 즉 프락시 할 객체이다. 두번째 매게변수는 가로챌 동작을 가리키는 핸들러이다. 여기서 프로퍼티에 접근하는 동작만 가로챘으며, get 함수가 핸들러이다. get 함수는 매개변수로 타켓, 프로퍼티키(문자열 또는 심볼), 수신자(프락시 자체 또는 프락시에서 파생되는 것)을 받는다.
해당 키가 타켓에 있는지 확인하고, 없으면 o을 반환한다.

마찬가지로, 프로퍼티에 값을 할당하려고 할 때 set 핸들러로 가로챌 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//객체에 위험한 프로퍼티가 있어서,
//한 단계를 더 거치지 않으면 값을 할당하거나 메서드를 호출할수 없게함
const cook = {
name: "Wait",
redPhosphours: 100, //위험하다고
water: 500 //안전하다고
};
const protectCook = new Proxy(cook, {
set(target, key, value) {
if (key === "redPhosphours") {
if (target.allowDangerousOperations)
return (target.redPhosphours = value);
else return console.log("Too dangerous");
}

//다른 프로퍼티는 모두 안전함
target[key] == value;
}
});
protectCook.water; //500
protectCook.redPhosphours; //100

protectCook.redPhosphours = 150; //Too dangerous
protectCook.allowDangerousOperations = true; // true

Comentarios

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×