As the total notes are nearly 4W words, they are divided into the first and second parts. The second part is planned to be updated in the next two days. If you think it’s a good one, please give it a star. If you want to read all the notes in the first and second parts, please click here to read the full text

Reading the book “In-depth Understanding of ES6”, sorting out notes (PART 1)

preface

Before the ECMAScript6 standard was finalized, there were already some experimental transpilers, such as Google’s Traceur, that could convert code from ECMAScript6 to ECMAScript5. But most of them have very limited functionality or are difficult to plug into existing JavaScript build pipes. But then came the new translator, 6TO5, that changed all that. It was easy to install, integrated well with existing tools, and generated readable code, so it spread like wildfire, and 6to5 became known as Babel.

Evolution of ECMAScript6

The core language features of JavaScript are defined in the standard ECMA-262. The language defined in this standard is called ECMAScript, which is a subset of JavaScript.Copy the code

Evolution:

  • The stagnantA gradual riseAjaxSet the dynamicWebCited in the New Era, while from the third edition of 1999ECMA-262Since its release,JavaScriptBut nothing has changed.
  • The turning point: in 2007,TC-39The committee put together a large number of draft specificationsECMAScript4The new language features include modules, classes, class inheritance, private object members, and many other features.
  • differences: however,TC-39Intra-organization pairECMAScript4The draft was deeply divisive, with some members arguing that too many new features should not be included in the fourth version of the standard at once, and technical leaders from Yahoo, Google and Microsoft co-submitting oneECMAScript3.1Draft as the next generationECMAScriptIn which this is only a small incremental change to an existing standard. Behavior is more focused on optimizing attribute features and supporting nativeJSONAnd adding new methods to existing objects.
  • ECMAScript4, never published: in 2008,JavaScriptThe founder ofBrendan EichannouncedTC-39On the one hand, the committee will make reasonable progressECMAScript3.1Standardization, on the other hand, will be temporaryECMAScript4Most of the syntax and feature changes proposed in the standard have been shelved.
  • ECMAScript5: StandardizedECMAScript3.1In the end asECMA-262The fifth edition was officially released in 2009 and was namedECMAScript5.
  • ECMAScript6In:ECMAScript5After the release,TC-39The commission was frozen in 2013ECMAScript6No new functionality will be added. In 2013,ECMAScript6The draft was released after 12 months of discussion and feedback. In 2015,ECMAScript6Officially released and named asECMAScript 2015.

Block-level scoped binding

The variable declaration mechanism in JavaScript has been confusing in the past. Most C-like languages create variables as well as declare them. In JavaScript, when to create a variable depends on how the variable is declared.Copy the code

Var declarations and variable promotion mechanisms

Q: What is the promotion mechanism (reactive)? A: Variables declared in function scope or global scope with the var keyword are treated as if they were declared at the top of the current scope, no matter where they are actually declared. This is what we call the promotion mechanism. The following example code illustrates this promotion mechanism:

function getValue (condition) {
  if (condition) {
    var value = 'value'
    return value
  } else {
    // We can access value, but the value is undefined
    console.log(value)
    return null
  }
}
getValue(false) / / output is undefined
Copy the code

As you can see in the above code, value is still accessible when we pass false because the JavaScript engine changes the code for the above function to the following form during precompilation:

function getValue (condition) {
  var value
  if (condition) {
    value = 'value'
    return value
  } else {
    console.log(value)
    return null}}Copy the code

From the example above, we can see that the declaration of value is promoted to the top of the function scope, while the initialization remains in place. Because value is only declared and not assigned, this code prints undefined.

Block-level statement

Block-level declarations are used to declare variables that are accessible outside of the specified scope. Block-level scopes exist inside functions and within blocks.

Let the statement:

  • letDeclarations andvarThe use of declarations is basically the same.
  • letDeclared variables are not promoted.
  • letAn error will be reported if an existing variable cannot be declared repeatedly in the same scope.
  • letThe declared variable scope exists only in the current block, created when the program enters the block at the beginning and destroyed when the program exits the block.

According to the rules declared in the LET, the above code would look like this:

function getValue (condition) {
  if (condition) {
    // The variable value exists only in this block.
    let value = 'value'
    return value
  } else {
    // Cannot access the value variable
    console.log(value)
    return null}}Copy the code

Const declarations: Most of the time, const and let declarations are the same. The only essential difference is that const is used to declare constants, and its value cannot be modified, which means that const declarations must be initialized.

const MAX_ITEMS = 30
/ / an error
MAX_ITEMS = 50
Copy the code
When we say that a const variable is immutable, there are two types: value type: The value of a variable cannot be changed. Reference type: The address of a variable cannot be changed, but its value can be changed.Copy the code
const num = 23
const arr = [1.2.3.4]
const obj = {
  name: 'why'.age: 23
}

/ / an error
num = 25

/ / is not an error
arr[0] = 11
obj.age = 32
console.log(arr) // [11, 2, 3, 4]
console.log(obj) // { name: 'why', age: 32 }

/ / an error
arr = [4.3.2.1]
Copy the code

Temporary dead zone

Since variables declared by let and const do not receive declaration promotion, any operation that accesses the variables before they are declared (even typeof) raises an error:

if (condition) {
  / / an error
  console.log(typeof value)
  let value = 'value'
}
Copy the code

Q: Why is an error reported? A: When the JavaScript engine scans the code for variable declarations, it either promotes them to the top of the scope (var declarations) or places them in TDZ(temporary dead zone) (let and const declarations). Accessing a variable in the TDZ will trigger an error, and the variable will only be removed from the TDZ after the variable declaration statement is executed before it can be accessed normally.

Global block-scoped binding

We all know that if we declare a variable in the global scope via var, the variable will be mounted on the global object window:

var name = 'why'
console.log(window.name) // why
Copy the code

But if we use let or const to create a new variable in the global scope, it will not be added to the window.

const name = 'why'
console.log('name' in window) // false
Copy the code

Evolution of block-level binding best practices

In the early days of ES6, it was widely believed that let should be used instead of VAR by default, because for developers, let was actually the same as var they wanted, and a straightforward substitution was logical. Over time, however, another practice has become more common: use const by default, and use let declarations only when you are certain that the value of a variable will need to be changed later, because most variables should not change after initialization, and unexpected changes in variable values are the source of many bugs.

string

The unicode and re sections of this section are not cleaned up.

Module literal

Template literals are syntactic sugar that extends ECMAScript’s basic syntax, providing a DSL that generates, queries, and manipulates content from other languages, protected from XSS injection attacks, SQL injection, and more.

Before ES6, JavaScript was missing a number of features:

  • Multi-line string: A formal concept of a multi-line string.
  • Basic string formatting: The ability to embed the value of a variable into a string.
  • HTML escapingTo:HTMLThe ability to insert a safely converted string.

In ECMAScript 6, template literals are used to address these issues. One of the simplest uses of template literals is as follows:

const message = `hello,world! `
console.log(message)        // hello,world!
console.log(typeof message) // string
Copy the code

One thing to note is that if we need to use a backapostrophe in a string, we need to use \ to escape it, as follows:

const message = `\`hello\`,world! `
console.log(message) // `hello`,world!
Copy the code

Multiline string

For as long as JavaScript has existed, developers have been trying to create multiline strings. Here’s how it was done before ES6: Adding \ to the beginning of a new line of a string continues the previous line of code. You can use this bug to create multiline strings.

const message = 'hello\ ,world! '
console.log(message) // hello,world
Copy the code

After ES6, we can use template literals to create multi-line strings with line breaks as follows:

In template literals, all whitespace characters in backapostrophes are part of the string.Copy the code
const message = `hello ,world! `
console.log(message) // hello
                     // ,world!
Copy the code

String placeholder

The biggest difference between template literals and regular strings is the placeholder function in template strings, which can contain any valid JavaScript expression, such as a variable, an operation, a function call, or even another template literal.

const age = 23
const name = 'why'
const message = `Hello ${name}, you are ${age}years old! `
console.log(message) // Hello why, you are 23 years old!
Copy the code

Template literals nested:

const name = 'why'
const message = `Hello, The ${`my name is ${name}`}. `
console.log(message) // Hello, my name is why.
Copy the code

The label template

A tag is a string marked before the first backapostrophe of a template literal. Each template tag can perform a conversion on the template literal and return the final string value.

// Tag is' Hello world! 'Template literal tag template
const message = tag`Hello world! `
Copy the code

A tag can be a function, and tag functions usually use the undefined parameter property to define placeholders to simplify data processing, as in the following example:

function tag(literals, ... substitutions) {
  // Returns a string
}
const name = 'why'
const age = 23
const message = tag`${name} is ${age}years old! `
Copy the code

Literals is an array that contains:

  • Blank string before the first placeholder: “”.
  • String between the first and second placeholders: “is”.
  • String after second placeholder: “Years old!”

Substitutions is also an array:

  • The first entry of the array is:name, namely:why.
  • The second entry of the array is:age, namely:23.

Through the above rules, we can find:

  • literals[0]Always represents the beginning of a string.
  • literalsBetter thansubstitutionsOne more.

Using the above pattern, we can interlace the arrays literals and substitutions to recreate a string to simulate the default behavior of template literals like this:

function tag(literals, ... substitutions) {
  let result = ' '
  for (let i = 0; i< substitutions.length; i++) {
    result += literals[i]
    result += substitutions[i]
  }

  // merge the last one
  result += literals[literals.length - 1]
  return result
}
const name = 'why'
const age = 23
const message = tag`${name} is ${age}years old! `
console.log(message) // why is 23 years old!
Copy the code

Native string information

The template tag provides access to the native string before the string escape is converted to an equivalent string.

const message1 = `Hello\nworld`
const message2 = String.raw`Hello\nworld`
console.log(message1) // Hello
                      // world
console.log(message2) // Hello\nworld
Copy the code

function

Parameter default value

Before ES6, you might create functions and provide default values for parameters in this mode:

function makeRequest (url, timeout, callback) {
  timeout = timeout || 2000
  callback = callback || function () {}}Copy the code

Code analysis: In the above example, timeout and callback are optional arguments, and if they are not passed they are given default values using logic or operators. However, this approach also has some drawbacks. If we want to pass timeout a value of 0, we will end up assigning timeout 2000 because of the existence of the or logical operator, although this value is legal.

In this case, we should rewrite the above example in a safer way (using Typeof) :

function makeRequest (url, timeout, callback) {
  timeout = typeoftimeout ! = ='undefined' ? timeout : 2000
  callback = typeofcallback ! = ='undefined' ? callback : function () {}}Copy the code

Code analysis: Although the above method is more secure, we still need to write more code to implement this very basic operation. In response, ES6 simplifies the process of providing default values for parameters, as follows:

For default arguments, the default is used unless undefined is passed or actively passed (if null is passed, it is a valid argument and the default is not used).Copy the code
function makeRequest (url, timeout = 2000, callback = function () {}) {
  // todo
}
// Use both timeout and callback defaults
makeRequest('https://www.taobao.com')
// Use the default callback value
makeRequest('https://www.taobao.com'.500)
// Do not use the default values
makeRequest('https://www.taobao.com'.500.function (res) = >{
  console.log(res)
})
Copy the code

Effects of parameter default values on arguments objects

In ES5 non-strict mode, if you change the values of arguments, the values of those arguments are synchronized to the arguments object as follows:

function mixArgs(first, second) {
  console.log(arguments[0]) // A
  console.log(arguments[1]) // B
  first = 'a'
  second = 'b'
  console.log(arguments[0]) // a
  console.log(arguments[1]) // b
}
mixArgs('A'.'B')
Copy the code

In ES5 strict mode, the value of the modified argument is no longer reflected in the arguments object as follows:

function mixArgs(first, second) {
  'use strict'
  console.log(arguments[0]) // A
  console.log(arguments[1]) // B
  first = 'a'
  second = 'b'
  console.log(arguments[0]) // A
  console.log(arguments[1]) // B
}
mixArgs('A'.'B')
Copy the code

For parameters that use ES6 defaults, the arguments object always behaves the same as ES5 strict mode, whether or not it is currently in strict mode, i.e. : Arguments is always equal to the value originally passed and does not change as arguments change. Arguments can always be restored to their original value using the arguments object as follows:

function mixArgs(first, second = 'B') {
  console.log(arguments.length) / / 1
  console.log(arguments[0])      // A
  console.log(arguments[1])      // undefined
  first = 'a'
  second = 'b'
  console.log(arguments[0])      // A
  console.log(arguments[1])      // undefined
}
The // arguments object is always equal to the value passed, and the parameter default is not reflected in arguments
mixArgs('A')
Copy the code

Default parameter expression

The default value of a function parameter, in addition to being the default value of the original value, can also be an expression, i.e., a variable. The function call is also valid.

function getValue () {
  return 5
}
function add (first, second = getValue()) {
  return first + second
}
console.log(add(1.1)) / / 2
console.log(add(1))    / / 6
Copy the code

Code analysis: when we first call the add(1,1) function, getValue is not called because the parameter defaults are not used. The getValue function is called only when add(1) uses the default value of second.

Since the default argument is evaluated at function call time, we can use the predefined argument in a postdefined argument expression, that is, we can treat the predefined argument as a variable or function call parameter, as follows:

function getValue(value) {
  return value + 5
}
function add (first, second = first + 1) {
  return first + second
}
function reduce (first, second = getValue(first)) {
  return first - second
}
console.log(add(1))     / / 3
console.log(reduce(1))  / / - 5
Copy the code

Temporary dead zone for default parameters

As mentioned earlier, let and const have temporary dead zones, that is, attempts to access the let and const variables before they are declared trigger errors. For the same reason, there are temporary dead zones in function default arguments as follows:

function add (first = second, second) {
  return first + second
}
add(1.1)         / / 2
add(undefined.1) // Throw an error
Copy the code

Code analysis: the first time we call add(1,1), we pass two arguments, so the add function does not use the default values; On the second call to add(undefined, 1), we passed undefined to the first argument, which uses the default value. Second has not yet been initialized, so an error is thrown.

Uncertain parameters

JavaScript function syntax dictates that no matter how many named arguments a function has defined, there is no limit to the number of actual arguments passed in when called. In ES6, when fewer arguments are passed, the parameter defaults are used; When a larger number of arguments are passed, indefinite arguments are used for processing.

Let’s take the pick method in the underscore. Js library as an example:

The pick method is used to: given an object, return a copy of the object with the specified properties.Copy the code
function pick(object) {
  let result = Object.create(null)
  for (let i = 1, len = arguments.length; i < len; i++) {
    let item = arguments[i]
    result[item] = object[item]
  }
  return result
}
const book = {
  title: 'Deep understanding of ES6'.author: 'Nicholas'.year: 2016
}
console.log(pick(book, 'title'.'author')) // {title: 'ES6', author:' Nicholas'}
Copy the code

Code analysis:

  • It is not easy to see that this function can take an arbitrary number of arguments.
  • When looking for properties to copy, you have to start at index 1.

In ES6, indefinite arguments are provided, and we can use the indeterminate argument feature to rewrite the pick function:

function pick(object, ... keys) {
  let result = Object.create(null)
  for (let i = 0, len = keys.length; i < len; i++) {
    let item = keys[i]
    result[item] = object[item]
  }
  return result
}
const book = {
  title: 'Deep understanding of ES6'.author: 'Nicholas'.year: 2016
}
console.log(pick(book, 'title'.'author')) // {title: 'ES6', author:' Nicholas'}
Copy the code

Limit of indeterminate parameters

Indefinite parameters have several limitations when used:

  • A function can have at most one indeterminate argument.
  • The indefinite parameter must be placed at the end of all parameters.
  • Cannot be in object literalssetterIs used with indefinite parameters.
// An error is reported with only one indeterminate argument
function add(first, ... rest1, ... rest2) {
  console.log(arguments)}// Error, indefinite parameter can only be placed in the last parameter
function add(first, ... rest, three) {
  console.log(arguments)}// Error, indefinite arguments cannot be used in object literals' setter '
constobject = { set name (... val) {console.log(val)
  }
}
Copy the code

Expansion operator

Among the new features of ES6, the expansion operator is most similar to the indeterminable parameter, which allows us to specify multiple independent parameters and access them through a consolidated array. The expansion operator lets you specify an array, break them up and pass them to the function as separate arguments. Before ES6, if we used the math. Max function to compare the maximum value in an array, we would do something like this:

const arr = [4.10.5.6.32]
console.log(Math.max.apply(Math, arr)) / / 32
Copy the code

Code analysis: Before ES6 there was nothing wrong with using this approach, but the key is to borrow the Apply method and be very careful with this(the first parameter). In ES6 we had a much simpler way to do this:

const arr = [4.10.5.6.32]
console.log(Math.max(... arr))/ / 32
Copy the code

Function name attribute

Q: Why did ES6 introduce the name attribute for functions? A: There are multiple ways to define functions in JavaScript, so identifying functions can be a challenging task, and the widespread use of anonymous function expressions makes debugging difficult. To address these problems, ESCAScript 6 adds a name attribute to all functions.

Regular name attribute

In function declarations and anonymous function expressions, the name attribute of a function is relatively fixed:

function doSomething () {
  console.log('do something')}let doAnotherThing = function () {
  console.log('do another thing')}console.log(doSomething.name)    // doSomething
console.log(doAnotherThing.name) // doAnotherThing
Copy the code

A special case of the name attribute

Although it is easy to determine the names of function declarations and function expressions, there are some other cases that are not particularly easy to recognize:

  • Anonymous function expressions show the case where a function name is supplied: the function name itself has a higher weight than the variable to which the function itself is assigned.
  • Object literal: Takes the name of an object literal without providing a function name; If the function name is provided, the name is provided
  • Properties of thegetterandsetter: exists on an objectGet + attributethegetorsetMethods.
  • throughbindThrough:bindFunction creates the function,nameFor, you knowboundThe prefix
  • By constructor: the function name is fixed toanonymous.
let doSomething = function doSomethingElse () {
  console.log('do something else')}let person = {
  // there is a get firstName method on the person object
  get firstName () {
    return 'why'
  },
  sayName: function () {
    console.log('why')},sayAge: function sayNewAge () {
    console.log(23)}}console.log(doSomething.name)         // doSomethingElse
console.log(person.sayName.name)      // sayName
console.log(person.sayAge.name)       // sayNewAge
console.log(doSomething.bind().name)  // bound doSomethingElse
console.log(new Function().name)      // anonymous
Copy the code

Multiple uses of the function

Functions have multiple functions in JavaScript. They can be used in conjunction with new. The value this inside the function points to a new object, and the function returns the new object as follows:

function Person (name) {
  this.name = name
}
const person = new Person('why')
console.log(person.toString()) // [object Object]
Copy the code

In ES6, functions have two different internal methods, which are:

Functions with [[Construct]] methods are called constructors, but not all functions have [[Construct]] methods, e.g., arrow functions.Copy the code
  • [[Call]]: If not passednewKeyword to call the function, then execute[[Call]]Function, which directly executes the function body in the code.
  • [[Construct]]: when throughnewWhen a keyword calls a function, it executes[[Construct]]Function, which is responsible for creating a new object and then executing the function body, willthisBind to the instance.

Before ES6, the most popular way to determine if a function is called with a new keyword was to use instanceof, for example:

function Person (name) {
  if (this instanceof Person) {
    this.name = name
  } else {
    throw new Error('Person must be called with the new keyword')}}const person = new Person('why')
const notPerson = Person('why') // Throw an error
Copy the code

Code analysis: In this code, the value of this is first evaluated to see if it is an instance of Person. If it is, execution continues. If it is not, an error is thrown. This is usually true, but not always true. There is a way to bind this to an instance of Person without relying on the new keyword:

function Person (name) {
  if (this instanceof Person) {
    this.name = name
  } else {
    throw new Error('Person must be called with the new keyword')}}const person = new Person('why')
const notPerson = Person.call(person, 'why') // No error, valid
Copy the code

To solve the problem of determining whether a function is called with the new keyword, ES6 introduces the new.target meta-attribute. A: Meta-attributes are non-object attributes that provide supplementary information about non-object targets. When the [[Construct]] method of a function is called, new.target is assigned to the target of the new operator, usually the instance of the newly created object, which is the constructor of this in the function body. If the [[Call]] method is called, the value of new.target is undefined.

New. Target (); new. Target ();

Using new.target outside of a function is a syntax error.Copy the code
function Person (name) {
  if (typeof new.target ! = ='undefined') {
    this.name = name
  } else {
    throw new Error('Person must be called with the new keyword')}}const person = new Person('why')
const notPerson = Person.call(person, 'why') // Throw an error
Copy the code

Block-level function

In ECMAScript 3 and an early version of a statement in the code block a block-level function is a strictly grammatical errors, but all the browser still support this feature, but because the browser differences lead to support degree is slightly different, it is best not to use this feature, if you want to use you can use the anonymous function expression.

In ES5 strict mode, declaring a function in a block of code will cause an error
// Under ES6, because of the concept of block-level scope, no error is reported whether you are in strict mode or not.
In ES6, however, when in strict mode, function declarations are promoted to the top of the current block-level scope
// When in non-strict mode, promote to outer scope
'use strict'
if (true) {
  function doSomething () {
    console.log('do something')}}Copy the code

Arrow function

One of the most interesting new features in ES6 is the arrow function, which is a new syntax for defining functions using arrows =>, but is slightly different from traditional JavaScript functions:

  • No this, super, arguments, and new.target bindings: in arrow functionthis,super,argumentsandnew.targetThese values are determined by the nearest layer of non-arrow functions.
  • Cannot be called by the new keyword: because the arrow function doesn’t have one[[Construct]]Delta function, so it can’t passnewKeyword is called if usednewMaking the call throws an error.
  • There is no prototype: Because it won’t passnewKeywords are called, so there’s no need to build prototypes, and that’s itprototypeThis property.
  • You cannot change the binding of this: Inside the arrow function,thisThe value of cannot be changed (that is, cannot passcall,applyorbindAnd so on.
  • Argument objects are not supportedThe arrow function does not have oneargumentsBinding, so parameters must be accessed using either named or indefinite parameters.
  • Duplicate named arguments are not supported: Arrow functions do not support duplicate named arguments, whether in strict mode or not.

Syntax for arrow functions

The syntax of the arrow function is variable, with many forms depending on the actual usage scenario. All variants consist of function arguments, arrows, and function bodies.

One form of expression:

// One representation: no arguments
let reflect = () = > 5
/ / equivalent to
let reflect = function () {
  return 5
}
Copy the code

Expression form two:

// Return a single value
let reflect = value= > value
/ / equivalent to
let reflect = function (value) {
  return value
}
Copy the code

Expression form three:

// Multiple parameters
let reflect = (val1, val2) = > val1 + val2
/ / or
let reflect = (val, val2) = > {
  return val1 + val2
}
/ / equivalent to
let reflect = function (val1, val2) {
  return val1 + val2
}
Copy the code

Form four:

// Return a literal
let reflect = (id) = > ({ id: id, name: 'why' })
/ / equivalent to
let reflect = function (id) {
  return {
    id: id,
    name: 'why'}}Copy the code

Arrow functions and arrays

The simple syntax of the arrow function makes it ideal for handling arrays.

const arr = [1.5.3.2]
// non-arrow function sort
arr.sort(function(a, b) {
  return a -b
})
// arrow function sort
arr.sort((a, b) = > a - b)
Copy the code

Tail-call optimization

A tail call is when a function is called as the last statement of another function.

Tail call example:

function doSomethingElse () {
  console.log('do something else')}function doSomething () {
  return doSomethingElse()
}
Copy the code

In the ECMAScript 5 engine, tail calls are implemented similarly to other function calls: a new stack frame is created and pushed onto the call stack to represent a function call, which means that each unused stack frame is stored in memory during a circular call, causing program problems when the call stack becomes too large.

To address the above possible problems, ES6 reduces the size of the tail-call stack in strict mode. When all of the following conditions are met, the tail-call does not create a new stack frame, but clears and reuses the current stack frame:

  • The last call does not access the variables of the current stack frame (the function is not a closure.)
  • The tail call is the last statement
  • The result of the tail call is returned as a function


An example of a tail call that meets the above criteria:

'use strict'
function doSomethingElse () {
  console.log('do something else')}function doSomething () {
  return doSomethingElse()
}
Copy the code

Examples of tail calls that do not meet the above criteria:

function doSomethingElse () {
  console.log('do something else')}function doSomething () {
  // Unable to optimize, no return
  doSomethingElse()
}
function doSomething () {
  // Unable to optimize, return value added another operation
  return 1 + doSomethingElse()
}
function doSomething () {
  // May not be optimized
  let result = doSomethingElse
  return result
}
function doSomething () {
  let number = 1
  let func = () = > number
  // cannot be optimized, the function is a closure
  return func()
}
Copy the code

Recursive function is the most important application scenario. When the computation of recursive function is large enough, tail-call optimization can greatly improve the performance of the program.

/ / before optimization
function factorial (n) {
  if (n <= 1) {
    return 1
  } else {
    // Cannot be optimized
    return n * factorial (n - 1)}}/ / after optimization
function factorial (n, p = 1) {
  if (n <= 1) {
    return 1 * p
  } else {
    let result = n * p
    return factorial(n -1, result)
  }
}
Copy the code

Object extension

An extension of an object literal

The object literal extension consists of two parts:

  • Short for property initialvalue: When the property of an object has the same name as the local variable, you do not need to write the colon and value, simply write the property.
  • Short for object method: Eliminates colon andfunctionThe keyword.
  • Computable property name: When defining an object, the property value of the object can be computed by a variable.

Methods created by object method shorthand syntax have a name attribute whose value is the name before the brace.

const name = 'why'
const firstName = 'first name'
const person = {
  name,
  [firstName]: 'ABC',
  sayName () {
    console.log(this.name)
  }
  
}
/ / equivalent to
const name = 'why'
const person = {
  name: name,
  'first name': 'ABC'.sayName: function () {
    console.log(this.name)
  }
}
Copy the code

The new method

Object.is

When comparing two values in JavaScript, we might be used to using == or === for judgment. Using congruent === is popular because it avoids casting when comparing values. But congruence === is not completely accurate, for example: +0===-0 returns true, NaN===NaN returns false. The Object. Is method is introduced in ES6 to compensate for the above situation.

// === and object. is most of the time the results are the same, only a few of the results are different
console.log(+0= = = -0)            // true
console.log(Object.is(+0, -0))    // false
console.log(NaN= = =NaN)          // false
console.log(Object.is(NaN.NaN))  // true
Copy the code

Object.assign

Q: What is a Mixin? A: Mixins are the most popular pattern for implementing object composition in JavaScript. In a mixin, one object accepts properties and methods from another object (mixin methods are shallow copies).

/ / a mixin method
function mixin(receiver, supplier) {
  Object.keys(supplier).forEach(function(key) {
    receiver[key] = supplier[key]
  })
  return receiver
}
const person1 = {
  age: 23.name: 'why'
}
const person2 = mixin({}, person1)
console.log(person2) // { age: 23, name: 'why' }
Copy the code

Because this mixed mode is so popular, ES6 introduces the object. assign method to do the same thing, which takes an Object and any number of source objects, and eventually returns the Object.

If there is an attribute with the same name in the source object, subsequent source objects override the attribute with the same name in the previous source object.Copy the code
const person1 = {
  age: 23.name: 'why'
}
const person2 = {
  age: 32.address: 'Guangzhou, Guangdong'
}
const person3 = Object.assign({}, person1, person2)
console.log(person3) // {age: 32, name: 'why', address: 'guangzhou'}
Copy the code
The Object.assign method cannot copy the property's GET and set.Copy the code
let receiver = {}
let supplier = {
  get name () {
    return 'why'}}Object.assign(receiver, supplier)
const descriptor = Object.getOwnPropertyDescriptor(receiver, 'name')
console.log(descriptor.value) // why
console.log(descriptor.get)   // undefined
console.log(receiver)         // { name: 'why' }
Copy the code

Duplicate object literal properties

In ECMAScript 5 strict mode, adding duplicate attributes to an object triggers an error:

'use strict'
const person = {
  name: 'AAA'.name: 'BBB' // The ES5 environment triggers an error
}
Copy the code

In ECMAScript 6, however, adding duplicate attributes does not generate an error, regardless of whether the current state is in strict mode. Instead, the last value is selected:

'use strict'
const person = {
  name: 'AAA'.name: 'BBB' // The ES6 environment does not report errors
}
console.log(person) // { name: 'BBB' }
Copy the code

Own property enumeration order

The order in which object attributes are enumerated is not defined in ES5 and is at the discretion of the browser vendor. In ES6, the order in which an object’s own properties are enumerated is strictly regulated.

Rules:

  • All numeric keys are sorted in ascending order.
  • All character keys are sorted in the order they are added to the object.
  • allSymbolKeys are sorted in the order in which they are added to the object.

According to the above rules, the following methods will be affected:

  • Object.getOwnPropertyNames().
  • Reflect.keys().
  • Object.assign().

Uncertain situation:

  • for-inThe loop is still vendor determined by the order of enumeration.
  • Object.keys()andJSON.stringify()Also the samefor-inAgain, the vendor determines the order of enumeration.
const obj = {
  a: 1.0: 1.c: 1.2: 1.b: 1.1: 1
}
obj.d = 1
console.log(Reflect.keys(obj).join(' ')) // 012acbd
Copy the code

Enhanced object prototype

In ES5, object stereotypes remain unchanged once instantiated. In ES6, the object.setPrototypeof () method was added to change this.

const person = {
  sayHello () {
    return 'Hello'}}const dog = {
  sayHello () {
    return 'wang wang wang'}}let friend = Object.create(person)
console.log(friend.sayHello())                        // Hello
console.log(Object.getPrototypeOf(friend) === person) // true
Object.setPrototypeOf(friend, dog)
console.log(friend.sayHello())                        // wang wang wang
console.log(Object.getPrototypeOf(friend) === dog)    // true
Copy the code

Super references to simplify stereotype access

In ES5, if we wanted to override the method of an object instance and needed to call the prototype method with the same name, we could do something like this:

const person = {
  sayHello () {
    return 'Hello'}}const dog = {
  sayHello () {
    return 'wang wang wang'}}const friend = {
  sayHello () {
    return Object.getPrototypeOf(this).sayHello.call(this) + '!!!!!! '}}Object.setPrototypeOf(friend, person)
console.log(friend.sayHello())                        // Hello!!!
console.log(Object.getPrototypeOf(friend) === person) // true
Object.setPrototypeOf(friend, dog)
console.log(friend.sayHello())                        // wang wang wang!!!
console.log(Object.getPrototypeOf(friend) === dog)    // true
Copy the code

Code analysis: It’s a bit complicated to remember exactly how to use the object.getPrototypeof () and XX.call (this) methods to call prototypical methods. In addition, object.getPrototypeof () is problematic when multiple inheritance exists. In response to the above problem, ES6 introduced the super keyword, where super is equivalent to a pointer to the object prototype, so the above code can be modified as follows:

The super keyword only appears in object shorthand methods and will cause an error when used in normal methods.Copy the code
const person = {
  sayHello () {
    return 'Hello'}}const dog = {
  sayHello () {
    return 'wang wang wang'}}const friend = {
  sayHello () {
    return super.sayHello.call(this) + '!!!!!! '}}Object.setPrototypeOf(friend, person)
console.log(friend.sayHello())                        // Hello!!!
console.log(Object.getPrototypeOf(friend) === person) // true
Object.setPrototypeOf(friend, dog)
console.log(friend.sayHello())                        // wang wang wang!!!
console.log(Object.getPrototypeOf(friend) === dog)    // true
Copy the code

Formal method definitions

The concept of a “method” was never formally defined before ES6, and a method is simply an object property with functionality rather than data. In ES6, a method is formally defined as a function, which has an internal [[HomeObject]] property to hold the object that the method belongs to.

const person = {
  [[HomeObject]] = person
  sayHello () {
    return 'Hello'}}// Not the way
function sayBye () {
  return 'goodbye'
}
Copy the code

Based on the above rules of [[HomeObject]], we can figure out how super works:

  • in[[HomeObject]]Call on propertyObject.getPrototypeOf()Method to retrieve a reference to the stereotype.
  • Search for the prototype to find the function with the same name.
  • Set up thethisBind and call the corresponding method.
const person = {
  sayHello () {
    return 'Hello'}}const friend = {
  sayHello () {
    return super.sayHello() + '!!!!!! '}}Object.setPrototypeOf(friend, person)
console.log(friend.sayHello()) // Hello!!!
Copy the code

Code analysis:

  • friend.sayHello()methods[[HomeObject]]Attribute values forfriend.
  • friendThe prototype isperson.
  • super.sayHello()The equivalent ofperson.sayHello.call(this).

deconstruction

Deconstruction is the process of breaking up data structures into smaller pieces.

Why destruct

In ECMAScript 5 and earlier versions, a lot of seemingly homogeneous code was written to get specific data from objects or arrays and assign values to variables:

const person = {
  name: 'AAA'.age: 23
}
const name = person.name
const age = person.age
Copy the code

Code analysis: We have to extract the values of name and age from the Person object and assign them to the corresponding variables of the same name, in a very similar process. If we want to extract a lot of variables, this process will be repeated many more times, and if there are nested structures involved, it will not be possible to find the real information just by walking through it.

In view of the above problems, ES6 introduces the concept of deconstruction, which can be divided into:

  • Object to deconstruct
  • An array of deconstruction
  • Mixed deconstruction
  • Deconstruction parameters

Object to deconstruct

Using the object structure in ES6, we can rewrite the example above:

const person = {
  name: 'AAA'.age: 23
}
const { name, age } = person
console.log(name) // AAA
console.log(age)  / / 23
Copy the code
An initializer must be provided for a deconstruction assignment, and an error will occur if the right side of the deconstruction is null or undefined.Copy the code
// The following code is an example of an error
var { name, age }
let { name, age }
const { name, age }
const { name, age } = null
const { name, age } = undefined
Copy the code

Deconstruction assignment

We can not only redefine variables during deconstruction, but also destruct assigned variables that already exist:

const person = {
  name: 'AAA'.age: 23
}
let name, age
// You must add () because {} is a code block, and syntax dictates that a code block cannot appear on the left side of an assignment.
({ name, age } = person)
console.log(name) // AAA
console.log(age)  / / 23
Copy the code

Deconstructing defaults

When using a destruct assignment expression, if the specified local variable name does not exist in the object, the local variable is assigned as undefined, and you can optionally specify a default value.

const person = {
  name: 'AAA'.age: 23
}
let { name, age, sex = 'male' } = person
console.log(sex) / / male
Copy the code

Assign values to variables that are not of the same name

So far when we’ve deconstructed assignments, the key to be deconstructed and the variable to be assigned have the same name, but how do we deconstruct assignments for variables that are not?

const person = {
  name: 'AAA'.age: 23
}
let { name, age } = person
/ / equivalent to
let { name: name, age: age } = person
Copy the code

Let {name: name, age: age} = person let {name: name, age: age} = person So, according to the above ideas, the assignment of a variable without the same name can be rewritten as follows:

const person = {
  name: 'AAA'.age: 23
}
let { name: newName, age: newAge } = person
console.log(newName) // AAA
console.log(newAge)  / / 23
Copy the code

Nested object structures

Deconstructing nested objects is still similar to object literal syntax, except that we can disassemble objects into what we want them to be.

const person = {
  name: 'AAA'.age: 23.job: {
    name: 'FE'.salary: 1000
  },
  department: {
    group: {
      number: 1000.isMain: true}}}let { job, department: { group } } = person
console.log(job)    // { name: 'FE', salary: 1000 }
console.log(group)  // { number: 1000, isMain: true }
Copy the code

Let {job, department: {group}} = person Let {job, department: {group}} = person let {job, department: {group} = person

An array of deconstruction

Array deconstruction assignment is similar to object deconstruction syntax, but much simpler. It uses array literals, and the deconstruction operation is all done in the array. The deconstruction process is extracted according to the position of the value in the array.

const colors = ['red'.'green'.'blue']
let [firstColor, secondColor] = colors
// Destruct as needed
let [,,threeColor] = colors
console.log(firstColor)   // red
console.log(secondColor)  // green
console.log(threeColor)   // blue
Copy the code

Like objects, arrays can be destructively assigned to existing variables, but without the extra parentheses required for objects:

const colors = ['red'.'green'.'blue']
let firstColor, secondColor
[firstColor, secondColor] = colors
console.log(firstColor)   // red
console.log(secondColor)  // green
Copy the code

Using this principle, we can easily extend the function of destructuring assignments (quickly swapping two variables) :

let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a); / / 2
console.log(b); / / 1
Copy the code

As with objects, array destruction can be set to a default value:

const colors = ['red']
const [firstColor, secondColor = 'green'] = colors
console.log(firstColor)   // red
console.log(secondColor)  // green
Copy the code

When there is a nested array, we can also use the same idea and deconstruct the nested object:

const colors = ['red'['green'.'lightgreen'].'blue']
const [firstColor, [secondColor]] = colors
console.log(firstColor)   // red
console.log(secondColor)  // green
Copy the code

Uncertain parameters

When deconstructing an array, any element must be placed last, and adding commas after it will cause an error.

In array destructuring, there is a function similar to the undefined parameter of a function: when destructing an array, you can use… The syntax assigns the remaining elements of an array to a specific variable:

let colors = ['red'.'green'.'blue']
let [firstColor, ...restColors] = colors
console.log(firstColor) // red
console.log(restColors) // ['green', 'blue']
Copy the code

We can implement the same array copy function as concat by using the above principle of unpacking the variable elements in an array:

const colors = ['red'.'green'.'blue']
const concatColors = colors.concat()
const [...restColors] = colors
console.log(concatColors) // ['red', 'green', 'blue']
console.log(restColors)   // ['red', 'green', 'blue']
Copy the code

Deconstruction parameters

When we decide on a function that takes a large number of arguments, we usually create an optional object and define the extra arguments as attributes of the object:

function setCookie (name, value, options) {
  options = options || {}
  let path = options.path,
      domain = options.domain,
      expires = options.expires
  // Other code
}

// Use destruct parameters
function setCookie (name, value, { path, domain, expires } = {}) {
  // Other code
}
Copy the code

{path, domain, Expires} = {} Must provide a default value. If it is not provided, an error will be reported if the third parameter is not passed:

function setCookie (name, value, { path, domain, expires }) {
  // Other code
}
/ / an error
setCookie('type'.'js')
// It is equivalent to deconstructing undefined, so an error will be reported
{ path, domain, expires } = undefined
Copy the code

Symbol and its Symbol property

Before ES6, the JavaScript language had only five primitive types: String, Number, Boolean, NULL, and undefiend. In ES6, a sixth primitive type was added: Symbol.

We can use typeof to check the Symbol type:

const symbol = Symbol('Symbol Test')
console.log(typeof symbol) // symbol
Copy the code

Create a Symbol

A Symbol can be created using the global Symbol function.

const firstName = Symbol(a)const person = {}
person[firstName] = 'AAA'
console.log(person[firstName]) // AAA
Copy the code

Passing an optional parameter in Symbol() allows us to add a text describing the Symbol we created, which is stored in the internal property [[Description]] and can only be read when the Symbol’s toString() method is called.

const firstName = Symbol('Symbol Description')
const person = {}
person[firstName] = 'AAA'
console.log(person[firstName]) // AAA
console.log(firstName)         // Symbol('Symbol Description')
Copy the code

Use of Symbol

Wherever you can use computable attribute names, you can use Symbol.

let firstName = Symbol('first name')
let lastName = Symbol('last name')
const person = {
  [firstName]: 'AAA'
}
Object.defineProperty(person, firstName, {
  writable: false
})
Object.defineProperties(person, {
  [lastName]: {
    value: 'BBB'.writable: false}})console.log(person[firstName])  // AAA
console.log(person[lastName])   // BBB
Copy the code

Symbol sharing system

ES6 provides the ability to create shared symbols using the symbol.for () method by providing an accessible global Symbol registry.

The parameter to the // symbol. for method is also used as the Symbol description
const uid = Symbol.for('uid')
const object = {
  [uid]: 12345
}
console.log(person[uid]) / / 12345
console.log(uid)         // Symbol(uid)
Copy the code

Code analysis:

  • Symbol.for()Methods will be global firstSymbolRegister to change the search key touidtheSymbolWhether there is.
  • If it exists, return the existing oneSymbol.
  • If it does not exist, create a new oneSymbolAnd use this key inSymbolRegistered in the global registry change, and then returns the newly createdSymbol.

Another feature related to Symbol sharing is that we can use the symbol.keyfor () method to retrieve the key associated with Symbol in the Symbol global registry and return it if it exists or undefined if it does not:

const uid = Symbol.for('uid')
const uid1 = Symbol('uid1')
console.log(Symbol.keyFor(uid))   // uid
console.log(Symbol.keyFor(uid1))  // undefined
Copy the code

Symbol and type cast

The other primitive types have no logical equivalent to Symbol, in particular the inability to cast Symbol to strings and numbers.

const uid = Symbol.for('uid')
console.log(uid)
console.log(String(uid))
/ / an error
uid = uid + ' '
uid = uid / 1
Copy the code

Code analysis: We use the console.log() method to print the Symbol, which calls the String() method of Symbol, so we can also directly call the String() method to print the Symbol. However, trying to concatenate Symbol with a string causes the program to throw an exception, and Symbol cannot be mixed with every mathematical operator, otherwise it will throw an error as well.

Symbol attribute retrieval

Object. The keys () and the Object. GetOwnPropertyNames () method can retrieve all the property name in the Object, the Object. The keys to return all the attributes can be enumerated, Object. GetOwnPropertyNames () whether attributes can be enumerated all returned, but these two methods are unable to return to Symbol attribute. Therefore ES6 introduces a new method Object. GetOwnPropertySymbols () method.

const uid = Symbol.for('uid')
let object = {
  [uid]: 123
}
const symbols = Object.getOwnPropertySymbols(object)
console.log(symbols.length) / / 1
console.log(symbols[0])     // Symbol(uid)
Copy the code

Symbol exposes the internal operation

ES6 exposes more of the language’s internal logic by defining properties associated with symbols on the prototype chain. These internal operations are as follows:

  • Symbol.hasInstance: One is executinginstanceofIs invoked to detect inheritance information about an object.
  • Symbol.isConcatSpreadable: A Boolean value used when passing a collection asArray.prototype.concat()Method, whether the elements in the collection should be organized to the same level.
  • Symbol.iterator: a method that returns an iterator.
  • Symbol.match: one is callingString.prototype.match()Method, used to compare strings.
  • Symbol.replace: one is callingString.prototype.replace()Method to replace a substring in a string.
  • Symbol.search: one is callingString,prototype.search()Method, used to locate substrings in a string.
  • Symbol.split: one is callingString.prototype.split()Method, used to split a string.
  • Symbol.species: constructor used to create derived objects.
  • Symbol.toPrimitive: a method that returns the original value of an object.
  • Symbol.toStringTag: one is callingObject.prototype.toString()Method to create an object description.
  • Symbol.unscopables: one defines something that cannot bewithThe collection of objects with the name of the object property referenced by the statement.

Overwriting a method defined by a well-known Symbol causes the default behavior inside the object to change so that a common object becomes a singular object.

Symbol.hasInstance

Each function has a symbol.hasinstance method that determines whether an object is an instance of a function and cannot be enumerated, written, or configured.

function MyObject () {
  / / empty function
}
Object.defineProperty(MyObject, Symbol.hasInstance, {
  value: function () {
    return false}})let obj = new MyObject()
console.log(obj instanceof MyObject) // false
Copy the code

Code analysis: Using object.defineProperty, overwrite symbol.hasinstance on MyObject and define a new function that always returns false, even though obj is indeed an instance of MyObject. But it still returns false on instanceof judgment.

Note that to trigger the symbol. hasInstance call, the left operator of instanceof must be an object. If it is not an object, instanceof will always return false.Copy the code

Symbol.isConcatSpreadable

In JavaScript arrays the concat() method is used to concatenate two arrays:

const colors1 = ['red'.'green']
const colors2 = ['blue']
console.log(colors1.concat(colors2, 'brown')) // ['red', 'green', 'blue', 'brown']
Copy the code

In the concat() method, we pass the second argument, which is a non-array element. If Symbol isConcatSpreadable is true, then said object has length attribute and numeric keys, the numeric keys will be independent of reason it is added to the concat calls in the results, it is an optional attribute of the object, used to enhance effect on the function of the specific object types concat method, Effectively simplifying its default features:

const obj = {
  0: 'hello'.1: 'world'.length: 2[Symbol.isConcatSpreadable]: true
}
const message = ['Hi'].concat(obj)
console.log(message) // ['Hi', 'hello', 'world']
Copy the code

Symbol. Match, Symbol. Replace, Symbol. Search, Symbol. Split

In JavaScript, strings and regular expressions are often used together, especially for several string-type methods that can accept regular expressions as arguments:

  • match: Determines whether the given string matches the regular expression.
  • replace: Replaces the part of the string that matches the regular expression with the given string.
  • search: Locates the index in the string that matches the re representation position.
  • split: Splits the string according to the elements matching the regular expression and stores the split result in an array.

Prior to ES6, the above methods could not use our own defined objects to replace regular expressions for string matching. After ES6, the corresponding Symbol was introduced to completely outsource the native features of the language’s built-in Regex objects.

const hasLengthOf10 = {
  [Symbol.match] (value) {
    return value.length === 10 ? [value] : null},Symbol.replace] (value, replacement) {
    return value.length === 10 ? replacement : value
  },
  [Symbol.search] (value) {
    return value.length === 10 ? 0 : -1},Symbol.split] (value) {
    return value.length === 10 ? [,] : [value]
  }
}
const message1 = 'Hello world'
const message2 = 'Hello John'
const match1 = message1.match(hasLengthOf10)
const match2 = message2.match(hasLengthOf10)
const replace1 = message1.replace(hasLengthOf10)
const replace2 = message2.replace(hasLengthOf10, 'AAA')
const search1 = message1.search(hasLengthOf10)
const search2 = message2.search(hasLengthOf10)
const split1 = message1.split(hasLengthOf10)
const split2 = message2.split(hasLengthOf10)
console.log(match1)     // null
console.log(match2)     // [Hello John]
console.log(replace1)   // Hello world
console.log(replace2)   // AAA
console.log(search1)    // -1
console.log(search2)    / / 0
console.log(split1)     // [Hello John]
console.log(split2)     / / /,
Copy the code

Symbol.toPrimitive

The symbol.toprimitive method is defined on each stereotype of the standard type and specifies the operation to be performed when the object is converted to its original value. Whenever the original value conversion is performed, the symbol.toprimitive method is always called and passed a value as an argument. For most standard objects, numeric patterns have the following features, in order of priority:

  • callvalueOf()Method, if the result is the original value.
  • Otherwise, calltoString()Method, if the result is the original value.
  • If there are no more options, an error is thrown.

Also for most standard objects, the string pattern has the following finite order:

  • calltoString()Method, if the result is the original value.
  • Otherwise, callvalueOf()Method, if the result is the original value.
  • If there are no more options, an error is thrown.

In most cases, standard objects treat the default schema as a number (except for Date objects, in which case the default schema is treated as a string), and you can override these default cast behaviors if you customize the symbol.toprimitive method.

function Temperature (degress) {
  this.degress = degress
}
Temperature.prototype[Symbol.toPrimitive] = function (hint) {
  switch (hint) {
    case 'string':
      return this.degress + '℃'
    case 'number':
      return this.degress
    case 'default':
      return this.deress + ' degress'}}const freezing = new Temperature(32)
console.log(freezing + ' ')      // 32 degress
console.log(freezing / 2)       / / 16
console.log(String(freezing))   / / 32 ℃
Copy the code

Code analysis: We’ve overridden symbol.toprimitive on the object Temperature prototype, with the new method returning different values based on the mode specified by the hint parameter, which is passed in by the JavaScript engine. Where the + operator triggers the default mode, hint is set to default; The/operator triggers the numeric mode, and hint is set to number; The String() function triggers String mode, and hint is set to String.

Symbol.toStringTag

In JavaScript, if we have multiple global execution environments, such as a page containing an IFrame tag in a browser, because the iframe and its outer pages represent different domains, each domain has its own global scope, its own global objects, arrays created in any domain, It’s a normal array. However, if this number is passed to another realm, the instanceof Array statement returns false, and the Array is already the constructor of the other realm. Obviously, the Array being tested was not created by that constructor.

For the above problems, we quickly found a relatively practical solution:

function isArray(value) {
  return Object.prototype.toString.call(value) === '[object Array]'
}
console.log(isArray([])) // true
Copy the code

In a similar case, prior to ES5 we might have introduced third-party libraries to create global JSON objects, but once browsers started implementing JSON global objects, it became necessary to distinguish between JSON objects provided by the JavaScript environment itself and those provided by third-party libraries:

function supportsNativeJSON () {
  return typeof JSON! = ='undefined' && Object.prototype.toString.call(JSON) = = ='[object JSON]'
}
Copy the code

In the ES6, through Symbol. This Symbol toStringTag changed the call Object. The prototype, the toString () returns the identity, It defines the Object of the call Object. The prototype. ToString, call () method returns the value of the:

function Person (name) {
  this.name = name
}
Person.prototype[Symbol.toStringTag] = 'Person'
const person = new Person('AAA')
console.log(person.toString())                        // [object Person]
console.log(Object.prototype.toString.call(person))   // [object Person]
Copy the code

Set and Map collections

A Set is a list of non-repeating elements, usually used to detect whether a given value is in a Set; Map sets contain multiple groups of key-value pairs. Each element in the set contains an accessible key name and its corresponding value. Map sets are often used to cache frequently accessed data.

Set and Map collections in ES5

Before ES6 officially introduced Set and Map collections, developers were already using object properties to simulate both collections:

const set = Object.create(null)
const map = Object.create(null)
set.foo = true
map.bar = 'bar'
/ / set to check
if (set.foo) {
  console.log('there')}/ / the map values
console.log(map.bar) // bar
Copy the code

The above procedure is very simple, indeed can use the object attributes to simulate the Set and Map Set, but in the actual use of the process has many inconvenient:

  • Object property names must be strings:
const map = Object.create(null)
map[5] = 'foo'
// It is intended to use the number 5 as the key name, but is automatically converted to a string
console.log(map['5']) // foo
Copy the code
  • Object cannot be an attribute name:
const map = Object.create(null)
const key1 = {}
const key2 = {}
map[key1] = 'foo'
// It is intended to use the key1 object as the attribute name, but is automatically converted to [object object]
// so map[key1] = map[key2] = map['[object object]']
console.log(map[key2]) // foo
Copy the code
  • Uncontrolled casts:
const map = Object.create(null)
map.count = 1
// Check to see if the count attribute exists, but actually check to see if the map.count attribute is true
if (map.count) {
  console.log(map.count)
}
Copy the code

Set in ES6

A Set is an ordered list of independent, non-repeating values in which no cast is performed on the stored values.

The Set Set involves the following attributes and methods:

  • SetConstructor: You can use this constructor to create oneSetCollection.
  • addMethod: Can be toSetAdd an element to the collection.
  • deleteMethod: You can remove itSetAn element of a set.
  • clearMethod: You can remove itSetAll the members of the set.
  • hasMethod: Determines whether the given element is inSetIn the collection.
  • sizeProperties:SetThe length of the set.

Create a Set

The constructor of a Set can take any iterable, such as an array, a Set, or a Map.

const set = new Set()
set.add(5)
set.add('5')
// Repeatedly added values are ignored
set.add(5)
console.log(set.size) / / 2
Copy the code

Remove elements

The delete() method removes a single value from the collection, and the clear() method removes all elements from the collection.

const set = new Set()
set.add(5)
set.add('5')
console.log(set.has(5)) // true
set.delete(5)
console.log(set.has(5)) // false
console.log(set.size)   / / 1
set.clear()
console.log(set.size)   / / 0
Copy the code

The forEach() method of the Set collection

The forEach() method on a Set is the same as the forEach() method on an array. The only difference is that the Set is traversed and the first and second arguments are the same.

const set = new Set([1.2])
set.forEach((value, key, arr) = > {
  console.log(`${value} ${key}`)
  console.log(arr === set)
})
/ / 1 1
// true
2 / / 2
// true
Copy the code

Sets are converted to arrays

Because sets cannot be indexed like arrays, it is best to convert sets to arrays.

const set = new Set([1.2.3.4])
// Method 1: expand operator
const arr1 = [...set]
// Method 2: array. from
const arr2 = Array.from(set)
console.log(arr1) // [1, 2, 3, 4]
console.log(arr2) // [1, 2, 3, 4]
Copy the code

Weak Set collection

The garbage collection mechanism cannot free the memory of a Set as long as there are references in the Set. Therefore, we consider the Set as a collection of strong references. To better handle garbage collection of Set sets, we introduce a Weak Set called Weak Set:

The Weak Set collection supports only three methods: add, HAS, and delete.Copy the code
const weakSet = new WeakSet(a)const key = {}
weakSet.add(key)
console.log(weakSet.has(key)) // true
weakSet.delete(key)
console.log(weakSet.has(key)) // false
Copy the code

Set sets and Weak sets share many features, but there are some differences between them:

  • Weak SetCollections can only store object elements, and adding non-object elements to them causes a throw error, as wellhas()anddelete()Passing non-objects also generates an error.
  • Weak SetCollections are not iterable, do not expose any iterators, and are therefore not supportedforEach()Methods.
  • Weak SetCollection not supportedsizeProperties.

Map set in ES6

The Map type in ES6 is an ordered list of many key-value pairs. The key names and their corresponding values support all data types. The equivalence of key names is determined by calling object. is.

const map = new Map(a)const key1 = {
  name: 'key1'
}
const key2 = {
  name: 'key2'
}
map.set(5.5)
map.set('5'.'5')
map.set(key1, key2)
console.log(map.get(5))     / / 5
console.log(map.get('5'))   / / '5'
console.log(map.get(key1))  // {name:'key2'}
Copy the code

Methods supported by the Map collection

Like Set collections, Map collections support the following methods:

  • has: Checks whether the specified key name existsMapExists in the set.
  • deleteIn:MapRemoves the specified key name and its value from the collection.
  • clearRemove:MapAll key-value pairs in the set.
const map = new Map()
map.set('name'.'AAA')
map.set('age'.23)
console.log(map.size)        / / 2
console.log(map.has('name')) // true
console.log(map.get('name')) // AAA
map.delete('name')
console.log(map.has('name')) // false
map.clear()
console.log(map.size)        / / 0
Copy the code

Initialization method of Map collection

It is also possible to initialize a Map collection by passing in an array like a Set, but each element in the array is a subarray that contains the key name and value of a key-value pair.

const map = new Map([['name'.'AAA'], ['age'.23]])
console.log(map.has('name'))  // true
console.log(map.has('age'))   // true
console.log(map.size)         / / 2
console.log(map.get('name'))  // AAA
console.log(map.get('age'))   / / 23
Copy the code

The forEach() method of the Map collection

The callback arguments to the forEach() method in the Map collection are similar to those to arrays, and each argument is interpreted as follows:

  • The first parameter is the key name
  • The second argument is the value
  • The third parameter isMapCollection itself
const map = new Map([['name'.'AAA'], ['age'.23]])
map.forEach((key, value, ownMap) = > {
  console.log(`${key} ${value}`)
  console.log(ownMap === map)
})
// name AAA
// true
// age 23
// true
Copy the code

Weak Map collections

Weak Map is an unordered list of key-value pairs. The key name in the collection must be an object. An error will be reported if a non-object key name is used.

The Weak Map set supports only set(), get(), has(), and delete().Copy the code
const key1 = {}
const key2 = {}
const key3 = {}
const weakMap = new WeakMap([[key1, 'AAA'], [key2, 23]])
weakMap.set(key3, 'in guangdong')

console.log(weakMap.has(key1)) // true
console.log(weakMap.get(key1)) // AAA
weakMap.delete(key1)
console.log(weakMap.has(key)) // false
Copy the code

The Map and Weak Map collections share many features, but there are some differences between them:

  • Weak MapThe key name of the collection must be object. Adding a non-object will cause an error.
  • Weak MapCollections are not iterable and therefore not supportedforEach()Methods.
  • Weak MapCollection not supportedclearMethods.
  • Weak MapCollection not supportedsizeProperties.

Iterators and Generators

Problem with loop statements

In the course of daily development, we have probably written code like the following:

var colors = ['red'.'gree'.'blue']
for(var i = 0, len = colors.length; i < len; i++) {
  console.log(colors[i])
}
// red
// green
// blue
Copy the code

Code analysis: even though the grammar of the loop statement is simple, but if multiple nested loop will need to track multiple variables, code complexity increases, a carelessly will use the other for loop error tracking variables, resulting in application error, and ES6 introduced the iterator is designed to eliminate the complexity and reduce errors in the loop.

What is an iterator

Q: What is an iterator? A: Iterators are a special type of object with a special interface designed for the iterative process. All iterators have a method called Next that returns a result object each time it is called. The result object has two properties, one is value which represents the value to be returned next time; The other is done, which is a Boolean value that returns true if there is no more data to return. The iterator also holds an internal pointer to the location of the value in the current collection, and each time the next method is called, the next available value is returned.

Now that we know the concept of iterators, we use ES5 syntax to create an iterator:

function createIterator (items) {
  var i = 0
  return {
    next: function () {
      var done = i >= items.length
      varvalue = ! done ? items[i++] :undefined
      return {
        done: done,
        value: value
      }
    }
  }
}
var iterator = createIterator([1.2.3])
console.log(iterator.next())  // { value: 1, done: false }
console.log(iterator.next())  // { value: 2, done: false }
console.log(iterator.next())  // { value: 3, done: false }
console.log(iterator.next())  // { value: undefined, done: true }
Copy the code

As mentioned above, we used ES5 syntax to create our own iterators, which have complex internal implementations, while ES6 introduced the concept of iterators as well as a concept called generators, which we can use to make the process of creating iterators a bit easier.

What is a generator

Q: What is a generator? A: A generator is a function that returns an iterator, denoted by the asterisk after the function keyword. The new yield keyword is used in the function.

function * createIterator () {
  yield 1
  yield 2
  yield 3
}
const iterator = createIterator()
console.log(iterator.next().value)  / / 1
console.log(iterator.next().value)  / / 2
console.log(iterator.next().value)  / / 3
Copy the code

As with our output above, it is consistent with the iterator output we created using ES5 syntax.

The most important thing about generator functions is that they terminate automatically at the end of each yield statement: before ES6, once a function was executed, it would continue down until the return statement was interrupted, but generator functions broke this convention: When a yield statement is executed, the function automatically stops execution unless the code manually calls the iterator’s next method.

We can also use generators in loops:

function * createIterator (items) {
  for(let i = 0, len = items.length; i < len; i++) {
    yield items[i]
  }
}
const it = createIterator([1.2.3])
console.log(it.next())  // { done: false, value: 1 }
console.log(it.next())  // { done: false, value: 2 }
console.log(it.next())  // { done: false, value: 3 }
console.log(it.next())  // { done: true, value: undefined }
Copy the code
The yield keyword can only be used within the generator, and using it elsewhere will result in a thrown error, even if used within a function within the generator.Copy the code
function * createIterator (items) {
  items.forEach(item= > {
    // Throw an error
    yield item + 1})}Copy the code

Iterable and for-of loops

Q: What are the characteristics of an iterable? A: An iterable has the Symbol. Iterator property and is an object closely related to an iterator. Symbol.iterator returns an iterator to a dependent object using the specified function. In ES6, all collection objects (arrays, sets, and maps) and strings are iterable objects with default iterators. Because generators assign values to the symbol. iterator property by default, all iterators created by generators are iterables.

ES6 introduced a for-of loop that calls the next method of the iterable every time it executes and stores the value attribute of the result object returned by the iterator in a variable. The loop will continue to execute this process until the value of the done attribute of the returned object is true.

const value = [1.2.3]
for (let num of value) {
  console.log(num);
}
/ / 1
/ / 2
/ / 3
Copy the code

Access the default iterator

The default iterator of an object can be accessed through symbol. iterator

const values = [1.2.3]
const it = values[Symbol.iterator]()
console.log(it.next())  // {done:false, value:1}
console.log(it.next())  // {done:false, value:2}
console.log(it.next())  // {done:false, value:3}
console.log(it.next())  // {done:true, value:undefined}
Copy the code

Since objects with the symbol. iterator attribute have a default iterator object, we can use it to check whether the object is iterable:

function isIterator (object) {
  return typeof object[Symbol.iterator] === 'function'
}

console.log(isIterator([1.2.3]))  // true
console.log(isIterator('hello'))    // true, strings can also be iterated, the same as arrays
console.log(isIterator(new Set()))  // true
console.log(isIterator(new Map))    // true
Copy the code

Create an iterable

By default, our self-defined objects are non-iterable, but we can make them iterable by adding a generator to the symbol. iterator property.

let collection = {
  items: [1.2.3[], *Symbol.iterator] () {
    for (let item of this.items) {
      yield item
    }
  }
}
for (let value of collection) {
  console.log(value)
}
/ / 1
/ / 2
/ / 3
Copy the code

Built-in iterators

Collection object iterator

There are three types of collection objects in ES6: arrays, sets, and maps, all of which have three types of iterators built in:

  • entries: returns an iterator with multiple key-value pairs.
  • values: returns an iterator with the value of the collection.
  • keys: returns an iterator with the value of all the key names in the collection.

Entries () iterator:

const colors = ['red'.'green'.'blue']
const set = new Set([1.2.3])
const map = new Map([['name'.'AAA'], ['age'.23], ['address'.'in guangdong']])

for (let item of colors.entries()) {
  console.log(item)
  // [0, 'red']
  // [1, 'green']
  // [2, 'blue']
}
for (let item of set.entries()) {
  console.log(item)
  / / [1, 1)
  / / (2, 2)
  / / [3, 3]
}
for (let item of map.entries()) {
  console.log(item)
  // ['name', 'AAA']
  // ['age', 23]
  // ['address', 'address']
}
Copy the code

Values iterator:

const colors = ['red'.'green'.'blue']
const set = new Set([1.2.3])
const map = new Map([['name'.'AAA'], ['age'.23], ['address'.'in guangdong']])

for (let item of colors.values()) {
  console.log(item)
  // red
  // green
  // blue
}
for (let item of set.values()) {
  console.log(item)
  / / 1
  / / 2
  / / 3
}
for (let item of map.values()) {
  console.log(item)
  // AAA
  / / 23
  / / guangdong
}
Copy the code

Keys iterator:

const colors = ['red'.'green'.'blue']
const set = new Set([1.2.3])
const map = new Map([['name'.'AAA'], ['age'.23], ['address'.'in guangdong']])

for (let item of colors.keys()) {
  console.log(item)
  / / 0
  / / 1
  / / 2
}
for (let item of set.keys()) {
  console.log(item)
  / / 1
  / / 2
  / / 3
}
for (let item of map.keys()) {
  console.log(item)
  // name
  // age
  // address
}
Copy the code

Default iterators for different collection types: Each collection type has a default iterator, which is used in for-of loops if no explicit iterator is specified:

  • Array andSetCollection: The default iterator isvalues.
  • MapCollection: The default isentries.
const colors = ['red'.'green'.'blue']
const set = new Set([1.2.3])
const map = new Map([['name'.'AAA'], ['age'.23], ['address'.'in guangdong']])
for (let item of colors) {
  console.log(item)
  // red
  // green
  // blue
}
for (let item of set) {
  console.log(item)
  / / 1
  / / 2
  / / 3
}
for (let item of map) {
  console.log(item)
  // ['name', 'AAA']
  // ['age', 23]
  // ['address', 'address']
}
Copy the code

Deconstruction and for-of loops: If you use the deconstruction syntax in for-of loops, you can simplify the coding process:

const map = new Map([['name'.'AAA'], ['age'.23], ['address'.'in guangdong']])
for (let [key, value] of map.entries()) {
  console.log(key, value)
  // name AAA
  // age 23
  / / address in guangdong
}
Copy the code

String iterator

Since the release of ES6, JavaScript strings have slowly become more array like:

let message = 'Hello'
for(let i = 0, len = message.length; i < len; i++) {
  console.log(message[i])
  // H
  // e
  // l
  // l
  // o
}
Copy the code

NodeList iterator

The DOM standard has a NodeList type that represents a collection of all elements in a page document. ES6 adds a default iterator that behaves like the default iterator for arrays:

let divs = document.getElementsByTagName('div')
for (let div of divs) {
  console.log(div)
}
Copy the code

Expansion operators and non-array iterables

As we already know, we can use the expansion operator to convert a Set to an array, as follows:

let set = new Set([1.2.3.4])
let array = [...set]
console.log(array) // [1, 2, 3, 4]
Copy the code

Code analysis: In our use of… The expansion operator operates on the default iterable of the Set (values), reads all the values from the iterator, and inserts them into the array in the order they are returned.

const map = new Map([['name'.'AAA'], ['age'.23], ['address'.'in guangdong']])
const array = [...map]
console.log(array) / / [[' name ', 'AAA'], [' age, 23], [' address ', 'guangdong]]
Copy the code

Code analysis: Before we use… In expanding the operator, it operates on the default iterable of the Map collection, reading groups of key-value pairs from the iterator and inserting them into the array.

const arr1 = ['red'.'green'.'blue']
const arr2 = ['yellow'.'white'.'black']
const array = [...arr1, ...arr2]
console.log(array) // ['red', 'green', 'blue', 'yellow', 'white', 'black']
Copy the code

Code analysis: Using… Operators are expanded using the same default iterators (values) as sets, and then inserted into the array in the order they are returned.

Advanced iterator functionality

Pass arguments to iterators

If you pass an argument to the iterator next() method, the value of that argument replaces the return value of the previous yield statement inside the generator.

function * createIterator () {
  let first = yield 1
  let second = yield first + 2
  yield second + 3
}
let it = createIterator()
console.log(it.next(11)) // {done: false, value: 1}
console.log(it.next(4))  // {done: false, value: 6}
console.log(it.next(5))  // {done: false, value: 8}
console.log(it.next())   // {done: true, value: undefined}
Copy the code

Code analysis: With the exception of the first iterator, we can quickly evaluate the iterators, but why is the argument to the first next() method invalid? This is because the argument passed to the next() method is a return value in place of the previous yield, and no yield statement is executed until the first call to the next() method, so passing arguments to the first call to the next() method, regardless of any value, is discarded.

Throw an error in an iterator

In addition to passing data to the iterator, you can also pass it an error condition to throw when it resumes execution.

function * createIterator () {
  let first = yield 1
  let second = yield first + 2
  // Will not be executed
  yield second + 3
}
let it = createIterator()
console.log(it.next())                    // {done: false, value: 1}
console.log(it.next(4))                   // {done: false, value: 6}
console.log(it.throw(new Error('break'))) // Throw an error
Copy the code

As we saw above, we want to pass an error object inside the generator that will be thrown when the iterator resumes execution. We can use a try-catch statement to catch such an error:

function * createIterator () {
  let first = yield 1
  let second
  try {
    second = yield first + 2
  } catch(ex) {
    second = 6
  }
  yield second + 3
}
let it = createIterator()
console.log(it.next())                    // {done: false, value: 1}
console.log(it.next(4))                   // {done: false, value: 6}
console.log(it.throw(new Error('break'))) // {done: false, value: 9}
console.log(it.next())                    // {done: true, value: undefined}
Copy the code

Generator return statement

Because generators are functions, you can exit function execution early with a return statement, and you can proactively specify a return value for the last next() method call.

function * createIterator () {
  yield 1
  return 2
  // Will not be executed
  yield 3
  yield 4
}
let it = createIterator()
console.log(it.next())  // {done: false, value: 1}
console.log(it.next())  // {done: false, value: 2}
console.log(it.next())  // {done: true, value: undefined}
Copy the code

Code analysis: In generators, a return statement indicates that all operations have been completed, the property value done is set to true, if a response value is provided, the property value is set to that value, and yield after the return statement is not executed.

The expansion operator and for-of loop simply ignore any return values specified by the return statement, because as soon as done is set to true, it stops reading other values.Copy the code
const obj = {
  items: [1.2.3.4.5[], *Symbol.iterator] () {
    for (let i = 0, len = this.items.length; i < len; i++) {
      if (i === 3) {
        return 300
      } else {
        yield this.items[i]
      }
    } 
  }
}
for (let value of obj) {
  console.log(value)
  / / 1
  / / 2
  / / 3
}
console.log([...obj]) / / [1, 2, 3]
Copy the code

Delegate generator

We can combine the two iterators so that we can create a generator and add an asterisk to the yield statement to delegate the process of generating data to other iterators.

function * createColorIterator () {
  yield ['red'.'green'.'blue']}function * createNumberIterator () {
  yield [1.2.3.4]}function * createCombineIterator () {
  yield * createColorIterator();
  yield * createNumberIterator();
}
let it = createCombineIterator()
console.log(it.next().value)  // ['red', 'green', 'blue']
console.log(it.next().value)  // [1, 2, 3, 4]
console.log(it.next().value)  // undefined
Copy the code

If you think it’s a good one, please give it a star. If you want to read all the notes in the first and second parts, please click here to read the full text

Reading the book “In-depth Understanding of ES6”, sorting out notes (PART 1)