Learning notes of javascript advanced programming | 8.4 class

Posted by nivosh on Mon, 31 Jan 2022 21:35:11 +0100

follow [front end Xiaoao] , read more original technical articles

class

  • The newly introduced class keyword in ES6 has the ability to formally define classes, and the concepts of prototype and constructor are still used behind it

Relevant code →

Class definition

  • Similar to function types, there are two main ways to define classes: class declaration and class expression. Both methods use the class keyword to enlarge parentheses
class Person {} // Class declaration
var animal = class {} // Class expression
  • Class expressions cannot be referenced before being evaluated (the same as function expressions), and class definitions cannot declare promotion (different from function expressions)
console.log(FunctionDeclaration) // [Function: FunctionDeclaration], function declaration in advance
function FunctionDeclaration() {}
console.log(ClassDeclaration) // Referenceerror: cannot access' classdeclaration 'before initialization, the class is not declared in advance
class ClassDeclaration {}
  • Class is limited by block level scope (function is limited by function scope)
{
  function FunctionDeclaration2() {}
  class ClassDeclaration2 {}
}
console.log(FunctionDeclaration2) // [Function: FunctionDeclaration2]
console.log(ClassDeclaration2) // ReferenceError: ClassDeclaration2 is not defined

Composition of classes

  • Classes can contain constructor methods, instance methods, get functions, set functions and static class methods, or empty class definitions
  • By default, the code in the class definition is executed in strict mode
  • Class names are recommended to be capitalized (like constructors) to distinguish between classes and instances
class Foo {} // Empty class definition
class Bar {
  constructor() {} // Constructor
  get myBaz() {} // Get function
  static myQux() {} // Static method
}
  • After assigning a class expression to a variable, you can access the class expression within the scope of the class expression and obtain the name of the class expression through the name attribute
var Person2 = class PersonName {
  identify() {
    console.log(PersonName) // Within the scope of class expression, access class expression
    console.log(Person2.name, PersonName.name) // Inside the scope of class expression, access the name of class expression
    /* 
      [class PersonName]
      PersonName PersonName
    */
  }
}
var p = new Person2()
p.identify()
console.log(Person2.name) // PersonName
console.log(PersonName) // ReferenceError: PersonName is not defined, the scope of class expression is external, and the class expression cannot be accessed

class constructor

  • Create the constructor of the class with the constructor keyword inside the class definition block:
    • When you use the new operator to create an instance of a class, you call the constructor method
    • The definition of the constructor is not required. Not defining the constructor is equivalent to defining the constructor as an empty function

instantiation

  • Using the new operator to call the constructor of the class will perform the following operations (the same as the constructor):
    • A new object (instance) was created
    • The [[prototype]] attribute inside the new object is assigned as the prototype attribute of the constructor (pointing to the prototype together)
    • Assign the scope of the constructor (i.e. this) to the new object
    • Execute the code in the constructor (that is, add a new attribute to this object)
    • Returns a new or non empty object
class Animal2 {}
class Person3 {
  constructor() {
    console.log('person ctor')
  }
}
class Vegetable {
  constructor() {
    this.color = 'orange'
  }
}
var a = new Animal2()
var p = new Person3() // 'person ctor'
var v = new Vegetable()
console.log(v.color) // 'orange'
  • When a class is instantiated, the parameters passed in will be used as the parameters of the constructor (if there is no parameter, the class name can be left without parentheses)
class Person4 {
  constructor(name) {
    console.log(arguments.length)
    this.name = name || null
  }
}

var p1 = new Person4() // 0, no parameter, parentheses after Person4 can also be omitted
console.log(p1.name) // null
var p2 = new Person4('Jake') // 1
console.log(p2.name) // 'Jake'
  • By default, the class constructor will return this object (class instance) after execution. If the returned object is not this object, the object is not associated with the class when detected with the instanceof operator
class Person5 {
  constructor() {
    this.foo = 'foo'
  }
}
var p3 = new Person5()
console.log(p3) // Person5 {foo: 'foo'}, class instance
console.log(p3.__proto__) // {}, class prototype
console.log(p3.constructor) // [class Person5], the class itself acts as a constructor
console.log(Person5 === p3.constructor) // true
console.log(Person5.prototype === p3.__proto__) // true
console.log(p3 instanceof Person5) // true, p3 is an instance of Person5 (Person5.prototype is on the prototype chain of p3)

class Person6 {
  constructor() {
    return {
      bar: 'bar', // Returns a completely new object (not an instance of this class)
    }
  }
}
var p4 = new Person6()
console.log(p4) // {bar: 'bar'} is not a class instance of Person6
console.log(p4.__proto__) // {}, Object prototype
console.log(p4.constructor) // [Function: Object], Object constructor
console.log(Object === p4.constructor) // true
console.log(Object.prototype === p4.__proto__) // true
console.log(p4 instanceof Person6) // false, p4 is not an instance of Person6 (Person6.prototype is not on the prototype chain of p4)
  • The main difference between class constructors and constructors is that class constructors must use the new operator, and ordinary constructors can not use the new operator (as ordinary function calls)
function Person7() {} // Ordinary constructor
class Animal3 {} // class constructor 
var p5 = Person7() // The constructor does not use the new operator and is called as an ordinary function
var a1 = Animal3() // TypeError: Class constructor Animal3 cannot be invoked without 'new', class constructor must be instantiated with new operator
var a2 = new Animal3()
  • After the class constructor is instantiated, it will become an ordinary instance method. You can use the new operator to reference it on the instance
class Person8 {
  constructor() {
    console.log('foo')
  }
}
var p6 = new Person8() // 'foo'
// p6.constructor() // TypeError: Class constructor Person8 cannot be invoked without 'new'
new p6.constructor() // 'foo'

Treat classes as special functions

  • Class is a special function, which can be detected by typeof operator
console.log(typeof Person8) // function
  • The class ID has a prototype attribute (pointing to the prototype), and the constructor attribute of the prototype (default) points to the class itself
console.log(Person8.prototype) // {}, prototype
console.log(Person8.prototype.constructor) // [class Person8], class itself
console.log(Person8.prototype.constructor === Person8) // true
  • You can use the instanceof operator to check whether the object pointed to by the class prototype exists in the prototype chain of the class instance
console.log(p6 instanceof Person8) // true, p6 is an instance of Person8
  • When using the new operator to call the class itself, the class itself is treated as a constructor, and the constructor of the class instance points to the class itself
  • When using the new operator to call the class constructor, * * class constructor (constructor()) * * is regarded as the constructor, and the constructor of the class instance points to the Function constructor
class Person9 {}
console.log(Person9.constructor) // [Function: Function], pointing to the constructor of the Function prototype, that is, the Function constructor

var p7 = new Person9() // new calls the class itself, which is treated as a constructor
console.log(p7.constructor) // [class Person9], the constructor points to the constructor, that is, the class itself
console.log(p7.constructor === Person9) // true
console.log(p7 instanceof Person9) // true, p7 is an instance of Person9

var p8 = new Person9.constructor() // new calls the class constructor, and the class constructor (constructor()) is used as the constructor
console.log(p8.constructor) // [Function: Function], constructor points to the constructor, that is, the Function constructor
console.log(p8.constructor === Function) // true
console.log(p8 instanceof Person9.constructor) // true, p8 is person9 Instance of constructor
  • You can pass classes as parameters
let classList = [
  class {
    constructor(id) {
      this._id = id
      console.log(`instance ${this._id}`)
    }
  },
]
function createInstance(classDefinition, id) {
  return new classDefinition(id)
}
var foo = new createInstance(classList[0], 3141) // 'instance 3141'
  • Class can be instantiated immediately
var p9 = new (class Foo2 {
  constructor(x) {
    console.log(x) // 'bar'
  }
})('bar')
console.log(p9) // Foo2 {}, class instance
console.log(p9.constructor) // [class Foo2], class itself

Instances, prototypes, and class members

  • The syntax of a class is very convenient to define members that should exist on instances, prototypes, and the class itself

Instance member

  • Inside the constructor() of a class, you can add its own properties to the instance
  • Each instance corresponds to a unique member object, and all members will not be shared on the prototype
class Person10 {
  constructor() {
    this.name = new String('Jack')
    this.sayName = function () {
      console.log(this.name)
    }
    this.nickNames = ['Jake', 'J-Dog']
  }
}
var p10 = new Person10()
var p11 = new Person10()

console.log(p10.name) // [String: 'Jack'], string wrapping object
console.log(p11.name) // [String: 'Jack'], string wrapping object
console.log(p10.name === p11.name) // false, not the same object (not shared)
console.log(p10.sayName) // ƒ () {console.log(this.name)}, function object
console.log(p11.sayName) // ƒ () {console.log(this.name)}, function object
console.log(p10.sayName === p11.sayName) // false, not the same object (similarly, not shared)
console.log(p10.nickNames === p11.nickNames) // false, similarly ↑

p10.name = p10.nickNames[0]
p11.name = p10.nickNames[1]
p10.sayName() // 'Jake', instance members do not affect each other
p11.sayName() // 'J-Dog', instance members do not affect each other

Prototype method and accessor

  • The method defined in the class block ({}) is used as the prototype method
class Person11 {
  constructor() {
    // Example method
    this.locate = () => {
      console.log('instance')
    }
  }
  locate() {
    // Prototype method
    console.log('prototype')
  }
  locate2() {
    // Prototype method
    console.log('prototype2')
  }
}
var p12 = new Person11()
p12.locate() // 'instance', the instance method covers the prototype method
p12.__proto__.locate() // 'prototype'
p12.locate2() // 'prototype2'
  • Methods can be defined in class constructors or class blocks, and attributes cannot be defined in class blocks
class Person12 {
  name: 'jack' // Uncaught SyntaxError: Unexpected identifier
}
  • You can use strings, symbols, or calculated values as keys for class methods
const symbolKey = Symbol('symbolKey')
class Person13 {
  stringKey() {
    // String as key
    console.log('stringKey')
  }
  [symbolKey]() {
    // Symbol as key
    console.log('symbolKey')
  }
  ['computed' + 'Key']() {
    // Calculated value as built
    console.log('computedKey')
  }
}
  • Define get and set accessors in the class block ({})
class Person14 {
  set setName(newName) {
    this._name = newName
  }
  get getName() {
    return this._name
  }
}
var p13 = new Person14()
p13.setName = 'Jake'
console.log(p13.getName) // 'Jake'

Static class method

  • Define static class methods in the class block ({}), which exist on the class itself
  • Each class can have only one static class member
class Person15 {
  constructor() {
    // The content added to this exists on different instances
    this.locate = () => {
      console.log('instance', this) // this here is a class instance
    }
  }
  locate() {
    // Defined on the prototype object of the class
    console.log('prototype', this) // this here is a class prototype
  }
  static locate() {
    // Defined on the class itself
    console.log('class', this) // this here is the class itself
  }
}
var p14 = new Person15()

p14.locate() // 'instance' Person15 { locate: [Function (anonymous)] }
p14.__proto__.locate() // 'prototype' {}
Person15.locate() // 'class' [class Person15]
  • Static class methods are well suited as instance factories
class Person16 {
  constructor(age) {
    this._age = age
  }
  sayAge() {
    console.log(this._age)
  }
  static create() {
    return new Person16(Math.floor(Math.random() * 100))
  }
}
console.log(Person16.create()) // Person16 { _age: ... }

Non functional prototypes and class members

  • You can manually add member data on prototypes and classes outside the class definition
  • The class definition does not show the method that supports adding data members. The instance should own the data referenced through this alone
class Person17 {
  sayName() {
    console.log(`${Person17.greeting} ${this.name}`)
  }
}
var p15 = new Person17()
Person17.greeting = 'My name is' // Define data on a class
Person17.prototype.name = 'Jake' // Define data on Prototype
p15.sayName() // 'My name is Jake'

Iterator and generator method

  • Generator methods can be defined on the prototype and the class itself
class Person18 {
  *createNicknameIterator() {
    // Define generator methods on prototypes
    yield 'Jack'
    yield 'Jake'
    yield 'J-Dog'
  }
  static *createJobIterator() {
    // Define generator methods in the class itself
    yield 'Butcher'
    yield 'Baker'
    yield 'Candlestick maker'
  }
}

var jobIter = Person18.createJobIterator() // Call the generator function to generate the generator object
console.log(jobIter.next().value) // 'Butcher'
console.log(jobIter.next().value) // 'Baker'
console.log(jobIter.next().value) // 'Candlestick maker'

var p16 = new Person18()
var nicknameIter = p16.createNicknameIterator() // Call the generator function to generate the generator object
console.log(nicknameIter.next().value) // 'Jack'
console.log(nicknameIter.next().value) // 'Jake'
console.log(nicknameIter.next().value) // 'J-Dog'
  • The generator method can be used as the default iterator to turn the class instance into an iteratable object
class Person19 {
  constructor() {
    this.nickNames = ['Jack', 'Jake', 'J-Dog']
  }
  *[Symbol.iterator]() {
    // Generator function as default iterator
    yield* this.nickNames.entries()
  }
}

var p17 = new Person19()
for (let [i, n] of p17) {
  console.log(i, n)
  /* 
    0 'Jack'
    1 'Jake'
    2 'J-Dog'
  */
}
  • Iterator instances can be returned directly
class Person20 {
  constructor() {
    this.nickNames = ['Jack', 'Jake', 'J-Dog']
  }
  [Symbol.iterator]() {
    // Returns an iterator instance
    return this.nickNames.entries()
  }
}
var p18 = new Person20()
for (let [i, n] of p18) {
  console.log(i, n)
  /* 
    0 'Jack'
    1 'Jake'
    2 'J-Dog'
  */
}

inherit

  • ES6 supports class inheritance mechanism and still uses prototype chain

Inheritance basis

  • ES6 class supports single inheritance. You can inherit any object with [[Construct]] and prototype (inheritable class or ordinary constructor) by using the extends keyword
class Vehicle {}
class Bus extends Vehicle {} // Inheritance class
var b = new Bus()
console.log(b instanceof Bus) // true
console.log(b instanceof Vehicle) // true

function Person21() {}
class Engineer extends Person21 {} // Inheritance constructor
var p19 = new Engineer()
console.log(p19 instanceof Engineer) // true
console.log(p19 instanceof Person21) // true
  • The extends keyword can be used in class expressions
var Bus2 = class extends Vehicle {}
  • The subclass accesses the parent class and the methods defined on the parent class prototype through the prototype chain. The value of this reflects the instance or class that calls the corresponding method
class Vehicle2 {
  identifyPrototype(id) {
    // Methods defined on the parent class prototype
    console.log(id, this)
  }
  static identifyClass(id) {
    // Methods defined by the parent class itself
    console.log(id, this)
  }
}
class Bus3 extends Vehicle2 {}

var v = new Vehicle2()
var b2 = new Bus3()

v.identifyPrototype('v') // 'v' Vehicle2 {}, this is the parent class instance
b2.identifyPrototype('b') // 'b' Bus3 {}, this is a subclass instance
v.__proto__.identifyPrototype('v') // 'v' {}, this is the parent class prototype
b2.__proto__.identifyPrototype('b') // 'b' Vehicle2 {}, this is the subclass prototype, that is, the parent class instance
Vehicle2.identifyClass('v') // v [class Vehicle2], this is the parent class itself
Bus3.identifyClass('b') // b [class Bus3 extends Vehicle2], this is the subclass itself

Constructors, HomeObject, and super()

  • In the subclass constructor, you can call the parent constructor through super()
class Vehicle3 {
  constructor() {
    this.hasEngine = true
  }
}
class Bus4 extends Vehicle3 {
  constructor() {
    super() // Call the constructor of the parent class
    console.log(this) // Bus4 {hasengine: true}, subclass instance, parent class constructor called
    console.log(this instanceof Vehicle3) // true
    console.log(this instanceof Bus4) // true
  }
}
new Bus4()
  • In the subclass static method, you can call the parent class static method through super()
class Vehicle4 {
  static identifyV() {
    // Parent class static method
    console.log('vehicle4')
  }
}
class Bus5 extends Vehicle4 {
  static identifyB() {
    // Subclass static method
    super.identifyV() // Call the static method of the parent class
  }
}
Bus5.identifyB() // 'vehicle4'
  • ES6 adds an internal feature [[HomeObject]] to class constructors and static methods, pointing to the object that defines the method, and super will always be defined as the prototype of [[HomeObject]]

  • Several problems needing attention when using super:

    • super can only be used in subclass constructors and subclass static methods
    • You cannot reference the super keyword alone. You can either call the constructor or reference a static method
    • Calling super() will call the parent constructor and assign the returned instance to this
    • When calling super(), if you need to pass parameters to the constructor of the parent class, you need to pass them in manually
    • If the subclass does not define a constructor, when instantiating it * * automatically calls super() * * and passes in all the parameters passed to the parent class
    • You cannot reference this in a subclass constructor or static method before calling super()
    • If the subclass explicitly defines the constructor, it can either call super() in it, or return a new object in it.
class Vehicle5 {
  constructor(id) {
    // Super() / / syntax error: 'super' keyword unexpected here, super can only be used in subclass constructors and subclass static methods
    this.id = id
  }
}
class Bus6 extends Vehicle5 {
  constructor(id) {
    // console. Log (super) / / syntax error: 'super' keyword unexpected here. Super cannot be referenced separately
    // console.log(this) // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor. This cannot be referenced before calling super()
    super(id) // Call the parent constructor, manually pass parameters to the parent constructor, and assign the returned instance to this (subclass instance)
    console.log(this) // Bus6 {ID: 5}, subclass instance
    console.log(this instanceof Vehicle5) // true
    console.log(this instanceof Bus6) // true
  }
}
new Bus6(5)

class Bus7 extends Vehicle5 {} // Subclass does not define constructor
console.log(new Bus7(6)) // Bus7 {ID: 6}. When instantiating, super() is automatically called and parameters are passed

class Bus8 extends Vehicle5 {
  // constructor() {} // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
  constructor(id) {
    super(id) // Subclasses explicitly define constructors or call super()
  }
}
class Bus9 extends Vehicle5 {
  // constructor() {} // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
  constructor(id) {
    return {} // Subclasses explicitly define constructors or return other objects
  }
}
console.log(new Bus8(7)) // Bus8 {ID: 7}, subclass instance
console.log(new Bus9(8)) // {}, return new object

Abstract base class

  • You can set that the parent class is only inherited by the child class and will not be instantiated. You can use new Target detects whether it is an abstract base class
  • A method must be defined for the subclass of the parent class. Check whether the corresponding method exists through this
class Vehicle6 {
  constructor() {
    console.log(new.target)
    if (new.target === Vehicle6) {
      // Prevent abstract base classes from being instantiated
      throw new Error('Vehicle6 cannot be directly instantiated')
    }
    if (!this.foo) {
      // Subclasses are required to define foo() methods
      throw new Error('Inheriting class must define foo()')
    }
  }
}
class Bus10 extends Vehicle6 {} // Subclass does not define foo() method
class Bus11 extends Vehicle6 {
  // Subclasses define foo() methods
  foo() {}
}

// new Vehicle6() // [class Vehicle6],Error: Vehicle6 cannot be directly instantiated
// new Bus10() // [class Bus10 extends Vehicle6],Error: Inheriting class must define foo()
new Bus11() // [class Bus11 extends Vehicle6]

Inherit built-in types

  • You can extend built-in types using class inheritance
class SuperArray extends Array {
  // Add method to subclass prototype: shuffle arbitrarily
  shuffle() {
    for (let i = this.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1))
      ;[this[i], this[j]] = [this[j], this[i]]
    }
  }
}
var a = new SuperArray(1, 2, 3, 4, 5)
console.log(a instanceof Array) // true, a is an instance of Array
console.log(a instanceof SuperArray) // true, a is an instance of SuperArray
console.log(a) // SuperArray(5) [ 1, 2, 3, 4, 5 ]
a.shuffle()
console.log(a) // SuperArray(5) [ 3, 1, 2, 5, 4 ]
  • Some methods of built-in types return new instances. By default, the returned instance type is the same as the original instance type
var a1 = new SuperArray(1, 2, 3, 4, 5)
var a2 = a1.filter((x) => !!(x % 2)) // The filter method returns a new instance, and the instance type is consistent with that of a1
console.log(a1) // SuperArray(5) [ 1, 2, 3, 4, 5 ]
console.log(a2) // SuperArray(3) [ 1, 3, 5 ]
console.log(a1 instanceof SuperArray) // true
console.log(a2 instanceof SuperArray) // true
  • You can use symbol Categories defines the getter method of the static getter, which overrides the class returned by the method of the built-in type when creating a new instance
class SuperArray2 extends Array {
  // Symbol. Categories defines a static getter method that overrides the class returned when a new instance is created
  static get [Symbol.species]() {
    return Array // When a method of built-in type creates a new instance, it returns the Array type
  }
}
var a3 = new SuperArray2(1, 2, 3, 4, 5)
var a4 = a3.filter((x) => !!(x % 2)) // The filter method returns a new instance, and the instance type has been overwritten (Array)
console.log(a3) // SuperArray(5) [ 1, 2, 3, 4, 5 ]
console.log(a4) // [ 1, 3, 5 ]
console.log(a3 instanceof SuperArray2) // true
console.log(a4 instanceof SuperArray2) // false

Class blending

  • The extends keyword can be followed by any expression that can be parsed into a class or constructor in addition to the parent class
class Vehicle7 {}
function getParentClass() {
  console.log('evaluated expression')
  return Vehicle7 // The expression is resolved to the Vehicle7 class
}
class Bus12 extends getParentClass {}
new Bus12() // 'evaluated expression'
  • Multiple mixed in elements can be concatenated in an expression through the mixed in mode, and the expression is finally resolved to a class that can be inherited
class Vehicle8 {}
let FooMixin = (SuperClass) =>
  // The expression receives a superclass as a parameter and returns a subclass
  class extends SuperClass {
    // Subclass prototype addition method
    foo() {
      console.log('foo')
    }
  }
let BarMixin = (SuperClass) =>
  // The expression receives a superclass as a parameter and returns a subclass
  class extends SuperClass {
    // Subclass prototype addition method
    bar() {
      console.log('bar')
    }
  }

class Bus13 extends BarMixin(FooMixin(Vehicle8)) {} // Nested level by level inheritance: FooMixin inherits Vehicle8, BarMixin inherits FooMixin, and Bus13 inherits BarMixin
var b3 = new Bus13()
console.log(b3) // Bus13 {}, subclass instance
b3.foo() // 'foo' inherits the methods on the superclass prototype
b3.bar() // 'bar' inherits the method on the superclass prototype
  • Expand the nested calls by writing an auxiliary function
function mix(BaseClass, ...Mixins) {
  /* 
    reduce Receive 2 parameters: merge function that will run for each item and initial value of merge starting point (not required)
    The merge function receives four parameters: the previous merge value, the current item, the current index, and the array itself
  */
  return Mixins.reduce(
    (pre, cur) => cur(pre), // Merge method: executes the current method, and the parameter is the previous merge value
    BaseClass // Merge initial value
  )
}
class Bus14 extends mix(Vehicle7, FooMixin, BarMixin) {}
var b4 = new Bus14()
console.log(b4) // Bus14 {}
b4.foo() // 'foo'
b4.bar() // 'bar'

Summary & ask questions

  • How to define JS classes? What are the similarities and differences between classes and functions?
  • What can a class contain? How do I access class expressions and their names?
  • What are the internal steps of class instantiation? What does the class constructor return by default? What is the impression of returning a new object?
  • What are the main differences between class constructors and ordinary constructors?
  • What data type does the class belong to? What does its prototype and constructor point to respectively? What is the difference between using the new operator to call the class itself and the class constructor?
  • Write two pieces of code to express "class as parameter" and "class instantiation immediately"
  • How to define instance members, prototype methods, accessor methods and static class methods of a class? Where are they defined in the class (instance / prototype / class itself)?
  • Write a piece of code and use the static class method as the instance factory
  • How to manually add class member data? Why does the class definition not show support for adding data members?
  • Where can a generator method be defined in a class? Write a piece of code, add the generator method when defining the class, and use it as the default iterator
  • Which objects can a class inherit through the extends keyword? What elements can be followed by extensions? The subclass can access the methods of the parent class through the prototype chain?
  • What is the function and usage of super keyword? What are the precautions when using it?
  • How to define an abstract base class? What is its function?
  • Write a piece of code to extend the built-in object Array with class inheritance, and the subclass returns the Array instance type after calling the concat() method
  • Write a piece of code, use the nested call of mixed mode to realize the level by level inheritance of multiple classes, and then use the auxiliary function to realize the nested expansion

Topics: Javascript Front-end Class OOP