This is the 10th day of my participation in the August More Text Challenge

  • The second articleThe Class inheritance

The basic syntax of Class

Introduction to the

The origin of the class

The traditional way to generate instance objects in the JavaScript language is through constructors. Here’s an example.

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ') ';
};

var p = new Point(1.2);
Copy the code

This is very different from traditional object-oriented languages such as C++ and Java, and can easily confuse new programmers.

ES6 provides a more traditional language approach, introducing the concept of classes as templates for objects. With the class keyword, you can define a class.

Basically, ES6 classes can be seen as a syntactic candy that does most of what ES5 does. The new class writing method simply makes object prototype writing clearer and more like object-oriented programming syntax. The above code is rewritten using ES6 class, as follows.

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ') '; }}Copy the code

The above code defines a “class”. You can see that it has a constructor() method, which is the constructor, and the this keyword represents the instance object. This new Class notation is essentially the same as the ES5 constructor Point at the beginning of this chapter.

In addition to the constructor, the Point class defines a toString() method. Note that when defining the toString() method, you do not need the function keyword in front of it, just put the function definition in it. In addition, methods do not need to be separated by commas.

ES6 classes can be thought of as just another way of writing constructors.

class Point {
  // ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true
Copy the code

The above code shows that the data type of a class is a function, and the class itself points to a constructor.

When used, the new command is used directly on the class, exactly as the constructor is used.

class Bar {
  doStuff() {
    console.log('stuff'); }}const b = new Bar();
b.doStuff() // "stuff"
Copy the code

The prototype property of the constructor continues on the ES6 “class”. In fact, all methods of a class are defined on the prototype property of the class.

class Point {
  constructor() {
    // ...
  }

  toString() {
    // ...
  }

  toValue() {
    // ...}}/ / is equivalent to

Point.prototype = {
  constructor() {},
  toString() {},
  toValue(){}};Copy the code

The constructor(), toString(), and toValue() methods are defined in point.prototype.

Therefore, calling a method on an instance of a class is actually calling a method on the prototype.

class B {}
const b = new B();

b.constructor === B.prototype.constructor // true
Copy the code

In the code above, B is an instance of class B, and its constructor() method is the constructor() method of the prototype of class B.

Since the class’s methods are defined on top of the Prototype object, new methods of the class can be added to the Prototype object. The object.assign () method makes it easy to add multiple methods to a class at once.

class Point {
  constructor(){
    // ...}}Object.assign(Point.prototype, {
  toString(){},
  toValue(){}});Copy the code

The Prototype object’s constructor() property, which points directly to the “class” itself, is consistent with ES5 behavior.

Point.prototype.constructor === Point // true
Copy the code

In addition, all methods defined inside a class are non-enumerable.

class Point {
  constructor(x, y) {
    // ...
  }

  toString() {
    // ...}}Object.keys(Point.prototype)
/ / []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
Copy the code

In the code above, the toString() method is defined internally by the Point class and is not enumerable. This is not consistent with ES5 behavior.

var Point = function (x, y) {
  // ...
};

Point.prototype.toString = function () {
  // ...
};

Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
Copy the code

The above code is written in ES5, where the toString() method is enumerable.

The constructor method

The constructor() method is the default method of the class and is automatically called when an object instance is generated using the new command. A class must have a constructor() method; if it is not explicitly defined, an empty constructor() method is added by default.

class Point {}/ / is equivalent to
class Point {
  constructor(){}}Copy the code

In the above code, we define an empty class Point, to which the JavaScript engine automatically adds an empty constructor() method.

The constructor() method returns the instance object (that is, this) by default; you can specify that another object should be returned.

class Foo {
  constructor() {
    return Object.create(null); }}new Foo() instanceof Foo
// false
Copy the code

In the code above, the constructor() function returns a brand new object, resulting in the instance object not being an instance of class Foo.

The class must be called with new or an error will be reported. This is a major difference from normal constructors, which can be executed without new.

class Foo {
  constructor() {
    return Object.create(null);
  }
}

Foo()
// TypeError: Class constructor Foo cannot be invoked without 'new'
Copy the code

Instances of the class

Instances of generated classes are written exactly as in ES5, using the new command. As mentioned earlier, if you forget to add new and call Class as a function, you will get an error.

class Point {
  // ...
}

/ / an error
var point = Point(2.3);

/ / right
var point = new Point(2.3);
Copy the code

As in ES5, the attributes of an instance are defined on the stereotype (class) unless they are explicitly defined on themselves (that is, on this object).

/ / define the class
class Point {

  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ') '; }}var point = new Point(2.3);

point.toString() / / (2, 3)

point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
Copy the code

In the above code, x and y are properties of the instance object point itself (because defined on this), so hasOwnProperty() returns true, and toString() is a property of the prototype object (because defined on point). So the hasOwnProperty() method returns false. These are consistent with ES5 behavior.

As with ES5, all instances of a class share a stereotype object.

var p1 = new Point(2.3);
var p2 = new Point(3.2);

p1.__proto__ === p2.__proto__
//true
Copy the code

In the code above, p1 and P2 are both instances of Point, and their prototype is Point.prototype, so the __proto__ attribute is equal.

This also means that methods can be added to a “class” through the __proto__ attribute of an instance.

__proto__ is not a feature of the language itself, it is a private attribute added by various manufacturers. Although many modern browsers provide this private attribute in the JS engine, it is still not recommended to use this attribute in production to avoid environment dependence. In production, we can use the Object.getProtoTypeof method to get the prototype of the instance Object and then add methods/properties to the prototype.

var p1 = new Point(2.3);
var p2 = new Point(3.2);

p1.__proto__.printName = function () { return 'Oops' };

p1.printName() // "Oops"
p2.printName() // "Oops"

var p3 = new Point(4.2);
p3.printName() // "Oops"
Copy the code

The code above adds a printName() method to p1’s prototype. Since P1’s prototype is P2’s prototype, P2 can also call this method. Furthermore, this method can be called by a newly created instance p3. This means that rewriting a prototype using the __proto__ attribute of an instance must be done with great caution and is not recommended because it changes the original definition of the “class”, affecting all instances.

Getters and setters

As in ES5, you can use the get and set keywords inside a “class” to set the store and value functions for an attribute and intercept the access behavior of that attribute.

class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value); }}let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'
Copy the code

In the above code, the prop property has corresponding store and value functions, so the assignment and read behavior are customized.

The store and value functions are set on the Descriptor object of the property.

class CustomHTMLElement {
  constructor(element) {
    this.element = element;
  }

  get html() {
    return this.element.innerHTML;
  }

  set html(value) {
    this.element.innerHTML = value; }}var descriptor = Object.getOwnPropertyDescriptor(
  CustomHTMLElement.prototype, "html"
);

"get" in descriptor  // true
"set" in descriptor  // true
Copy the code

In the above code, the store and value functions are defined on the description object of the HTML attribute, which is exactly the same as in ES5.

Attribute expression

The property name of the class, which can take an expression.

let methodName = 'getArea';

class Square {
  constructor(length) {
    // ...
  }

  [methodName]() {
    // ...}}Copy the code

In the code above, the Square class method name, getArea, is derived from the expression.

The Class expressions

Like functions, classes can be defined in the form of expressions.

const MyClass = class Me {
  getClassName() {
    returnMe.name; }};Copy the code

The above code defines a class using an expression. Note that the name of this Class is Me, but Me is only available inside the Class and refers to the current Class. Outside of Class, this Class can only be referenced by MyClass.

let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
Copy the code

The code above says that Me is defined only within the Class.

If the inner part of the class is not useful, you can omit Me, which can be written as follows.

const MyClass = class { / *... * / };
Copy the code

Using Class expressions, you can write classes that are executed immediately.

let person = new class {
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(this.name);
  }
}('Joe');

person.sayName(); // "/"
Copy the code

In the code above, Person is an instance of a class that executes immediately.

Pay attention to the point

(1) Strict mode

Inside classes and modules, the default is strict mode, so there is no need to use strict to specify the runtime mode. As long as your code is written in a class or module, only strict mode is available. Considering that all future code will actually run in modules, ES6 actually upgrades the entire language to strict mode.

(2) There is no promotion

Unlike ES5, there is no hoist for the class.

new Foo(); // ReferenceError
class Foo {}
Copy the code

In the code above, class Foo is used first and defined later, which is an error because ES6 does not elevate the class declaration to the header. The reason for this rule is related to inheritance, which is discussed below, and the need to ensure that subclasses are defined after their parents.

{
  let Foo = class {};
  class Bar extends Foo {}}Copy the code

The above code does not report an error because Foo is already defined when Bar inherits from Foo. However, if there is a class promotion, the above code will report an error because the class is promoted to the header, while the let command is not promoted, resulting in Bar inheriting Foo before Foo is defined.

(3) Name attribute

Because ES6 classes are essentially just a wrapper around ES5 constructors, many of the functions’ features are inherited by Class, including the name attribute.

class Point {}
Point.name // "Point"
Copy the code

The name attribute always returns the class name immediately after the class keyword.

(4) Generator method

If a method is preceded by an asterisk (*), it is a Generator function.

class Foo {
  constructor(. args) {
    this.args = args;
  }
  * [Symbol.iterator]() {
    for (let arg of this.args) {
      yieldarg; }}}for (let x of new Foo('hello'.'world')) {
  console.log(x);
}
// hello
// world
Copy the code

In the code above, the Symbol. Iterator method of class Foo is preceded by an asterisk indicating that the method is a Generator function. The symbol. iterator method returns a default iterator of class Foo, for… The of loop automatically calls this traverser.

(5) The direction of this

Class methods that contain this inside point to instances of the class by default. You must be careful, however, that you may get an error if you use this method alone.

class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }

  print(text) {
    console.log(text); }}const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
Copy the code

In the code above, this in the printName method points to an instance of the Logger class by default. However, if we extract this method and use it alone, this will refer to the environment in which the method was run (because the class is in strict mode, so this actually refers to undefined), and we will fail to find print.

An easy solution is to bind this to the constructor so that the print method won’t be missing.

class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }

  // ...
}
Copy the code

Another solution is to use arrow functions.

class Obj {
  constructor() {
    this.getThis = () = > this; }}const myObj = new Obj();
myObj.getThis() === myObj // true
Copy the code

The this inside the arrow function always points to the object at which it was defined. In the above code, the arrow function is inside the constructor, and its definition takes effect when the constructor executes. In this case, the arrow function must be running in an instance object, so this will always point to an instance object.

Another solution is to use a Proxy that automatically binds this when retrieving a method.

function selfish (target) {
  const cache = new WeakMap(a);const handler = {
    get (target, key) {
      const value = Reflect.get(target, key);
      if (typeofvalue ! = ='function') {
        return value;
      }
      if(! cache.has(value)) { cache.set(value, value.bind(target)); }returncache.get(value); }};const proxy = new Proxy(target, handler);
  return proxy;
}

const logger = selfish(new Logger());
Copy the code

A static method

A class is the prototype of an instance, and all methods defined in a class are inherited by the instance. If you prefix a method with the static keyword, it means that the method is not inherited by the instance, but is called directly from the class. This is called a “static method”.

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
Copy the code

In the code above, the static keyword precedes the classMethod method of class Foo, indicating that the method is a static method that can be called directly on class Foo (foo.classmethod ()), not on an instance of class Foo. If a static method is called on an instance, an error is thrown indicating that the method does not exist.

Note that if a static method contains the this keyword, this refers to the class, not the instance.

class Foo {
  static bar() {
    this.baz();
  }
  static baz() {
    console.log('hello');
  }
  baz() {
    console.log('world');
  }
}

Foo.bar() // hello
Copy the code

In the code above, the static method bar calls this.baz, where this refers to class Foo, not an instance of Foo, the same as calling foo. baz. Also, as you can see from this example, static methods can have the same name as non-static methods.

Static methods of a parent class that can be inherited by subclasses.

class Foo {
  static classMethod() {
    return 'hello'; }}class Bar extends Foo {
}

Bar.classMethod() // 'hello'
Copy the code

In the code above, the parent class Foo has a static method that the subclass Bar can call.

Static methods can also be called from super objects.

class Foo {
  static classMethod() {
    return 'hello'; }}class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}

Bar.classMethod() // "hello, too"
Copy the code

A new way to write instance attributes

Instance attributes can be defined at the top level of the class, in addition to above this in the constructor() method.

class IncreasingCounter {
  constructor() {
    this._count = 0;
  }
  get value() {
    console.log('Getting the current value! ');
    return this._count;
  }
  increment() {
    this._count++; }}Copy the code

In the above code, the instance attribute this._count is defined in the constructor() method. Another way to write this is that this property can also be defined at the top level of the class, all else being the same.

class IncreasingCounter {
  _count = 0;
  get value() {
    console.log('Getting the current value! ');
    return this._count;
  }
  increment() {
    this._count++; }}Copy the code

In the code above, the instance attribute _count is at the same level as the value() and increment() methods. At this point, you do not need to prefix the instance attribute with this.

The nice thing about this new approach is that all the attributes of the instance object itself are defined in the header of the class, so you can see at a glance what instance attributes the class has.

class foo {
  bar = 'hello';
  baz = 'world';

  constructor() {
    // ...}}Copy the code

As you can see at a glance from the above code, class Foo has two instance attributes. In addition, it is relatively simple to write.

Static attributes

Static properties refer to properties of the Class itself, class.propName, not properties defined on the instance object (this).

class Foo {
}

Foo.prop = 1;
Foo.prop / / 1
Copy the code

The above statement defines a static property prop for class Foo.

Currently, this is the only way to do it, because ES6 explicitly states that there are only static methods inside a Class and no static attributes. There is now a proposal that provides static attributes for classes written with the static keyword in front of instance attributes.

class MyClass {
  static myStaticProp = 42;

  constructor() {
    console.log(MyClass.myStaticProp); / / 42}}Copy the code

This new writing greatly facilitates the expression of static properties.

/ / the old way
class Foo {
  // ...
}
Foo.prop = 1;

/ / a new way
class Foo {
  static prop = 1;
}
Copy the code

In the code above, the old static property is defined outside the class. Once the entire class is generated, it is regenerated into static properties. It’s easy to ignore this static attribute, and it doesn’t follow the code organization principle that related code should be kept together. In addition, the new writing method is declarative rather than assignment, which has better semantics.

Private methods and private properties

Existing solutions

Private methods and properties are methods and properties that can only be accessed inside a class, not outside it. This is a common requirement that facilitates code encapsulation, but ES6 does not provide it and can only be simulated through workarounds.

One way to do this is to make a distinction in naming.

class Widget {

  // Public method
  foo (baz) {
    this._bar(baz);
  }

  // Private methods
  _bar(baz) {
    return this.snaf = baz;
  }

  // ...
}
Copy the code

The underscore before the _bar() method in the above code indicates that this is a private method that is limited to internal use. However, this naming is not safe, as this method can still be called outside of the class.

Another option is to simply move private methods out of the class, since all methods inside the class are visible.

class Widget {
  foo (baz) {
    bar.call(this, baz);
  }

  // ...
}

function bar(baz) {
  return this.snaf = baz;
}
Copy the code

In the code above, foo is the public method that calls bar.call(this, baz) internally. This makes bar() effectively the private method of the current class.

Another way is to take advantage of the uniqueness of the Symbol value by naming the private method a Symbol value.

const bar = Symbol('bar');
const snaf = Symbol('snaf');

export default class myClass{

  // Public method
  foo(baz) {
    this[bar](baz);
  }

  // Private methods
  [bar](baz) {
    return this[snaf] = baz;
  }

  // ...
};
Copy the code

In the above code, bar and SNaf are both Symbol values, which are not normally available, so the effect of private methods and properties is achieved. But not absolutely, reflect.ownkeys () can still get them.

const inst = new myClass();

Reflect.ownKeys(myClass.prototype)
// [ 'constructor', 'foo', Symbol(bar) ]
Copy the code

In the code above, the attribute name of the Symbol value can still be retrieved from outside the class.

Proposals for private properties

Currently, there is a proposal to add private attributes to class. The method is to use # before the attribute name.

class IncreasingCounter {
  #count = 0;
  get value() {
    console.log('Getting the current value! ');
    return this.#count;
  }
  increment() {
    this.#count++; }}Copy the code

In the above code, #count is a private attribute that can only be used inside the class (this.#count). If used outside the class, an error is reported.

const counter = new IncreasingCounter();
counter.#count / / an error
counter.#count = 42 / / an error
Copy the code

The code above is outside of the class, reads the private property, and reports an error.

Here’s another example.

class Point {
  #x;

  constructor(x = 0) {
    this.#x = +x;
  }

  get x() {
    return this.#x;
  }

  set x(value) {
    this.#x = +value; }}Copy the code

In the above code, #x is a private property that cannot be read outside the Point class. Since the pound sign # is part of the attribute name, it must be used with #, so #x and x are two different attributes.

The reason we introduced a new prefix # for private attributes rather than the private keyword is because JavaScript is a dynamic language with no type declarations, and using a separate symbol seems to be the only convenient and reliable way to accurately tell whether an attribute is private or not. In addition, the Ruby language uses @ for private properties, and ES6 uses # instead of this symbol because @ is already left to the Decorator.

This notation can be used to write private methods as well as private properties.

class Foo {
  #a;
  #b;
  constructor(a, b) {
    this.#a = a;
    this.#b = b;
  }
  #sum() {
    return this.#a + this.#b;
  }
  printSum() {
    console.log(this.#sum()); }}Copy the code

In the code above, #sum() is a private method.

In addition, private properties can also set getter and setter methods.

class Counter {
  #xValue = 0;

  constructor() {
    super(a);// ...
  }

  get #x() { return #xValue; }
  set #x(value) {
    this.#xValue = value; }}Copy the code

In the above code, #x is a private property, and it is read and written by get #x() and set #x().

Private attributes are not restricted to referencing from this; instances can also reference private attributes as long as they are inside the class.

class Foo {
  #privateValue = 42;
  static getPrivateValue(foo) {
    return foo.#privateValue;
  }
}

Foo.getPrivateValue(new Foo()); / / 42
Copy the code

The code above allows private attributes to be referenced from instance foo.

Private properties and methods can also be preceded by the static keyword to indicate that this is a static private property or method.

class FakeMath {
  static PI = 22 / 7;
  static #totallyRandomNumber = 4;

  static #computeRandomNumber() {
    return FakeMath.#totallyRandomNumber;
  }

  static random() {
    console.log('I heard you like random numbers... ')
    return FakeMath.#computeRandomNumber();
  }
}

FakeMath.PI / / 3.142857142857143
FakeMath.random()
// I heard you like random numbers...
/ / 4
FakeMath.#totallyRandomNumber / / an error
FakeMath.#computeRandomNumber() / / an error
Copy the code

In the above code, #totallyRandomNumber is a private property and #computeRandomNumber() is a private method that can only be called inside the FakeMath class.

The in operator.

try… The catch structure can be used to determine whether a private attribute exists.

class A {
  use(obj) {
    try {
      obj.#foo;
    } catch {
      The private attribute #foo does not exist}}}const a = new A();
a.use(a); / / an error
Copy the code

In the example above, class A does not have A private property #foo, so try… Catch reported an error.

This was cumbersome and unreadable, and the V8 engine improved the IN operator to make it available for private attributes as well.

class A {
  use(obj) {
    if (#foo in obj) {
      // The private attribute #foo exists
    } else {
      The private attribute #foo does not exist}}}Copy the code

In the example above, the in operator determines whether the current instance of class A has the private property #foo and returns true if it does, false otherwise.

In can also be used with this.

class A {
  #foo = 0;
  m() {
    console.log(#foo in this); // true
    console.log(#bar in this); // false}}Copy the code

Note that when determining a private property, in can only be used inside the class where the private property is defined.

class A {
  #foo = 0;
  static test(obj) {
    console.log(#foo in obj);
  }
}

A.test(new A()) // true
A.test({}) // false

class B {
  #foo = 0;
}

A.test(new B()) // false
Copy the code

In the example above, the private property #foo of class A can only be determined inside class A using the IN operator, and returns true only for instances of A and false for all other objects.

The private property that a subclass inherits from its parent can also be determined using the IN operator.

class A {
  #foo = 0;
  static test(obj) {
    console.log(#foo inobj); }}class SubA extends A {};

A.test(new SubA()) // true
Copy the code

In the example above, SubA inherits the private property #foo from its parent, and the in operator is also valid.

Note that the in operator is not valid for inheritance formed by Object.create() or object.setPrototypeof, because such inheritance does not pass private attributes.

class A {
  #foo = 0;
  static test(obj) {
    console.log(#foo inobj); }}const a = new A();

const o1 = Object.create(a);
A.test(o1) // false
A.test(o1.__proto__) // true

const o2 = {};
Object.setPrototypeOf(o2, A);
A.test(o2) // false
A.test(o2.__proto__) // true
Copy the code

In the example above, none of the subclasses can get the parent’s private property for the inheritance formed by modifying the stereotype chain, so the IN operator is invalid.

The new target attribute

New is the command to generate the instance object from the constructor. ES6 introduces a new.target attribute for the new command, which is typically used in constructors to return the constructor on which the new command is applied. If the constructor is not called with the new command or reflect.construct (), new.target returns undefined, so this property can be used to determine how the constructor is called.

function Person(name) {
  if (new.target ! = =undefined) {
    this.name = name;
  } else {
    throw new Error('Instance must be generated using the new command'); }}// Another way to write it
function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error('Instance must be generated using the new command'); }}var person = new Person('Joe'); / / right
var notAPerson = Person.call(person, 'Joe');  / / an error
Copy the code

The above code ensures that the constructor can only be called with the new command.

Class calls new.target internally to return the current Class.

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    this.length = length;
    this.width = width; }}var obj = new Rectangle(3.4); / / output true
Copy the code

Note that when a subclass inherits from its parent, new.target returns the subclass.

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    // ...}}class Square extends Rectangle {
  constructor(length, width) {
    super(length, width); }}var obj = new Square(3); / / output is false
Copy the code

In the code above, new.target returns a subclass.

Using this feature, you can write classes that cannot be used independently but must be inherited to be used.

class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error('This class cannot be instantiated'); }}}class Rectangle extends Shape {
  constructor(length, width) {
    super(a);// ...}}var x = new Shape();  / / an error
var y = new Rectangle(3.4);  / / right
Copy the code

In the above code, the Shape class cannot be instantiated and can only be used for inheritance.

Note that outside the function, using new.target raises an error.

  • The second articleThe Class inheritance