JS 개발자로서 이것이 나를 밤에 깨우게 하는 것입니다.

게시 됨: 2022-03-11

JavaScript는 이상한 언어입니다. Smalltalk에서 영감을 얻었지만 C와 같은 구문을 사용합니다. 절차적, 기능적, 객체 지향 프로그래밍(OOP) 패러다임의 측면을 결합합니다. 생각할 수 있는 거의 모든 프로그래밍 문제를 해결하기 위한 수많은, 종종 중복되는 접근 방식이 있으며 어떤 것이 선호되는지에 대해 강력하게 의견을 제시하지 않습니다. 그것은 약하고 동적으로 유형이 지정되며, 경험 많은 개발자도 실수하는 유형 강제에 대한 미로와 같은 접근 방식을 사용합니다.

JavaScript에는 사마귀, 함정 및 의심스러운 기능도 있습니다. 새로운 프로그래머는 비동기성, 폐쇄 및 호이스팅과 같은 좀 더 어려운 개념으로 어려움을 겪습니다. 다른 언어에 대한 경험이 있는 프로그래머는 비슷한 이름과 모양을 가진 것들이 JavaScript에서 동일한 방식으로 작동하고 종종 틀릴 것이라고 합리적으로 가정합니다. 배열은 실제로 배열이 아닙니다. this 에 대한 거래는 무엇이며, 프로토타입은 무엇이며, new 는 실제로 무엇을 합니까?

ES6 클래스의 문제

지금까지 최악의 범죄자는 JavaScript의 최신 릴리스 버전인 ECMAScript 6(ES6): classes 의 새로운 버전입니다. 수업에 대한 이야기 ​​중 일부는 솔직히 놀랍고 언어가 실제로 어떻게 작동하는지에 대한 뿌리 깊은 오해를 드러냅니다.

"JavaScript는 이제 클래스가 있는 진정한 객체 지향 언어입니다!"

또는:

"클래스는 JavaScript의 깨진 상속 모델에 대해 생각하지 않아도 됩니다."

또는:

"클래스는 JavaScript에서 유형을 생성하기 위한 더 안전하고 쉬운 접근 방식입니다."

이 문장은 프로토타입 상속에 문제가 있음을 암시하기 때문에 신경쓰지 않습니다. 그런 주장은 접어두자. 이 진술은 어느 것도 사실이 아니기 때문에 나를 귀찮게 하며 언어 설계에 대한 JavaScript의 "모든 사람을 위한 모든 것" 접근 방식의 결과를 보여줍니다. 이는 언어에 대한 프로그래머의 이해를 가능하게 하는 것보다 더 자주 손상시킵니다. 더 진행하기 전에 설명하겠습니다.

JavaScript 팝 퀴즈 #1: 이 코드 블록의 근본적인 차이점은 무엇입니까?

 function PrototypicalGreeting(greeting = "Hello", name = "World") { this.greeting = greeting this.name = name } PrototypicalGreeting.prototype.greet = function() { return `${this.greeting}, ${this.name}!` } const greetProto = new PrototypicalGreeting("Hey", "folks") console.log(greetProto.greet())
 class ClassicalGreeting { constructor(greeting = "Hello", name = "World") { this.greeting = greeting this.name = name } greet() { return `${this.greeting}, ${this.name}!` } } const classyGreeting = new ClassicalGreeting("Hey", "folks") console.log(classyGreeting.greet())

여기에 대한 대답은 하나가 없다는 것 입니다. 이것들은 효과적으로 동일한 일을 합니다. ES6 클래스 구문이 사용되었는지 여부에 대한 질문일 뿐입니다.

사실, 두 번째 예가 더 표현적입니다. 그 이유만으로도 class 가 언어에 좋은 추가 기능이라고 주장할 수 있습니다. 불행히도 문제는 조금 더 미묘합니다.

JavaScript 팝 퀴즈 #2: 다음 코드는 무엇을 합니까?

 function Proto() { this.name = 'Proto' return this; } Proto.prototype.getName = function() { return this.name } class MyClass extends Proto { constructor() { super() this.name = 'MyClass' } } const instance = new MyClass() console.log(instance.getName()) Proto.prototype.getName = function() { return 'Overridden in Proto' } console.log(instance.getName()) MyClass.prototype.getName = function() { return 'Overridden in MyClass' } console.log(instance.getName()) instance.getName = function() { return 'Overridden in instance' } console.log(instance.getName())

정답은 콘솔에 출력된다는 것입니다.

 > MyClass > Overridden in Proto > Overridden in MyClass > Overridden in instance

틀리게 답하면 class 가 실제로 무엇인지 이해하지 못합니다. 이것은 당신의 잘못이 아닙니다. Array 와 마찬가지로 class 는 언어 기능이 아니라 구문 모호성 입니다. 이는 프로토타입 상속 모델과 함께 제공되는 서투른 관용구를 숨기려고 하며 JavaScript가 하지 않는 일을 하고 있음을 암시합니다.

Java와 같은 언어를 사용하는 기존 OOP 개발자가 ES6 클래스 상속 모델에 더 익숙해지도록 하기 위해 class 가 JavaScript에 도입되었다는 말을 들었을 것입니다. 당신 그러한 개발자 중 한 명이라면 그 예가 당신을 끔찍하게 만들 것입니다. 그래야 한다. JavaScript의 class 키워드에는 클래스가 제공해야 하는 어떤 보장도 제공되지 않음을 보여줍니다. 또한 프로토타입 상속 모델의 주요 차이점 중 하나를 보여줍니다. 프로토타입은 유형 이 아니라 객체 인스턴스 입니다.

프로토타입 대 클래스

클래스 기반 상속과 프로토타입 기반 상속의 가장 중요한 차이점은 클래스는 런타임에 인스턴스화할 수 있는 유형 을 정의하는 반면 프로토타입 자체는 개체 인스턴스라는 것입니다.

ES6 클래스의 자식은 새로운 속성과 메서드로 부모를 확장하는 또 다른 유형 정의이며, 이는 차례로 런타임에 인스턴스화될 수 있습니다. 프로토타입의 자식은 자식에 구현되지 않은 모든 속성을 부모에게 위임하는 또 다른 개체 인스턴스 입니다.

참고 사항: 왜 내가 클래스 메서드를 언급했지만 프로토타입 메서드는 언급하지 않았는지 궁금할 것입니다. JavaScript에는 메서드 개념이 없기 때문입니다. 함수는 JavaScript의 일급이며 속성을 가지거나 다른 객체의 속성이 될 수 있습니다.

클래스 생성자는 클래스의 인스턴스를 만듭니다. JavaScript의 생성자는 객체를 반환하는 평범한 오래된 함수일 뿐입니다. JavaScript 생성자의 유일한 특별한 점은 new 키워드로 호출될 때 프로토타입을 반환된 객체의 프로토타입으로 할당한다는 것입니다. 그것이 당신에게 약간 혼란스럽게 들린다면 당신은 혼자가 아닙니다. 그것은 프로토타입이 제대로 이해되지 않는 이유의 큰 부분입니다.

정확히 말하자면 프로토타입의 자식은 프로토타입의 복사본 이 아니며 프로토타입과 모양 이 같은 객체도 아닙니다. 자식은 프로토타입에 대한 살아있는 참조 가지며 자식에 존재하지 않는 프로토타입 속성은 프로토타입에 있는 같은 이름의 속성에 대한 단방향 참조입니다.

다음을 고려하세요:

 let parent = { foo: 'foo' } let child = { } Object.setPrototypeOf(child, parent) console.log(child.foo) // 'foo' child.foo = 'bar' console.log(child.foo) // 'bar' console.log(parent.foo) // 'foo' delete child.foo console.log(child.foo) // 'foo' parent.foo = 'baz' console.log(child.foo) // 'baz'
참고: 실생활에서 이와 같은 코드는 거의 작성하지 않을 것입니다. 이것은 끔찍한 습관입니다. 그러나 원칙을 간결하게 보여줍니다.

이전 예에서 child.foo 는 undefined child.fooparent.foo 했습니다. foochild 에 정의하자마자 child.foo 는 'bar' 값을 가졌지만 child.foo 는 원래 값을 유지했습니다 parent.foo child.foo를 delete child.foo parent.foo 참조합니다. 이는 부모의 값을 변경할 때 child.foo 가 새 값을 참조한다는 것을 의미합니다.

방금 일어난 일을 살펴보겠습니다(더 명확한 설명을 위해 이것이 문자열 리터럴이 아니라 Strings 인 척 할 것입니다. 차이점은 여기에서 중요하지 않습니다):

JavaScript에서 누락된 참조를 처리하는 방법을 보여주기 위해 프로토타입 체인을 살펴봅니다.

이것이 내부에서 작동하는 방식, 특히 newthis 의 특성은 다른 날의 주제이지만, 더 읽고 싶다면 Mozilla에서 JavaScript의 프로토타입 상속 체인에 대한 자세한 기사를 볼 수 있습니다.

핵심은 프로토타입이 type 을 정의하지 않는다는 것입니다. 그것들은 그 자체가 instances 이고 런타임에 변경 가능하며 암시하고 수반하는 모든 것을 포함합니다.

아직도 나와 함꼐? JavaScript 클래스를 분석하는 것으로 돌아가자.

JavaScript 팝 퀴즈 #3: 수업에서 개인 정보를 어떻게 구현합니까?

위의 프로토타입 및 클래스 속성은 "캡슐화"된 것이 아니라 "위태롭게 창 밖에 매달려 있는" 것입니다. 우리는 그것을 고쳐야 하지만 어떻게?

여기에 코드 예제가 없습니다. 대답은 할 수 없다는 것입니다.

JavaScript에는 개인 정보 보호 개념이 없지만 클로저가 있습니다.

 function SecretiveProto() { const secret = "The Class is a lie!" this.spillTheBeans = function() { console.log(secret) } } const blabbermouth = new SecretiveProto() try { console.log(blabbermouth.secret) } catch(e) { // TypeError: SecretiveClass.secret is not defined } blabbermouth.spillTheBeans() // "The Class is a lie!"

방금 무슨 일이 일어났는지 이해합니까? 그렇지 않다면 클로저를 이해하지 못하는 것입니다. 괜찮습니다. 실제로는 그렇게 위협적이지 않고 매우 유용하며 시간을 내어 이에 대해 알아보아야 합니다.

JavaScript 팝 퀴즈 #4: class Keyword를 사용하여 위와 동일한 것은 무엇입니까?

죄송합니다. 이것은 또 다른 트릭 질문입니다. 기본적으로 동일한 작업을 수행할 수 있지만 다음과 같습니다.

 class SecretiveClass { constructor() { const secret = "I am a lie!" this.spillTheBeans = function() { console.log(secret) } } looseLips() { console.log(secret) } } const liar = new SecretiveClass() try { console.log(liar.secret) } catch(e) { console.log(e) // TypeError: SecretiveClass.secret is not defined } liar.spillTheBeans() // "I am a lie!"

SecretiveProto 보다 더 쉽거나 명확해 보이는지 알려주세요. 내 개인적인 견해로는 자바스크립트에서 class 선언의 관용적 사용을 깨고 자바에서 기대하는 것처럼 작동하지 않습니다. 이것은 다음에 의해 명확해질 것입니다:

JavaScript 팝 퀴즈 #5: SecretiveClass::looseLips() 는 무엇을 하나요?

알아 보자:

 try { liar.looseLips() } catch(e) { // ReferenceError: secret is not defined }

글쎄요... 그건 어색했습니다.

JavaScript 팝 퀴즈 #6: 숙련된 JavaScript 개발자는 프로토타입과 클래스 중 어느 것을 선호합니까?

그것은 또 다른 속임수 질문입니다. 숙련된 JavaScript 개발자는 가능하면 두 가지 모두를 피하는 경향이 있습니다. 관용적 JavaScript로 위의 작업을 수행하는 좋은 방법은 다음과 같습니다.

 function secretFactory() { const secret = "Favor composition over inheritance, `new` is considered harmful, and the end is near!" const spillTheBeans = () => console.log(secret) return { spillTheBeans } } const leaker = secretFactory() leaker.spillTheBeans()

이것은 상속의 고유한 추함을 피하거나 캡슐화를 시행하는 것에 관한 것이 아닙니다. 프로토타입이나 클래스로는 쉽게 할 수 없었던 secretFactoryleaker 자로 다른 무엇을 할 수 있을지 생각해 보십시오.

한 가지는 다음과 같은 컨텍스트에 this 걱정할 필요가 없기 때문에 구조를 해제할 수 있습니다.

 const { spillTheBeans } = secretFactory() spillTheBeans() // Favor composition over inheritance, (...)

정말 멋지네요. new 거나 this 말장난을 피하는 것 외에도 CommonJS 및 ES6 모듈과 상호 교환적으로 객체를 사용할 수 있습니다. 또한 구성이 조금 더 쉬워집니다.

 function spyFactory(infiltrationTarget) { return { exfiltrate: infiltrationTarget.spillTheBeans } } const blackHat = spyFactory(leaker) blackHat.exfiltrate() // Favor composition over inheritance, (...) console.log(blackHat.infiltrationTarget) // undefined (looks like we got away with it)

blackHat 의 클라이언트는 exfiltrate 이 어디에서 왔는지 걱정할 필요가 없으며 spyFactoryFunction::bind 컨텍스트 저글링 또는 깊이 중첩된 속성을 엉망으로 만들 필요가 없습니다. 간단한 동기 절차 코드에서는 this 대해 크게 걱정할 필요가 없지만, 비동기 코드에서는 모든 종류의 문제를 야기하므로 피하는 것이 좋습니다.

조금만 생각해보면 spyFactory 는 모든 종류의 침투 대상, 즉 외관을 처리할 수 있는 매우 정교한 스파이 도구로 개발될 수 있습니다.

물론 클래스를 사용하거나 오히려 여러 클래스를 사용하여 그렇게 할 수도 있습니다. 모든 클래스는 abstract class 또는 interface 에서 상속됩니다. 단, JavaScript에는 추상 또는 인터페이스 개념이 없습니다.

팩토리로 구현하는 방법을 보기 위해 Greetinger 예제로 돌아가 보겠습니다.

 function greeterFactory(greeting = "Hello", name = "World") { return { greet: () => `${greeting}, ${name}!` } } console.log(greeterFactory("Hey", "folks").greet()) // Hey, folks!

이러한 공장이 진행되면서 점점 더 간결해지고 있음을 눈치 챘을 수도 있지만 걱정하지 마십시오. 동일한 작업을 수행합니다. 훈련용 바퀴가 나가고 있습니다, 여러분!

동일한 코드의 프로토타입이나 클래스 버전보다 이미 상용구가 적습니다. 둘째, 속성을 보다 효과적으로 캡슐화합니다. 또한 어떤 경우에는 메모리 및 성능 풋프린트가 더 낮습니다(언뜻 보기에는 그렇지 않은 것처럼 보일 수 있지만 JIT 컴파일러는 중복을 줄이고 유형을 유추하기 위해 백그라운드에서 조용히 작동합니다).

그래서 더 안전하고 종종 더 빠르며 이와 같은 코드를 작성하는 것이 더 쉽습니다. 왜 다시 수업이 필요한가요? 아, 물론 재사용성. 불행하고 열성적인 인사말 변형을 원하면 어떻게 됩니까? 글쎄, 우리가 ClassicalGreeting 클래스를 사용하고 있다면, 우리는 아마도 클래스 계층 구조를 꿈꾸는 것으로 직접 뛰어들 것입니다. 구두점을 매개변수화해야 한다는 것을 알고 있으므로 약간의 리팩토링을 수행하고 일부 자식을 추가합니다.

 // Greeting class class ClassicalGreeting { constructor(greeting = "Hello", name = "World", punctuation = "!") { this.greeting = greeting this.name = name this.punctuation = punctuation } greet() { return `${this.greeting}, ${this.name}${this.punctuation}` } } // An unhappy greeting class UnhappyGreeting extends ClassicalGreeting { constructor(greeting, name) { super(greeting, name, " :(") } } const classyUnhappyGreeting = new UnhappyGreeting("Hello", "everyone") console.log(classyUnhappyGreeting.greet()) // Hello, everyone :( // An enthusiastic greeting class EnthusiasticGreeting extends ClassicalGreeting { constructor(greeting, name) { super(greeting, name, "!!") } greet() { return super.greet().toUpperCase() } } const greetingWithEnthusiasm = new EnthusiasticGreeting() console.log(greetingWithEnthusiasm.greet()) // HELLO, WORLD!!

누군가 와서 계층 구조에 완전히 맞지 않는 기능을 요청하고 모든 것이 이해가 되지 않을 때까지는 훌륭한 접근 방식입니다. 우리가 팩토리와 동일한 기능을 작성하려고 하는 동안 그 생각에 핀을 넣으십시오.

 const greeterFactory = (greeting = "Hello", name = "World", punctuation = "!") => ({ greet: () => `${greeting}, ${name}${punctuation}` }) // Makes a greeter unhappy const unhappy = (greeter) => (greeting, name) => greeter(greeting, name, ":(") console.log(unhappy(greeterFactory)("Hello", "everyone").greet()) // Hello, everyone :( // Makes a greeter enthusiastic const enthusiastic = (greeter) => (greeting, name) => ({ greet: () => greeter(greeting, name, "!!").greet().toUpperCase() }) console.log(enthusiastic(greeterFactory)().greet()) // HELLO, WORLD!!

이 코드가 조금 더 짧더라도 더 나은지는 분명하지 않습니다. 사실, 당신은 그것이 더 읽기 어렵다고 주장할 수 있고 아마도 이것은 둔한 접근 방식일 것입니다. 우리는 unhappyGreeterFactoryenthusiasticGreeterFactory GreeterFactory를 가질 수 없었습니까?

그런 다음 고객이 다가와 "불만족스러워서 방 전체에 이 사실을 알리고 싶어하는 새로운 인사가 필요합니다!"라고 말합니다.

 console.log(enthusiastic(unhappy(greeterFactory))().greet()) // HELLO, WORLD :(

이 열성적으로 불행한 인사말을 두 번 이상 사용해야 하는 경우 스스로 쉽게 만들 수 있습니다.

 const aggressiveGreeterFactory = enthusiastic(unhappy(greeterFactory)) console.log(aggressiveGreeterFactory("You're late", "Jim").greet())

프로토타입 또는 클래스와 함께 작동하는 이러한 구성 스타일에 대한 접근 방식이 있습니다. 예를 들어 UnhappyGreetingEnthusiasticGreeting 을 데코레이터로 다시 생각할 수 있습니다. 위에서 사용된 기능적 스타일 접근 방식보다 여전히 더 많은 상용구가 필요하지만 실제 클래스의 안전과 캡슐화를 위해 지불하는 대가입니다.

문제는 JavaScript에서 자동 안전을 얻지 못한다는 것입니다. class 사용을 강조하는 JavaScript 프레임워크는 이러한 종류의 문제에 대해 많은 "마술"을 수행하고 클래스가 스스로 작동하도록 합니다. 언젠가 Polymer의 ElementMixin 소스 코드를 살펴보십시오. 감히 말씀드립니다. 이것은 자바스크립트 비전의 대마법사 수준이며, 아이러니나 풍자가 없는 것을 의미합니다.

물론 Object.freeze 또는 Object.defineProperties 를 사용하여 위에서 논의한 문제 중 일부를 더 많거나 더 적게 수정할 수 있습니다. 그러나 왜 자바와 같은 언어에서는 찾을 수 없는 JavaScript 기본적으로 제공하는 도구를 무시하면서 기능이 없는 형식을 모방합니까? 도구 상자 옆에 실제 스크루드라이버가 있는 경우 "스크루드라이버"라고 표시된 망치를 사용하여 나사를 조이시겠습니까?

좋은 부품 찾기

JavaScript 개발자는 종종 구어체로 그리고 같은 이름의 책을 참조하여 언어의 좋은 부분을 강조합니다. 우리는 더 의심스러운 언어 디자인 선택에 의해 설정되는 함정을 피하고 깨끗하고 읽기 쉽고 오류를 최소화하고 재사용 가능한 코드를 작성할 수 있는 부분을 고수합니다.

JavaScript의 어떤 부분이 자격이 있는지에 대한 합리적인 주장이 있지만 class 가 그 중 하나가 아니라는 것을 확신했기를 바랍니다. 그렇지 않으면 JavaScript의 상속이 혼란스러울 수 있고 해당 class 가 이를 수정하지도 않고 프로토타입을 이해해야 하는 번거로움을 덜어주지도 않는다는 점을 이해하시기 바랍니다. 객체 지향 디자인 패턴이 클래스나 ES6 상속 없이도 잘 작동한다는 힌트를 얻었다면 추가 크레딧.

class 을 완전히 피하라는 것이 아닙니다. 때로는 상속이 필요하고 class 는 이를 위한 더 깨끗한 구문을 제공합니다. 특히 class X extends Y 를 확장하여 이전 프로토타입 접근 방식보다 훨씬 좋습니다. 그 외에도 많은 인기 있는 프론트 엔드 프레임워크에서 사용을 권장하므로 원칙적으로만 이상한 비표준 코드를 작성하는 것은 피해야 합니다. 나는 이것이 어디로 가고 있는지 좋아하지 않습니다.

내 악몽에서 JavaScript 라이브러리의 전체 세대는 class 를 사용하여 작성되었으며 다른 인기 있는 언어와 유사하게 작동할 것으로 예상합니다. 완전히 새로운 종류의 버그(말장난 의도)가 발견되었습니다. class 트랩에 부주의하게 빠지지 않았다면 Malformed JavaScript의 묘지에 쉽게 남을 수 있었던 오래된 것들이 부활합니다. 숙련된 JavaScript 개발자는 이러한 괴물에 시달리고 있습니다. 인기 있는 것이 항상 좋은 것은 아니기 때문입니다.

결국 우리 모두는 좌절감을 포기하고 Rust, Go, Haskell 또는 그 밖의 무엇을 아는 사람에서 바퀴를 재발명하기 시작합니다. 그런 다음 웹용 Wasm으로 컴파일하면 새로운 웹 프레임워크와 라이브러리가 다국어 무한대로 확산됩니다.

그것은 정말로 나를 밤에 깨우지 않는다.