Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

The basic syntax of ES6 Class


May 08, 2021 ES6


Table of contents


1. Introduction

The from which the class came from

In the JavaScript language, the traditional way to generate instance objects is through 构造函数 Here's an example.

  1. function Point(x, y) {
  2. this.x = x;
  3. this.y = y;
  4. }
  5. Point.prototype.toString = function () {
  6. return '(' + this.x + ', ' + this.y + ')';
  7. };
  8. var p = new Point(1, 2);

The above writing is very different from traditional object-oriented languages, such as C+ and Java, and can easily confuse new programmers learning the language.

ES6 provides a writing closer to traditional languages, Class(类) as a template for objects. The class keyword allows you to define a class.

Basically, ES6's class 语法糖 can be thought of as just a syntax sugar, most of its functions, ES5 can do, the new class writing just makes the object prototype writing clearer, more like object-oriented programming syntax. The code above is rewritten with class of ES6, and that's it.

  1. class Point {
  2. constructor(x, y) {
  3. this.x = x;
  4. this.y = y;
  5. }
  6. toString() {
  7. return '(' + this.x + ', ' + this.y + ')';
  8. }
  9. }

The code above defines a "class" in which you can see that there is a constructor method, which is the construction method, and the this keyword represents the instance object. That is, the constructor Point of ES5, the method of constructing the Point class corresponding to ES6.

In addition to constructing methods, the Point class defines a toString method. N ote that when defining a "class" method, you don't need to add the keyword function before, just put the function definition in. In addition, there is no need for comma separation between methods, and errors are added.

The class of ES6 can be thought of as another way of writing a constructor.

  1. class Point {
  2. // ...
  3. }
  4. typeof Point // "function"
  5. Point === Point.prototype.constructor // true

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

When used, the new command is also used directly on the class, which is exactly the same as the use of the constructor.

  1. class Bar {
  2. doStuff() {
  3. console.log('stuff');
  4. }
  5. }
  6. var b = new Bar();
  7. b.doStuff() // "stuff"

The prototype property of the constructor continues to exist above the Class of ES6. In fact, all methods of the class are defined above the class's prototype property.

  1. class Point {
  2. constructor() {
  3. // ...
  4. }
  5. toString() {
  6. // ...
  7. }
  8. toValue() {
  9. // ...
  10. }
  11. }
  12. // 等同于
  13. Point.prototype = {
  14. constructor() {},
  15. toString() {},
  16. toValue() {},
  17. };

Calling a method above an instance of a class is actually calling a method on a prototype.

  1. class B {}
  2. let b = new B();
  3. b.constructor === B.prototype.constructor // true

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

Because the methods of the class are defined above the prototype object, the new methods of the class can be added above the prototype object. The Object.assign method makes it easy to add multiple methods to a class at once.

  1. class Point {
  2. constructor(){
  3. // ...
  4. }
  5. }
  6. Object.assign(Point.prototype, {
  7. toString(){},
  8. toValue(){}
  9. });

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

  1. Point.prototype.constructor === Point // true

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

  1. class Point {
  2. constructor(x, y) {
  3. // ...
  4. }
  5. toString() {
  6. // ...
  7. }
  8. }
  9. Object.keys(Point.prototype)
  10. // []
  11. Object.getOwnPropertyNames(Point.prototype)
  12. // ["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 the behavior of ES5.

  1. var Point = function (x, y) {
  2. // ...
  3. };
  4. Point.prototype.toString = function() {
  5. // ...
  6. };
  7. Object.keys(Point.prototype)
  8. // ["toString"]
  9. Object.getOwnPropertyNames(Point.prototype)
  10. // ["constructor","toString"]

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

The constructor method

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

  1. class Point {
  2. }
  3. // 等同于
  4. class Point {
  5. constructor() {}
  6. }

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

The constructor method returns the instance object (this) by default, which can be specified to return another object.

  1. class Foo {
  2. constructor() {
  3. return Object.create(null);
  4. }
  5. }
  6. new Foo() instanceof Foo
  7. // false

In the above code, the constructor function returns a completely new object, resulting in an instance object that is not an instance of the Foo class.

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

  1. class Foo {
  2. constructor() {
  3. return Object.create(null);
  4. }
  5. }
  6. Foo()
  7. // TypeError: Class constructor Foo cannot be invoked without 'new'

An instance of the class

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

  1. class Point {
  2. // ...
  3. }
  4. // 报错
  5. var point = Point(2, 3);
  6. // 正确
  7. var point = new Point(2, 3);

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

  1. //定义类
  2. class Point {
  3. constructor(x, y) {
  4. this.x = x;
  5. this.y = y;
  6. }
  7. toString() {
  8. return '(' + this.x + ', ' + this.y + ')';
  9. }
  10. }
  11. var point = new Point(2, 3);
  12. point.toString() // (2, 3)
  13. point.hasOwnProperty('x') // true
  14. point.hasOwnProperty('y') // true
  15. point.hasOwnProperty('toString') // false
  16. point.__proto__.hasOwnProperty('toString') // true

In the above code, both x and y are properties of the instance object point itself (because it is defined on the this variable), so the hasOwnProperty method returns true, and toString is the 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.

  1. var p1 = new Point(2,3);
  2. var p2 = new Point(3,2);
  3. p1.__proto__ === p2.__proto__
  4. //true

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

This also means that methods can be added to the Class through the instance's proto property.

Proto is not a feature of the language itself, it is a private property added by major vendors when it is specifically implemented, and although this private property is currently available in the JS engine of many modern browsers, it is not recommended to use this property in production to avoid dependency on the environment. In a production environment, we can use the Object.getPrototypeOf method to get a prototype of an instance object, and then add methods/attributes to the prototype.

  1. var p1 = new Point(2,3);
  2. var p2 = new Point(3,2);
  3. p1.__proto__.printName = function () { return 'Oops' };
  4. p1.printName() // "Oops"
  5. p2.printName() // "Oops"
  6. var p3 = new Point(4,2);
  7. p3.printName() // "Oops"

The above code adds a printName method to the prototype of p1, which can also be called because the prototype of p1 is the prototype of p2. A lso, the new instance p3 can then call this method. This means that overwriteing the prototype with the proto property of the instance must be fairly cautious and not recommended because it changes the original definition of "class" and affects all instances.

Take-value functions (getter) and save functions (setter)

As with ES5, the get and set keywords can 存值函数 be used inside “类” to set a value-saving function and 取值函数 on a property to block access behavior for that property. set

  1. class MyClass {
  2. constructor() {
  3. // ...
  4. }
  5. get prop() {
  6. return 'getter';
  7. }
  8. set prop(value) {
  9. console.log('setter: '+value);
  10. }
  11. }
  12. let inst = new MyClass();
  13. inst.prop = 123;
  14. // setter: 123
  15. inst.prop
  16. // 'getter'

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

The value-saving function and the value-taking function are set on the Descriptor object of the property.

  1. class CustomHTMLElement {
  2. constructor(element) {
  3. this.element = element;
  4. }
  5. get html() {
  6. return this.element.innerHTML;
  7. }
  8. set html(value) {
  9. this.element.innerHTML = value;
  10. }
  11. }
  12. var descriptor = Object.getOwnPropertyDescriptor(
  13. CustomHTMLElement.prototype, "html"
  14. );
  15. "get" in descriptor // true
  16. "set" in descriptor // true

In the code above, the memory function and the value-taking function are defined above the description object of the html property, which is exactly the same as ES5.

Property expression

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

  1. let methodName = 'getArea';
  2. class Square {
  3. constructor(length) {
  4. // ...
  5. }
  6. [methodName]() {
  7. // ...
  8. }
  9. }

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

Class expression

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

  1. const MyClass = class Me {
  2. getClassName() {
  3. return Me.name;
  4. }
  5. };

The above code defines a class using expressions. N ote that the name of this class is Me, but Me is only available inside Class and refers to the current class. Outside class, this class can only be referenced with MyClass.

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

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

If the inside of the class is not used, me can be omitted, that is, it can be written in the following form.

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

With a Class expression, you can write a Class that executes immediately.

  1. let person = new class {
  2. constructor(name) {
  3. this.name = name;
  4. }
  5. sayName() {
  6. console.log(this.name);
  7. }
  8. }('张三');
  9. person.sayName(); // "张三"

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

Note the point

(1) Strict mode

Inside classes and modules, the default is strict mode, so you don't need to use use strict to specify the run mode. A s long as your code is written in a class or module, only strict mode is available. Considering that all future code is actually running in modules, ES6 actually upgraded the entire language to strict mode.

(2) There is no ascension

does 变量提升 promotion (hoist), which is completely different from ES5.

  1. new Foo(); // ReferenceError
  2. class Foo {}

In the above code, the Foo class is used before and defined later, which will report an error because ES6 does not elevate the class's declaration to the head of the code. The reason for this provision is related to the inheritance to be mentioned below, and it is important to ensure that the child class is defined after the parent class.

  1. {
  2. let Foo = class {};
  3. class Bar extends Foo {
  4. }
  5. }

The code above doesn't report an error because When Bar inherits Foo, Foo is already defined. However, if there is an elevation of class, the above code will report an error because class is promoted to the head of the code and the let command is not promoted, so foo is not defined when Bar inherits Foo.

(3) name property

Because the class of ES6 is essentially just a layer of wrapper for the constructor of ES5, many of the properties of the Class including the name property.

  1. class Point {}
  2. Point.name // "Point"

The name property always returns the class name that follows the class keyword.

(4) Generator method

If a method is previously asterisked with an asterisk, it means that the method is a Generator function.

  1. class Foo {
  2. constructor(...args) {
  3. this.args = args;
  4. }
  5. * [Symbol.iterator]() {
  6. for (let arg of this.args) {
  7. yield arg;
  8. }
  9. }
  10. }
  11. for (let x of new Foo('hello', 'world')) {
  12. console.log(x);
  13. }
  14. // hello
  15. // world

In the code above, the Symbol.iterator method of the Foo class has an asterisk in front of it, indicating that the method is a Generator function. T he Symbol.iterator method returns a default traverser for the Foo class, for... The of loop automatically calls the traverser.

(5) The point of this

If the method of the class contains this, it points by default to the instance of the class. However, you must be very careful that once you use this method alone, you are likely to report an error.

  1. class Logger {
  2. printName(name = 'there') {
  3. this.print(`Hello ${name}`);
  4. }
  5. print(text) {
  6. console.log(text);
  7. }
  8. }
  9. const logger = new Logger();
  10. const { printName } = logger;
  11. printName(); // TypeError: Cannot read property 'print' of undefined

In the code above, this in the printName method points by default to an instance of the Logger class. However, if this method is extracted for use alone, this points to the environment in which the method runs (because the class is in strict mode, so this actually points to undefined), resulting in an error that cannot be found for the print method.

An easier solution is to bind this in the construction method so that you don't find the print method.

  1. class Logger {
  2. constructor() {
  3. this.printName = this.printName.bind(this);
  4. }
  5. // ...
  6. }

Another solution is to use the arrow function.

  1. class Obj {
  2. constructor() {
  3. this.getThis = () => this;
  4. }
  5. }
  6. const myObj = new Obj();
  7. myObj.getThis() === myObj // true

This inside the arrow function always points to the object where it was defined. I n the code above, the arrow function is inside the constructor, and its definition takes effect when the constructor executes. At this point, the running environment in which the arrow function is located must be the instance object, so this always points to the instance object.

Another workaround is to use Proxy, which automatically binds this when you get the method.

  1. function selfish (target) {
  2. const cache = new WeakMap();
  3. const handler = {
  4. get (target, key) {
  5. const value = Reflect.get(target, key);
  6. if (typeof value !== 'function') {
  7. return value;
  8. }
  9. if (!cache.has(value)) {
  10. cache.set(value, value.bind(target));
  11. }
  12. return cache.get(value);
  13. }
  14. };
  15. const proxy = new Proxy(target, handler);
  16. return proxy;
  17. }
  18. const logger = selfish(new Logger());

2. Static method

The class is the prototype of the instance, and all methods defined in the class are inherited by the instance. If you add the static keyword before a method, it means that the method is not inherited by the instance, but is called directly through the class, which is called a "static method."

  1. class Foo {
  2. static classMethod() {
  3. return 'hello';
  4. }
  5. }
  6. Foo.classMethod() // 'hello'
  7. var foo = new Foo();
  8. foo.classMethod()
  9. // TypeError: foo.classMethod is not a function

In the code above, the ClassMethod method of the Foo class is previously followed by the static keyword, indicating that the method is a static method that can be called directly on the Foo class (Foo.classMethod() rather than on an instance of the Foo class. If a static method is called on an instance, an error is thrown indicating that the method does not exist.

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

  1. class Foo {
  2. static bar() {
  3. this.baz();
  4. }
  5. static baz() {
  6. console.log('hello');
  7. }
  8. baz() {
  9. console.log('world');
  10. }
  11. }
  12. Foo.bar() // hello

In the code above, 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, as can be seen from this example, static methods can be renamed with non-static methods.

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

  1. class Foo {
  2. static classMethod() {
  3. return 'hello';
  4. }
  5. }
  6. class Bar extends Foo {
  7. }
  8. Bar.classMethod() // 'hello'

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

Static methods can also be called from the super object.

  1. class Foo {
  2. static classMethod() {
  3. return 'hello';
  4. }
  5. }
  6. class Bar extends Foo {
  7. static classMethod() {
  8. return super.classMethod() + ', too';
  9. }
  10. }
  11. Bar.classMethod() // "hello, too"

3. A new way to write an instance property

实例属性 Can also be defined at the top of the class in addition to the this that is defined within constructor() method.

  1. class IncreasingCounter {
  2. constructor() {
  3. this._count = 0;
  4. }
  5. get value() {
  6. console.log('Getting the current value!');
  7. return this._count;
  8. }
  9. increment() {
  10. this._count++;
  11. }
  12. }

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

  1. class IncreasingCounter {
  2. _count = 0;
  3. get value() {
  4. console.log('Getting the current value!');
  5. return this._count;
  6. }
  7. increment() {
  8. this._count++;
  9. }
  10. }

In the code above, the instance _count is at the same level as the value() and increment() methods of the value function. At this point, you do not need to add this before the instance property.

The advantage of this new approach is that the properties of all instance objects themselves are defined at the head of the class and look neat, so you can see at a glance what instance properties of the class are.

  1. class foo {
  2. bar = 'hello';
  3. baz = 'world';
  4. constructor() {
  5. // ...
  6. }
  7. }

The above code, you can see at a glance, foo class has two instance properties, at a glance. In addition, it is more concise to write.

4. Static properties

静态属性 refer Class properties of Class itself, Class.propName rather than properties defined on instance objects (this).

  1. class Foo {
  2. }
  3. Foo.prop = 1;
  4. Foo.prop // 1

The above is written with a static property prop defined for the Foo class.

Currently, only this writing is feasible because ES6 clearly states that there are only static 静态方法 静态属性 Class Now there is a proposal that provides static properties for the class, written in front of the instance properties, plus the static keyword.

  1. class MyClass {
  2. static myStaticProp = 42;
  3. constructor() {
  4. console.log(MyClass.myStaticProp); // 42
  5. }
  6. }

This new writing greatly facilitates the expression of static properties.

  1. // 老写法
  2. class Foo {
  3. // ...
  4. }
  5. Foo.prop = 1;
  6. // 新写法
  7. class Foo {
  8. static prop = 1;
  9. }

In the code above, the static properties of the old writing are defined outside the class. A fter the entire class is generated, static properties are generated. T his makes it easy to ignore this static property and does not conform to the principle of code organization that the relevant code should put together. In addition, the new approach is explicit declaration, rather than assignment processing, with better semantics.

5. 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 externally. This is a common requirement that facilitates the encapsulation of code, but ES6 is not available and can only be implemented through work-through simulation.

One approach is to distinguish between naming.

  1. class Widget {
  2. // 公有方法
  3. foo (baz) {
  4. this._bar(baz);
  5. }
  6. // 私有方法
  7. _bar(baz) {
  8. return this.snaf = baz;
  9. }
  10. // ...
  11. }

In the code above, _bar before the method, indicating that this is a private method that is limited to internal use. However, this naming is not safe, and can still be called to this method outside the class.

Another approach is to move the private method out of the module, because all methods inside the module are visible to the outside world.

  1. class Widget {
  2. foo (baz) {
  3. bar.call(this, baz);
  4. }
  5. // ...
  6. }
  7. function bar(baz) {
  8. return this.snaf = baz;
  9. }

In the code above, foo is an exposed method that calls bar.call (this, baz) internally. This makes bar actually a private method of the current module.

Another approach is to take advantage Symbol uniqueness of the Symbol value and name the private method after a Symbol value.

  1. const bar = Symbol('bar');
  2. const snaf = Symbol('snaf');
  3. export default class myClass{
  4. // 公有方法
  5. foo(baz) {
  6. this[bar](baz);
  7. }
  8. // 私有方法
  9. [bar](baz) {
  10. return this[snaf] = baz;
  11. }
  12. // ...
  13. };

In the above code, bar and snaf are Symbol values that are generally not available and therefore have the effect of private methods and private properties. But it's not absolutely not, and Reflect.ownKeys() can still get them.

  1. const inst = new myClass();
  2. Reflect.ownKeys(myClass.prototype)
  3. // [ 'constructor', 'foo', Symbol(bar) ]

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

Proposals for private properties

Currently, there is a proposal to add a private property 私有属性 The method is to use the s before the property name.

  1. class IncreasingCounter {
  2. #count = 0;
  3. get value() {
  4. console.log('Getting the current value!');
  5. return this.#count;
  6. }
  7. increment() {
  8. this.#count++;
  9. }
  10. }

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

  1. const counter = new IncreasingCounter();
  2. counter.#count // 报错
  3. counter.#count = 42 // 报错

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

Here's another example.

  1. class Point {
  2. #x;
  3. constructor(x = 0) {
  4. this.#x = +x;
  5. }
  6. get x() {
  7. return this.#x;
  8. }
  9. set x(value) {
  10. this.#x = +value;
  11. }
  12. }

In the code above, #x is a private property that cannot be read outside the Point class. #x and x are two different properties because the hashtag is part of the property name and must be used with the hashtag.

The reason for introducing a new prefix, s, that represents a private property between private attributes and not the private keyword is that JavaScript is a dynamic language with no type declarations, and using separate symbols seems to be the only convenient and reliable way to accurately distinguish whether a property is private or not. In addition, the Ruby language uses s for private properties, and ES6 does not use the symbol because s has been left to Decorator.

This type of writing can be used not only to write private properties, but also to write private methods.

  1. class Foo {
  2. #a;
  3. #b;
  4. constructor(a, b) {
  5. this.#a = a;
  6. this.#b = b;
  7. }
  8. #sum() {
  9. return #a + #b;
  10. }
  11. printSum() {
  12. console.log(this.#sum());
  13. }
  14. }

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

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

  1. class Counter {
  2. #xValue = 0;
  3. constructor() {
  4. super();
  5. // ...
  6. }
  7. get #x() { return #xValue; }
  8. set #x(value) {
  9. this.#xValue = value;
  10. }
  11. }

In the code above, #x is a private property that reads and writes through get #x() and set #x().

Private properties are not limited to references from this, and instances can refer to private properties as long as they are inside the class.

  1. class Foo {
  2. #privateValue = 42;
  3. static getPrivateValue(foo) {
  4. return foo.#privateValue;
  5. }
  6. }
  7. Foo.getPrivateValue(new Foo()); // 42

The above code allows private properties to be referenced from above 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 a private method.

  1. class FakeMath {
  2. static PI = 22 / 7;
  3. static #totallyRandomNumber = 4;
  4. static #computeRandomNumber() {
  5. return FakeMath.#totallyRandomNumber;
  6. }
  7. static random() {
  8. console.log('I heard you like random numbers…')
  9. return FakeMath.#computeRandomNumber();
  10. }
  11. }
  12. FakeMath.PI // 3.142857142857143
  13. FakeMath.random()
  14. // I heard you like random numbers…
  15. // 4
  16. FakeMath.#totallyRandomNumber // 报错
  17. FakeMath.#computeRandomNumber() // 报错

In the above code, #totallyRandomNumber is a private property, #computeRandomNumber() is a private method, which can only be called internally by the FakeMath class, and the external call will report an error.

6. New.target property

new a command to generate an instance object from a constructor. E S6 introduces a new.target which is typically used 构造函数 constructors and returns the constructor to which the new command works. If the constructor is not called by the new command or Reflect.construct(), new.target returns undefined, so this property can be used to determine how the constructor is called.

  1. function Person(name) {
  2. if (new.target !== undefined) {
  3. this.name = name;
  4. } else {
  5. throw new Error('必须使用 new 命令生成实例');
  6. }
  7. }
  8. // 另一种写法
  9. function Person(name) {
  10. if (new.target === Person) {
  11. this.name = name;
  12. } else {
  13. throw new Error('必须使用 new 命令生成实例');
  14. }
  15. }
  16. var person = new Person('张三'); // 正确
  17. var notAPerson = Person.call(person, '张三'); // 报错

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

Class calls new.target internally, returning the current Class.

  1. class Rectangle {
  2. constructor(length, width) {
  3. console.log(new.target === Rectangle);
  4. this.length = length;
  5. this.width = width;
  6. }
  7. }
  8. var obj = new Rectangle(3, 4); // 输出 true

It is important to note that when a child class inherits a parent class, new.target returns the child class.

  1. class Rectangle {
  2. constructor(length, width) {
  3. console.log(new.target === Rectangle);
  4. // ...
  5. }
  6. }
  7. class Square extends Rectangle {
  8. constructor(length, width) {
  9. super(length, width);
  10. }
  11. }
  12. var obj = new Square(3); // 输出 false

In the above code, new.target returns sub-classes.

This feature allows you to write classes that cannot be used independently and must be inherited before they can be used.

  1. class Shape {
  2. constructor() {
  3. if (new.target === Shape) {
  4. throw new Error('本类不能实例化');
  5. }
  6. }
  7. }
  8. class Rectangle extends Shape {
  9. constructor(length, width) {
  10. super();
  11. // ...
  12. }
  13. }
  14. var x = new Shape(); // 报错
  15. var y = new Rectangle(3, 4); // 正确

In the code above, 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.