ホームページ  >  記事  >  ウェブフロントエンド  >  jsのクラスを従来のクラスパターンに変換するコンストラクターを詳しく解説(例付き)

jsのクラスを従来のクラスパターンに変換するコンストラクターを詳しく解説(例付き)

不言
不言オリジナル
2018-09-21 10:41:172289ブラウズ

この記事では、js のクラスを従来のクラス パターンに変換するコンストラクターについて詳しく説明します (例を示します)。必要な方は参考にしていただければ幸いです。あなたは助けてくれました。

序文

JS のプロトタイプ ベースの「クラス」は、フロントエンドの職業に転職したプログラマーを常に驚かせてきましたが、クラス キーワードを使用した定義の出現です。従来のモデルに近いことにより、一部のフロントエンド同僚が非常に残念に思い、「独自の JS を返してください」、「実体のないものをいくつか構築してください」、「クラスがないので、人の授業に頼る」、さらには「転職した」など。結局のところ、感情があるのは正常のことであり、新しい知識を得るにはより多くの時間とエネルギーを費やす必要があり、ただ目を閉じて楽しむことができるものではありません。

しかし、歴史の軸は前に進み続けます。授業に関して確かなことの 1 つは、面接官に「お願いします、私が理解していないのではなく、単に私が理解していないだけです」とは言えないということです。 「理解したくない。質問を変更してください!」 class は単なる構文糖ですが、extends は継承を非常によく改善します。一方、将来「クラス」に登場する可能性のある新しい機能は、コンストラクターではなくクラスによって実行される必要があります。それが将来どのように美しくなるかは誰にもわかりません。さあ、この熱々の黒砂糖と生姜のスープをゆっくり飲みましょう

1 クラス

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 変数との比較

キーワード クラスは、関数を定義するキーワード関数と似ており、宣言と式 (匿名および名前付き) の 2 つの定義方法があります。宣言式によって定義された変数の性質は、関数の性質とは異なります。これらは事前に解析されず、変数の昇格がなく、グローバル スコープにリンクされず、一時的なデッド ゾーンがあります。クラス定義によって生成される変数はコンストラクターであるため、クラスは即時実行モードで作成できます。

// --- 声明式
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 オブジェクトとの比較

クラスの内容 ({} 内) の形式は、オブジェクト リテラルの形式と似ています。ただし、クラスのコンテンツで定義できるのはメソッドのみであり、属性は定義できません。メソッドの形式は関数の省略形のみであり、メソッドをカンマで区切ることはできません。メソッド名には、括弧で囲まれた式またはシンボル値を指定できます。メソッドは、コンストラクター メソッド (コンストラクター メソッド)、プロトタイプ メソッド (コンストラクターのプロトタイプ属性に存在)、および静的メソッド (コンストラクター自体に存在) の 3 つのカテゴリに分類されます。

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

プロパティは直接定義できません。表現クラスはプロトタイプや静的プロパティを持つことができません。クラスを解決するとコンストラクターが形成されるため、コンストラクターにプロパティを追加するのと同じように、クラスにプロパティを追加するだけです。読み取り専用プロパティの定義にはゲッター関数のみを使用する方が簡単であり、推奨されます。プロパティを直接設定できないのはなぜですか?技術が未熟なのでしょうか?当局はある考えを伝えたいと考えているのでしょうか?それとも作者が勝手に投げかけた質問なのでしょうか?

// --- 直接在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 を通じてのみであり、直接呼び出すことはできません。クラス内で定義されたすべてのメソッドは列挙可能ではありませんが、コンストラクター自体とプロトタイプに追加されたプロパティとメソッドは列挙可能です。クラス内で定義されたメソッドはデフォルトで厳密モードになっており、明示的に宣言する必要はありません。上記の 3 つの点により、クラスの厳密性が高まりますが、プライベート プロパティとメソッドを直接定義する方法はまだありません。

// --- 能否直接调用
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 キーワードを追加すると、そのメソッドがクラス自体に存在し、インスタンスから直接アクセスできないことを示します。静的メソッドの this はクラス自体を指します。静的メソッドとプロトタイプ メソッドは異なるオブジェクト上にあるため、同じ名前を持つことができます。 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 extends

ES5 の古典的な継承方法は、寄生結合継承であり、サブクラスは親クラスのインスタンスとプロトタイプのプロパティとメソッドをそれぞれ継承します。 ES6 における継承の本質は同じですが、次のコードに示すように実装方法が変更されています。プロトタイプの継承は extends キーワードを使用した従来の言語に近い形式であり、インスタンスの継承は super を呼び出すことでサブクラスの形成を完了することであることがわかります。表面的には、このアプローチはより統一され、簡潔になっています。

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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。