preface

This article will give you some of the interesting things I came across while reading JavaScript You Don’t Know (Volume 2), and the feeling of opening up a new world. I hope I can bring you some fun in my spare time.

JavaScript is an excellent language. It’s easy to learn some of them, but it’s hard to master them all. Developers often blame difficulties on the language itself rather than reflect on how poorly they understand the language. JavaScript You Don’t Know aims to solve this problem and make readers genuinely love the language.

Cast casting

Value type conversion

var a = 42;
var b = a + ""; // Implicit cast
var c = String(a); // An explicit cast
Copy the code

Abstract value operation

Document. all is a false value object. That is!!!!! The document.all value is false.

Displays casts

Date display converted to numbers:

Use date.now () to get the current timestamp and use new Date(..). .getTime() to get the timestamp for the specified time.

The peculiar ~ operator:

~x is roughly the same thing as – x+1. Strange, but relatively easy to illustrate: ~42; / / – (42 + 1) = = > – 43

IndexOf (..) for strings in JavaScript This convention is also followed by the method, which searches the string for the specified substring and returns the position of the substring (starting at 0) if it is found, or -1 otherwise.

Together with indexOf(), ~ casts (really just casts) the result to true/false values:

var a = "Hello World";
~a.indexOf("lo"); // 4 <-- true!

if (~a.indexOf("lo")) { // true
  // Find a match!
}
Copy the code

Parsing non-strings:

ParseInt (..) A pit of:

parseInt( 1/0.19 ); / / 18
Copy the code

ParseInt (1/0, 19) is actually parseInt(“Infinity”, 19). The first character is “I”, which has the value 18 in base 19.

Here are some other examples that seem strange but actually make sense:

parseInt(0.000008); // 0 ("0" comes from "0.000008")
parseInt(0.0000008); // 8 ("8" comes from "8e-7")
parseInt(false.16); // 250 ("fa" comes from "false")
parseInt(parseInt.16); // 15 ("f" comes from "function.." )
parseInt("0x10"); / / 16
parseInt("103".2); / / 2
Copy the code

Implicit cast

Implicit cast between a string and a number

Such as:

var a = "42";
var b = "0";
var c = 42;
var d = 0;
a + b; / / "420"
c + d; / / 42
Copy the code

Such as:

var a = [1.2];
var b = [3.4];
a + b; / / "1,23,4"
Copy the code

According to section 11.6.1 of the ES5 specification, + will be concatenated if an operand is a string or can be converted to a string by the following steps. If one of the operands is an object (including an array), the ToPrimitive abstract operation is first called on it (Section 9.1 of the specification), which in turn calls [[DefaultValue]] (Section 8.12.8 of the specification), with the number as the context.

You may notice that this is the same way the ToNumber abstraction works with objects (see Section 4.2.2). Because the valueOf() operation on the array cannot get a simple primitive value, it calls toString() instead. So the two arrays in the above example become “1,2” and “3,4”. + concatenate them and return “1,23,4”.

In simple terms, if one of the operands of + is a string (or can be obtained by the above steps), string concatenation is performed; Otherwise, add numbers.

The cast of symbols

ES6 allows explicit casts from symbols to strings, but implicit casts can produce errors for reasons beyond the scope of this book.

Such as:

var s1 = Symbol("cool");
String(s1); // "Symbol(cool)"
var s2 = Symbol("not cool");
s2 + ""; // TypeError
Copy the code

Symbols cannot be cast to numbers (both explicit and implicit yield errors), but they can be cast to booleans (both explicit and implicit yield true).

Because of the lack of consistency in the rules, we need to be careful about casting symbols in ES6.

Fortunately, because of the special use of symbols, we don’t use them very often.

Loose equality and strict equality

A common misconception is “== checks for value equality, === checks for value and type equality”. That sounds reasonable, but it’s not quite accurate. A lot of JavaScript books and blogs explain this as well, but sadly they’re all wrong.

The correct interpretation is: “== allows casting in equality comparisons, whereas === does not.”

Equality comparison between strings and numbers:

  • If Type(x) is a number and Type(y) is a string, the result x == ToNumber(y) is returned.
  • If Type(x) is a string and Type(y) is a number, return ToNumber(x) == y.

Comparison of equality between other types and Boolean types:

  • If Type(x) is Boolean, return ToNumber(x) == y;
  • If Type(y) is Boolean, return x == ToNumber(y).

Equality between null and undefined:

  • If x is null and y is undefined, the result is true.
  • If x is undefined and y is null, the result is true.

Equality comparison between objects and non-objects:

  • If Type(x) is a string or number and Type(y) is an object, return x == ToPrimitive(y);
  • Return ToPromitive(x) == y if Type(x) is an object and Type(y) is a string or number.

grammar

error

Use variables ahead of time

The ES6 specification defines a new concept called TDZ (Temporal Dead Zone).

TDZ refers to a situation where a variable in your code cannot be referenced because it has not been initialized.

The most intuitive example of this is the let block scope in the ES6 specification:

{
  a = 2; // ReferenceError!
  let a;
}
Copy the code

A = 2 attempts to use this variable before let A initializes A (whose scope is {.. }, here is the TDZ of A, will generate an error.

Interestingly, using typeof on undeclared variables does not generate an error (see Chapter 1), but does in TDZ:

{
  typeof a; // undefined
  typeof b; // ReferenceError! (TDZ)
  let b;
}
Copy the code

The callback

Save the callback

Construct a timeout validation tool:

function timeoutify(fn, delay) {
  var intv = setTimeout(function() {
    intv = null
    fn(new Error('Timeout! '))
  }, delay)

  return function() {
    // Have not timed out yet?
    if (intv) {
      clearTimeout(intv)
      fn.apply(this.arguments)}}}Copy the code

Here’s how to use it:

// Use the 'error-first style' callback design
function foo(err, data) {
  if (err) {
    console.error(err)
  }
  else {
    console.log(data)
  }
}

ajax('http://some.url.1', timeoutify(foo, 500))
Copy the code

What if you’re not sure if the API you’re interested in will execute asynchronously forever? You can create a version of asyncify(..) similar to this validation concept. Tools:

function asyncify(fn) {
  var orig_fn = fn,
    intv = setTimeout(function() {
      intv = null
      if (fn) fn()
    }, 0)

  fn = null

  return function() {
    // Trigger too fast, before timer inTV triggers indicating asynchronous conversion occurs?
    if (intv) {
      fn = orig_fn.bind.apply(
        orig_fn,
        // Add the wrapper's this to bind(..) In the arguments to the call,
        // All incoming arguments that can be curried
        [this].concat([].slice.call(arguments)))}// It is already asynchronous
    else {
      // Call the original function
      orig_fn.apply(this.arguments)}}}Copy the code

You can use asyncify(..) like this :

function result(data) {
  console.log(a)
}

var a = 0

ajax('.. pre-cached-url.. ', asyncify(result))
a++
Copy the code

Whether the Ajax request is already in the cache and attempts to invoke the callback immediately, or is fetched from the network and done asynchronously in the future, this code always outputs 1 instead of 0 — result(..) Can only be called asynchronously, which means a++ has a chance at result(..) Run before.

Promise

Promise trust problem

The callback is not called

Provide a solution to timeout handling:

// A tool used to timeout a Promise
function timeoutPromise(delay) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      reject('Timeout! ')
    }, delay)
  })
}

// Set foo() to timeout
Promise.race([
  foo(),
  timeoutPromise(3000)
])
.then(
  function() {
    // foo(..) Finish in time!
  },
  function(err) {
    // Either foo() was rejected, or just didn't finish on time
 	// Check err to see what the case is})Copy the code

Chain flow

To further illustrate links, let’s generalize the delayed Promise creation (no resolution message) process into a tool that can be reused in multiple steps:

function delay(time) {
  return new Promise(function(resolve, reject) {
    setTimeout(resolve, time)
  })
}

delay(100) / / step 1
  .then(function STEP2() {
    console.log("step 2 (after 100ms)")
    return delay(200)
  })
  .then(function STEP3() {
    console.log("step 3 (after another 200ms)")
  })
  .then(function STEP4() {
    console.log("step 4 (next Job)")
    return delay(50)
  })
  .then(function STEP5() {
    console.log("step 5 (after another 50ms)")})Copy the code

Calling Delay (200) creates a promise that will complete after 200ms, and then we start with the first then(..) Return the promise in the completion callback, which results in the second THEN (..) Wait for the 200ms promise.

Promise the limitations

Sequential error handling

The design limitations of promises (chain-calling) create an easy trap, in which errors in the Promise chain are easily ignored without meaning to.

There are other things to consider about the Promise error. Because a Promise chain is simply member promises linked together, the entire chain is not identified as an individual entity, which means that there is no external method to watch for errors that might occur.

If you build a Promise chain without error handlers, any errors anywhere in the chain will propagate through the chain until a rejection handler is registered at some step. In this particular case, a single reference to the last promise in the chain is sufficient (p in the code below), because you can register the rejection handler there and the handler will be notified of all propagated errors:

// foo(..) , STEP2(..) And step 3 (..) Are tools that support Promise
var p = foo(42)
  .then(STEP2)
  .then(STEP3);
Copy the code

While this may be confusing, the P here does not point to the first promise in the chain (the one generated by calling foo(42)), but to the last promise, the one from calling THEN (STEP3).

Also, none of the steps in the Promise chain explicitly handles its own errors. This means that you can register a rejection error handler on p that will be notified of any errors that occur anywhere in the chain:

p.catch(handleErrors); 
Copy the code

However, if any step in the chain actually does its own error handling (possibly in a hidden or abstract invisible way), then your handleErrors(..) You wouldn’t have been notified. This may be what you want — this is a “processed rejection,” after all — but it may not be. The inability to get clear error notification (for a specific “processed” rejection) is also a defect that limits the functionality of some use cases.

Basically, this is equivalent to try.. Limitations of catch: Try.. A catch might catch an exception and simply swallow it. So this isn’t a limitation unique to Promise, but it may be a trap we want to get around.

Unfortunately, many times there are no references reserved for intermediate steps in the Promise chain sequence. Therefore, without such references, you cannot correlate error handlers to reliably check for errors.

A single value

By definition, a Promise can only have a completion value or a reason for rejection. In simple examples, this is not a problem, but in more complex scenarios, you may find it a limitation.

The general recommendation is to construct a value wrapper (such as an object or array) to hold such multiple pieces of information. This solution worked, but it was ugly and unwieldy to encapsulate and unencapsulate each step in the Promise chain.

  1. Split the value

Sometimes, you can take this as a sign that you should break the question down into two or more promises.

Imagine you have a tool foo(..) , it can asynchronously produce two values (x and y) :

function getY(x) { 
  return new Promise(function(resolve, reject){ 
    setTimeout(function(){ 
      resolve((3 * x) - 1); 
    }, 100); 
  });
} 

function foo(bar, baz) { 
  var x = bar * baz; 
  return getY(x).then(function(y){ 
    // Encapsulate the two values in a container
    return [x, y]; 
  }); 
} 

foo(10.20).then(function(msgs){ 
  var x = msgs[0]; 
  var y = msgs[1]; 
  console.log(x, y); / / 200 599
}); 
Copy the code

First, let’s reorganize foo(..) So that you no longer need to wrap x and y in an array of values to be transmitted through a promise. Instead, we can wrap each value into its own promise:

function foo(bar, baz) { 
  var x = bar * baz; 
  
  // Return two promises
  return [ 
    Promise.resolve(x), 
    getY(x) 
  ]; 
} 

Promise.all( 
  foo(10.20) 
).then(function(msgs){ 
  var x = msgs[0]; 
  var y = msgs[1]; 
  console.log(x, y); 
}); 
Copy the code

Is an array of promises really better than an array of values passed to a single promise? Grammatically speaking, this is not much of an improvement.

However, this approach is more in line with Promise’s design philosophy. This approach is much easier if you need to refactor your code later to separate the x and y calculations. Instead of placing this detail in foo(..), it is up to the calling code to decide how to arrange the two promises. Internal abstraction, which is cleaner and more flexible. Promise.all([..] ), of course, this is not the only option.

  1. Passing parameters

var x = .. And var y =.. Assignment is still a cumbersome overhead. We can use a certain functional trick in an assistive tool:

function spread(fn) { 
  return Function.apply.bind(fn, null); 
} 

Promise.all( 
  foo(10.20) 
).then(spread(function(x, y){ 
  console.log(x, y); / / 200 599
})) 
Copy the code

That’s better! Of course, you can make this function trick online to avoid extra AIDS:

Promise.all( 
  foo(10.20) 
).then(Function.apply.bind( 
  function(x, y){ 
    console.log(x, y); / / 200 599
  },
  null
)); 
Copy the code

These tricks may be neat, but ES6 offers a better answer: deconstruction. Array destruct assignment looks like this:

Promise.all( 
  foo(10.20) 
).then(function(msgs){ 
  var [x, y] = msgs; 
  console.log(x, y); / / 200 599
}); 
Copy the code

Best of all, though, ES6 provides array argument deconstruction:

Promise.all( 
  foo(10.20) 
) 
.then(function([x, y]){ 
  console.log(x, y); / / 200 599
}); 
Copy the code

Now we’re in line with the concept of “one Promise per Promise” while keeping the amount of boilerplate code repetition to a minimum!

Single resolution

An essential feature of promises is that they can only be made once (fulfilled or rejected). In many asynchronous cases, you only get a value once, so this can work fine.

However, there are many asynchronous situations where another pattern fits — one that is similar to an event or data flow pattern. On the face of it, it’s not clear that Promise would work well, if not completely unusable, in such use cases. If you don’t build significant abstractions on top of promises, promises can’t support multi-valued resolution processing at all.

Imagine a scenario where you might initiate a series of asynchronous steps in response to some kind of stimulus (like an event) that can happen multiple times, such as a button click.

This may not work as you expect:

// click(..) Bind the "click" event to a DOM element
// request(..) Is the Promise enabled Ajax defined earlier
var p = new Promise(function(resolve, reject){ 
  click("#mybtn", resolve); 
}); 

p.then(function(evt){ 
  var btnID = evt.currentTarget.id; 
  return request("http://some.url.1/? id=" + btnID); 
}).then(function(text){ 
  console.log(text); 
}); 
Copy the code

This only works if your app only needs to respond to a button click once. If the button is clicked a second time, promise P has already resolved, so the second resolve(..) The call is ignored.

Therefore, you might want to transform this paradigm and create an entire new Promise chain for each occurrence:

click("#mybtn".function(evt){ 
  var btnID = evt.currentTarget.id; 
  request("http://some.url.1/? id=" + btnID).then(function(text){ 
    console.log(text); 
  }); 
}); 
Copy the code

This works because an entire new sequence of Promises is launched for each “click” event on the button.

This is ugly because you need to define the entire Promise chain in the event handler. In addition, the design somewhat undermines the idea of separation of focus and function (SoC). You’ll probably want to put the definition of the event handler and the definition of the response to the event (that Promise chain) in different places in your code. This is hard to do in this mode without a helper mechanism.

Thank you

If this article helped you, give it a thumbs up! Thanks for reading.