In the JavaScript language, the traditional method of generating instance objects is through constructors. Here 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 the traditional object-oriented languages (such as C + + and Java), which is easy to confuse the programmers who are new to the language.
ES6 provides a writing method closer to the traditional language, and introduces the concept of class as the template of objects. You can define a class by using the class keyword.
Basically, the class of ES6 can be regarded as just a syntax sugar. ES5 can do most of its functions. The new class writing method only makes the writing method of object prototype clearer and more like the syntax of object-oriented programming. The above code is rewritten with the class of ES6, which is as follows.
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, which is the construction method, and the this keyword represents the instance object. This new Class writing method is essentially consistent with the constructor Point of ES5 at the beginning of this chapter.
In addition to constructing methods, 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 of it. Just put the function definition in it. In addition, methods do not need to be separated by commas, and an error will be reported if it is added.
The class of ES6 can be regarded as another writing method of constructor.
class Point { // ... } typeof Point // "function" Point === Point.prototype.constructor // true
The above code shows that the data type of a class is a function, and the class itself points to a constructor.
When using, it also directly uses the new command 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 attribute of the constructor continues to exist on the "class" of ES6. In fact, all methods of the class are defined on the prototype attribute of the class.
class Point { constructor() { // ... } toString() { // ... } toValue() { // ... } } // Equivalent to Point.prototype = { constructor() {}, toString() {}, toValue() {}, };
In the above code, constructor(), toString(), toValue() are actually defined on 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 class B, and its constructor() method is the constructor() method of class B prototype.
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 makes it easy to add multiple methods to a class at a time.
class Point { constructor(){ // ... } } Object.assign(Point.prototype, { toString(){}, toValue(){} });
The constructor() attribute 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 defined methods inside 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 an internally defined method of the Point class, which cannot be enumerated. This is inconsistent with the behavior of ES5.
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. It is called automatically when an 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 the instance object (that is, this) by default. 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 new object, resulting in the instance object being not an instance of the Foo class.
Class must be called with new, or an error will be reported. This is a major difference between it and ordinary constructors, which can be executed without new.
class Foo { constructor() { return Object.create(null); } } Foo() // TypeError: Class constructor Foo cannot be invoked without 'new'
Class
The writing method of the instance of the generated Class is exactly the same as that of ES5. It also uses the new command. As mentioned earlier, if you forget to add new and call Class like a function, an error will be reported.
class Point { // ... } // report errors var point = Point(2, 3); // correct var point = new Point(2, 3);
Like ES5, the attributes of an instance are defined on the prototype (that is, on the class) unless they are explicitly defined on the instance itself (that is, 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, x and y are the properties of the instance object point itself (because they are defined on this object), so the hasOwnProperty() method returns true, while toString() is the property of the prototype object (because they are defined on the point class), so the hasOwnProperty() method returns false. These are consistent with the behavior of ES5.
As with 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 Point.prototype, so__ proto__ Properties are equal.
This also means that you can use the__ proto__ Property to add a method to the class.
"_proto_ is not a feature of the language itself. It is a private attribute added by major manufacturers during specific implementation. Although this private attribute is provided in the JS engine of many modern browsers, it is still not recommended to use this attribute in production to avoid dependence on the environment. In the production environment, we can use the Object.getPrototypeOf method to obtain 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"
The above code adds a printName() method to the prototype of p1. Since the prototype of p1 is the prototype of p2, p2 can also call this method. In addition, the newly created instance p3 can also call this method. This means that you must be very careful to use the _ property of the instance to change the prototype. It is not recommended because it will change the "class" The original definition of affects all instances.
Value taking function (getter) and stored value function (setter)
Like ES5, the get and set keywords can be used inside the "class" to set the save value function and value function for a property to intercept the access behavior of the property.
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 the corresponding save value function and value function, so the assignment and reading behavior are customized.
The stored value function and value function 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
In the above code, the stored value function and value function are defined on the description object of html attribute, which is completely consistent with ES5.
Attribute expression
Class, you can use an expression.
let methodName = 'getArea'; class Square { constructor(length) { // ... } [methodName]() { // ... } }
In the above code, the method name getArea of the Square class is obtained from the expression.
Class expression
Like functions, classes can be defined in the form of expressions.
const MyClass = class Me { getClassName() { return Me.name; } };
The above code defines a Class using an expression. 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 the Class, this Class can only be referenced by 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 it is not used inside the class, Me can be omitted, that is, it can be written in the following form.
const MyClass = class { /* ... */ };
Using the Class expression, you can write out the immediately executed Class.
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 executes immediately.
Attention
(1) Strict mode
The internal of classes and modules is strict mode by default, so there is no need to specify the operation mode using use strict. As long as your code is written in a class or module, only strict patterns are available. Considering that all future codes are actually running in modules, ES6 actually upgrades the whole language to strict mode.
(2) There is no promotion
Class does not have a hoist, which is completely different from ES5.
new Foo(); // ReferenceError class Foo {}
In the above code, the Foo class is used before and defined after, which will report an error, because ES6 will not promote the class declaration to the code header. The reason for this provision is related to the inheritance mentioned below. It must be ensured 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 been defined. However, if there is a class promotion, the above code will report an error, because the class will be promoted to the code header, and the let command does not promote, so when Bar inherits Foo, Foo has not been defined.
(3) name attribute
In essence, the Class of ES6 is only a layer of wrapper of the constructor of ES5, 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 class name 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 Foo class, indicating that the method is a Generator function. The Symbol.iterator method returns a default iterator of the Foo class, which will be called automatically by the for...of loop.
(5) The direction of this
If this is contained in the method of the class, it points to the instance of the class by default. However, you must be very careful. Once this method is used alone, it is likely to report 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, this in the printName method points to the instance of the Logger class by default. However, if this method is extracted and used alone, this will point to the environment where the method runs (because the class is in strict mode, this actually points to undefined), resulting in an error when the print method cannot be found.
A simple solution is to bind this in the constructor so that the print method cannot be found.
class Logger { constructor() { this.printName = this.printName.bind(this); } // ... }
Another solution is to use the arrow function.
class Obj { constructor() { this.getThis = () => this; } } const myObj = new Obj(); myObj.getThis() === myObj // true
This inside the arrow function always points to the object where it is defined. In the above code, the arrow function is located inside the constructor, and its definition takes effect when the constructor is executed. At this time, the running environment of the arrow function 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 methods.
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
Class is equivalent to the prototype of an instance. 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 "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, there is a static keyword before the classMethod method of Foo class, indicating that the method is a static method and can be called directly on the Foo class (Foo.classMethod()), rather than on the instance of 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 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. Here this refers to the Foo class, not the 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 child class Bar can call this method.
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"
New writing method of instance attribute
In addition to defining this in the constructor() method, the instance attribute can also be defined at the top level of the class.
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 property this_ Count is defined in the constructor() method. Another way to write it is that this attribute can also be defined at the top level of the class, and everything else remains the same.
class IncreasingCounter { _count = 0; get value() { console.log('Getting the current value!'); return this._count; } increment() { this._count++; } }
In the above code, the instance property_ count is at the same level as the value() and increment() methods of the value function. In this case, you do not need to add this before the instance attribute.
The advantage of this new writing method is that the attributes of all instance objects are defined in the head of the class, which looks neat. You can see what instance attributes this class has at a glance.
class foo { bar = 'hello'; baz = 'world'; constructor() { // ... } }
The above code can be seen at a glance that foo class has two instance properties, which are clear at a glance. In addition, it is also relatively simple to write.
Static properties
Static attributes refer to the attributes of Class itself, that is, Class.propName, rather than the attributes defined on the instance object (this).
class Foo { } Foo.prop = 1; Foo.prop // 1
The above method defines a static attribute prop for the Foo class.
At present, only this writing method is feasible, because ES6 clearly stipulates that Class has only static methods and no static attributes. Now there is a proposal [1] that provides the static attribute of the Class. It is written by adding the static keyword in front of the instance attribute.
class MyClass { static myStaticProp = 42; constructor() { console.log(MyClass.myStaticProp); // 42 } }
This new method greatly facilitates the expression of static attributes.
// Old writing class Foo { // ... } Foo.prop = 1; // neographism class Foo { static prop = 1; }
In the above code, the static attribute of the old writing method is defined outside the class. After the entire class is generated, static attributes are generated. This makes it easy to ignore this static attribute and does not comply with the code organization principle that relevant code should be put together. In addition, the new writing method is explicit declaration rather than assignment processing, which has better semantics.
Private methods and properties
Existing solutions
Private methods and properties are methods and properties that can only be accessed inside the class, but not outside. This is a common requirement, which is conducive to code encapsulation, but ES6 does not provide it. It can only be simulated through alternative methods.
One approach is to distinguish between names.
class Widget { // public Method foo (baz) { this._bar(baz); } // Private method _bar(baz) { return this.snaf = baz; } // ... }
In the code above_ The underscore in front of the bar() method indicates that this is a private method for internal use only. However, this naming is not safe. You can still call this method outside the class.
Another way is to simply move private methods out of the class, because all methods inside the class are visible to the outside world.
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 the private method of the current class.
Another method is to use the uniqueness of Symbol value to name 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, both bar and snaf are Symbol values. Generally, they cannot be obtained, so the effect of private methods and private attributes is achieved. But it's 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.
Private property proposal
At present, there is a proposal [2] to add private attributes to class. The method is to use # representation before the property 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 attribute, which can only be used inside the class (this.#count). If it is used outside the class, an error will be reported.
const counter = new IncreasingCounter(); counter.#count / / an error is reported counter.#count = 42 / / an error is reported
The above code is outside the class. If you read the private attribute, an error will be reported.
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 it 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 # together, so #x and X are two different attributes.
The reason for introducing a new prefix # to represent private attributes without using the private keyword is that JavaScript is a dynamic language without type declaration. Using independent symbols seems to be the only convenient and reliable method to accurately distinguish whether a property is a private attribute. In addition, the Ruby language uses @ to represent private attributes. ES6 does not use this symbol #, because @ has been left to the Decorator.
This writing method can be used not only to write private properties, but also to 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 set 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 completed through get #x() and set #x().
Private attributes are not limited to being referenced from this. As long as they are inside a class, instances can also reference private attributes.
class Foo { #privateValue = 42; static getPrivateValue(foo) { return foo.#privateValue; } } Foo.getPrivateValue(new Foo()); // 42
The above code allows private properties to be referenced from the instance foo.
The static keyword can also be added before the private property and private method to indicate that it 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 / / an error is reported FakeMath.#computeRandomNumber() / / an error is reported
In the above code, #totallyRandomNumber () is a private property and #computeRandomNumber() is a private method. It can only be called inside the FakeMath class, and an error will be reported when calling outside.
in operator
The try...catch structure can be used to determine whether a private attribute exists.
class A { use(obj) { try { obj.#foo; } catch { // Private property #foo does not exist } } } const a = new A(); a.use(a); // report errors
In the above example, class A does not have a private attribute #foo, so try...catch reports an error.
This writing method is very troublesome and poor readability. The V8 engine improves the in operator so that it can also be used to judge private attributes.
class A { use(obj) { if (#foo in obj) { // Private property #foo exists } else { // Private property #foo does not exist } } }
In the above example, the in operator determines whether the instance of the current class A has a private property #foo. If so, it returns true; otherwise, it returns false.
in can also be used 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 class a can only be judged by using the in operator inside class A, and only true is returned for instances of class A, and false is returned for other objects.
You can also use the in operator to determine the private attributes inherited from the parent class.
class A { #foo = 0; static test(obj) { console.log(#foo in obj); } } class SubA extends A {}; A.test(new SubA()) // true
In the above example, SubA inherits the private property from the parent class #foo, and the in operator is also valid.
Note that the in operator is invalid for inheritance formed by Object.create() and Object.setPrototypeOf, because this inheritance will not pass private attributes.
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, the subclass cannot get the private attribute of the parent class, so the in operator is invalid.
new.target attribute
New is the command to generate an instance object from the constructor. ES6 introduces a new.target attribute for the new command, which is generally used in the constructor and returns the constructor on which the new command works. If the constructor is not called through the new command or Reflect.construct(), new.target will return undefined, so this attribute can be used to determine how the constructor is called.
function Person(name) { if (new.target !== undefined) { this.name = name; } else { throw new Error('Must use new Command generation instance'); } } // Another way of writing function Person(name) { if (new.target === Person) { this.name = name; } else { throw new Error('Must use new Command generation instance'); } } var person = new Person('Zhang San'); // correct var notAPerson = Person.call(person, 'Zhang San'); // report errors
The above code ensures that the constructor can only be called through 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 a 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 can only be used after inheritance.
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 errors 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 using new.target outside the function will report an error.
Original address
- https://juejin.cn/post/7000891889465425957
Node community
I have established a Node.js community with a very good atmosphere. There are many Node.js partners in it. If you are interested in learning Node.js (you can also have a plan later), we can communicate, learn and build Node.js together. Add koala friends below to reply to "Node".