서문에
[class_ES6 노트 10](/articles/class-es6 노트 10/) 에서 class, constructor, static 등의 키워드가 클래스 정의를 간소화한다고 언급했습니다. 더 간단한 getter/setter 와 다양한 함수 속성의 간소화 정의 방식을 포함합니다. 이뿐이라면, ES6 의 클래스는 아직 무언가 부족한 것처럼 보입니다
네, 상속입니다. ES5 이전에는 6 종류의 상속 방안 이 있었고, ES5 는 공식판 beget 메서드 (Object.create()) 를 제공하여 기생조합식 상속への지원을 나타냈습니다. ES6 는 단순히 네이티브 라벨을 붙인 툴 함수가 아닌, 더 강력하고 편리한 변혁을 가져오고자 합니다
따라서 클래스 정의를 간소화하는 것은 오브젝트 정의 방식을 간소화할 때顺便에 완료한 것이며, ES6 가 진정으로 말하고 싶은 것은: 이제 우리는더 편리한 상속 메커니즘을 제공한다는 것입니다
一.어떻게 정적 속성을 상속하는가?
잘 떠올려 보세요. JS 상속을 논의할 때, 우리는결코정적 속성에 대해 언급한 적이 없습니다. 예를 들어:
function Super() {}
Super.staticAttr = 'static';
function Sub() {}
Sub.prototype = new Super();
console.log(Sub.staticAttr); // undefined
이렇게 하면 당연히 정적 속성을 상속할 수 없습니다. 이와 유사한 다른 5 종류의 방안도 정적 속성의的感受성을 고려하지 않았습니다. 정적 속성은 JS 중의应用场景이 많지 않고, 상속할 수 없어도 아무 영향이 없지만, 상속 메커니즘にとって, 정적 속성을 상속할 수 없는 것은 다소 결憾이라고 할 수 있습니다
만약 정적 속성을 상속해야 한다면, 방법이 있을까요? 분석해 봅시다:
// Function 인스턴스 Type 을 정의, 소위 "클래스"
function Type() {}
// Function 인스턴스의 프로토타입은 당연히 소속 클래스 Function 의 prototype 속성이 가리키는 것
Type.__proto__ === Function.prototype
// 반대로, 클래스 Type 의 prototype 속성이 가리키는 것은 당연히该类인스턴스의 프로토타입
Type.prototype === new Type().__proto__
// 따라서, 이는 2 개의 다른 것
Type.prototype !== Type.__proto__
마지막 행의 왼쪽 Type 은 커스텀 클래스라고 불러야 하고, 오른쪽 Type 은 Function 인스턴스라고 불러야 합니다. Type.prototype 이 가리키는 것은 커스텀 클래스 Type 의 모든 인스턴스에 영향을 미치며, 그것들은 모두 이 것에 액세스할 수 있습니다; 한편 Type.__proto__ 가 가리키는 것은 Type 소속 클래스의 모든 인스턴스에 영향을 미치며, Type 자체도 그 중 하나입니다. prototype 은 미래에만 영향을 미칠 수 있고, __proto__ 는 역사를 개변합니다:
function SubType() {}
var p = new Type();
SubType.prototype = p;
Type.__proto__ = {a: 1};
console.log(new SubType().a); // undefined
// p.__proto__ = {a: 1};
// console.log(new SubType().a); // 1
각 오브젝트는 내부 속성 __proto__ 를 가지며, 속성 액세스 시의 프로토타입 체인 검색은 이 속성을 거슬러 올라가는 과정입니다.上記의 차이가 존재하는 이유는, SubType 인스턴스의 프로토타입 체인과 SubType 자체의 프로토타입 체인의 유일한 교점은 Object.prototype 이고, 그 외는 무관하기 때문입니다:
// SubType 인스턴스의 프로토타입 체인
new SubType().__proto__ === SubType.prototype
new SubType().__proto__.__proto__ === Type.prototype
new SubType().__proto__.__proto__.__proto__ === Object.prototype
new SubType().__proto__.__proto__.__proto__.__proto__ === null
// SubType 자체의 프로토타입 체인
SubType.__proto__ === Function.prototype
SubType.__proto__.__proto__ === Object.prototype
SubType.__proto__.__proto__.__proto__ === null
new 한 인스턴스의 프로토타입 체인 위에는 기본적으로 Function.prototype 이 환은 존재하지 않습니다. 除非 그 어떤 조상의 프로토타입이 function 타입이지만, 이는 그다지 가능성이 없습니다. 왜냐하면 이렇게 할 이유가 전혀 없기 때문입니다:
// 除非 이렇게 하지만, 이 함수는 어떻게 호출하는가?
Type.prototype = function() {};
이제 모두清楚了. 그렇다면 정적 속성을 상속하고 싶다면, 우리는다른 한 ��의 프로토타입 체인을 수정해야 합니다:
Sub.__proto__ = Super; // 정적 속성 상속
첫 번째 예로 효과를 테스트:
function Super() {}
Super.staticAttr = 'static';
function Sub() {}
Sub.prototype = new Super(); // 인스턴스 속성과 프로토타입 속성 상속
Sub.__proto__ = Super; // 정적 속성 상속
console.log(Sub.staticAttr); // static
이는 정적 속성을 지원하는 간단한 프로토타입 체인 상속이지만, 안타깝게도, 우리는치트했습니다. __proto__ 라는 내부 속성은 널리 호환되지 않아, 실망스럽게도 헛수고했음을 알게 됩니다.这也是왜 지금까지 정적 속성의 상속이라는 말이 없었는지, 왜냐하면 우리는 직접 Function 인스턴스의 프로토타입을 수정해야 하지만, 그러나 이는 정말로做不到이기 때문입니다
ES6 는 이 문제를 해결했습니다. 완전한 상속 메커니즘을 제공하고자 하기 때문입니다
二.프로토타입 조작 API
ES6 는 내부 속성 __proto__ 를 대신해 Object.get/setPrototypeOf() 를 제공합니다. 예를 들어:
let obj = {};
Object.setPrototypeOf(obj, Array.prototype);
obj.push(1);
console.log(obj.pop()); // 1
console.log(obj.length); // 0
console.log(obj instanceof Array); // true
Object.setPrototypeOf() 를 통해 일반 오브젝트의 프로토타입을 직접 수정하여, 그것을 배열의 인스턴스로 만듭니다
매우 강력하지만, 주의가 필요합니다:
Changing the [[Prototype]] of an object is, by the nature of how modern JavaScript engines optimize property accesses, a very slow operation, in every browser and JavaScript engine. The effects on performance of altering inheritance are subtle and far-flung, and are not limited to simply the time spent in obj.proto = ... statement, but may extend to any code that has access to any object whose [[Prototype]] has been altered. If you care about performance you should avoid setting the [[Prototype]] of an object. Instead, create a new object with the desired [[Prototype]] using Object.create().
(Object.setPrototypeOf() - JavaScript | MDN 에서 인용)
大意는 이 API 의성능이 매우 나쁘다는것으로, 수동으로 프로토타입 체인을 篡改하면 JS 엔진이 속성 액세스를 최적화할 수 없게 되어, 가져오는 성능 영향은 obj.__proto__ 를 篡改하는 것보다 훨씬 큽니다. 왜냐하면 篡改된 프로토타입의 오브젝트에 액세스하는 모든 코드가 영향을 받기 때문입니다. 성능을 신경 쓴다면, Object.create() 로 새로운 프로토타입을 생성하고, 이를 템플릿으로 필요한 오브젝트를 생성하는 것을 권장합니다
성능이 나쁜 이유는 __proto__ 를 篡改하는 것이역사를 개변하는 (과거로 穿越하여, 鍵人物을 교체하고, 해당 점 이후의 모든 역사를 개변하는) 것이고, 한편 Type.prototype 을 수정하는 것은 성능이 나쁘지 않습니다. 왜냐하면미래를 铺垫하며, 이미 존재하는 것에 영향을 주지 않기 때문입니다. 고전적인 예:
function Super() {
this.key = 'value';
}
function Sub() {}
var obj1 = new Sub();
// 미래를 铺垫
Sub.prototype = new Super();
var obj2 = new Sub();
console.log(obj1.key); // undefined
console.log(obj2.key); // value
prototype 를 수정하는 것은 이미 존재하는 것 (obj1) 에 영향을 주지 않으므로, 성능 영향은 자연스럽게 작아집니다. 한편 __prototype__ 는 분명히 다릅니다:
function Super() {
this.key = 'value';
}
function Sub() {}
var obj1 = new Sub();
// 역사를 개변
Sub.prototype.__proto__ = new Super();
var obj2 = new Sub();
console.log(obj1.key); // value
console.log(obj2.key); // value
2 개의 예는 대조 시험은 아니지만, 후자는 obj 의 하하일환을 수정하고 있습니다. 그러나 우리는 성공적으로 역사를 개변했습니다. 한편 prototype 을 사용하는 것은 확실히做不到입니다 (Object.prototype 을 수정? 확실히 할 수 있지만, 이렇게 하면, 상속을 논의하는还有什么의미가 있습니까?)
따라서, 상속에는 대가를 치러야 하며, 네이티브 클래스를 상속하는 대가는 더욱 큽니다 (네이티브 클래스 내부에는 몇 개의 盤根錯節한 것이 있고, moreover 배열 오브젝트와 일반 오브젝트의 메모리布局도 다릅니다. 생각하는 것만으로도 힘듭니다)
三.가장 완벽한 상속
ES5 에서 가장 완벽한 상속 방식은 기생조합식으로, 다음과 같습니다:
function Super(){
// 여기서만 기본 속성과 참조 속성 선언
this.val = 1;
this.arr = [1];
}
Super.staticProp = 1; // 정적 속성
// 여기서 함수 선언
Super.prototype.fun1 = function(){};
Super.prototype.fun2 = function(){};
//Super.prototype.fun3...
function Sub(){
// 1.인스턴스 속성 val, arr 상속
Super.call(this);
// ...
}
// 2.프로토타입 속성 fun1, fun2 상속
var proto = Object.create(Super.prototype);
proto.constructor = Sub;
Sub.prototype = proto;
완벽하게 인스턴스 속성과 프로토타입 속성을 상속하고, 프로토타입 참조가 서브클래스 인스턴스 간에 공유되는 문제를 회피하며, 여분의那份의 인스턴스 속성을 잘라냈습니다
하지만 이것도 마찬가지로고려하지 않았습니다정적 속성의 상속 문제. 수동으로 추가합니다:
function Super(){
// 여기서만 기본 속성과 참조 속성 선언
this.val = 1;
this.arr = [1];
}
Super.staticProp = 1; // 정적 속성
// 여기서 함수 선언
Super.prototype.fun1 = function(){};
Super.prototype.fun2 = function(){};
//Super.prototype.fun3...
function Sub(){
// 1.인스턴스 속성 val, arr 상속
Super.call(this);
// ...
}
// 2.프로토타입 속성 fun1, fun2 상속
Object.setPrototypeOf(Sub.prototype, Super.prototype);
// 3.정적 속성 상속
Object.setPrototypeOf(Sub, Super);
여기서 상속 프로토타입 속성의 3 구를 1 구로 간소화했습니다. 비교해 봅시다:
/* 이전의 3 구 */
// 1.Super.prototype 을 익명 오브젝트의 프로토타입에 넣고, 이 익명 오브젝트를 반환
var proto = Object.create(Super.prototype);
// 2.constructor 속성 수정
proto.constructor = Sub;
// 3.서브클래스 인스턴스가 익명 오브젝트 프로토타입 속성의 액세스권을 획득하게 함
// new Sub().__proto__ === proto
Sub.prototype = proto;
/* 1 구로 간소화 */
// 효과는 위 3 구와 동등 (서브클래스 인스턴스가 부모 클래스 프로토타입 속성의 액세스권을 획득), 구현 방식은 "하하일환" 과 유사
Object.setPrototypeOf(Sub.prototype, Super.prototype); // Sub.prototype.__proto__ = Super.prototype 과 동등
이렇게 하는 것은 코드를 간소화하는 (3 행이 1 행으로) 것 외, 큰 의미는 없습니다. 성능을 고려하면, 더욱이 이 종류의 간소화를 할 이유가 없습니다
하지만 Object.setPrototypeOf(Sub, Super) 는 피할 수 없습니다. 이는 ES6 까지, 유일합법적으로 진정한 의미로 "역사를 개변" 할 수 있는 수단입니다
정적 속성 상속 지원을 추가한 후의 완전한 예는 다음과 같습니다:
function Super(){
// 여기서만 기본 속성과 참조 속성 선언
this.val = 1;
this.arr = [1];
}
Super.staticProp = 1; // 정적 속성
// 여기서 함수 선언
Super.prototype.fun1 = function(){};
Super.prototype.fun2 = function(){};
//Super.prototype.fun3...
function Sub(){
// 1.인스턴스 속성 val, arr 상속
Super.call(this);
// ...
}
// 2.프로토타입 속성 fun1, fun2 상속
Object.setPrototypeOf(Sub.prototype, Super.prototype);
// 3.정적 속성 상속
Object.setPrototypeOf(Sub, Super);
// test
var sub = new Sub();
console.log(sub.val); // 1
console.log(sub.arr); // [1]
console.log(Super.staticProp); // 1
console.log(Sub.staticProp); // 1
이것이 가장 완벽한 상속입니다. 정적 속성을 지원하는 기생조합식 상속. 하지만 문제가 존재합니다. "어디서 무엇을 선언해야 하는가" 는 단지도덕적 제약으로, 이 약한 제약은 차간 생산에 불리합니다. 우리는더 강한 제약이 필요합니다
四.ES6 상속
우리는 이미 class, static 을 배웠습니다 ([class_ES6 노트 10](/articles/class-es6 노트 10/) 참조). 또한 어떻게 정적 속성을 상속하는지도 알고 있습니다. 그렇다면 ES6 의 상속은 이렇게 되어야 합니다:
class Super {
constructor(sub) {
console.log(sub);
this.greeting = 'hello' + (sub && `, ${sub.name}`);
}
}
class Sub {
constructor() {
// 인스턴스 속성 상속
this.name = 'sam';
return new Super(this);
}
}
// 프로토타입 속성 상속
Object.setPrototypeOf(Sub.prototype, Super.prototype);
// 정적 속성 상속
Object.setPrototypeOf(Sub, Super);
// test
console.log(new Sub().greeting); // hello, sam
꽤 간결해지고, 매우 완벽해 보입니다. 하지만:
console.log(new Sub() instanceof Super); // true
console.log(new Sub() instanceof Sub); // false
타입이果然凌亂했습니다. 수동으로 constructor 를 수정? 너무 번거롭습니다. ES6 도 이 점을 발견하고, 따라서 extends 키워드를 제공했습니다:
class Super {
constructor() {
// 인스턴스 속성
this.val = 1;
this.arr = [1];
}
// 정적 속성
static get staticProp() {
return this._staticProp || 1;
}
static set staticProp(val) {
this._staticProp = val;
}
// 프로토타입 속성
fun1() {}
fun2() {}
}
class Sub extends Super {
// ...
}
// test
var obj = new Sub();
console.log(obj.val); // 1
console.log(obj.arr); // [1]
console.log(Super.staticProp); // 1
console.log(Sub.staticProp); // 1
console.log(obj instanceof Super); // true
console.log(obj instanceof Sub); // true
이제 몇 개의新增 키워드의 진정한 작용이 보입니다. 다음과 같습니다:
-
"어디서 무엇을 선언해야 하는가" 에 대해 강한 제약을 제공
-
정적 속성 상속을 지원
-
자동으로 타입을 유지
class Sub extends Super 구문으로, Super 는 다른 클래스, 프로토타입 상속 기반의 함수, 일반 함수, 함수 또는 클래스를 포함하는 변수, 오브젝트 상의某个 속성, 함수 호출可以是. 더욱이, Object.prototype 에서 상속하고 싶지 않다면 extends null 도 할 수 있습니다. 이는 충분히 큰유연성을 제공하여, 새로운 구문을 사용해 기존 클래스 및 제 3 자 클래스를 확장하는 것을 쉽게 합니다
상속 메커니즘의 기본 요소가有了, 자연스럽게 몇 개의 더 고급인 수요가 생깁니다. 예를 들어, 어떻게 부모 클래스 속성에 액세스하는가? 어떻게 부모 클래스 생성자에 파라미터를 전달하는가?
조상 클래스 속성에 액세스
super 키워드를 통해 조상 클래스 속성에 액세스하고, 부모 클래스 생성자를 호출할 수 있습니다. 다음과 같습니다:
class A {
constructor(name) {
// 인스턴스 속성
this.name = name;
}
// 프로토타입 속성
fn() {
console.log('fn at A');
}
// 정적 속성
static get staticProp() {
return this._staticProp || 1;
}
static set staticProp(val) {
this._staticProp = val;
}
}
class B extends A {
constructor(name) {
super(name.toUpperCase());
super.fn();
}
fn() {
super.fn();
}
}
var b = new B('BextendsfromA'); // fn at A
console.log(b.name); // BEXTENDSFROMA
super 는 서브클래스 중에 정의된 속성을 스킵하고, 직접 서브클래스 프로토타입에서查找을 시작합니다
내부는 여전히 프로토타입 체인查找이므로, super 가 액세스할 수 있는 조상 클래스 속성은프로토타입 속성에 한정되며, 조상 클래스의 정적 속성과 인스턴스 속성에 액세스할 수 없습니다. 예를 들어:
class B extends A {
constructor(name) {
super(name.toUpperCase());
super.fn();
// 서브클래스 인스턴스의 프로토타입에서 프로토타입 체인查找을 수행하므로, 당연히 다른 한 개의 프로토타입 체인 상에 있는 조상 클래스 정적 속성은 찾을 수 없음
console.log(super.staticProp); // undefined
// 프로토타입 체인을 통해 조상 클래스의 인스턴스 속성을 찾으려는 것은 더욱 불가능하며, 분명히 `this.key` 로 찾아야 함 (인스턴스 속성의 상속 방식은 값 복사이며, 속성 액세스권을持有하는 것이 아님)
console.log(super.key); // undefined
}
...
}
주의:super 는 this 에很像으로 보입니다. this 는内置의 변수명처럼, 다른 작용역에서 다른 값을 가리킬 수 있습니다. 그러나 super 는 다릅니다. super 는키워드입니다:
typeof super; // Uncaught SyntaxError: 'super' keyword unexpected here
super instanceof SuperType; // 同上
super.xxx/super['xxx'] 로 부모 클래스 속성에 액세스하거나, super() 로 부모 클래스 생성자를 호출하는 둘 중 하나이며, 다른 형식은 모두非法입니다
네이티브 타입의 확장을 지원
만약 CharArray extends Array 라면, Array.isArray() 檢測은 true 를 반환해야 하고, instanceof 檢測은 true 를 반환해야 하며, moreover slice 등의 메서드도 CharArray 를 반환해야 합니다 (Chrome47 은 Array 를 반환하지만, 현재 53 은 이미 올바르게 CharArray 를 반환할 수 있습니다). 예는 다음과 같습니다:
class CharArray extends Array {
constructor(str) {
console.log(typeof str, str);
if (str.length > 1) {
// 먼저 인스턴스 [] 를 생성, 否则 this 가 오류
// Uncaught ReferenceError: this is not defined
super();
// 다음 push
super.push.apply(this, str.split(''));
}
else {
super(str);
}
}
toUpperCase() {
return this.map(function(c) {
return c.toUpperCase();
});
}
}
var ca1 = new CharArray('abcde'); // string abcde
var ca2 = new CharArray('c'); // string c
console.log(ca1); // ["a", "b", "c", "d", "e"]
console.log(ca2); // ["c"]
console.log(ca1.slice(1)); // ["b", "c", "d", "e"] number 4
console.log(ca1.toUpperCase()); // ["A", "B", "C", "D", "E"] number 5
console.log(Array.isArray(ca1)); // true
console.log(ca1 instanceof CharArray); // true
//! 이론上 true 를 반환해야 함
console.log(ca1.slice(2) instanceof CharArray); // true number 3
console.log(ca1 instanceof Array); // true
네이티브 클래스의 확장 클래스는 진짜처럼 기존의 모든 타입 檢測에 통과할 수 있는 것 외, 상속한 모든 네이티브 메서드도 동일한 동작을 가져야 합니다. 上記 코드 중의 하나의 鍵이 되는 점은 console.log(typeof str, str); 로, 우리가 예상하는 첫 번째 파라미터는 string 이어야 하지만, 실제로는 가끔 이 파라미터는 number 입니다 (slice() 를 호출할 때). 이는 네이티브 slice() 는 아마도 이렇다는 것을 보여줍니다:
Array.prototype.slice = function(start) {
// start 가 음수인 情況은暂且 고려하지 않음
let size = this.length - start;
let res = new Array(size);
for (let i = 0; i < size; i++) {
res[i] = this[start + i];
}
return res;
}
new Array(size) 가 가끔 생성자가 number 를 받는 이유입니다. 이는 커스텀의 가짜와 진짜가내부 메커니즘이 완전히 일치하고 있음을 보여줍니다
서브클래스 인스턴스의 생성 과정
서브클래스 constructor 중에서, this 는 super() 를 호출하여 획득해야 합니다. super() 전에 this 를 사용하면 ReferenceError 가 오류가 됩니다 (이 제약은 Java 클래스의 생성자 중에서, super() 가 반드시 첫 행에 위치하는 것과 유사한 이치). 예를 들어:
class C {}
class D extends C {
constructor() {
// this.a = 1; // Uncaught ReferenceError: this is not defined
super(); // C 의 기본 빈 생성자 호출
// 다른 오브젝트를 직접 return 할 수도 있음. this 를 사용하지 않음
// return {a: 1};
}
}
// test
console.log(new D());
서브클래스 constructor 를 정의하는 것은 JS 에 "서브클래스 인스턴스를 생성하는 것은 우리에게 맡기고, 당신이 관리할 필요는 없다" 고 알리는 것입니다
따라서 서브클래스 생성자 중에서, 우리는 부모 클래스 생성자를 호출하여 적절한 인스턴스를 생성하고, 조상 속성을 차용하여 인스턴스를 초기화할 수 있습니다. 더욱 난폭하게는, this 를 포기하고, 수동으로 무관한 다른 오브젝트를 반환하여, new 연산의 결과로 할 수도 있습니다
물론, constructor 를 정의하지 않을 수도 있습니다. 서브클래스 인스턴스의 생성 과정을 신경 쓰지 않는다는 것을 나타내며, 그 경우该类의 빈 인스턴스가 생성됩니다
this 에 액세스하기 전에 반드시 super() 를 호출하는 것은理所当然입니다. 否则 this 의 구조가 불확정적입니다 (Array 인지 일반 오브젝트인지?)
new.target
조상 클래스 생성자 중에서 new.target 의 값을 檢測하여누가 해당 생성자를 호출하고 있는지알 수 있습니다
서브클래스 생성자는 반드시 먼저 super() 를 실행한 후에야 this 를 획득할 수 있으므로, 某些兄弟 서브클래스는 본질적인 차이가 존재합니다 (예를 들어 Array 와 일반 오브젝트의 메모리布局이 다릅니다). 따라서 부모类는 어떤 종류의 오브젝트를 호출자의 this 로서 반환해야 하는지 알 필요가 있고, new.target 은 이 문제를 해결하기 위해 있습니다. 예를 들어:
class E {
constructor() {
switch(new.target) {
case E:
console.log('call from E');
break;
case F:
console.log('call from F');
break;
case G:
console.log('call from G');
break;
case H:
console.log('call from H');
break;
default: break;
}
}
}
// 상속 나무를建立
// E
// / \
// F G
// |
// H
class F extends E {} // 기본으로 부모 클래스 생성자 호출
class G extends E {}
class H extends G {}
// test
new E(); // call from E
new F(); // call from F
new G(); // call from G
new H(); // call from H
P.S.new.target 은 임의의 함수 중에서 합법입니다. 만약 함수가 new 를 통해 호출되지 않는다면, new.target 은 undefined 에賦値됩니다
基类가 서브클래스 정보를 알아야 하는 것은 이해하기 어렵고, 추상이 역으로 구체에 의존하는ように 느껴집니다. 그렇다면 基类는 무엇을 new 하고 싶은 서브클래스를 알아야 할까?
上記와 같이, 왜냐하면
某些兄弟 서브클래스는 본질적인 차이가 존재함
차이가 이렇게 크다면, 왜 2 개의 基类로 분리하지 않는가? Java 에서 이렇게 하면 확실히 문제를 해결할 수 있지만, JS 는 할 수 없습니다. 왜냐하면JS 의 상속 나무는只有一个根 노드——Object 이기 때문입니다. 따라서 Object 는 우리가 일반 Object 를 new 하고 싶은지 Array 를 new 하고 싶은지 알 필요가 있습니다
说白了就是因为 JS 가 모든 것을 Object 이根 노드 아래에 걸었기 때문에, "兄弟 서브클래스의 차이가 거대" 라는 문제가 발생하고, 그리고 new.target 의 해결 방안이 생겼습니다. 한편Java 의 상속 나무는不止一个根 노드이므로, 이 문제는 존재하지 않습니다
五.다중 상속
class extends Mixin 이라는 방식이 있습니다:
function mix(...mixins) {
class Mix {}
function copy(target, source) {
// 명명 함수에는 name 속성이 있음
let filterSet = new Set(['constructor', 'prototype', 'name']);
// for (let key of Reflect.ownKeys(source)) {
for (let key in source) {
// if (!filterSet.has(key)) {
if (source.hasOwnProperty(key) && !filterSet.has(key)) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
for (let mixin of mixins) {
// 만약 mixin 이 "클래스" 라면, 그 정적 속성과 프로토타입 속성을 "상속"
// 만약 mixin 이 일반 오브젝트라면, 그 인스턴스 속성을 "상속"
copy(Mix, mixin);
copy(Mix.prototype, mixin.prototype);
}
return Mix;
}
이전의 mix/extends 방안은 여러 개의 오브젝트를揉み合쳐 하나의 오브젝트로 만드는 것이었고, 지금은 한 걸음 더 나아가, 여러 개의 mixin 을揉み合쳐 하나의 클래스로 만듭니다. 이 클래스의 인스턴스는 템플릿*"클래스"*의 정적 속성과 프로토타입 속성 및 템플릿 오브젝트의 인스턴스 속성을 "상속" 할 수 있습니다
주의: "클래스" 에 이중 인용부호가 붙어 있는 것은鍵입니다. 왜냐하면 여기에서의 클래스는 class 키워드로 정의된 클래스를 가리키는 것이 아니라, function 키워드로 정의된 타입을 가리키기 때문입니다. 차이는 전자는 열거 불가 (这也是 class封裝性의体现) 로, copy 방안이失效하며,那就什么都 "상속" 할 수 없게 됩니다
효과를 테스트해 봅시다:
let obj = {key: 'value'};
function Type() {}
Type.staticProp = 'static value from Type';
Type.prototype.fn = function() {
return 'proto fn from Type';
};
let M = mix(obj, Type);
console.log(M.key); // value
console.log(M.staticProp); // static value from Type
console.log(new M().fn()); // proto fn from Type
怎么说, 효과가有限하게 느껴집니다. 이전의 mix/extends 방안보다 조금 강력하지만, 효과가 비교적有限합니다. 특히 mixin 이 class 키워드로 정의된 클래스를 지원하지 않기 때문에, 이 방안은 더욱 발전하기가 어렵습니다
따라서, JS 는 여전히 다중 상속이 없습니다.
六.まとめ
만약 전문을 ES5 부터 한 걸음 한 걸음 따라왔다면, ES6 의 class 를 배척할 이유가 없습니다. 왜냐하면 그것은 "어디서 무엇을 선언해야 하는가" 와 "더 우아한 상속 구현" 등 오랫동안 존재해 온 많은 문제를 해결했기 때문입니다
참고 자료
- 『ES6 in Depth』:InfoQ 中文站에서 제공하는 무료 전자책
아직 댓글이 없습니다