3
头图
"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 have this alone (note that the use of this in different situations will be slightly different. For detailed this 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 to this .
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 references this .
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.


小和山的菜鸟们
377 声望2.1k 粉丝

每日进步的菜鸟,分享前端学习手册,和有心学习前端技术的小伙伴们互相探讨,一同成长。