Basic syntax of Class

Introduction

Origin of the class

In the JavaScript language, the traditional method of generating instance objects is through constructors. Below is 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);

The above writing method is very different from traditional object-oriented languages ​​(such as C++ and Java), and it is easy to confuse programmers who are new to this language.

ES6 provides a way of writing closer to traditional languages, introducing the concept of Class as a template for objects. Through the class keyword, you can define classes.

Basically, ES6's class can be regarded as just a syntactic sugar. Most of its functions can be achieved by ES5. The new class writing method just makes the writing of object prototypes clearer and more like object-oriented programming. It's just grammar. The above code is rewritten with ES6's class, which looks like this.

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

  toString() {
    return "(" + this.x + ", " + this.y + ")";
  }
}

The above code defines a "class", you can see that there is a constructor() method in it, which is the construction method, and the this keyword represents the instance object. This new way of writing Class is essentially the same as the ES5 constructor Point at the beginning of this chapter.

In addition to the constructor method, the Point class also defines a toString() method. Note that when defining the toString() method, you don't need to add the keyword function in front, just put the function definition in it. In addition, the method does not need to be separated by a comma, and an error will be reported if it is added.

ES6 classes can be regarded as another way of writing constructors.

class Point {
  // ...
}

typeof Point; // "function"
Point === Point.prototype.constructor; // true

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

When using it, you also use the new command directly on the class, which is exactly the same as the usage of the constructor.

class Bar {
  doStuff() {
    console.log("stuff");
  }
}

const b = new Bar();
b.doStuff(); // "stuff"

The prototype property of the constructor continues to exist 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() {
    // ...
  }
}

// Equivalent to

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

In the above code, the three methods of constructor(), toString(), and toValue() are actually 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

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

Since the methods of the class are defined on the prototype object, new methods of the class can be added on the prototype object. The Object.assign() method can easily add multiple methods to a class at once.

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

Object.assign(Point.prototype, {
  toString() {},
  toValue() {},
});

The constructor() property of the prototype object directly points to the "class" itself, which is consistent with the behavior of ES5.

Point.prototype.constructor === Point; // true

In addition, all methods defined within the class are non-enumerable.

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

  toString() {
    // ...
  }
}

Object.keys(Point.prototype);
// []
Object.getOwnPropertyNames(Point.prototype);
// ["constructor","toString"]

In the above code, the toString() method is a method defined internally by the Point class, and it is not enumerable. This is inconsistent with ES5's behavior.

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

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

Object.keys(Point.prototype);
// ["toString"]
Object.getOwnPropertyNames(Point.prototype);
// ["constructor","toString"]

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

constructor method

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

class Point {}

// Equivalent to
class Point {
  constructor() {}
}

In the above code, an empty class Point is defined, and the JavaScript engine will automatically add an empty constructor() method to it.

The constructor() method returns an instance object (ie this) by default, and you can specify to return another object.

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

new Foo() instanceof Foo;
// false

In the above code, the constructor() function returns a brand new object, and as a result, the instance object is not an instance of the Foo class.

The class must be called with new, otherwise an error will be reported. This is one of the main differences between it and the ordinary constructor, which can be executed without new.

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

Foo();
// TypeError: Class constructor Foo cannot be invoked without'new'

Class instance

The writing of the instance of the generated class is exactly the same as ES5, and the new command is also used. As mentioned earlier, if you forget to add new and call Class like a function, an error will be reported.

class Point {
  // ...
}

// report an error
var point = Point(2, 3);

// correct
var point = new Point(2, 3);

Like ES5, the properties of an instance are defined on the prototype (that is, defined on the class) unless they are explicitly defined on itself (that is, defined on the this object).

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

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

Like ES5, all instances of a class share a prototype object.

var p1 = new Point(2, 3);
var p2 = new Point(3, 2);

p1.__proto__ === p2.__proto__;
//true

In the above code, p1 and p2 are both instances of Point, and their prototypes are both Point.prototype, so the properties of __proto__ are equal.

This also means that you can add methods to the "class" through the __proto__ property of the instance.

__proto__ is not a feature of the language itself. It is a private attribute added by major manufacturers during the specific implementation. Although this private attribute is currently provided in the JS engines of many modern browsers, it is still not recommended to use it in production Attributes to avoid dependence on the environment. In the production environment, we can use the Object.getPrototypeOf method to get the prototype of the instance object, and then add methods/attributes 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"

The above code adds a printName() method to the prototype of p1. Since the prototype of p1 is the prototype of p2, this method can also be called by p2. Moreover, the newly created instance p3 can also call this method. This means that using the __proto__ property of an instance to rewrite the prototype must be very careful and not recommended, because this will change the original definition of "class" and affect all instances.

Getter and setter

Like ES5, the keywords get and set can be used inside the "class" to set the value function and the value function of an attribute to intercept the access behavior of the 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'

In the above code, the prop attribute has corresponding storage and retrieval functions, so the assignment and reading behaviors are customized.

The stored value function and the value function are set on the Descriptor object of the attribute.

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

In the above code, the stored value function and the value function are defined on the description object of the html attribute, which is completely consistent with ES5.

Attribute expression

The attribute name of the class can be an expression.

let methodName = "getArea";

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

  [methodName]() {
    // ...
  }
}

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

Class expression

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

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};

The above code uses an expression to define a class. It should be noted 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 with MyClass.

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

The above code indicates that Me is only defined inside Class.

If the inner part of the class is not used, Me can be omitted, which means it can be written in the following form.

const MyClass = class {
  /* ... */
};

Using the Class expression, you can write a Class that is executed immediately.

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

  sayName() {
    console.log(this.name);
  }
})("Zhang San");

person.sayName(); // "Zhang San"

In the above code, person is an instance of a class that is executed immediately.

be careful

(1) Strict mode

The internals of classes and modules are in strict mode by default, so there is no need to use use strict to specify the operating mode. As long as your code is written in a class or module, only strict mode is available. Considering that all future codes will actually run in modules, ES6 actually upgraded the entire language to strict mode.

(2) There is no promotion

There is no hoist for classes, which is completely different from ES5.

new Foo(); // ReferenceError
class Foo {}

In the above code, the Foo class is used before and defined after. This will report an error because ES6 does not promote the declaration of the class to the head of the code. The reason for this provision is related to the inheritance mentioned below. It is necessary to ensure that the subclass is defined after the parent class.

{
  let Foo = class {};
  class Bar extends Foo {}
}

The above code will not report an error, because when Bar inherits Foo, Foo has already been defined. However, if there is a promotion of class, the above code will report an error, because class will be promoted to the head of the code, and the let command is not promoted, so when Bar inherits Foo, Foo has not been defined yet.

(3) name attribute

In essence, the ES6 class is just a wrapper of the ES5 constructor, so many features of the function are inherited by Class, including the name attribute.

class Point {}
Point.name; // "Point"

The name attribute always returns the name of the class immediately following the class keyword.

(4) Generator method

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

class Foo {
  constructor(...args) {
    this.args = args;
  }
  *[Symbol.iterator]() {
    for (let arg of this.args) {
      yield arg;
    }
  }
}

for (let x of new Foo("hello", "world")) {
  console.log(x);
}
// hello
// world

In the above code, there is an asterisk before the Symbol.iterator method of the Foo class, indicating that the method is a Generator function. The Symbol.iterator method returns a default iterator of the Foo class, and the for...of loop will automatically call this iterator.

(5) Point to this

If the method of a class contains this, it points to an instance of the class by default. However, you must be very careful. Once you use this method alone, you may get an error.

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

In the above code, the this in the printName method refers to an instance of the Logger class by default. However, if you extract this method and use it alone, this will point to the environment where the method is running (because the class is in strict mode, this actually points to undefined), resulting in no print Method and error.

A simpler solution is to bind this in the constructor so that the print method will not be found.

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

  // ...
}

Another solution is to use arrow functions.

class Obj {
  constructor() {
    this.getThis = () => this;
  }
}

const myObj = new Obj();
myObj.getThis() === myObj; // true

The this inside the arrow function always points to the object where it was defined. In the above code, the arrow function is located inside the constructor. When its definition takes effect, it is when the constructor is executed. At this time, the operating environment where the arrow function is located must be the instance object, so this will always point to the instance object.

Another solution is to use Proxy to automatically bind this when obtaining the method.

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

const logger = selfish(new Logger());

Static method

The class is equivalent to the prototype of the instance, and all methods defined in the class will be inherited by the instance. If you add the static keyword before a method, it means that the method will not be inherited by the instance, but will be called directly through the class, which 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

In the above code, the static keyword before the classMethod method of the Foo class indicates that the method is a static method and can be called directly on the Foo class (Foo.classMethod()) instead of Called on an instance of the Foo class. If you call a static method on an instance, an error will be thrown, indicating that the method does not exist.

Note that if the static method contains the keyword this, this 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

In the above code, the static method bar calls this.baz, where this refers to the Foo class, not an instance of Foo, which is equivalent to calling Foo.baz. In addition, it can be seen from this example that static methods can have the same name as non-static methods.

The static method of the parent class can be inherited by the child class.

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

class Bar extends Foo {}

Bar.classMethod(); //'hello'

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

Static methods can also be called from the super object.

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

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

Bar.classMethod(); // "hello, too"

New way of writing instance attributes

Instance attributes can be defined at the top level of the class in addition to the 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++;
  }
}

In the above code, the instance attribute this._count is defined in the constructor() method. Another way of writing is that this attribute can also be defined at the top level of the class, and everything else remains unchanged.

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

In the above code, the instance attribute _count and the value function value() and increment() methods are at the same level. At this time, there is no need to add this in front of the instance attribute.

The advantage of this new way of writing is that all the properties of the instance objects themselves are defined in the head of the class, which looks neat, and you can see at a glance which instance properties the class has.

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

  constructor() {
    // ...
  }
}

The above code can be seen at a glance that the foo class has two instance attributes, which are clear at a glance. In addition, it is relatively concise to write.

Static properties

Static properties refer to the properties of the Class itself, namely Class.propName, rather than the properties defined on the instance object (this).

class Foo {}

Foo.prop = 1;
Foo.prop; // 1

The above wording defines a static property prop for the Foo class.

At present, only this way of writing is feasible, because ES6 clearly stipulates that there are only static methods inside a Class, and no static properties. Now there is a proposal that provides static properties of the class, written in front of the instance properties, plus the static keyword.

class MyClass {
  static myStaticProp = 42;

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

This new wording greatly facilitates the expression of static properties.

// Old way of writing
class Foo {
  // ...
}
Foo.prop = 1;

// new wording
class Foo {
  static prop = 1;
}

In the above code, the old static properties are defined outside the class. After the entire class is generated, static properties are generated. This makes it easy to ignore this static property, and it does not conform to the code organization principle that related code should be put together. In addition, the new wording is an explicit declaration (declarative) instead of assignment processing, which has better semantics.

Private methods and private properties

Existing solutions

Private methods and private properties are methods and properties that can only be accessed inside the class, and cannot be accessed outside. This is a common requirement, which is conducive to the encapsulation of code, but ES6 does not provide it, and can only be achieved by simulation through a workaround.

One way is to distinguish in naming.

class Widget {
  // public method
  foo(baz) {
    this._bar(baz);
  }

  // private method
  _bar(baz) {
    return (this.snaf = baz);
  }

  // ...
}

In the above code, the underscore in front of the _bar() method indicates that this is a private method only for internal use. However, this naming is not safe, and this method can still be called outside the class.

Another way is to simply move the private methods out of the class, because all methods inside the class are visible to the outside.

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

  // ...
}

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

In the above code, foo is a public method, and bar.call(this, baz) is called internally. This makes bar() actually a private method of the current class.

Another method is to use the uniqueness of the Symbol value to name the name of the private method as a Symbol value.

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

export default class myClass {
  // public method
  foo(baz) {
    this[bar](baz);
  }

  // private method
  [bar](baz) {
    return (this[snaf] = baz);
  }

  // ...
}

In the above code, bar and snaf are both Symbol values, and they cannot be obtained under normal circumstances, so the effect of private methods and private properties is achieved. But it is not absolutely impossible, Reflect.ownKeys() can still get them.

const inst = new myClass();

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

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

Proposal for private attributes

Currently, there is a proposal that adds private properties 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++;
  }
}

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

const counter = new IncreasingCounter();
counter.#count; // report an error
counter.#count = 42; // error

The above code is outside the class, and an error will be reported when reading private properties.

Here is another example.

class Point {
  #x;

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

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

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

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

The reason for introducing a new prefix # to denote private properties, instead of using the private keyword, is because JavaScript is a dynamic language with no type declarations. Using independent symbols seems to be the only more convenient and reliable method. , Can accurately distinguish whether an attribute is private. In addition, the Ruby language uses @ to indicate private attributes. ES6 does not use this symbol but uses # because @ has been reserved for Decorator.

This way of writing can not only write private properties, but also write private methods.

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

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

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

class Counter {
  #xValue = 0;

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

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

In the above code, #x is a private property, and its reading and writing are done by get #x() and set #x().

Private properties are not limited to reference from this, as long as they are inside the class, the instance can also refer to private properties.

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

Foo.getPrivateValue(new Foo()); // 42

The above code allows to reference private properties from the instance foo.

Before private properties and private methods, you can also add the static keyword to indicate that this is a static private property or private 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; // report an error
FakeMath.#computeRandomNumber(); // report an error

In the above code, #totallyRandomNumber is a private property, and #computeRandomNumber() is a private method, which can only be called inside the class of FakeMath, and an error will be reported when called outside.

in operator

The try...catch structure can be used to determine whether there is a private attribute.

class A {
  use(obj) {
    try {
      obj.#foo;
    } catch {
      // Private attribute #foo does not exist
    }
  }
}

const a = new A();
a.use(a); // report an error

In the above example, the class A does not have a private property #foo, so try...catch reports an error.

This way of writing is very troublesome and poor in readability. The V8 engine has improved the in operator so that it can also be used to determine private attributes.

class A {
  use(obj) {
    if (#foo in obj) {
      // Private attribute #foo exists
    } else {
      // Private attribute #foo does not exist
    }
  }
}

In the above example, the in operator judges whether the current instance of class A has a private attribute #foo, if there is a private property #foo, it returns true, otherwise it returns false.

in can also be used together with this.

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

Note that when judging a private property, in can only be used inside the class that defines the private property.

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

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

The private attributes inherited by the subclass from the parent can also be judged using the in operator.

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

class SubA extend A {};

A.test(new SubA()) // true

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

Note that the in operator is invalid for the inheritance formed by Object.create() and Object.setPrototypeOf, because this inheritance does not transfer private properties.

class A {
  #foo = 0;
  static test(obj) {
    console.log(#foo in obj);
  }
}
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

In the above example, for the inheritance formed by modifying the prototype chain, none of the subclasses can get the private properties of the parent class, so the in operator is invalid.

new.target attribute

new is a command to generate instance objects from the constructor. ES6 introduces a new.target property for the new command. This property is generally used in the constructor and returns the constructor that the new command acts on. If the constructor is not called by the new command or Reflect.construct(), new.target will return 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("You must use the new command to generate an instance");
  }
}

// Another way of writing
function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error("You must use the new command to generate an instance");
  }
}

var person = new Person("Zhang San"); // correct
var notAPerson = Person.call(person, "Zhang San"); // report an error

The above code ensures that the constructor can only be called by 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

It should be noted that when the subclass inherits the parent class, new.target will return 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 false

In the above code, new.target will return subclasses.

Using this feature, you can write classes that cannot be used independently and must be inherited before they can 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();
    // ...
  }
}

var x = new Shape(); // report an error
var y = new Rectangle(3, 4); // correct

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 will report an error.