>  기사  >  웹 프론트엔드  >  js의 클래스를 전통적인 클래스 패턴으로 변환하는 생성자에 대한 자세한 설명(예제 포함)

js의 클래스를 전통적인 클래스 패턴으로 변환하는 생성자에 대한 자세한 설명(예제 포함)

不言
不言원래의
2018-09-21 10:41:172293검색

이 글은 js의 클래스를 전통적인 클래스 패턴으로 변환하는 방법에 대한 자세한 설명을 제공합니다. 도움이 필요한 친구들이 참고할 수 있기를 바랍니다. .

머리말

JS 프로토타입 기반 '클래스'는 프런트엔드 경력으로 전환한 코더들에게 항상 놀라움을 선사해왔습니다. 그러나 클래스 키워드 정의를 사용하는 전통적인 모델의 등장은 일부 프런트엔드 동료들에게 깊은 안타까움을 안겨주었습니다. 그리고 "나의 고유한 JS를 돌려주세요", "그냥 별것 아닌 것 좀 만들어 보세요", "저는 수업이 없어서 다른 사람의 수업에 의존해야 합니다", 심지어 "직업을 바꿨습니다" 등의 메시지를 남깁니다. . 결국 새로운 지식을 갖는다는 것은 더 많은 시간과 에너지를 소모한다는 것을 의미하며 단순히 눈을 감고 즐길 수 있는 것이 아닙니다.

그러나 역사의 축은 계속해서 전진합니다. 수업에 대해 한 가지 확실한 점은 면접관에게 "제발, 이해하지 못하는 것이 아니라 이해하고 싶지 않은 것뿐입니다."라고 말할 수 없다는 것입니다. 이해하세요. 질문을 바꿔주세요!" 한편으로는 클래스가 구문적 설탕일 뿐이지만 확장에 의한 상속 개선은 여전히 ​​좋습니다. 반면에 미래에 "클래스"에 나타날 수 있는 새로운 기능은 생성자 대신 클래스에 의해 수행되어야 합니다. 그것이 미래에 얼마나 아름다울지는 아무도 확신하지 못합니다. 자, 자, 김이 모락모락 나는 이 흑설탕과 생강 수프 한 그릇을 천천히 마셔보세요

1 class

ECMAScript에는 클래스 개념이 없습니다. 우리의 예는 프로토타입을 기반으로 생성자에 의해 생성된 동적 속성과 메서드를 가진 객체입니다. 그러나 국제 표준에 부합하고 이를 보다 간단하고 우아하게 설명하기 위해 "클래스"라는 단어는 계속 사용됩니다. 따라서 JS 클래스는 생성자와 동일합니다. ES6 클래스는 단지 구문적 설탕일 뿐이며 해당 정의에 의해 생성된 객체는 여전히 생성자입니다. 하지만 생성자 패턴과 구별하기 위해 클래스 패턴이라고 부릅니다. 학습 수업에는 생성자 및 프로토타입 객체에 대한 지식이 필요합니다. 자세한 내용은 Baidu에서 검색할 수 있습니다.

// --- 使用构造函数
function C () {
  console.log('New someone.');
}

C.a = function () { return 'a'; }; // 静态方法

C.prototype.b = function () { return 'b'; }; // 原型方法


// --- 使用class
class C {
  static a() { return 'a'; } // 静态方法
  
  constructor() { console.log('New someone.'); } // 构造方法
  
  b() { return 'b'; } // 原型方法
};

1.1 변수와 비교

클래스 키워드는 함수를 정의하는 함수 키워드와 유사합니다. 선언적 방식과 표현형(익명 및 명명)이라는 두 가지 방식으로 정의됩니다. 선언적 표현식을 통해 정의된 변수의 특성은 함수의 특성과 다르며, 사전에 구문 분석되지 않고 변수 승격이 없으며 전역 범위에 연결되지 않고 임시 데드존이 있다는 점에서 let 및 const와 더 유사합니다. 클래스 정의에 의해 생성된 변수는 생성자이므로 즉시 실행 모드로 클래스를 작성할 수 있습니다.

// --- 声明式
class C {}
function F() {}

// --- 匿名表达式
let C = class {};
let F = function () {};

// --- 命名表达式
let C = class CC {};
let F = function FF() {};

// --- 本质是个函数
class C {}
console.log(typeof C); // 'function'
console.log(Object.prototype.toString.call(C)); // '[object Function]'
console.log(C.hasOwnProperty('prototype')); // true

// --- 不存在变量提升
C; // 报错,不存在C。
class C {}
// 存在提前解析和变量提升
F; // 不报错,F已被声明和赋值。
function F() {}

// --- 自执行模式
let c = new (class {
})();
let f = new (function () {
})();

1.2 객체와의 비교

클래스 콘텐츠({} 내부)의 형식은 객체 리터럴의 형식과 유사합니다. 그러나 클래스 내용에는 메소드만 정의할 수 있으며 속성은 정의할 수 없습니다. 메소드의 형식은 함수 약어만 사용할 수 있으며 메소드를 쉼표로 구분할 수 없습니다. 메소드 이름은 괄호로 묶인 표현식 또는 기호 값일 수 있습니다. 메소드는 생성자 메소드(생성자 메소드), 프로토타입 메소드(생성자의 프로토타입 속성에 존재) 및 정적 메소드(생성자 자체에 존재)의 세 가지 범주로 나뉩니다.

class C {
  // 原型方法a
  a() { console.log('a'); }
  // 构造方法,每次生成实例时都会被调用并返回新实例。
  constructor() {}
  // 静态方法b,带static关键字。
  static b() { console.log('b'); }
  // 原型方法,带括号的表达式
  ['a' + 'b']() { console.log('ab'); }
  // 原型方法,使用Symbol值
  [Symbol.for('s')]() { console.log('symbol s'); }
}

C.b(); // b

let c = new C();
c.a(); // a
c.ab(); // ab
c[Symbol.for('s')](); // symbol s

속성을 직접 정의할 수 없다고 해서 클래스는 프로토타입 또는 정적 속성을 가질 수 없습니다. 클래스를 해석하면 생성자가 형성되므로 생성자에 속성을 추가하는 것처럼 클래스에 속성을 추가하면 됩니다. 읽기 전용 속성을 정의하려면 getter 함수만 사용하는 것이 더 간단하며 권장됩니다. 속성을 직접 설정할 수 없는 이유는 무엇입니까? 기술이 미성숙한 걸까요? 공무원이 특정 아이디어를 전달하기를 희망합니까? 아니면 작가가 던진 무작위 질문인가요?

// --- 直接在C类(构造函数)上修改
class C {}
C.a = 'a';
C.b = function () { return 'b'; };
C.prototype.c = 'c';
C.prototype.d = function () { return 'd'; };

let c = new C();
c.c; // c
c.d(); // d

// --- 使用setter和getter
// 定义只能获取不能修改的原型或静态属性
class C {
  get a() { return 'a'; }
  static get b() { return 'b'; }
}

let c = new C();
c.a; // a
c.a = '1'; // 赋值没用,只有get没有set无法修改。

1.3 생성자와 비교

다음은 생성자와 클래스를 사용하여 동일한 기능을 구현하는 코드입니다. 직관적으로 클래스는 코드를 단순화하고 콘텐츠를 더욱 집계합니다. 생성자 메서드 본문은 생성자 함수의 함수 본문과 동일합니다. 이 메서드를 명시적으로 정의하지 않으면 기본적으로 빈 생성자 메서드가 추가되어 새 인스턴스를 반환합니다. ES5와 마찬가지로 새 인스턴스 대신 다른 객체를 반환하도록 사용자 정의할 수도 있습니다.

// --- 构造函数
function C(a) {
  this.a = a;
}

// 静态属性和方法
C.b = 'b';
C.c = function () { return 'c'; };

// 原型属性和方法
C.prototype.d = 'd';
C.prototype.e = function () { return 'e'; };
Object.defineProperty(C.prototype, 'f', { // 只读属性
  get() {
    return 'f';
  }
});

// --- 类
class C {
  static c() { return 'c'; }
  
  constructor(a) {
    this.a = a;
  }
  
  e() { return 'e'; }
  get f() { return 'f'; }
}

C.b = 'b';
C.prototype.d = 'd';

클래스는 함수이지만 new를 통해서만 인스턴스를 생성할 수 있고 직접 호출할 수 없습니다. 클래스 내에 정의된 모든 메서드는 열거 가능하지 않으며 생성자 자체와 프로토타입에 추가된 속성과 메서드는 열거 가능합니다. 클래스 내에 정의된 메서드는 기본적으로 엄격 모드이므로 명시적으로 선언할 필요가 없습니다. 위의 세 가지 사항은 클래스의 엄격함을 증가시킵니다. 불행하게도 프라이빗 속성과 메서드를 직접 정의할 수 있는 방법은 아직 없습니다.

// --- 能否直接调用
class C {}
C(); // 报错

function C() {}
C(); // 可以


// --- 是否可枚举
class C {
  static a() {} // 不可枚举
  b() {} // 不可枚举
}

C.c = function () {}; // 可枚举
C.prototype.d = function () {}; // 可枚举

isEnumerable(C, ['a', 'c']); // a false, c true
isEnumerable(C.prototype, ['b', 'd']); // b false, d true

function isEnumerable(target, keys) {
  let obj = Object.getOwnPropertyDescriptors(target);
  keys.forEach(k => {
    console.log(k, obj[k].enumerable);
  });
}


// --- 是否为严格模式
class C {
  a() {
    let is = false;
    try {
      n = 1;
    } catch (e) {
      is = true;
    }
    console.log(is ? 'true' : 'false');
  }
}

C.prototype.b = function () {
  let is = false;
  try {
    n = 1;
  } catch (e) {
    is = true;
  }
  console.log(is ? 'true' : 'false');
};

let c = new C();
c.a(); // true,是严格模式。
c.b(); // false,不是严格模式。

메소드 앞에 static 키워드를 추가하면 이 메소드가 클래스 자체에 존재하며 인스턴스에서 직접 액세스할 수 없음을 나타냅니다. 정적 메서드에서 이는 클래스 자체를 가리킵니다. 정적 메서드와 프로토타입 메서드는 서로 다른 개체에 있기 때문에 동일한 이름을 가질 수 있습니다. ES6에서는 new 이후에 생성자 또는 클래스를 참조하는 새로운 명령 new.target을 추가합니다. 이 명령의 사용에는 특정 제한 사항이 있습니다. 자세한 내용은 다음 예를 참조하세요.

// --- static
class C {
  static a() { console.log(this === C); }
  a() { console.log(this instanceof C); }
}

let c = new C();
C.a(); // true
c.a(); // true


// --- new.target
// 构造函数
function C() {
  console.log(new.target);
}

C.prototype.a = function () { console.log(new.target); };

let c = new C(); // 打印出C
c.a(); // 在普通方法中为undefined。

// --- 类
class C {
  constructor() { console.log(new.target); }
  a() { console.log(new.target); }
}

let c = new C(); // 打印出C
c.a(); // 在普通方法中为undefined。

// --- 在函数外部使用会报错
new.target; // 报错

2 확장

ES5의 고전적인 상속 방법은 기생 결합 상속입니다. 하위 클래스는 부모 클래스 인스턴스와 프로토타입의 속성과 메서드를 각각 상속합니다. ES6에서도 상속의 본질은 동일하지만 구현 방식이 다음 코드와 같이 변경되었습니다. 프로토타입에서의 상속은 extends 키워드를 사용하여 전통적인 언어에 가까운 형태이고, 인스턴스에서의 상속은 super를 호출하여 서브클래스 this의 형태를 완성하는 것임을 알 수 있습니다. 표면적으로는 접근 방식이 더욱 통일되고 간결해졌습니다.

class C1 {
  constructor(a) { this.a = a; }
  b() { console.log('b'); }
}

class C extends C1 { // 继承原型数据
  constructor() {
    super('a'); // 继承实例数据
  }
}

2.1 与构造函数对比

使用extends继承,不仅仅会将子类的prototype属性的原型对象(__proto__)设置为父类的prototype,还会将子类本身的原型对象(__proto__)设置为父类本身。这意味着子类不单单会继承父类的原型数据,也会继承父类本身拥有的静态属性和方法。而ES5的经典继承只会继承父类的原型数据。不单单是财富,连老爸的名气也要获得,不错不错。

class C1 {
  static get a() { console.log('a'); }
  static b() { console.log('b'); }
}

class C extends C1 {
}
// 等价,没有构造方法会默认添加。
class C extends C1 {
  constructor(...args) {
    super(...args);
  }
}

let c = new C();
C.a; // a,继承了父类的静态属性。
C.b(); // b,继承了父类的静态方法。
console.log(Object.getPrototypeOf(C) === C1); // true,C的原型对象为C1
console.log(Object.getPrototypeOf(C.prototype) === C1.prototype); // true,C的prototype属性的原型对象为C1的prototype

ES5中的实例继承,是先创造子类的实例对象this,再通过call或apply方法,在this上添加父类的实例属性和方法。当然也可以选择不继承父类的实例数据。而ES6不同,它的设计使得实例继承更为优秀和严谨。

在ES6的实例继承中,是先调用super方法创建父类的this(依旧指向子类)和添加父类的实例数据,再通过子类的构造函数修饰this,与ES5正好相反。ES6规定在子类的constructor方法里,在使用到this之前,必须先调用super方法得到子类的this。不调用super方法,意味着子类得不到this对象。

class C1 {
  constructor() {
    console.log('C1', this instanceof C);
  }
}

class C extends C1 {
  constructor() {
    super(); // 在super()之前不能使用this,否则报错。
    console.log('C');
  }
}

new C(); // 先打印出C1 true,再打印C。

2.2 super

关键字super比较奇葩,在不同的环境和使用方式下,它会指代不同的东西(总的说可以指代对象或方法两种)。而且在不显式的指明是作为对象或方法使用时,比如console.log(super),会直接报错。

作为函数时。super只能存在于子类的构造方法中,这时它指代父类构造函数。

作为对象时。super在静态方法中指代父类本身,在构造方法和原型方法中指代父类的prototype属性。不过通过super调用父类方法时,方法的this依旧指向子类。即是说,通过super调用父类的静态方法时,该方法的this指向子类本身;调用父类的原型方法时,该方法的this指向该(子类的)实例。而且通过super对某属性赋值时,在子类的原型方法里指代该实例,在子类的静态方法里指代子类本身,毕竟直接在子类中通过super修改父类是很危险的。

很迷糊对吧,疯疯癫癫的,还是结合着代码看吧!

class C1 {
  static a() {
    console.log(this === C);
  }
  b() {
    console.log(this instanceof C);
  }
}

class C extends C1 {
  static c() {
    console.log(super.a); // 此时super指向C1,打印出function a。
    
    this.x = 2; // this等于C。
    super.x = 3; // 此时super等于this,即C。
    console.log(super.x); // 此时super指向C1,打印出undefined。
    console.log(this.x); // 值已改为3。

    super.a(); // 打印出true,a方法的this指向C。
  }

  constructor() {
    super(); // 指代父类的构造函数
    
    console.log(super.c); // 此时super指向C1.prototype,打印出function c。

    this.x = 2; // this等于新实例。
    super.x = 3; // 此时super等于this,即实例本身。
    console.log(super.x); // 此时super指向C1.prototype,打印出undefined。
    console.log(this.x); // 值已改为3。

    super.b(); // 打印出true,b方法的this指向实例本身。
  }
}

2.3 继承原生构造函数

使用构造函数模式,构建继承了原生数据结构(比如Array)的子类,有许多缺陷的。一方面由上文可知,原始继承是先创建子类this,再通过父类构造函数进行修饰,因此无法获取到父类的内部属性(隐藏属性)。另一方面,原生构造函数会直接忽略call或apply方法传入的this,导致子类根本无法获取到父类的实例属性和方法。

function MyArray(...args) {
  Array.apply(this, args);
}

MyArray.prototype = Array.prototype;
// MyArray.prototype.constructor = MyArray;

let arr = new MyArray(1, 2, 3); // arr为对象,没有储存值。
arr.push(4, 5); // 在arr上新增了0,1和length属性。
arr.map(d => d); // 返回数组[4, 5]
arr.length = 1; // arr并没有更新,依旧有0,1属性,且arr[1]为5。

创建类的过程,是先构造一个属于父类却指向子类的this(绕口),再通过父类和子类的构造函数进行修饰。因此可以规避构造函数的问题,获取到父类的实例属性和方法,包括内部属性。进而真正的创建原生数据结构的子类,从而简单的扩展原生数据类型。另外还可以通过设置Symbol.species属性,使得衍生对象为原生类而不是自定义子类的实例。

class MyArray extends Array { // 实现是如此的简单
  static get [Symbol.species]() { return Array; }
}

let arr = new MyArray(1, 2, 3); // arr为数组,储存有1,2,3。
arr.map(d => d); // 返回数组[1, 2, 3]
arr.length = 1; // arr正常更新,已包含必要的内部属性。

需要注意的是继承Object的子类。ES6改变了Object构造函数的行为,一旦发现其不是通过new Object()这种形式调用的,构造函数会忽略传入的参数。由此导致Object子类无法正常初始化,但这不是个大问题。

class MyObject extends Object {
  static get [Symbol.species]() { return Object; }
}

let o = new MyObject({ id: 1 });
console.log(o.hasOwnPropoty('id')); // false,没有被正确初始化

위 내용은 js의 클래스를 전통적인 클래스 패턴으로 변환하는 생성자에 대한 자세한 설명(예제 포함)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.