이 기사는 Ye Yiyi가 Huawei Cloud 커뮤니티에서 공유한 " 코드를 더욱 설명적이고 표현력이 풍부하며 유연하게 만드는 메타프로그래밍 "입니다.
배경
작년 하반기에 위챗 책장에 다양한 카테고리의 기술 서적을 많이 추가했고, 그 중 일부를 간헐적으로 읽었습니다.
계획 없이 읽으면 결과가 거의 나오지 않습니다.
새해가 시작되면서 나는 독서 주간과 같은 다른 것을 시도할 준비가 되었습니다. 매달 1~2주간 비연속적으로 책을 한 권씩 읽으세요.
이 '노는 방법'은 평범하고 딱딱하지만 3개월 동안 읽어보니 효과적이다.
4월 독서계획은 2권으로, 『당신이 모르는 자바스크립트』 시리즈가 끝났습니다.
메타프로그래밍
함수 이름
프로그램에서 함수를 표현하는 방법에는 여러 가지가 있지만 함수의 "이름"이 무엇인지 항상 명확하지는 않습니다.
더 중요한 것은 함수의 "이름"이 단순히 이름 속성인지(예, 함수에는 이름이라는 속성이 있음), 아니면 bar(){와 같이 어휘적으로 바인딩된 이름을 가리키는지 여부를 결정해야 한다는 것입니다. 안에 .}.
name 속성은 메타프로그래밍 목적으로 사용됩니다.
기본적으로 함수의 어휘 이름(있는 경우)도 해당 이름 속성으로 설정됩니다. 실제로 ES5(및 이전) 사양에서는 이러한 동작을 공식적으로 요구하지 않습니다. 이름 속성의 설정은 비표준이지만 여전히 비교적 안정적입니다. 이는 ES6에서 표준화되었습니다.
ES6에는 이제 함수에 사용 가능한 어휘 이름이 없더라도 함수의 name 속성에 값을 합리적으로 할당할 수 있는 일련의 파생 규칙이 있습니다.
예를 들어:
var abc = 함수() { // .. }; abc.이름; // "abc"
ES6의 이름 파생(또는 부족)의 몇 가지 다른 형태는 다음과 같습니다.
(기능(){ .. }); // 이름: (기능*(){ .. }); // 이름: window.foo = function(){ .. }; // 이름: 수업 최고 { constructor() { .. } // 이름: 최고 funny() { .. } // 이름: funny } var c = 클래스 최고 { .. }; // 이름: 최고 var o = { foo() { .. }, // 이름: foo *bar() { .. }, // 이름: 바 baz: () => { .. }, // 이름: baz bam: function(){ .. }, // 이름: bam get what() { .. }, // 이름: get what set fuz() { .. }, // 이름: set fuz ["b" + "iz"]: function(){ .. }, // 이름: biz [기호( "버즈" )]: function(){ .. } // 이름: [buz] }; var x = o.foo.bind(o); // 이름: 바운드 foo (function(){ .. }).bind( o ); // 이름: 바운드 기본 내보내기 function() { .. } // 이름: 기본값 var y = 새로운 함수(); // 이름: 익명 여기서 GeneratorFunction = 기능*(){}. 프로토 .constructor; var z = 새로운 GeneratorFunction(); // 이름: 익명
기본적으로 name 속성은 쓸 수 없지만 구성 가능합니다. 즉, 필요한 경우 Object.defineProperty(..)를 사용하여 수동으로 수정할 수 있습니다.
메타 속성
메타 속성은 다른 방법으로는 얻을 수 없는 속성 액세스 형식으로 특별한 메타 정보를 제공합니다.
new.target을 예로 들면 new 키워드는 속성 액세스를 위한 컨텍스트로 사용됩니다. 분명히 new 자체는 객체가 아니기 때문에 이 함수는 매우 특별합니다. new.target이 생성자 호출(new에 의해 트리거되는 함수/메서드) 내에서 사용되면 new는 가상 컨텍스트가 되어 new.target이 new를 호출하는 대상 생성자를 가리킬 수 있습니다.
이는 메타프로그래밍 작업의 명확한 예입니다. 그 목적은 일반적으로 내부 검사(유형/구조 확인) 또는 정적 속성 액세스를 위해 원래 새 대상이 무엇인지 생성자 호출 내부에서 확인하는 것이기 때문입니다.
예를 들어 생성자가 직접 호출되는지 또는 하위 클래스를 통해 호출되는지에 따라 생성자 내에서 다른 작업을 수행할 수 있습니다.
클래스 부모 { 생성자() { if (new.target === 부모) { console.log('부모 인스턴스화됨'); } 또 다른 { console.log('인스턴스화된 자식'); } } } 클래스 Child는 Parent {}를 확장합니다. var a = 새로운 부모(); // 부모 인스턴스화 var b = 새로운 자식(); // 인스턴스화된 자식
구문에서 클래스가 생성자와 별도의 엔터티임을 암시하더라도 Parent 클래스 정의 내의 constructor()에는 실제로 클래스의 어휘 이름(Parent)이 지정됩니다.
공공 상징
JavaScript는 공개 기호(Well-Known Symbol, WKS)라는 일부 내장 기호를 미리 정의합니다.
이러한 기호는 주로 특화된 메타 속성을 제공하여 이러한 메타 속성이 JavaScript 프로그램에 노출되어 JavaScript 동작을 더 잘 제어할 수 있도록 정의됩니다.
Symbol.iterator
Symbol.iterator는 모든 개체의 특수 위치(속성)를 나타냅니다. 언어 메커니즘은 이 위치에서 자동으로 메서드를 찾아 이 개체의 값을 사용합니다. 많은 객체 정의에는 이 기호에 대한 기본값이 있습니다.
그러나 기본 반복자를 재정의하더라도 Symbol.iterator 속성을 정의하여 임의의 개체 값에 대한 자체 반복자 논리를 정의할 수도 있습니다. 여기서 메타프로그래밍 측면은 정의된 객체를 처리할 때 JavaScript의 다른 부분(예: 연산자 및 루프 구성)에서 사용할 수 있는 동작 속성을 정의한다는 것입니다.
예를 들어:
var 흉터 = [4, 5, 6, 7, 8, 9]; for (var v of arr) { console.log(v); } // 4 5 6 7 8 9 // 홀수 인덱스 값에서만 값을 생성하는 반복자를 정의합니다. arr[Symbol.iterator] = 함수* () { 여기서 idx = 1; 하다 { 이것을 산출하다[idx]; } while ((idx += 2) < this.length); }; for (var v of arr) { console.log(v); } // 5 7 9
Symbol.toStringTag 및 Symbol.hasInstance
가장 일반적인 메타프로그래밍 작업 중 하나는 값을 조사하여 그 값이 어떤 종류인지 알아보고 일반적으로 어떤 작업을 수행하기에 적합한지 결정하는 것입니다. 객체의 경우 가장 일반적으로 사용되는 자체 검사 기술은 toString() 및 instanceof입니다.
ES6에서는 다음 작업의 동작을 제어할 수 있습니다.
함수 Foo(인사말) { this.greeting = 인사말; } Foo.prototype[Symbol.toStringTag] = 'Foo'; Object.defineProperty(Foo, Symbol.hasInstance, { 값: 함수(inst) { return inst.greeting == '안녕하세요'; }, }); var a = new Foo('안녕하세요'), b = new Foo('세계'); b[Symbol.toStringTag] = '멋져요'; a.toString(); // [객체 푸] 문자열(b); // [객체 쿨] Foo의 인스턴스; // 진실 b Foo 인스턴스; // 거짓
프로토타입(또는 인스턴스 자체)의 @@toStringTag 표기법은 [object]가 문자열화될 때 사용되는 문자열 값을 지정합니다.
@@hasInstance 표기법은 인스턴스 객체 값을 받아들이고 해당 값이 인스턴스로 간주될 수 있는지 여부를 나타내기 위해 true 또는 false를 반환하는 생성자 함수의 메서드입니다.
기호.종
Array의 하위 클래스를 생성하고 상속된 메서드(예: Slice(..))를 정의하려는 경우 사용할 생성자(Array(..) 또는 사용자 정의 하위 클래스) 기본적으로 Array 하위 클래스의 인스턴스에서 Slice(..)를 호출하면 이 하위 클래스의 새 인스턴스가 생성됩니다.
이 요구 사항은 클래스의 기본 @@species 정의를 재정의하여 메타 프로그래밍할 수 있습니다.
클래스 쿨 { // @@species를 서브클래스로 연기 정적 get [Symbol.species]() { 이거 돌려줘; } 다시() { 새로운 this.constructor[Symbol.species]()를 반환합니다. } } 클래스 Fun은 Cool을 확장합니다. {} 클래스 Awesome은 Cool을 확장합니다. //@@species를 상위 생성자로 지정하도록 강제합니다. 정적 get [Symbol.species]() { 쿨을 반환; } } var a = 새로운 Fun(), b = 새로운 멋진(), c = a.다시(), d = b.다시(); c 재미의 인스턴스; // 진실 d 인스턴스의 최고; // 거짓 d 인스턴스 쿨; // 진실
내장 네이티브 생성자에서 Symbol.species의 기본 동작은 이를 반환하는 것입니다. 사용자 클래스에는 기본값이 없지만 표시된 것처럼 이 동작 기능은 시뮬레이션하기 쉽습니다.
새 인스턴스를 생성하는 방법을 정의해야 하는 경우 new this.constructor(..) 또는 new XYZ(..)를 하드코딩하는 대신 new this.constructor[Symbol.species](..) 패턴 메타프로그래밍을 사용하세요. 그런 다음 상속 클래스는 Symbol.species를 사용자 정의하여 이러한 인스턴스를 생성하는 생성자를 제어할 수 있습니다.
연기
ES6의 가장 확실한 새로운 메타프로그래밍 기능 중 하나는 프록시 기능입니다.
프록시는 다른 일반 객체를 "캡슐화"하거나 이 일반 객체 앞에 위치하는 특수 객체입니다. 프록시 객체에 특수 처리 기능(즉, 트랩)을 등록할 수 있습니다. 이 프로그램은 프록시에서 다양한 작업을 수행할 때 호출됩니다. 이러한 핸들러에는 원래 대상/캡슐화된 개체에 작업을 전달하는 것 외에도 추가 논리를 수행할 수 있는 기회가 있습니다.
프록시에 정의할 수 있는 트랩 처리기 함수의 예로는 개체의 속성에 액세스하려고 할 때 [[Get]] 작업을 가로채는 get이 있습니다.
var obj = { a: 1 }, 핸들러 = { get(대상, 키, 컨텍스트) { // 참고: 대상 === obj, // 컨텍스트 === pobj console.log('액세스 중: ', 키); return Reflect.get(대상, 키, 컨텍스트); }, }, pobj = new Proxy(obj, 핸들러); obj.a; // 1 pobj.a; // 접근 중: // 1
대상 객체 참조(obj), 키 속성 이름("a") 및 본문 리터럴을 허용하는 핸들러(Proxy(..)의 두 번째 매개변수) 객체에 대해 get(..) 처리 함수 명명 방법을 선언합니다. 자기/수신자/에이전트(pobj).
대행사 제한
객체에 대해 수행할 수 있는 광범위한 기본 작업 세트는 이러한 메타프로그래밍 함수 트랩을 통해 처리될 수 있습니다. 그러나 (적어도 지금은) 가로챌 수 없는 일부 작업이 있습니다.
var obj = { a:1, b:2 }, 핸들러 = { .. }, pobj = new Proxy( obj, 핸들러 ); 객체 유형; 문자열(obj); obj + ""; obj == pobj; obj === pobj
요약하다
이 글의 주요 내용을 요약해 보겠습니다.
- ES6 이전에는 JavaScript에 이미 많은 메타프로그래밍 기능이 있었으며 ES6은 메타프로그래밍 기능을 크게 향상시키는 몇 가지 새로운 기능을 제공합니다.
- 익명 함수에 대한 함수 이름 파생부터 생성자 호출 방법에 대한 정보를 제공하는 메타 속성까지, 이전보다 프로그램 런타임 구조를 더 자세히 살펴볼 수 있습니다. 기호를 노출하면 객체에서 기본 유형으로의 유형 변환과 같은 원래 기능을 재정의할 수 있습니다. 프록시는 객체의 다양한 기본 작업을 가로채고 사용자 정의할 수 있으며 Reflect는 이를 시뮬레이션하는 도구를 제공합니다.
- 원저자는 다음을 권장합니다. 먼저, 이 언어의 핵심 메커니즘이 어떻게 작동하는지 이해하는 데 집중하세요. JavaScript 자체의 작동 방식을 실제로 이해했다면 이제 이러한 강력한 메타프로그래밍 기능을 사용하여 언어를 추가로 적용할 차례입니다.
화웨이 클라우드의 신기술에 대해 빨리 알아보고 팔로우하려면 클릭하세요~
Linus는 커널 개발자가 탭을 공백으로 대체하는 것을 막기 위해 문제를 직접 해결했습니다. 그의 아버지는 코드를 작성할 수 있는 몇 안 되는 리더 중 한 명이고, 둘째 아들은 오픈 소스 기술 부서의 책임자이며, 막내 아들은 핵심입니다. Huawei: 일반적으로 사용되는 모바일 애플리케이션 5,000개를 변환하는 데 1년이 걸렸습니다. Hongmeng으로의 포괄적인 마이그레이션 Java는 타사 취약점에 가장 취약한 언어입니다. Hongmeng의 아버지인 Wang Chenglu: 오픈 소스 Hongmeng은 유일한 아키텍처 혁신입니다. 중국 기초 소프트웨어 분야의 마화텅(Ma Huateng)과 저우홍이(Zhou Hongyi)가 악수를 하며 "원한을 풀다" 전 마이크로소프트 개발자: 윈도우 11 성능은 "터무니없을 정도로 나쁘다" 라오샹지가 오픈소스인 것은 코드는 아니지만 그 이유는 다음과 같다. Google이 대규모 구조 조정을 발표 했습니다 .