Home > Web Front-end > JS Tutorial > Detailed explanation of the constructor that transforms classes in js into traditional class patterns (with examples)

Detailed explanation of the constructor that transforms classes in js into traditional class patterns (with examples)

不言
Release: 2018-09-21 10:41:17
Original
2329 people have browsed it

This article brings you a detailed explanation of the constructor (with examples) about the transformation of classes in js into traditional class patterns. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you. helped.

Preface

The prototype-based 'class' of JS has always been amazed by coders who have switched to the front-end profession. However, the emergence of definitions using the class keyword that is close to the traditional model has caused some front-end colleagues to They felt deeply regretful and left messages one after another: "Give me back my unique JS", "Build some insubstantial things", "I don't have classes so I have to rely on other people's classes", and even "I have changed careers" and so on. It's normal to have emotions. After all, new knowledge means more time and energy expenditure, and it's not something you can simply enjoy with your eyes closed.

However, the axis of history continues to move forward. One thing that is certain about class is that you cannot say to the interviewer: "Please, it's not that I don't understand, it's just that I don't want to understand. Please change the question!" Although class is just syntax sugar, extends improves inheritance quite well. On the other hand, new features that may appear on "classes" in the future should be carried by classes instead of constructors. No one is sure how beautiful it will turn out in the future. Therefore, come on, slowly drink this bowl of steaming brown sugar and ginger soup

1 class

There is no concept of class in ECMAScript. Our instances are generated by constructors based on prototypes. Objects with dynamic properties and methods. However, in order to be in line with international standards and describe it more simply and elegantly, the word "class" will still be used. So JS classes are equivalent to constructors. The ES6 class is just syntactic sugar, and the objects generated by its definition are still constructors. But to distinguish it from the constructor pattern, we call it the class pattern. Learning classes requires knowledge of constructors and prototype objects. For details, you can search on 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'; } // 原型方法
};
Copy after login

1.1 Comparison with variables

The keyword class is similar to the keyword function that defines a function. There are two ways of definition: declaration and expression (anonymous and named). The nature of variables defined through declarative expressions is different from that of functions. They are more similar to let and const. They are not parsed in advance, have no variable promotion, are not linked to the global scope, and have temporary dead zones. The variable generated by the class definition is a constructor, and therefore, the class can be written in immediate execution mode.

// --- 声明式
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 () {
})();
Copy after login

1.2 Comparison with objects

The form of class content (inside {}) is similar to that of object literals. However, only methods can be defined in the class content, not attributes. The form of methods can only be function abbreviations, and methods cannot be separated by commas. The method name can be a parenthesized expression or a Symbol value. Methods are divided into three categories, constructor methods (constructor methods), prototype methods (existing on the prototype attribute of the constructor) and static methods (existing on the constructor itself)

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
Copy after login

Properties cannot be defined directly, and Representation classes cannot have prototypes or static properties. Resolving the class will form a constructor, so just add properties to the class just like you add properties to the constructor. It is more straightforward and recommended to only use getter functions to define read-only properties. Why can't I set properties directly? Is the technology immature? Does the official hope to convey a certain idea? Or is it just a random question thrown by the author?

// --- 直接在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无法修改。
Copy after login

1.3 Comparison with constructors

The following is the code that uses constructors and classes to achieve the same function. Intuitively, classes simplify the code and make the content more aggregated. The constructor method body is equivalent to the function body of the constructor function. If this method is not explicitly defined, an empty constructor method will be added by default to return a new instance. Like ES5, it can also be customized to return another object instead of a new instance.

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

Although the class is a function, it can only generate instances through new and cannot be called directly. All methods defined within the class are non-enumerable, and the properties and methods added to the constructor itself and prototype are enumerable. Methods defined within a class are in strict mode by default and do not need to be explicitly declared. The above three points increase the rigor of the class. Unfortunately, there is still no way to directly define private properties and methods.

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

Adding the static keyword before the method indicates that the method is a static method. It exists in the class itself and cannot be directly accessed by the instance. This in a static method points to the class itself. Because they are on different objects, static methods and prototype methods can have the same name. ES6 adds a new command new.target, which refers to the constructor or class behind new. There are certain restrictions on the use of this command. Please see the following example for details.

// --- 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; // 报错
Copy after login

2 extends

The classic inheritance method in ES5 is parasitic combined inheritance. The subclass will inherit the properties and methods on the parent class instance and prototype respectively. The essence of inheritance in ES6 is also the same, but the implementation method has changed, as shown in the following code. It can be seen that the inheritance on the prototype is a form closer to traditional language using the extends keyword, and the inheritance on the instance is to complete the shaping of the subclass this by calling super. On the surface, the approach is more unified and concise.

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

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

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
Copy after login

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。
Copy after login

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指向实例本身。
  }
}
Copy after login

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。
Copy after login

创建类的过程,是先构造一个属于父类却指向子类的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正常更新,已包含必要的内部属性。
Copy after login

需要注意的是继承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,没有被正确初始化
Copy after login

The above is the detailed content of Detailed explanation of the constructor that transforms classes in js into traditional class patterns (with examples). For more information, please follow other related articles on the PHP Chinese website!

source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template