프로그래밍에서 우리는 종종 무언가를 가져와서 확장하고 싶어합니다.
예를 들어, 속성과 메서드가 있는 user
개체가 있고 admin
과 guest
약간 수정된 변형으로 만들고 싶습니다. 우리는 user
에 있는 것을 재사용하고 싶습니다. 메소드를 복사/재구현하는 것이 아니라 그 위에 새 객체를 구축하기만 하면 됩니다.
프로토타입 상속 은 이를 돕는 언어 기능입니다.
JavaScript에서 객체에는 null
이거나 다른 객체를 참조하는 특별한 숨겨진 속성 [[Prototype]]
(사양에 이름 지정)이 있습니다. 해당 객체를 "프로토타입"이라고 합니다.
object
에서 속성을 읽었는데 해당 속성이 누락되면 JavaScript는 자동으로 프로토타입에서 해당 속성을 가져옵니다. 프로그래밍에서는 이를 "프로토타입 상속"이라고 합니다. 그리고 곧 우리는 그러한 상속의 많은 예와 이를 기반으로 구축된 더 멋진 언어 기능을 연구할 것입니다.
[[Prototype]]
속성은 내부에 숨겨져 있지만 설정하는 방법은 다양합니다.
그 중 하나는 다음과 같이 특수 이름 __proto__
사용하는 것입니다.
동물 = { 먹는다: 사실이다 }; 토끼 = { 점프: 사실 }; Rabbit.__proto__ = 동물; // 토끼를 설정합니다.[[Prototype]] = 동물
이제 rabbit
에서 속성을 읽었는데 해당 속성이 누락된 경우 JavaScript는 자동으로 animal
에서 해당 속성을 가져옵니다.
예를 들어:
동물 = { 먹는다: 사실이다 }; 토끼 = { 점프: 사실 }; Rabbit.__proto__ = 동물; // (*) // 이제 Rabbit에서 두 속성을 모두 찾을 수 있습니다. 경고(rabbit.eats); // 진실 (**) 경고(rabbit.jumps); // 진실
여기서 (*)
줄은 animal
rabbit
의 프로토타입으로 설정합니다.
그런 다음 alert
rabbit.eats
(**)
속성을 읽으려고 시도하면 rabbit
에 없으므로 JavaScript는 [[Prototype]]
참조를 따르고 animal
에서 이를 찾습니다(아래에서 위로 봅니다).
여기서 우리는 " animal
rabbit
의 원형이다" 또는 " rabbit
원형적으로 animal
로부터 상속을 받는다"라고 말할 수 있다.
따라서 animal
유용한 속성과 메서드가 많이 있으면 rabbit
에서 자동으로 사용할 수 있게 됩니다. 이러한 속성을 "상속"이라고 합니다.
animal
에 메소드가 있으면 rabbit
에서 호출할 수 있습니다.
동물 = { 먹는다: 사실, 걷다() { Alert("동물 산책"); } }; 토끼 = { 점프: 사실, __proto__: 동물 }; // 걷기는 프로토타입에서 가져옵니다. 토끼.걷기(); // 동물 산책
메소드는 다음과 같이 프로토타입에서 자동으로 가져옵니다.
프로토타입 체인은 더 길어질 수 있습니다.
동물 = { 먹는다: 사실, 걷다() { Alert("동물 산책"); } }; 토끼 = { 점프: 사실, __proto__: 동물 }; longEar = {라고 놔두세요 귀 길이: 10, __proto__: 토끼 }; // 걷기는 프로토타입 체인에서 가져옵니다. longEar.walk(); // 동물 산책 경고(longEar.jumps); // true(토끼에서)
이제 longEar
에서 무언가를 읽었는데 누락된 경우 JavaScript는 rabbit
에서 찾은 다음 animal
에서 찾습니다.
두 가지 제한사항만 있습니다.
참조는 서클에 들어갈 수 없습니다. 서클에 __proto__
할당하려고 하면 JavaScript에서 오류가 발생합니다.
__proto__
의 값은 객체이거나 null
일 수 있습니다. 다른 유형은 무시됩니다.
또한 이는 명백할 수도 있지만 여전히 [[Prototype]]
하나만 있을 수 있습니다. 객체는 다른 두 객체로부터 상속받을 수 없습니다.
__proto__
[[Prototype]]
에 대한 역사적인 getter/setter입니다.
이 둘의 차이점을 모르는 것은 초보 개발자의 일반적인 실수입니다.
__proto__
내부 [[Prototype]]
속성과 동일하지 않습니다 . [[Prototype]]
에 대한 getter/setter입니다. 나중에 우리는 이것이 중요한 상황을 보게 될 것입니다. 지금은 JavaScript 언어에 대한 이해를 구축하면서 이를 염두에 두겠습니다.
__proto__
속성은 약간 구식입니다. 이는 역사적인 이유로 존재하며, 현대 JavaScript에서는 프로토타입을 가져오거나 설정하는 대신 Object.getPrototypeOf/Object.setPrototypeOf
함수를 사용해야 한다고 제안합니다. 나중에 이러한 기능도 다루겠습니다.
사양에 따르면 __proto__
브라우저에서만 지원되어야 합니다. 실제로 서버 측을 포함한 모든 환경은 __proto__
지원하므로 이를 사용하는 것이 매우 안전합니다.
__proto__
표기법은 좀 더 직관적으로 명확하므로 예제에서는 이를 사용합니다.
프로토타입은 속성을 읽는 데에만 사용됩니다.
쓰기/삭제 작업은 개체에 직접 적용됩니다.
아래 예에서는 자체 walk
메소드를 rabbit
에 할당합니다.
동물 = { 먹는다: 사실, 걷다() { /* 토끼는 이 방법을 사용하지 않습니다 */ } }; 토끼 = { __proto__: 동물 }; Rabbit.walk = 함수() { Alert("토끼! 바운스-바운스!"); }; 토끼.걷기(); // 토끼! 바운스 바운스!
이제부터 rabbit.walk()
호출은 프로토타입을 사용하지 않고 객체에서 즉시 메서드를 찾아 실행합니다.
접근자 속성은 예외입니다. 할당은 setter 함수에 의해 처리되기 때문입니다. 따라서 이러한 속성에 쓰는 것은 실제로 함수를 호출하는 것과 같습니다.
이러한 이유로 admin.fullName
아래 코드에서 올바르게 작동합니다.
사용자 = { 이름: "존", 성: "스미스", 전체 이름(값) 설정 { [this.name, this.surname] = value.split(" "); }, 전체 이름() 가져오기 { return `${this.name} ${this.surname}`; } }; 관리자 = { __proto__: 사용자, isAdmin: 사실 }; 경고(admin.fullName); // 존 스미스(*) // 세터가 트리거됩니다! admin.fullName = "앨리스 쿠퍼"; // (**) 경고(admin.fullName); // Alice Cooper, 관리자 상태가 수정됨 경고(사용자.전체 이름); // John Smith, 보호된 사용자 상태
여기 (*)
줄의 admin.fullName
속성에는 user
프로토타입에 getter가 있으므로 호출됩니다. 그리고 (**)
줄의 속성에는 프로토타입에 setter가 있으므로 호출됩니다.
위의 예에서 흥미로운 질문이 발생할 수 있습니다. set fullName(value)
내부의 this
값은 무엇입니까? this.name
및 this.surname
속성은 user
또는 admin
어디에 기록됩니까?
대답은 간단합니다. 프로토 this
의 영향을 전혀 받지 않습니다.
메소드가 어디에서 발견되든 상관없습니다: 객체에서든 프로토타입에서든 말이죠. 메서드 호출에서 this
항상 점 앞에 있는 개체입니다.
따라서 setter 호출 admin.fullName=
user
아닌 admin
this
로 사용합니다.
이는 실제로 매우 중요한 것입니다. 왜냐하면 우리는 많은 메서드를 포함하는 큰 개체를 가질 수 있고, 그로부터 상속받는 개체를 가질 수 있기 때문입니다. 그리고 상속하는 개체가 상속된 메서드를 실행할 때 큰 개체의 상태가 아닌 자신의 상태만 수정합니다.
예를 들어 여기에서 animal
"메서드 저장"을 나타내고 rabbit
이를 활용합니다.
rabbit.sleep()
호출은 rabbit
객체에 this.isSleeping
설정합니다.
// 동물에는 메소드가 있습니다 동물 = { 걷다() { if (!this.isSleeping) { Alert(`나는 걷는다`); } }, 잠() { this.isSleeping = 사실; } }; 토끼 = { 이름: "흰 토끼", __proto__: 동물 }; // Rabbit.isSleeping을 수정합니다. 토끼.수면(); 경고(rabbit.isSleeping); // 진실 경고(animal.isSleeping); // 정의되지 않음(프로토타입에는 해당 속성이 없음)
결과 그림:
bird
, snake
등과 같은 다른 개체가 animal
에서 상속된 경우 해당 개체도 animal
의 메서드에 액세스할 수 있습니다. 그러나 각 메소드 호출에서 this
animal
아닌 호출 시간(점 전)에 평가되는 해당 객체가 됩니다. 따라서 this
에 데이터를 쓰면 해당 개체에 저장됩니다.
결과적으로 메서드는 공유되지만 객체 상태는 공유되지 않습니다.
for..in
루프는 상속된 속성도 반복합니다.
예를 들어:
동물 = { 먹는다: 사실이다 }; 토끼 = { 점프: 사실, __proto__: 동물 }; // Object.keys는 자신의 키만 반환합니다. 경고(Object.keys(토끼)); // 점프 // for..in은 자신의 키와 상속된 키를 모두 반복합니다. for(토끼에 소품을 넣어두세요) Alert(prop); // 점프한 후 먹는다
이것이 우리가 원하는 것이 아니고 상속된 속성을 제외하려는 경우 내장 메서드 obj.hasOwnProperty(key)가 있습니다. obj
에 key
라는 이름의 (상속되지 않은) 자체 속성이 있으면 true
반환합니다.
따라서 상속된 속성을 필터링하거나 다른 작업을 수행할 수 있습니다.
동물 = { 먹는다: 사실이다 }; 토끼 = { 점프: 사실, __proto__: 동물 }; for(토끼의 소품을 보자) { let isOwn = Rabbit.hasOwnProperty(prop); if (isOwn) { Alert(`우리: ${prop}`); // 우리의: 점프 } 또 다른 { Alert(`상속됨: ${prop}`); // 상속됨: 먹는다 } }
여기에는 다음과 같은 상속 체인이 있습니다. rabbit
animal
에서 상속하고 Object.prototype
에서 상속합니다( animal
리터럴 객체 {...}
이므로 기본적으로 지정됨). 그 위에는 null
있습니다.
참고로 웃긴게 하나 있습니다. rabbit.hasOwnProperty
메소드는 어디에서 오는가? 우리는 그것을 정의하지 않았습니다. 체인을 살펴보면 해당 메소드가 Object.prototype.hasOwnProperty
에 의해 제공된다는 것을 알 수 있습니다. 즉, 유전되었습니다.
...하지만 for..in
상속된 속성을 나열하는 경우, eats
및 jumps
처럼 for..in
루프에 hasOwnProperty
표시되지 않는 이유는 무엇입니까?
대답은 간단합니다. 열거할 수 없습니다. Object.prototype
의 다른 모든 속성과 마찬가지로 enumerable:false
플래그가 있습니다. 그리고 for..in
열거 가능한 속성만 나열합니다. 이것이 바로 이 속성과 나머지 Object.prototype
속성이 나열되지 않은 이유입니다.
거의 모든 다른 키/값 가져오기 방법은 상속된 속성을 무시합니다.
Object.keys
, Object.values
등과 같은 거의 모든 다른 키/값 가져오기 메서드는 상속된 속성을 무시합니다.
객체 자체에만 작동합니다. 프로토타입의 속성은 고려되지 않습니다 .
JavaScript에서 모든 객체에는 다른 객체이거나 null
인 숨겨진 [[Prototype]]
속성이 있습니다.
obj.__proto__
사용하여 액세스할 수 있습니다(역사적 getter/setter, 곧 다룰 다른 방법이 있습니다).
[[Prototype]]
에 의해 참조되는 객체를 "프로토타입"이라고 합니다.
obj
의 속성을 읽거나 메서드를 호출하려고 하는데 해당 속성이 존재하지 않으면 JavaScript는 프로토타입에서 해당 속성을 찾으려고 시도합니다.
쓰기/삭제 작업은 객체에 직접 작용하며 프로토타입을 사용하지 않습니다(세터가 아닌 데이터 속성이라고 가정).
obj.method()
호출하고 해당 method
프로토타입에서 가져온 경우 this
여전히 obj
참조합니다. 따라서 메소드는 상속된 경우에도 항상 현재 객체에서 작동합니다.
for..in
루프는 자신의 속성과 상속된 속성을 모두 반복합니다. 다른 모든 키/값 가져오기 메서드는 개체 자체에서만 작동합니다.
중요도: 5
다음은 객체 쌍을 생성한 다음 이를 수정하는 코드입니다.
프로세스에는 어떤 값이 표시됩니까?
동물 = { 점프: null }; 토끼 = { __프로토__: 동물, 점프: 사실 }; 경고(rabbit.jumps); // ? (1) 토끼.점프 삭제; 경고(rabbit.jumps); // ? (2) 동물.점프 삭제; 경고(rabbit.jumps); // ? (3)
답변은 3개여야 합니다.
true
, rabbit
에서 가져옴.
null
, animal
에서 가져옴.
undefined
더 이상 그러한 속성이 없습니다.
중요도: 5
이 작업은 두 부분으로 구성됩니다.
다음 객체가 주어지면:
머리를 맡기다 = { 안경: 1 }; 테이블 = { 펜: 3 }; 잠자리에 들게 놔두다 = { 시트: 1, 베개: 2 }; 주머니 = { 돈: 2000 };
__proto__
사용하면 모든 속성 조회가 pockets
→ bed
→ table
→ head
경로를 따르도록 프로토타입을 할당할 수 있습니다. 예를 들어, pockets.pen
3
( table
에 있음)이어야 하고 bed.glasses
1
( head
에 있음)이어야 합니다.
질문에 대답하십시오. glasses
pockets.glasses
또는 head.glasses
로 얻는 것이 더 빠릅니까? 필요한 경우 벤치마킹하세요.
__proto__
추가해 봅시다:
머리를 맡기다 = { 안경: 1 }; 테이블 = { 펜: 3, __proto__: 머리 }; 잠자리에 들게 놔두다 = { 시트: 1, 베개: 2, __proto__: 테이블 }; 주머니 = { 돈: 2000, __proto__: 침대 }; 경고(pockets.pen); // 3 경고( bed.glasses ); // 1 경고( table.money ); // 한정되지 않은
최신 엔진에서는 성능 측면에서 객체에서 속성을 가져오든 프로토타입에서 속성을 가져오든 차이가 없습니다. 그들은 속성이 발견된 위치를 기억하고 다음 요청에서 이를 재사용합니다.
예를 들어, pockets.glasses
의 경우 glasses
찾은 위치( head
)를 기억하고 다음 번에는 바로 그곳에서 검색합니다. 또한 무언가 변경되면 내부 캐시를 업데이트할 만큼 똑똑하므로 최적화가 안전합니다.
중요도: 5
우리는 animal
로부터 물려받은 rabbit
가지고 있습니다.
rabbit.eat()
호출하면 animal
또는 rabbit
어떤 개체가 full
속성을 받습니까?
동물 = { 먹다() { this.full = 사실; } }; 토끼 = { __proto__: 동물 }; 토끼.먹기();
답은 rabbit
.
이는 점 앞에 있는 객체 this
때문에 rabbit.eat()
rabbit
수정합니다.
속성 조회와 실행은 서로 다릅니다.
rabbit.eat
메서드는 프로토타입에서 처음 발견된 다음 this=rabbit
으로 실행됩니다.
중요도: 5
일반 hamster
객체를 상속받아 speedy
햄스터와 lazy
햄스터 두 마리가 있습니다.
그 중 하나를 먹이면 다른 하나도 가득 차 있습니다. 왜? 어떻게 해결할 수 있나요?
햄스터 = { 위: [], 먹다(음식) { this.stomach.push(food); } }; 빨리하자 = { __proto__: 햄스터 }; 게으르다 = { __proto__: 햄스터 }; // 이것은 음식을 찾았습니다 speedy.eat("사과"); 경고(speedy.stomach); // 사과 // 이것도 있는데 왜죠? 고쳐주세요. 경고(lazy.stomach); // 사과
speedy.eat("apple")
호출에서 무슨 일이 일어나고 있는지 주의 깊게 살펴보겠습니다.
speedy.eat
메소드는 프로토타입( =hamster
)에서 찾은 다음 this=speedy
(점 앞의 객체)로 실행됩니다.
그런 다음 this.stomach.push()
stomach
속성을 찾아 push
호출해야 합니다. this
( =speedy
)에서 stomach
찾았으나 아무것도 발견되지 않았습니다.
그런 다음 프로토타입 체인을 따라가며 hamster
에서 stomach
찾습니다.
그런 다음 push
호출하여 프로토타입의 위장 에 음식을 추가합니다.
그래서 모든 햄스터는 하나의 위를 공유합니다!
lazy.stomach.push(...)
및 speedy.stomach.push()
의 경우, 위 속성 stomach
프로토타입에서 발견되며(객체 자체에는 없으므로) 새 데이터가 프로토타입에 푸시됩니다.
this.stomach=
간단한 할당의 경우에는 이러한 일이 발생하지 않습니다.
햄스터 = { 위: [], 먹다(음식) { // this.stomach.push 대신 this.stomach에 할당 this.stomach = [음식]; } }; 빨리하자 = { __proto__: 햄스터 }; 게으르다 = { __proto__: 햄스터 }; // 빠른 사람이 음식을 찾았습니다. speedy.eat("사과"); 경고(speedy.stomach); // 사과 // 게으른 자의 배는 비어 있다 경고(lazy.stomach); // <아무것도>
이제 모든 것이 잘 작동합니다. 왜냐하면 this.stomach=
stomach
조회를 수행하지 않기 때문입니다. 값은 this
객체에 직접 기록됩니다.
또한 각 햄스터에게 자신만의 위가 있는지 확인하면 문제를 완전히 피할 수 있습니다.
햄스터 = { 위: [], 먹다(음식) { this.stomach.push(food); } }; 빨리하자 = { __proto__: 햄스터, 위: [] }; 게으르다 = { __proto__: 햄스터, 위: [] }; // 빠른 사람이 음식을 찾았습니다. speedy.eat("사과"); 경고(speedy.stomach); // 사과 // 게으른 자의 배는 비어 있다 경고(lazy.stomach); // <아무것도>
일반적인 해결책으로 위 stomach
같이 특정 개체의 상태를 설명하는 모든 속성을 해당 개체에 기록해야 합니다. 그런 문제를 예방해줍니다.