"Code tailor" provides technical-related information and a series of basic articles for front-end developers. Follow the "Novices of Xiaoheshan" public account on WeChat to get the latest articles in time.
Preface
Before you start learning, what we want to tell you is that this article is a JavaScript
"Objects, Classes and Object-Oriented Programming" in 060ab5d0ce79d4 language knowledge. If you have mastered the following knowledge, you can skip this link Go directly to the exercise
- Basic structure of the object
- Object declaration and use
- class
- Object structure assignment
- inherit
- Packaging object
If you have forgotten some parts, 👇🏻 is ready for you!
Summary
ECMA-262
defines an object as an unordered collection of a set of attributes. Strictly speaking, this means that an object is a set of values in no particular order. Each property or method of an object is identified by a name, which maps to a value. Because of this (and other reasons that have not yet been discussed), you can think ECMAScript
as a hash table, where the content is a set of name/value pairs, and the values can be data or functions.
Basic structure of the object
The usual way to create a custom object is to create a new instance of Object, and then add properties and methods to it, as shown in the following example:
let person = new Object()
person.name = 'XHS-rookies'
person.age = 18
person.job = 'Software Engineer'
person.sayName = function () {
console.log(this.name)
}
This example creates an person
with three properties ( name
, age
and job
) and one method ( sayName()
). sayName()
method will display this.name
, and this attribute will be parsed as person.name
. Early JavaScript
developers frequently used this method to create new objects. A few years later, object literals became a more popular way. If the previous example uses object literals, it can be written like this:
let person = {
name: 'XHS-rookies',
age: 18,
job: 'Software Engineer',
sayName() {
console.log(this.name)
},
}
The person
person
object in the previous example, and their properties and methods are the same. These attributes have their own characteristics, and these characteristics determine their behavior JavaScript
Object declaration and use
Looking at ECMAScript
specification, the characteristics of each version seem to be unexpected. ECMAScript 5.1
does not officially support object-oriented structures such as classes or inheritance. However, as will be introduced in the next few sections, clever use of prototypal inheritance can successfully simulate the same behavior. ECMAScript 6
officially supports classes and inheritance. ES6
class is designed to completely cover the prototype-based inheritance model designed by the previous specification. However, no matter how you look at it, the ES6
class is just ES5.1
constructor plus prototype inheritance.
Factory mode
The factory pattern is a well-known design pattern, which is widely used in the field of software engineering to abstract the process of creating specific objects. The following example shows a way to create objects according to a specific interface:
function createPerson(name, age, job) {
let o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName = function () {
console.log(this.name)
}
return o
}
let person1 = createPerson('XHS-rookies', 18, 'Software Engineer')
let person2 = createPerson('XHS-boos', 18, 'Teacher')
Here, the function createPerson()
receives 3 parameters, and an object Person
You can call this function multiple times with different parameters, and each time it will return an object containing 3 properties and 1 method. Although this factory model can solve the problem of creating multiple similar objects, it does not solve the problem of object identification (that is, the type of the newly created object).
Constructor pattern
ECMAScript
is used to create a specific type of object. Native constructors like Object
and Array
can be used directly in the execution environment at runtime. Of course, you can also customize the constructor to define properties and methods for your own object types in the form of functions. For example, the previous example can be written like this using the constructor pattern:
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
console.log(this.name)
}
}
let person1 = new Person('XHS-rookies', 18, 'Software Engineer')
let person2 = new Person('XHS-boos', 18, 'Teacher')
person1.sayName() // XHS-rookies
person2.sayName() // XHS-boos
In this example, the Person()
constructor replaces the createPerson()
factory function. In fact, Person()
internal code with createPerson()
basically the same, except the following differences.
- The object is not created explicitly.
- The attributes and methods are directly assigned to
this
. - There is no
return
.
In addition, note that the first letter of the Person
By convention, the first letter of the constructor name should be capitalized, and the non-constructor function should start with a lowercase letter. This is borrowed from the object-oriented programming language, which helps to distinguish between constructors and ordinary functions ECMAScript
After all ECMAScript
a function that can create objects.
To create an instance of Person
new
operator should be used. Calling the constructor in this way will perform the following operations.
(1) Create a new object in memory.
(2) The [[Prototype]]
property inside this new object is assigned to the prototype
property of the constructor.
(3) this
inside the constructor is assigned to this new object (that is, this
points to the new object).
(4) Execute the code inside the constructor (add attributes to the new object).
(5) If the constructor returns a non-empty object, return that object; otherwise, return the new object just created.
At the end of the previous example, person1
and person2
respectively store different instances Person
Both objects have a constructor
attribute pointing to Person
, as shown below:
console.log(person1.constructor == Person) // true
console.log(person2.constructor == Person) // true
constructor
was originally used to identify the object type. However, it is generally believed that the instanceof
operator is a more reliable way to determine the object type. Each object in the previous example is Object
instance, also Person
example, as will invoke instanceof
result of the operator is as follows:
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person2 instanceof Object) // true
console.log(person2 instanceof Person) // true
Defining a custom constructor can ensure that the instance is identified as a specific type, which is a big advantage compared to the factory pattern. In this example, person1
and person2
are also considered Object
because all custom objects inherit from Object
(more on this later). The constructor does not have to be written in the form of a function declaration. Function expressions assigned to variables can also represent constructors:
let Person = function (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
console.log(this.name)
}
}
let person1 = new Person('XHS-rookies', 18, 'Software Engineer')
let person2 = new Person('XHS-boos', 18, 'Teacher')
person1.sayName() // XHS-rookies
person2.sayName() // XHS-boos
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person2 instanceof Object) // true
console.log(person2 instanceof Person) // true
During instantiation, if you don't want to pass parameters, the parentheses after the constructor can be added or not. As long as there is the new
operator, you can call the corresponding constructor:
function Person() {
this.name = 'rookies'
this.sayName = function () {
console.log(this.name)
}
}
let person1 = new Person()
let person2 = new Person()
person1.sayName() // rookies
person2.sayName() // rookies
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person2 instanceof Object) // true
console.log(person2 instanceof Person) // true
1. The constructor is also a function
The only difference between a constructor and an ordinary function is the calling method. In addition, the constructor is also a function. There is no special syntax for defining a function as a constructor. As long as any function new
operator, it is a constructor, and the function without the new
operator is called a normal function. Person()
defined in the previous example can be called as follows:
// 作为构造函数
let person = new Person('XHS-rookies', 18, 'Software Engineer')
person.sayName() // "XHS-rookies"
// 作为函数调用
Person('XHS-boos', 18, 'Teacher') // 添加到 window 对象
window.sayName() // "XHS-boos"
// 在另一个对象的作用域中调用
let o = new Object()
Person.call(o, 'XHS-sunshineboy', 25, 'Nurse')
o.sayName() // "XHS-sunshineboy"
This example first shows the typical way of calling the constructor, that is, using the new
operator to create a new object. Then the normal function is called, this time without the use of new
operator invocation Person()
, the result will add properties and methods to window
object. Keep in mind here that when a function is called without explicitly setting the this
value (that is, there is no method call as an object, or call()/apply()
not called), this
always points to the Global
object (in the browser, it is the window
object). Therefore, after the above call, there is a sayName()
window
object, calling it will return "Greg"
. The last call method shown is to call()
(or apply()
), and at the same time specify a specific object as the scope. Here call object o
designated Person()
inside this
value, the function code After execution, all the properties and sayName()
methods are added to the object o
above.
2. Problems with the constructor
Although the constructor is useful, it is not without problems. The main problem with the constructor is that its defined method will be created on every instance. So for the previous example, person1
and person2
are sayName()
methods, but these two methods are not the same Function
instance. We know ECMAScript
is an object, so every time a function is defined, an object is initialized. Logically speaking, this constructor actually looks like this:
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = new Function('console.log(this.name)') // 逻辑等价
}
By understanding this constructor in this way, you can know more clearly that each Person
instance will have its own Function
instance to display the name
attribute. Of course, creating functions in this way will bring about different scope chains and identifier resolutions. But the mechanism for creating a new Function
instance is the same. Therefore, although the functions on different instances have the same name, they are not equal, as shown below:
console.log(person1.sayName == person2.sayName) // false
Because they all do the same thing, there is no need to define two different Function
instances. Moreover, the this
object can postpone the binding of functions and objects until runtime. To solve this problem, you can move the function definition outside the constructor:
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = sayName
}
function sayName() {
console.log(this.name)
}
let person1 = new Person('XHS-rookies', 18, 'Software Engineer')
let person2 = new Person('XHS-boos', 18, 'Teacher')
person1.sayName() // XHS-rookies
person2.sayName() // XHS-boos
Here, sayName()
is defined outside the constructor. Inside the constructor, the sayName
attribute is equal to the global sayName()
function. Because this time the sayName
attribute contains only a pointer to an external function, so person1
and person2
sayName()
function defined in the global scope. Although this solves the problem of repeated definitions of functions with the same logic, the global scope is also messed up because that function can actually only be called on one object. If this object requires multiple methods, then multiple functions must be defined in the global scope. This will cause the code referenced by the custom type to not be gathered together well. This new problem can be solved through the prototype model.
Prototype mode
Each function creates a prototype
property, which is an object containing properties and methods that should be shared by instances of a specific reference type. In fact, this object is the prototype of the object created by calling the constructor. The advantage of using a prototype object is that the properties and methods defined on it can be shared by object instances. The values originally assigned to object instances directly in the constructor can be directly assigned to their prototypes, as shown below:
function Person() {}
Person.prototype.name = 'XHS-rookies'
Person.prototype.age = 18
Person.prototype.job = 'Software Engineer'
Person.prototype.sayName = function () {
console.log(this.name)
}
let person1 = new Person()
person1.sayName() // "XHS-rookies"
let person2 = new Person()
person2.sayName() // "XHS-rookies"
console.log(person1.sayName == person2.sayName) // true
You can also use function expressions:
let Person = function () {}
Person.prototype.name = 'XHS-rookies'
Person.prototype.age = 18
Person.prototype.job = 'Software Engineer'
Person.prototype.sayName = function () {
console.log(this.name)
}
let person1 = new Person()
person1.sayName() // "XHS-rookies"
let person2 = new Person()
person2.sayName() // "XHS-rookies"
console.log(person1.sayName == person2.sayName) // true
Here, all the properties and sayName()
methods are added directly to the Person
of prototype
on the property, constructor body nothing. But after this definition, the new object created by calling the constructor still has the corresponding properties and methods. Unlike the constructor pattern, the properties and methods defined using this prototype pattern are shared by all instances. Therefore, person1
and person2
access the same attributes and the same sayName()
function. To understand this process, one must understand the ECMAScript
of the prototype in 060ab5d0ce84c5. (For detailed study of the prototype in ECMAScript
object prototype )
Other prototype syntax
Some readers may have noticed that in the previous example, Person.prototype
rewritten every time a property or method was defined. In order to reduce code redundancy, and to better encapsulate the prototype function visually, it has become a common practice to directly rewrite the prototype through an object literal containing all properties and methods, as shown in the following example:
function Person() {}
Person.prototype = {
name: 'XHS-rookies',
age: 18,
job: 'Software Engineer',
sayName() {
console.log(this.name)
},
}
In this example, Person.prototype
is set equal to a new object created by the object literal. The end result is the same, only one question: After this rewrite, Person.prototype
of constructor
property is not pointing Person
up. When a function is created, its prototype
constructor
attribute of this prototype will be automatically assigned a value. The above writing completely rewrites the default prototype
object, so its constructor
attribute also points to a completely different new object ( Object
constructor), and no longer points to the original constructor. Although the instanceof
operator can reliably return a value, we can no longer rely on the constructor
attribute to identify the type, as shown in the following example:
let friend = new Person()
console.log(friend instanceof Object) // true
console.log(friend instanceof Person) // true
console.log(friend.constructor == Person) // false
console.log(friend.constructor == Object) // true
Here, instanceof
still Object
and Person
return true
. But the constructor
attribute is now equal to Object
instead of Person
. If constructor
is important, you can specifically set its value when rewriting the prototype object as follows:
function Person() {}
Person.prototype = {
constructor: Person,
name: 'XHS-rookies',
age: 18,
job: 'Software Engineer',
sayName() {
console.log(this.name)
},
}
This time the code specifically includes the constructor
attribute and sets it to Person
to ensure that this attribute still contains the appropriate value. But be aware that restoring the constructor
attribute in this way will create an attribute with [[Enumerable]]
as true
. The native constructor
attribute is non-enumerable by default. Therefore, if you are using the JavaScript
ECMAScript
, you may use the Object.defineProperty()
method to define the constructor
attribute:
function Person() {}
Person.prototype = {
name: 'XHS-rookies',
age: 18,
job: 'Software Engineer',
sayName() {
console.log(this.name)
},
}
// 恢复 constructor 属性
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person,
})
class
The previous sections explained in depth how to use ECMAScript 5
the features of 060ab5d0ce8630 to simulate behavior similar to the class ( class-like
). It is not difficult to see that various strategies have their own problems and corresponding compromises. Because of this, the code to implement inheritance is also very verbose and confusing.
To solve these problems, ECMAScript 6
newly introduced class
keyword has the ability to formally define the class. The class ( class
) is a ECMAScript
, so you may not get used to it when you first touch it. Although the ECMAScript 6
class seems to support formal object-oriented programming on the surface, in fact it still uses the concept of prototype and constructor behind it.
Class definition
Similar to function types, there are two main ways to define classes: class declarations and class expressions. Both methods use the class
keyword to increase the brackets:
// 类声明
class Person {}
// 类表达式
const Animal = class {}
Similar to function expressions, class expressions cannot be referenced before they are evaluated. However, unlike function definitions, although function declarations can be promoted, class definitions cannot:
console.log(FunctionExpression) // undefined
var FunctionExpression = function () {}
console.log(FunctionExpression) // function() {}
console.log(FunctionDeclaration) // FunctionDeclaration() {}
function FunctionDeclaration() {}
console.log(FunctionDeclaration) // FunctionDeclaration() {}
console.log(ClassExpression) // undefined
var ClassExpression = class {}
console.log(ClassExpression) // class {}
console.log(ClassDeclaration) // ReferenceError: ClassDeclaration is not defined
class ClassDeclaration {}
console.log(ClassDeclaration) // class ClassDeclaration {}
Another difference from function declarations is that functions are restricted by function scope, while classes are restricted by block scope:
{
function FunctionDeclaration() {}
class ClassDeclaration {}
}
console.log(FunctionDeclaration) // FunctionDeclaration() {}
console.log(ClassDeclaration) // ReferenceError: ClassDeclaration is not defined
The composition of the class
Classes can contain constructor methods, instance methods, get functions, set functions, and static class methods, but these are not required. Empty class definitions are still valid. By default, the code in the class definition is executed in strict mode.
Like Constructor, most programming style recommendations are the first letter of the class name should be capitalized, in order to distinguish it from examples created by it (for example, by class Foo {}
create an instance foo
):
// 空类定义,有效
class Foo {}
// 有构造函数的类,有效
class Bar {
constructor() {}
}
// 有获取函数的类,有效
class Baz {
get myBaz() {}
}
// 有静态方法的类,有效
class Qux {
static myQux() {}
}
The name of the class expression is optional. After assigning the class expression to the variable, the name string of the class expression name
But this identifier cannot be accessed outside the scope of the class expression.
let Person = class PersonName {
identify() {
console.log(Person.name, PersonName.name)
}
}
let p = new Person()
p.identify() // PersonName PersonName
console.log(Person.name) // PersonName
console.log(PersonName) // ReferenceError: PersonName is not defined
Class constructor
constructor
keyword is used to create the constructor of the class inside the class definition block. The method name constructor
tells the interpreter that this function should be called when a new instance of the class is created new
The definition of the constructor is not necessary. Not defining the constructor is equivalent to defining the constructor as an empty function.
instantiates
The operation of instantiating Person using the new
operator is equivalent to calling its constructor new
The only perceivable difference is that the JavaScript
interpreter knows that using new
and the class means that it should be instantiated constructor
Using new
call the constructor of the class will perform the following operations.
(1) Create a new object in memory.
(2) The [[Prototype]]
pointer inside this new object is assigned to the prototype
attribute of the constructor.
(3) this
inside the constructor is assigned to this new object (that is, this
points to the new object).
(4) Execute the code inside the constructor (add attributes to the new object).
(5) If the constructor returns a non-empty object, return that object; otherwise, return the new object just created.
Consider the following example:
class Animal {}
class Person {
constructor() {
console.log('person ctor')
}
}
class Vegetable {
constructor() {
this.color = 'orange'
}
}
let a = new Animal()
let p = new Person() // person ctor
let v = new Vegetable()
console.log(v.color) // orange
The parameters passed in when the class is instantiated will be used as the parameters of the constructor. If no parameters are required, the parentheses after the class name are also optional:
class Person {
constructor(name) {
console.log(arguments.length)
this.name = name || null
}
}
let p1 = new Person() // 0
console.log(p1.name) // null
let p2 = new Person() // 0
console.log(p2.name) // null
let p3 = new Person('Jake') // 1
console.log(p3.name) // Jake
By default, the class constructor will return the this
object after execution. The object returned by the constructor will be used as an instantiated object. If there is nothing to refer to the newly created this
object, then this object will be destroyed. However, if the returned this
object, but other objects, then this object will not be instanceof
operator, because the prototype pointer of this object has not been modified.
class Person {
constructor(override) {
this.foo = 'foo'
if (override) {
return {
bar: 'bar',
}
}
}
}
let p1 = new Person(),
p2 = new Person(true)
console.log(p1) // Person{ foo: 'foo' }
console.log(p1 instanceof Person) // true
console.log(p2) // { bar: 'bar' }
console.log(p2 instanceof Person) // false
The main difference between a class constructor and a constructor is that the new
operator must be used to call the class constructor. If the ordinary constructor does not use new
call, then the global this
(usually window
) will be used as the internal object. new
when calling the class constructor, an error will be thrown:
function Person() {}
class Animal {}
// 把 window 作为 this 来构建实例
let p = Person()
let a = Animal()
// TypeError: class constructor Animal cannot be invoked without 'new'
There is nothing special about the class constructor. After instantiation, it will become a normal instance method (but as a class constructor, it still needs to be new
). Therefore, it can be referenced on the instance after instantiation:
class Person {}
// 使用类创建一个新实例
let p1 = new Person()
p1.constructor()
// TypeError: Class constructor Person cannot be invoked without 'new'
// 使用对类构造函数的引用创建一个新实例
let p2 = new p1.constructor()
Instances, prototypes, and class members
The syntax of the class can be very convenient to define the members that should exist on the instance, the members that should exist on the prototype, and the members that should exist on the class itself.
1. Instance member
Every time the new
, the class constructor is executed. Inside this function, you can add "own" attributes this
There are no restrictions on what attributes to add. In addition, after the constructor is executed, you can still add new members to the instance.
Each instance corresponds to a unique member object, which means that all members will not be shared on the prototype:
class Person {
constructor() {
// 这个例子先使用对象包装类型定义一个字符串
// 为的是在下面测试两个对象的相等性
this.name = new String('xhs-rookies')
this.sayName = () => console.log(this.name)
this.nicknames = ['xhs-rookies', 'J-Dog']
}
}
let p1 = new Person(),
p2 = new Person()
p1.sayName() // xhs-rookies
p2.sayName() // xhs-rookies
console.log(p1.name === p2.name) // false
console.log(p1.sayName === p2.sayName) // false
console.log(p1.nicknames === p2.nicknames) // false
p1.name = p1.nicknames[0]
p2.name = p2.nicknames[1]
p1.sayName() // xhs-rookies
p2.sayName() // J-Dog
2. Prototype method and accessor
In order to share methods between instances, the class definition syntax uses the methods defined in the class block as prototype methods.
class Person {
constructor() {
// 添加到 this 的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance')
}
// 在类块中定义的所有内容都会定义在类的原型上
locate() {
console.log('prototype')
}
}
let p = new Person()
p.locate() // instance
Person.prototype.locate() // prototype
You can define methods in the class constructor or class block, but you cannot add primitive values or objects as member data to the prototype in the class block:
class Person {
name: 'xhs-rookies'
}
// Uncaught SyntaxError: Unexpected token
Class methods are equivalent to object properties, so you can use strings, symbols, or calculated values as keys:
const symbolKey = Symbol('symbolKey')
class Person {
stringKey() {
console.log('invoked stringKey')
}
[symbolKey]() {
console.log('invoked symbolKey')
}
['computed' + 'Key']() {
console.log('invoked computedKey')
}
}
let p = new Person()
p.stringKey() // invoked stringKey
p[symbolKey]() // invoked symbolKey
p.computedKey() // invoked computedKey
The class definition also supports getting and setting accessors. The syntax and behavior are the same as ordinary objects:
class Person {
set name(newName) {
this.name_ = newName
}
get name() {
return this.name_
}
}
let p = new Person()
p.name = 'xhs-rookies'
console.log(p.name) // xhs-rookies
3. Static class method
You can define static methods on the class. These methods are generally used to perform operations that are not specific to an instance and do not require an instance of the class to exist. Similar to prototype members, there can only be one static member per class. Static class members use the static
keyword as a prefix in the class definition. Among static members, this
refers to the class itself. All other conventions are the same as prototype members:
class Person {
constructor() {
// 添加到 this 的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance', this)
}
// 定义在类的原型对象上
locate() {
console.log('prototype', this)
}
// 定义在类本身上
static locate() {
console.log('class', this)
}
}
let p = new Person()
p.locate() // instance, Person {}
Person.prototype.locate() // prototype, {constructor: ... }
Person.locate() // class, class Person {}
Static class methods are very suitable as instance factories:
class Person {
constructor(age) {
this.age_ = age
}
sayAge() {
console.log(this.age_)
}
static create() {
// 使用随机年龄创建并返回一个 Person 实例
return new Person(Math.floor(Math.random() * 100))
}
}
console.log(Person.create()) // Person { age_: ... }
4. Non-function prototypes and class members
Although the class definition does not explicitly support adding member data to the prototype or class, it can be added manually outside the class definition:
class Person {
sayName() {
console.log(`${Person.greeting} ${this.name}`)
}
}
// 在类上定义数据成员
Person.greeting = 'My name is'
// 在原型上定义数据成员
Person.prototype.name = 'xhs-rookies'
let p = new Person()
p.sayName() // My name is xhs-rookies
Note that the class definition does not explicitly support the addition of data members, because adding variable (modifiable) data members on shared targets (prototypes and classes) is an anti-pattern. Generally speaking, the object instance should havethis
alone (note that the use ofthis
in different situations will be slightly different. For detailedthis
learning, please see this-MDN ).
5. Iterator and generator methods
The class definition syntax supports the definition of generator methods on the prototype and the class itself:
class Person {
// 在原型上定义生成器方法
*createNicknameIterator() {
yield 'xhs-Jack'
yield 'xhs-Jake'
yield 'xhs-J-Dog'
}
// 在类上定义生成器方法
static *createJobIterator() {
yield 'xhs-Butcher'
yield 'xhs-Baker'
yield 'xhs-Candlestick maker'
}
}
let jobIter = Person.createJobIterator()
console.log(jobIter.next().value) // xhs-Butcher
console.log(jobIter.next().value) // xhs-Baker
console.log(jobIter.next().value) // xhs-Candlestick maker
let p = new Person()
let nicknameIter = p.createNicknameIterator()
console.log(nicknameIter.next().value) // xhs-Jack
console.log(nicknameIter.next().value) // xhs-Jake
console.log(nicknameIter.next().value) // xhs-J-Dog
Because the generator method is supported, you can turn the class instance into an iterable object by adding a default iterator:
class Person {
constructor() {
this.nicknames = ['xhs-Jack', 'xhs-Jake', 'xhs-J-Dog']
}
*[Symbol.iterator]() {
yield* this.nicknames.entries()
}
}
let p = new Person()
for (let [idx, nickname] of p) {
console.log(nickname)
}
// xhs-Jack
// xhs-Jake
// xhs-J-Dog
//也可以只返回迭代器实例:
class Person {
constructor() {
this.nicknames = ['xhs-Jack', 'xhs-Jake', 'xhs-J-Dog']
}
[Symbol.iterator]() {
return this.nicknames.entries()
}
}
let p = new Person()
for (let [idx, nickname] of p) {
console.log(nickname)
}
// xhs-Jack
// xhs-Jake
// xhs-J-Dog
Object destructuring assignment
ECMAScript 6
adds new object deconstruction syntax, which can use nested data to implement one or more assignment operations in a statement. Simply put, object deconstruction is to use the structure that matches the object to realize the assignment of object properties. The following example shows two equivalent pieces of code, first without using object destructuring:
// 不使用对象解构
let person = {
name: 'xhs-Matt',
age: 18,
}
let personName = person.name,
personAge = person.age
console.log(personName) // xhs-Matt
console.log(personAge) // 18
Then, it uses object destructuring:
// 使用对象解构
let person = {
name: 'xhs-Matt',
age: 18,
}
let { name: personName, age: personAge } = person
console.log(personName) // xhs-Matt
console.log(personAge) // 18
Using destructuring, you can declare multiple variables in a structure similar to an object literal, and perform multiple assignment operations at the same time. If you want the variable to use the name of the attribute directly, you can use a shorthand syntax, such as:
let person = {
name: 'xhs-Matt',
age: 18,
}
let { name, age } = person
console.log(name) // xhs-Matt
console.log(age) // 18
Deconstruction is unsuccessful and some default values can be specified for object deconstruction. These details can be found in our Deconstruction Assignment article. We don't need to go into more details in the object.
inherit
Earlier in this chapter, I spent a lot of space discussing how to use ES5
to implement inheritance. ECMAScript 6
of the most outstanding new features of 060ab5d0ce8bea is the native support for the class inheritance mechanism. Although the new syntax is used for class inheritance, the prototype chain is still used behind the scenes.
Inheritance basis
ES6
class supports single inheritance. Using the extends
keyword, you can inherit any [[Construct]]
and a prototype. To a large extent, this means that you can inherit not only a class, but also ordinary constructors (to maintain backward compatibility):
class Vehicle {}
// 继承类
class Bus extends Vehicle {}
let b = new Bus()
console.log(b instanceof Bus) // true
console.log(b instanceof Vehicle) // true
function Person() {}
// 继承普通构造函数
class Engineer extends Person {}
let e = new Engineer()
console.log(e instanceof Engineer) // true
console.log(e instanceof Person) // true
Derived classes will access the methods defined on the class and prototype through the prototype chain. this
will reflect the instance or class that calls the corresponding method:
class Vehicle {
identifyPrototype(id) {
console.log(id, this)
}
static identifyClass(id) {
console.log(id, this)
}
}
class Bus extends Vehicle {}
let v = new Vehicle()
let b = new Bus()
b.identifyPrototype('bus') // bus, Bus {}
v.identifyPrototype('vehicle') // vehicle, Vehicle {}
Bus.identifyClass('bus') // bus, class Bus {}
Vehicle.identifyClass('vehicle') // vehicle, class Vehicle {}
Note: extends
keywords can also be used in class expressions, so let Bar = class extends Foo {}
is a valid syntax.
Constructor, HomeObject and super()
Methods of derived classes can refer to their prototypes super
This keyword can only be used in derived classes and is limited to class constructors, instance methods, and static methods. super
in the class constructor to call the parent class constructor.
class Vehicle {
constructor() {
this.hasEngine = true
}
}
class Bus extends Vehicle {
constructor() {
// 不要在调用 super()之前引用 this,否则会抛出 ReferenceError
super() // 相当于 super.constructor()
console.log(this instanceof Vehicle) // true
console.log(this) // Bus { hasEngine: true }
}
}
new Bus()
In the static method, you can call the static method defined on the inherited class super
class Vehicle {
static identify() {
console.log('vehicle')
}
}
class Bus extends Vehicle {
static identify() {
super.identify()
}
}
Bus.identify() // vehicle
Note: ES6
to the class constructor and a method of adding an internal static characteristics [[HomeObject]]
, this feature is a pointer pointing to the object of the method is defined. This pointer is automatically assigned and can only be accessed inside the JavaScript engine. super
will always be defined as the prototype [[HomeObject]]
There are several issues to pay attention to when using super
super
can only be used in derived class constructors and static methods.
class Vehicle {
constructor() {
super()
// SyntaxError: 'super' keyword unexpected
}
}
- You cannot quote the
super
keyword alone, either use it to call the constructor or use it to reference static methods.
class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(super)
// SyntaxError: 'super' keyword unexpected here
}
}
- Calling
super()
will call the parent class constructor and assign the returned instance tothis
.
class Vehicle {}
class Bus extends Vehicle {
constructor() {
super()
console.log(this instanceof Vehicle)
}
}
new Bus() // true
super()
behaves like calling the constructor. If you need to pass parameters to the parent class constructor, you need to pass it in manually.
class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate
}
}
class Bus extends Vehicle {
constructor(licensePlate) {
super(licensePlate)
}
}
console.log(new Bus('1337H4X')) // Bus { licensePlate: '1337H4X' }
- If the class constructor is not defined,
super()
will be called when the derived class is instantiated, and all the parameters passed to the derived class will be passed in.
class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate
}
}
class Bus extends Vehicle {}
console.log(new Bus('1337H4X')) // Bus { licensePlate: '1337H4X' }
- In the class constructor, not during the call
super()
before referencesthis
.
class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(this)
}
}
new Bus()
// ReferenceError: Must call super constructor in derived class
// before accessing 'this' or returning from derived constructor
- If the constructor is explicitly defined in the derived class, then either
super()
must be called in it, or an object must be returned in it.
class Vehicle {}
class Car extends Vehicle {}
class Bus extends Vehicle {
constructor() {
super()
}
}
class Van extends Vehicle {
constructor() {
return {}
}
}
console.log(new Car()) // Car {}
console.log(new Bus()) // Bus {}
console.log(new Van()) // {}
Packaging object
Original value packaging type
In order to facilitate the operation of the original value, ECMAScript
provides three special reference types: Boolean
, Number
and String
. These types have the same characteristics as the other reference types introduced in this chapter, but they also have special behaviors corresponding to their primitive types. Whenever a method or attribute of an original value is used, an object of the corresponding original packaging type will be created in the background, thereby exposing various methods of manipulating the original value. Consider the following example:
let s1 = 'xhs-rookies'
let s2 = s1.substring(2)
Here, s1
is a variable containing a string, which is a primitive value. Then in the second row s1
on call substring()
method, and the results stored in s2
in. We know that the original value itself is not an object, so logically there should be no method. In fact, this example did run as expected. This is because a lot of processing is done in the background to achieve the above operations. Specifically, when the second line accesses s1
, it is accessed in read mode, that is, the value saved by the variable is read from the memory. Whenever the string value is accessed in read mode, the background will perform the following 3 steps:
(1) Create an String
type 060ab5d0ce8fee;
(2) Call a specific method on the instance;
(3) Destroy the instance.
You can imagine these 3 steps as executing the following 3 lines of ECMAScript
code:
let s1 = new String('xhs-rookies')
let s2 = s1.substring(2)
s1 = null
This behavior allows the original value to have the behavior of the object. For Boolean values and numerical values, the above 3 steps will also happen in the background, but the Boolean
and Number
packaging types are used. The main difference between a reference type and a primitive value packaging type is the life cycle of the object. After new
, the resulting instance will be destroyed when it leaves the scope, and the automatically created original value wrapper object only exists during the execution of the line of code that accesses it. This means that properties and methods cannot be added to the original value at runtime. For example, the following example:
let s1 = 'xhs-rookies'
s1.color = 'red'
console.log(s1.color) // undefined
The second line of code here tries to add a color
attribute to the string s1. However, when the third line of code accesses the color
attribute, it disappears. String
object will be temporarily created when the second line of code is run, and when the third line of code is executed, this object has been destroyed. In fact, the third line of code here creates its own String
object, but this object does not have the color
attribute.
You can explicitly use the Boolean
, Number
and String
constructors to create primitive value wrapping objects. However, you should do this when really necessary, otherwise it is easy for developers to wonder whether they are original values or reference values. typeof
on an instance of the original value wrapping type will return "object"
, and all original value wrapping objects will be converted to the Boolean value true
.
In addition, the Object
constructor function as a factory method can return an instance of the corresponding original value packaging type according to the type of the incoming value. such as:
let obj = new Object('xhs-rookies')
console.log(obj instanceof String) // true
If the Object
is a string, an instance of String
If it is a number, an instance of Number
Boolean value will get an instance of Boolean
Note that using new
call the constructor of the original value packaging type is not the same as calling the transformation function of the same name. E.g:
let value = '18'
let number = Number(value) // 转型函数
console.log(typeof number) // "number"
let obj = new Number(value) // 构造函数
console.log(typeof obj) // "object"
In this example, the variable number
stores an original value of 25, and the variable obj
stores an instance of Number
Although it is not recommended to explicitly create instances of primitive value packaging types, they are important for the functionality of manipulating primitive values. Each primitive value packaging type has a corresponding set of methods to facilitate data manipulation.
Self-test
One: All objects have prototypes.
- A: Yes
- B: wrong
2: Which of the following will have side effects on the object person?
const person = {
name: 'Lydia Hallie',
address: {
street: '100 Main St',
},
}
Object.freeze(person)
- A:
person.name = "Evan Bacon"
- B:
delete person.address
- C:
person.address.street = "101 Main St"
- D:
person.pet = { name: "Mara" }
3: Which constructor can be used to successfully inherit the Dog
class?
class Dog {
constructor(name) {
this.name = name
}
}
class Labrador extends Dog {
// 1
constructor(name, size) {
this.size = size
}
// 2
constructor(name, size) {
super(name)
this.size = size
}
// 3
constructor(size) {
super(name)
this.size = size
}
// 4
constructor(name, size) {
this.name = name
this.size = size
}
}
- A: 1
- B: 2
- C: 3
- D: 4
Problem analysis
One,
Answer:B
Except for basic objects ( base object
), all objects have prototypes. Basic objects can access some methods and properties, such as .toString
. This is why you can use the built-in JavaScript
method! All these methods are available on the prototype. Although JavaScript
cannot find these methods directly on the object, JavaScript
will find them along the prototype chain for your convenience.
two,
Answer:C
Use method Object.freeze
to freeze an object. You cannot add, modify, or delete attributes.
However, it only shallow freezes the object, which means that only the direct properties of the object are frozen. If the attribute is another object
address
in the case, the address
in 060ab5d0ce93a6 is not frozen and can still be modified.
three,
Answer:B
In the subclass, the this
keyword cannot be accessed before super
If you do, it will throw a ReferenceError:1
and 4 will cause a reference error.
Using the super
keyword, you need to call the constructor of the parent class with the given parameters. Parent class constructor receives name
parameters, so we need to name
pass to super
.
Labrador
class receives two parameters. The name
parameter is because it inherits Dog
, and size
Labrador
class. They all need to be passed to Labrador
, so use constructor 2 to complete it correctly.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。