클래스 상속은 한 클래스가 다른 클래스를 확장하는 방법입니다.
따라서 우리는 기존 기능 위에 새로운 기능을 만들 수 있습니다.
Animal
클래스가 있다고 가정해 보겠습니다.
클래스 동물 { 생성자(이름) { this.속도 = 0; this.name = 이름; } 실행(속도) { this.speed = 속도; Alert(`${this.name}은(는) ${this.speed} 속도로 실행됩니다.`); } 멈추다() { this.속도 = 0; Alert(`${this.name}이(가) 가만히 있습니다.`); } } let Animal = new Animal("나의 동물");
animal
객체와 Animal
클래스를 그래픽으로 표현하는 방법은 다음과 같습니다.
...그리고 또 다른 class Rabbit
만들고 싶습니다.
토끼는 동물이므로 Rabbit
클래스는 Animal
기반으로 해야 하며 동물 메서드에 액세스할 수 있어야 합니다. 그러면 토끼도 "일반적인" 동물이 할 수 있는 일을 할 수 있습니다.
다른 클래스를 확장하는 구문은 다음과 같습니다. class Child extends Parent
.
Animal
상속받는 class Rabbit
만들어 보겠습니다.
Rabbit 클래스는 Animal {를 확장합니다. 숨다() { Alert(`${this.name}이(가) 숨겨졌습니다!`); } } let Rabbit = new Rabbit("흰 토끼"); 토끼.run(5); // 흰토끼는 속도 5로 달린다. 토끼.숨기기(); // 흰토끼가 숨는다!
Rabbit
클래스의 객체는 rabbit.hide()
와 같은 Rabbit
메서드와 rabbit.run()
과 같은 Animal
메서드에 모두 액세스할 수 있습니다.
내부적으로는 오래된 프로토타입 메커니즘을 사용하여 키워드 작업을 extends
. Rabbit.prototype.[[Prototype]]
Animal.prototype
으로 설정합니다. 따라서 Rabbit.prototype
에서 메서드를 찾을 수 없으면 JavaScript는 Animal.prototype
에서 해당 메서드를 가져옵니다.
예를 들어, rabbit.run
메소드를 찾기 위해 엔진은 다음을 확인합니다(그림의 상향식).
rabbit
객체( run
없음)
해당 프로토타입은 Rabbit.prototype
입니다( hide
있지만 run
없음).
그 프로토타입, 즉 ( extends
로 인해) Animal.prototype
은 최종적으로 run
메소드를 갖습니다.
네이티브 프로토타입 장에서 기억할 수 있듯이 JavaScript 자체는 내장 객체에 대한 프로토타입 상속을 사용합니다. 예를 들어 Date.prototype.[[Prototype]]
Object.prototype
입니다. 이것이 바로 날짜가 일반 객체 메서드에 액세스할 수 있는 이유입니다.
extends
후에는 모든 표현이 허용됩니다.
클래스 구문을 사용하면 클래스뿐만 아니라 extends
후의 모든 표현식을 지정할 수 있습니다.
예를 들어 상위 클래스를 생성하는 함수 호출은 다음과 같습니다.
함수 f(문구) { 반환 클래스 { sayHi() { 경고(문구); } }; } 클래스 사용자 확장 f("Hello") {} 새로운 사용자().sayHi(); // 안녕하세요
여기서 class User
f("Hello")
의 결과를 상속받습니다.
이는 함수를 사용하여 여러 조건에 따라 클래스를 생성하고 클래스에서 상속할 수 있는 고급 프로그래밍 패턴에 유용할 수 있습니다.
이제 앞으로 나아가서 메서드를 재정의해 보겠습니다. 기본적으로 class Rabbit
에 지정되지 않은 모든 메서드는 class Animal
에서 "있는 그대로" 직접 가져옵니다.
그러나 stop()
과 같이 Rabbit
에 자체 메소드를 지정하면 대신 사용됩니다.
Rabbit 클래스는 Animal {를 확장합니다. 멈추다() { // ...이제 이는 Rabbit.stop()에 사용됩니다. // Animal 클래스의 stop() 대신 } }
그러나 일반적으로 우리는 상위 메소드를 완전히 대체하기보다는 상위 메소드를 기반으로 기능을 조정하거나 확장하기를 원합니다. 우리는 메소드에서 어떤 작업을 수행하지만 그 전/후 또는 프로세스에서 상위 메소드를 호출합니다.
클래스는 이를 위해 "super"
키워드를 제공합니다.
super.method(...)
상위 메소드를 호출합니다.
super(...)
부모 생성자를 호출합니다(생성자 내부에서만).
예를 들어, 토끼가 멈추면 자동으로 숨도록 하세요.
클래스 동물 { 생성자(이름) { this.속도 = 0; this.name = 이름; } 실행(속도) { this.speed = 속도; Alert(`${this.name}은(는) ${this.speed} 속도로 실행됩니다.`); } 멈추다() { this.속도 = 0; Alert(`${this.name}이(가) 가만히 있습니다.`); } } Rabbit 클래스는 Animal {를 확장합니다. 숨다() { Alert(`${this.name}이(가) 숨겨졌습니다!`); } 멈추다() { 슈퍼.스톱(); // 부모 중지 호출 this.hide(); // 그리고 숨긴다 } } let Rabbit = new Rabbit("흰 토끼"); 토끼.run(5); // 흰토끼는 속도 5로 달린다. 토끼.정지(); // 흰토끼는 가만히 서 있다. 흰토끼가 숨어있어요!
이제 Rabbit
에는 프로세스에서 상위 super.stop()
호출하는 stop
메소드가 있습니다.
화살표 함수에는 super
없습니다.
다시 살펴보는 화살표 함수 장에서 언급했듯이 화살표 함수에는 super
없습니다.
액세스되면 외부 함수에서 가져옵니다. 예를 들어:
Rabbit 클래스는 Animal {를 확장합니다. 멈추다() { setTimeout(() => super.stop(), 1000); // 1초 후에 상위 중지를 호출합니다. } }
화살표 함수의 super
는 stop()
과 동일하므로 의도한 대로 작동합니다. 여기서 "일반" 함수를 지정하면 오류가 발생합니다.
// 예상치 못한 슈퍼 setTimeout(function() { super.stop() }, 1000);
생성자를 사용하면 약간 까다로워집니다.
지금까지 Rabbit
에는 자체 constructor
없었습니다.
사양에 따르면 클래스가 다른 클래스를 확장하고 constructor
없는 경우 다음과 같은 "빈" constructor
생성됩니다.
Rabbit 클래스는 Animal {를 확장합니다. // 자체 생성자 없이 클래스를 확장하기 위해 생성됨 생성자(...args) { 슈퍼(...args); } }
보시다시피 기본적으로 모든 인수를 전달하는 상위 constructor
호출합니다. 우리가 직접 생성자를 작성하지 않으면 이런 일이 발생합니다.
이제 Rabbit
에 사용자 정의 생성자를 추가해 보겠습니다. name
외에 earLength
도 지정합니다.
클래스 동물 { 생성자(이름) { this.속도 = 0; this.name = 이름; } // ... } Rabbit 클래스는 Animal {를 확장합니다. 생성자(이름, 귀길이) { this.속도 = 0; this.name = 이름; this.earLength = 귀길이; } // ... } // 작동하지 않습니다! let Rabbit = new Rabbit("흰 토끼", 10); // 오류: 정의되지 않았습니다.
앗! 오류가 발생했습니다. 이제 우리는 토끼를 만들 수 없습니다. 무엇이 잘못되었나요?
짧은 대답은 다음과 같습니다.
상속 클래스의 생성자는 super(...)
호출해야 하며, this
사용하기 전에 이를 (!) 수행해야 합니다.
…그런데 왜? 여기서 무슨 일이 일어나고 있는 걸까요? 실제로 요구 사항이 이상해 보입니다.
물론 설명도 있습니다. 자세한 내용을 살펴보겠습니다. 그러면 무슨 일이 일어나고 있는지 확실히 이해할 수 있을 것입니다.
JavaScript에서는 상속 클래스의 생성자 함수(소위 "파생 생성자")와 다른 함수 간에 차이가 있습니다. 파생 생성자에는 특별한 내부 속성 [[ConstructorKind]]:"derived"
가 있습니다. 그것은 특별한 내부 라벨입니다.
해당 레이블은 new
의 동작에 영향을 미칩니다.
new
사용하여 일반 함수를 실행하면 빈 객체가 생성되어 this
에 할당됩니다.
그러나 파생 생성자가 실행될 때는 이 작업을 수행하지 않습니다. 상위 생성자가 이 작업을 수행할 것으로 예상합니다.
따라서 파생 생성자는 부모(기본) 생성자를 실행하기 위해 super
호출해야 합니다. 그렇지 않으면 this
대한 객체가 생성되지 않습니다. 그러면 오류가 발생합니다.
Rabbit
생성자가 작동하려면 다음과 같이 this
사용하기 전에 super()
호출해야 합니다.
클래스 동물 { 생성자(이름) { this.속도 = 0; this.name = 이름; } // ... } Rabbit 클래스는 Animal {를 확장합니다. 생성자(이름, 귀길이) { 슈퍼(이름); this.earLength = 귀길이; } // ... } // 이제 괜찮아 let Rabbit = new Rabbit("흰 토끼", 10); 경고(토끼.이름); // 흰토끼 경고(rabbit.earLength); // 10
고급 메모
이 노트에서는 여러분이 다른 프로그래밍 언어로 수업을 들은 경험이 있다고 가정합니다.
이는 언어에 대한 더 나은 통찰력을 제공하고 버그의 원인이 될 수 있는 동작을 설명합니다(그러나 자주는 아님).
이해하기 어렵다면 계속해서 읽고 잠시 후에 다시 읽어보세요.
메소드뿐만 아니라 클래스 필드도 재정의할 수 있습니다.
하지만 부모 생성자의 재정의된 필드에 액세스할 때 대부분의 다른 프로그래밍 언어와는 상당히 다른 까다로운 동작이 있습니다.
다음 예를 고려하십시오.
클래스 동물 { 이름 = '동물'; 생성자() { 경고(this.name); // (*) } } Rabbit 클래스는 Animal {를 확장합니다. 이름 = '토끼'; } 새로운 동물(); // 동물 새로운 토끼(); // 동물
여기서 Rabbit
클래스는 Animal
확장하고 name
필드를 자체 값으로 재정의합니다.
Rabbit
에는 자체 생성자가 없으므로 Animal
생성자가 호출됩니다.
흥미로운 점은 new Animal()
및 new Rabbit()
두 경우 모두에서 (*)
줄의 alert
에 animal
표시된다는 것입니다.
즉, 상위 생성자는 항상 재정의된 필드 값이 아닌 자체 필드 값을 사용합니다.
무엇이 이상한가요?
아직 명확하지 않다면 방법을 비교해 보세요.
여기에 동일한 코드가 있지만 this.name
필드 대신 this.showName()
메서드를 호출합니다.
클래스 동물 { showName() { // this.name = 'animal' 대신 경고('동물'); } 생성자() { this.showName(); // 대신에 경고(this.name); } } Rabbit 클래스는 Animal {를 확장합니다. 쇼이름() { 경고('토끼'); } } 새로운 동물(); // 동물 새로운 토끼(); // 토끼
참고: 이제 출력이 다릅니다.
그리고 그것이 우리가 자연스럽게 기대하는 것입니다. 파생 클래스에서 부모 생성자가 호출되면 재정의된 메서드를 사용합니다.
…하지만 클래스 필드의 경우에는 그렇지 않습니다. 말했듯이 상위 생성자는 항상 상위 필드를 사용합니다.
왜 차이가 있나요?
그런데 그 이유는 필드 초기화 순서 때문입니다. 클래스 필드가 초기화됩니다.
기본 클래스의 생성자(아무것도 확장하지 않음) 이전에
파생 클래스의 super()
바로 뒤에 있습니다.
우리의 경우 Rabbit
파생 클래스입니다. 거기에는 constructor()
없습니다. 이전에 말했듯이 이는 super(...args)
만 있는 빈 생성자가 있는 것과 같습니다.
따라서 new Rabbit()
super()
호출하여 부모 생성자를 실행하고 (파생 클래스의 규칙에 따라) 해당 클래스 필드가 초기화된 후에만 실행됩니다. 상위 생성자 실행 시 아직 Rabbit
클래스 필드가 없으므로 Animal
필드가 사용됩니다.
필드와 메소드 사이의 이러한 미묘한 차이는 JavaScript에만 해당됩니다.
운 좋게도 이 동작은 재정의된 필드가 상위 생성자에서 사용되는 경우에만 나타납니다. 그렇다면 무슨 일이 일어나고 있는지 이해하기 어려울 수 있으므로 여기서는 설명하겠습니다.
문제가 발생하면 필드 대신 메소드나 getter/setter를 사용하여 문제를 해결할 수 있습니다.
고급정보
튜토리얼을 처음 읽는 경우 이 섹션을 건너뛸 수 있습니다.
상속과 super
이면의 내부 메커니즘에 관한 것입니다.
super
의 내부를 조금 더 자세히 살펴보겠습니다. 우리는 그 과정에서 몇 가지 흥미로운 것들을 보게 될 것입니다.
먼저, 우리가 지금까지 배운 모든 것에서 super
작동하는 것은 전혀 불가능합니다!
예, 실제로 스스로에게 물어봅시다. 기술적으로 어떻게 작동해야 할까요? 객체 메서드가 실행되면 현재 객체를 this
로 가져옵니다. super.method()
호출하면 엔진은 현재 객체의 프로토타입에서 method
가져와야 합니다. 하지만 어떻게?
이 작업은 간단해 보이지만 그렇지 않습니다. 엔진은 현재 객체 this
를 알고 있으므로 상위 method
this.__proto__.method
로 가져올 수 있습니다. 불행하게도 이러한 "순진한" 솔루션은 작동하지 않습니다.
문제를 보여드리겠습니다. 클래스 없이 단순함을 위해 일반 객체를 사용합니다.
자세한 내용을 알고 싶지 않다면 이 부분을 건너뛰고 아래의 [[HomeObject]]
하위 섹션으로 이동하세요. 그건 해를 끼치 지 않을 것입니다. 또는 내용을 심층적으로 이해하는 데 관심이 있다면 계속 읽어보세요.
아래 예에서는 rabbit.__proto__ = animal
. 이제 시도해 보겠습니다. rabbit.eat()
에서 this.__proto__
사용하여 animal.eat()
호출합니다.
동물 = { 이름: "동물", 먹다() { Alert(`${this.name}이(가) 먹습니다.`); } }; 토끼 = { __proto__: 동물, 이름: "토끼", 먹다() { // 이것이 아마도 super.eat()가 작동하는 방식일 것입니다. this.__proto__.eat.call(this); // (*) } }; 토끼.먹기(); // 토끼가 먹는다.
(*)
줄에서는 프로토타입( animal
)에서 eat
가져와서 현재 객체의 컨텍스트에서 호출합니다. 여기서는 .call(this)
이 중요합니다. 간단한 this.__proto__.eat()
현재 객체가 아닌 프로토타입의 컨텍스트에서 상위 eat
실행하기 때문입니다.
그리고 위의 코드에서는 실제로 의도한 대로 작동합니다. 올바른 alert
있습니다.
이제 체인에 객체를 하나 더 추가해 보겠습니다. 우리는 일이 어떻게 깨지는지 볼 것입니다:
동물 = { 이름: "동물", 먹다() { Alert(`${this.name}이(가) 먹습니다.`); } }; 토끼 = { __proto__: 동물, 먹다() { // ...토끼 스타일로 돌아다니며 부모(동물) 메서드를 호출합니다. this.__proto__.eat.call(this); // (*) } }; longEar = {라고 놔두세요 __proto__: 토끼, 먹다() { // ...긴 귀로 뭔가를 하고 부모(토끼) 메서드를 호출합니다. this.__proto__.eat.call(this); // (**) } }; longEar.eat(); // 오류: 최대 호출 스택 크기를 초과했습니다.
코드가 더 이상 작동하지 않습니다! longEar.eat()
호출하는 동안 오류가 발생하는 것을 볼 수 있습니다.
그다지 명확하지 않을 수도 있지만, longEar.eat()
호출을 추적하면 그 이유를 알 수 있습니다. (*)
와 (**)
두 줄 모두에서 this
값은 현재 개체( longEar
)입니다. 그것은 필수적입니다. 모든 객체 메서드는 현재 객체를 프로토타입이나 다른 것이 아닌 this
로 가져옵니다.
따라서 (*)
와 (**)
두 줄 모두에서 this.__proto__
의 값은 정확히 동일합니다: rabbit
. 둘 다 무한 루프에서 체인을 올라가지 않고 rabbit.eat
호출합니다.
무슨 일이 일어나는지 그림은 다음과 같습니다.
longEar.eat()
내부에서 라인 (**)
은 this=longEar
를 제공하여 rabbit.eat
를 호출합니다.
// longEar.eat() 내부에는 다음이 있습니다. = longEar this.__proto__.eat.call(this) // (**) // 된다 longEar.__proto__.eat.call(this) // 그건 Rabbit.eat.call(this);
그런 다음 rabbit.eat
의 라인 (*)
에서 체인의 더 높은 호출을 전달하고 싶지만 this=longEar
이므로 this.__proto__.eat
는 다시 rabbit.eat
입니다!
// Rabbit.eat() 내부에는 this = longEar도 있습니다. this.__proto__.eat.call(this) // (*) // 된다 longEar.__proto__.eat.call(this) // 또는 (다시) Rabbit.eat.call(this);
…그래서 rabbit.eat
더 이상 상승할 수 없기 때문에 무한 루프에서 자신을 호출합니다.
this
만으로는 문제를 해결할 수 없습니다.
[[HomeObject]]
솔루션을 제공하기 위해 JavaScript는 함수에 대한 하나 이상의 특수 내부 속성인 [[HomeObject]]
를 추가합니다.
함수가 클래스 또는 객체 메서드로 지정되면 해당 [[HomeObject]]
속성이 해당 객체가 됩니다.
그런 다음 super
이를 사용하여 상위 프로토타입과 해당 메서드를 해결합니다.
먼저 일반 객체를 사용하여 어떻게 작동하는지 살펴보겠습니다.
동물 = { 이름: "동물", eat() { // 동물.eat.[[HomeObject]] == 동물 Alert(`${this.name}이(가) 먹습니다.`); } }; 토끼 = { __proto__: 동물, 이름: "토끼", eat() { // 토끼.eat.[[HomeObject]] == 토끼 super.eat(); } }; longEar = {라고 놔두세요 __proto__: 토끼, 이름: "긴 귀", eat() { // longEar.eat.[[HomeObject]] == longEar super.eat(); } }; // 올바르게 작동합니다 longEar.eat(); // 긴 귀가 먹는다.
[[HomeObject]]
메커니즘으로 인해 의도한 대로 작동합니다. longEar.eat
와 같은 메소드는 [[HomeObject]]
를 알고 프로토타입에서 상위 메소드를 가져옵니다. this
전혀 사용하지 않고.
이전에 알고 있듯이 일반적으로 함수는 "무료"이며 JavaScript의 개체에 바인딩되지 않습니다. 따라서 객체 간에 복사할 수 있고 다른 this
로 호출할 수 있습니다.
[[HomeObject]]
의 존재 자체가 이 원칙을 위반하는 것입니다. 왜냐하면 메소드가 해당 객체를 기억하기 때문입니다. [[HomeObject]]
변경할 수 없으므로 이 결합은 영원합니다.
언어에서 [[HomeObject]]
사용되는 유일한 곳은 super
입니다. 따라서 메서드가 super
사용하지 않는 경우에도 여전히 해당 메서드를 무료로 간주하고 개체 간에 복사할 수 있습니다. 그러나 super
에서는 일이 잘못될 수 있습니다.
복사 후 잘못된 super
결과의 데모는 다음과 같습니다.
동물 = { 안녕하세요() { Alert(`나는 동물입니다`); } }; // 토끼는 동물로부터 상속받습니다. 토끼 = { __proto__: 동물, 안녕하세요() { super.sayHi(); } }; 식물 = { 안녕하세요() { Alert("나는 식물입니다"); } }; // 나무는 식물로부터 상속받습니다. 나무 = { __proto__: 식물, sayHi: 토끼.sayHi // (*) }; tree.sayHi(); // 나는 동물이다(?!?)
tree.sayHi()
를 호출하면 "나는 동물입니다"가 표시됩니다. 확실히 틀렸어요.
이유는 간단합니다.
(*)
행에서는 tree.sayHi
메서드가 rabbit
에서 복사되었습니다. 어쩌면 우리는 코드 중복을 피하고 싶었을까요?
[[HomeObject]]
는 rabbit
에서 생성되었으므로 rabbit
입니다. [[HomeObject]]
변경할 방법이 없습니다.
tree.sayHi()
의 코드에는 super.sayHi()
내부에 있습니다. rabbit
에서 올라가서 animal
의 방식을 따온 것입니다.
무슨 일이 일어나는지 보여주는 다이어그램은 다음과 같습니다.
[[HomeObject]]
클래스와 일반 객체 모두의 메서드에 대해 정의됩니다. 그러나 객체의 경우 메소드는 "method: function()"
이 아닌 method()
로 정확하게 지정되어야 합니다.
그 차이는 우리에게는 중요하지 않을 수 있지만 JavaScript에서는 중요합니다.
아래 예에서는 메서드가 아닌 구문이 비교에 사용되었습니다. [[HomeObject]]
속성이 설정되지 않아 상속이 작동하지 않습니다.
동물 = { eat: function() { // 의도적으로 eat() 대신 이렇게 작성합니다 {... // ... } }; 토끼 = { __proto__: 동물, 먹다: 함수() { super.eat(); } }; 토끼.먹기(); // super 호출 오류 ([[HomeObject]]가 없기 때문에)
클래스를 확장하려면: class Child extends Parent
.
즉, Child.prototype.__proto__
Parent.prototype
이므로 메서드가 상속됩니다.
생성자를 재정의하는 경우:
this
사용하기 전에 Child
생성자에서 부모 생성자를 super()
로 호출해야 합니다.
다른 메서드를 재정의하는 경우:
Child
메소드에서 super.method()
사용하여 Parent
메소드를 호출할 수 있습니다.
내부:
메소드는 내부 [[HomeObject]]
속성에 해당 클래스/객체를 기억합니다. 이것이 바로 super
상위 메소드를 해결하는 방법입니다.
따라서 super
사용하여 한 개체에서 다른 개체로 메서드를 복사하는 것은 안전하지 않습니다.
또한:
화살표 함수에는 자체 this
또는 super
가 없으므로 주변 컨텍스트에 투명하게 들어맞습니다.
중요도: 5
다음은 Rabbit
확장한 Animal
사용한 코드입니다.
안타깝게도 Rabbit
객체는 생성할 수 없습니다. 무슨 일이야? 문제를 해결하세요.
클래스 동물 { 생성자(이름) { this.name = 이름; } } Rabbit 클래스는 Animal {를 확장합니다. 생성자(이름) { this.name = 이름; this.created = Date.now(); } } let Rabbit = new Rabbit("흰 토끼"); // 오류: 정의되지 않았습니다. 경고(토끼.이름);
그 이유는 자식 생성자가 super()
호출해야 하기 때문입니다.
수정된 코드는 다음과 같습니다.
클래스 동물 { 생성자(이름) { this.name = 이름; } } Rabbit 클래스는 Animal {를 확장합니다. 생성자(이름) { 슈퍼(이름); this.created = Date.now(); } } let Rabbit = new Rabbit("흰 토끼"); // 이제 알았어 경고(토끼.이름); // 흰토끼
중요도: 5
Clock
클래스가 있습니다. 현재로서는 매초마다 시간을 인쇄합니다.
클래스 시계 { 생성자({ 템플릿 }) { this.template = 템플릿; } 렌더링() { 날짜 = 새 날짜()를 설정합니다. 시간 = date.getHours(); if (시간 < 10) 시간 = '0' + 시간; mins = date.getMinutes(); if (분 < 10) mins = '0' + mins; 초 = date.getSeconds(); if (초 < 10) 초 = '0' + 초; 출력 = this.template .replace('h', 시간) .replace('m', 분) .replace('s', 초); console.log(출력); } 멈추다() { ClearInterval(this.timer); } 시작() { this.render(); this.timer = setInterval(() => this.render(), 1000); } }
Clock
상속하고 매개변수 precision
("틱" 사이의 ms
수)를 추가하는 새 클래스 ExtendedClock
만듭니다. 기본적으로 1000
(1초)이어야 합니다.
코드는 extended-clock.js
파일에 있어야 합니다.
원래 clock.js
수정하지 마세요. 연장하세요.
작업에 대한 샌드박스를 엽니다.
ExtendedClock 클래스는 Clock {을 확장합니다. 생성자(옵션) { 슈퍼(옵션); let { 정밀도 = 1000 } = 옵션; this.precision = 정밀도; } 시작() { this.render(); this.timer = setInterval(() => this.render(), this.precision); } };
샌드박스에서 솔루션을 엽니다.