Maison  >  Article  >  interface Web  >  Explication détaillée du constructeur qui transforme les classes en js en modèles de classes traditionnels (avec exemples)

Explication détaillée du constructeur qui transforme les classes en js en modèles de classes traditionnels (avec exemples)

不言
不言original
2018-09-21 10:41:172248parcourir

Ce que cet article vous apporte, c'est une explication détaillée du constructeur (avec des exemples) sur la transformation des classes en js en modèles de classe traditionnels. Il a une certaine valeur de référence. J'espère que les amis dans le besoin pourront s'y référer. vous être utile aidé.

Avant-propos

La « classe » basée sur des prototypes de JS a toujours été étonnée par les codeurs qui sont passés au front-end. Cependant, l'émergence de la définition de mot-clé de classe est proche du mode traditionnel. a fait des collègues front-end Ils ont ressenti de profonds regrets et ont laissé des messages les uns après les autres : "Rendez-moi mon JS unique", "Construisez des choses insignifiantes", "Je n'ai pas de cours donc je dois compter sur les cours des autres". ", et même "J'ai changé de carrière" et ainsi de suite. Il est normal d'avoir des émotions. Après tout, de nouvelles connaissances signifient plus de temps et d'énergie, et ce n'est pas quelque chose que l'on peut simplement apprécier les yeux fermés.

Cependant, l'axe de l'histoire continue d'avancer. Une chose qui est sûre en matière de classe, c'est qu'on ne peut pas dire à l'intervieweur : "S'il vous plaît, ce n'est pas que je ne comprends pas, c'est juste que je ne comprends pas. Je ne veux pas comprendre. S'il vous plaît, changez la question!" Bien que la classe ne soit qu'un sucre de syntaxe, extends améliore assez bien l'héritage. D'un autre côté, les nouvelles fonctionnalités qui pourraient apparaître dans les "classes" à l'avenir devraient être portées par les classes plutôt que par les constructeurs. Personne ne sait à quel point cela se révélera beau à l'avenir. Alors allez, buvez lentement ce bol fumant de soupe à la cassonade et au gingembre

1 classe

Il n'y a pas de notion de classe dans ECMAScript, nos instances sont générées par des constructeurs à partir de prototypes d'objets avec propriétés et méthodes dynamiques. Cependant, afin d'être en adéquation avec les standards internationaux et de le décrire de manière plus simple et plus élégante, le mot « classe » sera toujours utilisé. Les classes JS sont donc équivalentes aux constructeurs. La classe ES6 n'est qu'un sucre syntaxique, et les objets générés par sa définition sont toujours des constructeurs. Mais pour le distinguer du modèle constructeur, nous l’appelons le modèle de classe. Le cours d'apprentissage nécessite une connaissance des constructeurs et des objets prototypes. Pour plus de détails, vous pouvez effectuer une recherche sur 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 Comparaison avec les variables

La classe de mots-clés est similaire au mot-clé fonction qui définit une fonction. Il existe deux modes de définition : la déclaration et l'expression (anonyme et nommée). La nature des variables définies via des expressions déclaratives est différente de celle des fonctions. Elles sont plus similaires à let et const. Elles ne sont pas analysées à l'avance, n'ont pas de promotion de variable, ne sont pas liées à la portée globale et ont des zones mortes temporaires. La variable générée par la définition de classe est un constructeur et la classe peut donc être écrite en mode exécution immédiate.

// --- 声明式
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 Comparaison avec les objets

La forme du contenu de la classe (à l'intérieur de {}) est similaire à celle des littéraux d'objet. Cependant, seules les méthodes peuvent être définies dans le contenu de la classe, pas les attributs. La forme des méthodes ne peut être que des abréviations de fonctions et les méthodes ne peuvent pas être séparées par des virgules. Le nom de la méthode peut être une expression entre parenthèses ou une valeur de symbole. Les méthodes sont divisées en trois catégories, les méthodes constructeur (méthodes constructeur), les méthodes prototypes (existantes sur l'attribut prototype du constructeur) et les méthodes statiques (existantes sur le constructeur lui-même)

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

Les attributs ne peuvent pas être définis directement . Cela ne signifie pas que les classes ne peuvent pas avoir de prototypes ou de propriétés statiques. La résolution de la classe formera un constructeur, il suffit donc d'ajouter des propriétés à la classe comme vous le feriez pour ajouter des propriétés au constructeur. Il est plus simple et recommandé d'utiliser uniquement des fonctions getter pour définir des propriétés en lecture seule. Pourquoi ne puis-je pas définir les propriétés directement ? La technologie est-elle immature ? Le fonctionnaire espère-t-il transmettre une certaine idée ? Ou s'agit-il simplement d'une question posée au hasard par l'auteur ?

// --- 直接在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 Comparaison avec les constructeurs

Ce qui suit est le code qui utilise des constructeurs et des classes pour réaliser la même fonction. Intuitivement, les classes simplifient le code et rendent le contenu plus agrégé. Le corps de la méthode constructeur est équivalent au corps de la fonction constructeur. Si cette méthode n'est pas explicitement définie, une méthode constructeur vide sera ajoutée par défaut pour renvoyer une nouvelle instance. Comme ES5, il peut également être personnalisé pour renvoyer un autre objet au lieu d'une nouvelle instance. Bien que la classe

// --- 构造函数
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';

soit une fonction, elle ne peut générer des instances que via new et ne peut pas être appelée directement. Toutes les méthodes définies dans la classe ne sont pas énumérables, et les propriétés et méthodes ajoutées au constructeur lui-même et au prototype sont énumérables. Les méthodes définies au sein d'une classe sont en mode strict par défaut et n'ont pas besoin d'être explicitement déclarées. Les trois points ci-dessus augmentent la rigueur de la classe. Malheureusement, il n'existe toujours aucun moyen de définir directement les propriétés et méthodes privées.

// --- 能否直接调用
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,不是严格模式。

L'ajout du mot-clé static avant une méthode indique que cette méthode est une méthode statique. Elle existe dans la classe elle-même et n'est pas directement accessible par les instances. Ceci, dans une méthode statique, pointe vers la classe elle-même. Comme elles concernent des objets différents, les méthodes statiques et les méthodes prototypes peuvent porter le même nom. ES6 ajoute une nouvelle commande new.target, qui fait référence au constructeur ou à la classe après new. Il existe certaines restrictions sur l'utilisation de cette commande. Veuillez consulter l'exemple suivant pour plus de détails.

// --- 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

La méthode d'héritage classique dans ES5 est un héritage combiné parasite. La sous-classe héritera respectivement des propriétés et des méthodes de l'instance de classe parent et du prototype. L'essence de l'héritage dans ES6 est également la même, mais la méthode d'implémentation a changé, comme le montre le code suivant. On peut voir que l'héritage sur le prototype est une forme plus proche du langage traditionnel utilisant le mot-clé extends, et l'héritage sur l'instance consiste à compléter la mise en forme de la sous-classe this en appelant super. En apparence, l’approche est plus unifiée et concise.

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,没有被正确初始化

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn