一.문제 배경
시나리오는 다음과 같습니다:
'use strict';
var F = function() {
this.arr = [1, 2, 3, 4, 5, 6, 7];
var self = this;
Object.defineProperty(self, 'value', {
get: function() {
if (!self._value) {
self._value = self.doStuff();
}
return self._value;
},
set: function(value) {
return self._value = value;
}
})
}
F.prototype.doStuff = function() {
return this.arr.reduce(function(acc, value) {return acc + value}, 0);
};
F 의 인스턴스는 value 속성을 가지고 있지만, new 할 때 속성 값을 초기화하고 싶지 않습니다 (이 값은 반드시 사용된다고 할 수 없고, 계산 비용이 높으며, 또는 new 할 때 아직 계산할 수 없을 수 있기 때문입니다). 자연스럽게 getter 를 정의하여 "필요에 따라 계산"을 구현하는 것을 생각합니다:
var f = new F();
// 이때 f 에는 value 속성이 있지만, 값이 무엇인지는 아직 모릅니다
// 처음 이 속성에 액세스할 때 초기 값을 계산합니다 (doStuff 를 통해)
f.value
var tmpF = new F()
// value 속성에 액세스하지 않으면, 초기 값을 계산할 필요가 영원히 없습니다
이를 통해 사전에 불필요한 고비용 작업을 피할 수 있습니다. 예를 들어:
-
DOM 조회
-
layout(getComputedStyle()등) -
깊이 우선 탐색
물론, 직접 getValue() 를 추가해도 원하는 효과를 얻을 수 있지만, getter 가 사용자에게 더 친절하며, 외부에서는 값이 미리 계산된 것인지 현장에서 계산된 것인지 전혀 알 수 없습니다
delete 의이상한 동작은 2 부분으로 나뉩니다:
// 1.defineProperty 로 정의된 속성을 delete 하면 오류
// Uncaught TypeError: Cannot delete property 'value' of #<F>
delete f.value
// 2.플레이스홀더 초기 값을 추가하면, 정상적으로 delete 할 수 있게 됨
// F 의 value 정의 부분을 다음과 같이 변경
var self = this;
self.value = null; // 플레이스홀더, delete 오류 회피
Object.defineProperty(self, 'value', {/*...*/});
二.원인 분석
delete 오류
delete 연산자의 규칙은: delete 성공 시 true 를 반환하고, 그렇지 않으면 false 를 반환하는 것을 기억할 것입니다
삭제 성공 여부와 관계없이, 오류가 발생해서는 안 됩니다. 실제로 오류가 발생하는 것은 엄격 모드를 활성화했기 때문입니다:
Third, strict mode makes attempts to delete undeletable properties throw (where before the attempt would simply have no effect):
(Strict mode - JavaScript | MDN 에서 인용)
엄격 모드에서는 삭제할 수 없으면 오류가 발생합니다. 그러나 이미 defineProperty() 로 value 속성을 추가했는데, 왜 삭제할 수 없는 것일까요? configurable 이 원인입니다:
configurable
true if and only if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object.
Defaults to false.
이것이 기본적으로 false 라니.調べて보니 다른 몇 가지도 기본적으로 false 인 것을 발견했습니다:
configurable Defaults to false.
enumerable Defaults to false.
writable Defaults to false.
value, get, set Defaults to undefined.
descriptor 를 정의하면 속성의 읽기 쓰기 방식이 변경되므로, !writable 는 아직 합리적이지만, !enumerable 는 조금 강하고, !configurable 는 지나칩니다. 그러나 규칙이 이렇기 때문에, 이상한 동작 1 은 합리적입니다
플레이스홀더 초기 값
속성이 이미 존재하는 경우, defineProperty() 는 조금 양보하여 원래 descriptor 의 기분을 고려한다고 추측:
var obj = {};
obj.value = null;
var _des = Object.getOwnPropertyDescriptor(obj, 'value');
Object.defineProperty(obj, 'value', {
get: function() {},
set: function() {}
});
var des = Object.getOwnPropertyDescriptor(obj, 'value');
console.log(_des);
console.log(des);
결과는 다음과 같습니다:
// _des
{
configurable: true,
enumerable: true,
value: null,
writable: true
}
// des
{
configurable: true,
enumerable: true,
get: (),
set: ()
}
defineProperty() 후, configurable 와 enumerable 는 원래 그대로 변하지 않는 것을 발견했습니다. 따라서 플레이스홀더 값을 추가하면 삭제할 수 있게 됩니다. 또한 writable 는 없어졌습니다. getter/setter 를 정의한 후, 쓰기 가능 여부는 getter/setter 의 구체적인 구현에 달려 있으며, 한눈에 알 수 없기 때문입니다 (예를 들어 setter 가 새로운 값을 버리거나, getter 가 불변의 값을 반환하는 경우, 효과는 모두 쓰기 불가입니다)
三.delete 의 규칙
delete 문제를 만난 김에, 조금 더 자세히 살펴보겠습니다
delete var
일반적으로 delete 는 var 선언한 변수는 삭제할 수 없고, 속성은 삭제할 수 있다고 생각합니다. 실제로는완전히 정확하지는 않습니다. 예를 들어:
var x = 1;
delete x === false
// var 선언한 변수를 삭제할 수 있음
eval('var evalX = 1');
delete evalX === true
// 속성이라도 삭제할 수 없는 경우 있음
var arr = [];
delete arr.length === false
var F = function() {};
delete F.prototype === false
// DOM, BOM 객체에서 말을 듣지 않는 것이 더 많음
적어도 형식적으로는, delete 는 var 선언한 변수를 삭제할 수 없다는 정확하지 않습니다. evalX 가 삭제될 수 있는 이유는 흥미롭고, 몇 가지를 이해할 필요가 있습니다: 실행 환경, 변수 객체/활성화 객체, eval 환경의 특수성
실행 환경
실행 환경은 3 종류 있습니다: Global 환경 (script 태그로 둘러싸인 환경 등), Function 환경 (onclick 속성 값의 실행 환경, 함수 호출로 생성되는 실행 환경 등), eval 환경 (eval 에 전달된 코드의 실행 환경)
변수 객체/활성화 객체
각 실행 환경은 변수 객체에 대응하며, 소스 코드에서 선언된 변수와 함수는 변수 객체의 속성으로 존재합니다. 따라서 전역 작용역에서 선언된 것은 global 의 속성이 됩니다. 예를 들어:
var p = 'value';
function f() {}
window.p === p
window.f === f
Function 실행 환경의 경우, 변수 객체는 일반적으로 global 이 아니며, 활성화 객체라고 합니다. Function 실행 환경에 들어갈 때마다 활성화 객체가 생성되며, 함수 본문에서 선언된 변수와 함수 외에도 각 인수 및 arguments 객체도 활성화 객체의 속성으로 존재하지만, 직접 검증할 수는 없습니다
주의: 변수 객체와 활성화 객체는 추상적인 내부 메커니즘으로, 변수 작용역을 유지하고 환경을 격리하는 등에 사용되며, 직접 액세스할 수 없습니다. Global 환경에서 변수 객체가 global 인 것처럼 보여도, 이 global 도완전히내부의 변수 객체는 아닙니다 (속성 액세스上で 교차점이 있을 뿐입니다)
P.S.변수 객체와 활성화 객체와 같은 "현묘"한 것은 너무 깊이 추구할 필요는 없습니다. 각각이 무엇인지, 어떤 관계가 있는지는 중요하지 않으며, 그 작용을 이해하면 됩니다
eval 환경의 특수성
eval 실행 환경에서 선언된 속성과 함수는 호출 환경 (즉, 상위 실행 환경) 의 변수 객체의 속성으로 존재합니다. 이는 다른 두 환경과 다른 점이지만, 물론 직접 검증할 수도 없습니다 (변수 객체에 직접 액세스할 수 없기 때문)
변수 객체의 속성에는 몇 가지 내부 특징이 있습니다. 예를 들어 눈에 보이는 configurable, enumerable, writable 입니다 (물론 내부 구분은 더 세밀할 수 있으며, 삭제 가능 여부는 configurable 의 일부일 뿐입니다)
따르는규칙은: 선언을 통해 생성된 변수와 함수에는 삭제할 수 없다는 천부의 권리가 있고, 명시적 또는 암시적 속성 대입을 통해 생성된 변수와 함수에는 이 천부의 권리가 없다는 것입니다
내장된 몇 가지 객체 속성에도 삭제할 수 없다는 천부의 권리가 있습니다. 예를 들어:
var arr = [];
delete arr.length === false
void function(arg) {console.log(delete arg === false);}(1);
속성 대입을 통해 생성된 변수와 함수에는 삭제할 수 없다는 천부의 권리가 없으므로, 대입을 통해 생성된 변수와 함수는 삭제할 수 있습니다. 예를 들어:
x = 1;
delete x === true
window.a = 1
delete window.a === true
마찬가지로 global 에 추가되는 전역 변수 선언을 통해 생성된 것은 삭제할 수 없습니다:
var y = 2;
delete window.y === false
생성 방식이 다를 뿐이며, 생성 시 천부의 권리가 결정됩니다
게다가, 흥미로운 시도가 있습니다. eval 이 직접 바깥쪽의 변수 객체를 가져오고, eval 환경에서 선언된 것에는 삭제할 수 없다는 천부의 권리가 없다면, 둘을 결합하면 강제로 삭제를 오버라이드할 수 있을까요?
var x = 1;
/* Can't delete, `x` has DontDelete */
delete x; // false
typeof x; // "number"
eval('function x(){}');
/* `x` property now references function, and should have no DontDelete */
typeof x; // "function"
delete x; // should be `true`
typeof x; // should be "undefined"
결과는 오버라이드 후에도 삭제할 수 없습니다. 변수 객체 위에서 선언 방식으로 내부적으로 추가된 속성은, descriptor 변경을 금지하는 것 같습니다. 위의 x 값은 오버라이드되었지만, 삭제할 수 없다는 천부의 권리는 남아 있습니다
四.정리
defineProperty() 로 정의된 새로운 속성은, 그 descriptor 의 기본 몇 가지 속성이 모두 false 입니다. 즉, 열거 불가, descriptor 변경 불가, 삭제 불가입니다. 예를 들어:
var obj = {};
Object.defineProperty(obj, 'a', {configurable: true, value: 10});
Object.defineProperty(obj, 'a', {configurable: true, value: 100});
delete obj.a === true
Object.defineProperty(obj, 'b', {value: 11});
delete obj.b === false
// 오류, descriptor 변경을 허용하지 않음
// Uncaught TypeError: Cannot redefine property: b
Object.defineProperty(obj, 'b', {value: 110});
또한, delete 연산자의 간단한 규칙은 다음과 같습니다:
-
피연산자가 참조가 아닌 경우, 직접 true 를 반환
-
변수 객체/활성화 객체에 이 속성이 없는 경우, true 를 반환
-
속성이 존재하지만, 삭제할 수 없다는 천부의 권리가 있는 경우, false 를 반환
-
그렇지 않은 경우, 속성을 삭제하고, true 를 반환
따라서:
delete 1 === true
기본 값은 첫 단계에서 true 가 됩니다. 삭제했는지 여부는 알 수 없지만
아직 댓글이 없습니다