Skip to content

Latest commit

 

History

History
1406 lines (1148 loc) · 47.9 KB

JS-br.md

File metadata and controls

1406 lines (1148 loc) · 47.9 KB

Table of Contents generated with DocToc

Tipos incorporados

O JavaScript define sete tipos incorporados, dos quais podem ser divididos em duas categorias Primitive Type e Object.

Existem seis tipos primitivos: null, undefined, boolean, number, string e symbol .

Em JavaScript, não existe inteiros de verdade, todos os números são implementados em dupla-precisão 64-bit em formato binário IEEE 754. Quando nós usamos números de pontos flutuantes, iremos ter alguns efeitos colaterais. Aqui está um exemplo desses efeitos colaterais.

0.1 + 0.2 == 0.3 // false

Para tipos primitivos, quando usamos literais para inicializar uma variável, ela tem apenas um valor literal, ela não tem um tipo. Isso será convertido para o tipo correspondente apenas quando necessário.

let a = 111 // apenas literais, não um número
a.toString() // convertido para o objeto quando necessário

Objeto é um tipo de referência. Nós iremos encontrar problemas sobre cópia rasa e cópia profunda quando usando ele.

let a = { name: 'FE' }
let b = a
b.name = 'EF'
console.log(a.name) // EF

Conversão de Tipos

Convertendo para Boleano

Quando a condição é julgada, que não seja undefined, null, false, NaN, '', 0, -0, os esses valores, incluindo objetos, são convertidos para true.

De objetos para tipos primitivos

Quando objetos são convertidos, valueOf e toString serão chamados, respectivamente em ordem. Esses dois métodos também são sobrescritos.

let a = {
    valueOf() {
        return 0
    }
}

Operadores Aritméticos

Apenas para adicão, se um dos parâmentros for uma string, o outro será convertido para uma string também. Para todas as outras operações, enquanto se um dos parâmetros for um número, o outro será convertido para um número.

Adicões invocaram três tipos de conversões de tipos: para tipos primitivos, para números e string:

1 + '1' // '11'
2 * '2' // 4
[1, 2] + [2, 1] // '1,22,1'
// [1, 2].toString() -> '1,2'
// [2, 1].toString() -> '2,1'
// '1,2' + '2,1' = '1,22,1'

Observe a expressão 'a' + + 'b' para adição:

'a' + + 'b' // -> "aNaN"
// uma vez que + 'b' -> NaN
// Você deve ter visto + '1' -> 1

== operador

toPrimitive na figura acima é convertido objetos para tipos primitivos.

=== é geralmente recomendado para comparar valores. Contudo, se você gostaria de checar o valor null, você pode usar xx == null.

Vamos dar uma olhada no exemplo [] == ![] // -> true. O processo seguinte explica por que a expressão é true:

// [] convertendo para true, então pegue o oposto para false
[] == false
// com #8
[] == ToNumber(false)
[] == 0
// com #10
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// com #6
0 == 0 // -> true

Operador de comparação

  1. Se for um objeto, toPrimitive é usado.
  2. Se for uma string, o caractere índice unicode é usado.

Typeof

typeof também permite exibir o tipo correto de tipos primitivos, exceto null:

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b não foi declarado, mas ainda pode ser exibido como undefined

Para objeto, typeof irá sempre exibir object, exceto function:

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

Quanto a null, ele é sempre tratado como um object pelo typeof, apesar de ser um tipo primitivo, e esse é um bug que que existe a um bom tempo.

typeof null // 'object'

Por que isso acontece? Porque a versão inicial do JS era baseada em sistemas de 32-bits, do qual armazenava a informação do tipo de variável em bits mais baixos por considerações de performance. Essas começam com objetos 000, e todos os bits de null são zero, então isso é erroneamente tratado como um objeto. Apesar do código atual verificar se os tipos internos mudaram, esse bug foi passado para baixo.

Nós podemos usar Object.prototype.toString.call(xx) se quisermos pegar o tipo de dado correto da variável, e então obtemos uma string como [object Type]:

let a
// Podemos declarar `undefined` da seguinte maneira
a === undefined
// mas a palavra não reservada `undefined` pode ser re assinada em versões antigas dos browsers
let undefined = 1
// vai dar errado declarar assim
// então nós podemos usar o seguinte método, com menos código
// ele sempre vai retornar `undefined`, tanto faz vir seguido de `void`
a === void 0

New

  1. Crie um novo objeto
  2. Encadei o prototype
  3. Ligue o this
  4. Retorne um novo objeto

Os quatro passos acima vão acontecer no processo chamado new. Podemos também tentar implementar o new nós mesmos:

function create() {
  // Crie um objeto vázio
  let obj = new Object()
  // Obtenha o construtor
  let Ctor = [].shift.call(arguments)
  // Encadeie para o prototype
  obj.__proto__ = Ctor.prototype
  // Ligue o this, execute o construtor
  let result = Con.apply(obj, arguments)
  // Tenha certeza que o novo é um objeto
  return typeof result === 'object'? result : obj
}

Instância de um novo objeto são todas criadas com new, seja ele function Foo(), ou let a = { b: 1 } .

É recomendado criar os objetos usando notação literal (seja por questões de performance ou legibilidade), uma vez que é necessário um look-up para Object atravessar o escopo encadeado quando criando um objeto usando new Object(), mas você não precisa ter esse tipo de probelma quando usando literais.

function Foo() {}
// Função são sintáticamente amigáveis
// Internamente é equivalente a new Function() 
let a = { b: 1 }
// Dentro desse lireal, `new Object()` também é usado

Para new, também precisamos prestar atenção ao operador precedente:

function Foo() {
    return this;
}
Foo.getName = function () {
    console.log('1');
};
Foo.prototype.getName = function () {
    console.log('2');
};

new Foo.getName();   // -> 1
new Foo().getName(); // -> 2

Como você pode ver na imagem acima, new Foo() possui uma alta prioridade sobre new Foo, então podemos dividir a ordem de execução do código acima assim:

new (Foo.getName());
(new Foo()).getName();

Para a primeira função, Foo.getName() é executado primeiro, então o resultado é 1; Para mais tarte, ele primeiro executa new Foo() para criar uma instância, então encontrar a função getName no Foo via cadeia de prototype, então o resultado é 2.

This

This, um conceito que é confuso para maioria das pessoas, atualmente não é difícil de entender enquanto você lembrar as seguintes regras:

function foo() {
  console.log(this.a);
}
var a = 1;
foo();

var obj = {
  a: 2,
  foo: foo
};
obj.foo();

// Nas duas situações acima, `this` depende apenas do objeto ser chamado antes da função,
// e o segundo caso tem uma alta prioriade sobre o primeiro caso.

// o seguinte cenário tem uma alta prioridade, `this` só ficará ligado para c,
// e não existe uma maneira de mudar o que `this` está limitado

var c = new foo();
c.a = 3;
console.log(c.a);

// finalmente, usando `call`, `apply`, `bind` para mudar o que o `this` é obrigado,
// em outro cenário onde essa prioridade é apenas o segundo `new`

Entendendo sobre as várias situações acima, nós não vamos ser confundidos pelo this na maioria dos casos. Depois, vamos dar uma olhada no this nas arrow functions:

function a() {
  return () => {
    return () => {
      console.log(this);
    };
  };
}
console.log(a()()());

Atualmente, as arrow function não tem o this, this na função acima apenas depende da primeira função externa que não é uma arrow function. Nesse caso, this é o padrão para window porque chamando a iguala a primeira condição nos códigos acima. Também, o que o this está ligado não ira ser mudado por qualquer código uma vez que o this estiver ligado em um contexto.

Instanceof

O operador instanceof consegue checar corretamente o tipo dos objetos, porque o seu mecanismo interno encontra se o tipo do prototype pode ser encontrado na cadeia de prototype do objeto.

vamos tentar implementar ele:

function instanceof(left, right) {
    // obtenha o type do `prototype`
    let prototype = right.prototype
    // obtenha o `prototype` do objeto
    left = left.__proto__
    // verifique se o tipo do objeto é igual ao prototype do tipo
    while (true) {
    	if (left === null)
    		return false
    	if (prototype === left)
    		return true
    	left = left.__proto__
    }
}

Scope

Executar código JS deveria gerar execução do contexto, enquanto o código não é escrito na função, ele faz parte da execução do contexto global. O código na função vai gerar executação do contexto da função. Existe também uma execução do contexto do eval, do qual basicamente não é mais usado, então você pode pensar apenas em duas execuções de contexto.

O atributo [[Scope]] é gerado no primeiro estágio de geração de contexto, que é um ponteiro, corresponde a linked list do escopo, e o JS vai procurar variáveis através dessas linked list no contexto global.

Vamos olhar um exemplo common, var:

b() // chama b
console.log(a) // undefined

var a = 'Hello world'

function b() {
	console.log('call b')
}

Ele sabe que funcões e variáveis são içadas acima em relação aos outputs. A explicação usual para o hoisting diz que as declarações são ‘movidas’ para o topo do código, e não existe nada de errado com isso e é fácil de todo mundo entender. Mas para um explicação mais precisa deveria ser algo como:

Haveria dois estágios quando a execução do contexto é gerada. O primeiro estágio é o estágio de criação(para ser mais epecífico, o passo de geração variáveis objeto), no qual o interpretador de JS deveria encontrar variáveis e funções que precisam ser içadas, e aloca memória para eles atecipadamente, então as funções deveriam ser guardadas na memória internamente, mas variáveis seriam apenas declaradas e assinadas para undefined, assim sendo, nós podemos usar elas adiante no segundo estágio (a execução do código no estágio)

No processo de içar, a mesma função deveria sobrescrever a última função, e funções tem alta prioridade sobre variáveis içadas.

b() // chama segundo b

function b() {
	console.log('chama b primeiro')
}
function b() {
	console.log('chama b segundo')
}
var b = 'Hello world'

Usando var é mais provável error-prone, portanto ES6 introduziu uma nova palava-chave let. let tem uma característica importante que ela não pode ser usada antes de declarada, que conflita com o ditado comum que let não tem a habilidade de içar. De fato, let iça a declaracão, mas não é assinada, por causa da temporal dead zone.

Closure

A definição de closure é simples: a função A retorna a função B, e a função b consegue acessar as variáveis da função A, portanto a função B é chamada de closure.

function A() {
  let a = 1
  function B() {
      console.log(a)
  }
  return B
}

Se você estiver se perguntando por que a função B também consegue se referenciar as variáveis da função A enquanto a função A aparece a partir da stack de chamadas? Porque as variáveis na função A são guardadas na pilha nesse momento. O motor atual do JS consegue indentificar quais variáveis precisam ser salvas na heap e quais precisam ser salvas na stack por análise de fuga.

Uma pergunta classica de entrevista é usando closure em loops para resolver o problema de usar var para definir funções:

for ( var i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
)

Em primeirio lugar, todos os loops vão ser executados completamente porque setTimeout é uma função assíncrona, e nesse momento i é 6, então isso vai exibir um bando de 6.

Existe três soluções, closure é a primeira:

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}

A segunda é fazer o uso do terceiro parâmetro do setTimeout:

for ( var i=1; i<=5; i++) {
    setTimeout( function timer(j) {
        console.log( j );
    }, i*1000, i);
}

A terceira é definir o i usando let:

for ( let i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}

Para let, ele vai criar um escopo de block-level, do qual é equivalente a:

{
    // Forma o escopo block-level
  let i = 0
  {
    let ii = i
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
  }
  i++
  {
    let ii = i
  }
  i++
  {
    let ii = i
  }
  ...
}

Prototypes

Cada função, além de Function.prototype.bind(), tem uma propriedade interna, denotado como prototype, do qual é uma referência para o prototype.

Cada objeto tem uma propriedade interna, denotada como __proto__, que é uma referência para o prototype do construtor que criou o objeto. Essa propriedade é atualmente referenciada ao [[prototype]], mas o [[prototype]] é uma propriedade interna que nós não podemos acessar, então usamos o __proto__ para acessar ele.

Objetos podem usar __proto__ para procurar propriedade que não fazem parte do objeto, e __proto__ conecta os objetos juntos para formar uma cadeida de prototype.

Herança

No ES5, podemos resolve os problema de herança usando os seguintes passos:

function Super() {}
Super.prototype.getNumber = function() {
  return 1
}

function Sub() {}
let s = new Sub()
Sub.prototype = Object.create(Super.prototype, {
  constructor: {
    value: Sub,
    enumerable: false,
    writable: true,
    configurable: true
  }
})

A idéia de herança implementada acima é para definir o prototype da classe filho como o prototype da classe pai.

No ES6, podemos facilmente resolver esse problema com a sintaxe class:

class MyDate extends Date {
  test() {
    return this.getTime()
  }
}
let myDate = new MyDate()
myDate.test()

Contudo, ES6 não é compátivel com todos os navegadores, então usamos o Babel para compilar esser código.

Se chamar myDate.test() com o código compilado, você vai ficar surpreso de ver que existe um erro:

Porque existem restrições no baixo nível do JS, se a instância não for construida pelo Date, ele não pode chamar funções no Date, que também explica a partir de outro aspecto que herança de Class no ES6 é diferente das heranças gerais na sintaxe do ES5.

Uma vez o baixo nível dos limites do JS que a instância deve ser construido pelo Date, nós podemos tentar outra maneira de implementar herança:

function MyData() {

}
MyData.prototype.test = function () {
  return this.getTime()
}
let d = new Date()
Object.setPrototypeOf(d, MyData.prototype)
Object.setPrototypeOf(MyData.prototype, Date.prototype)

A implementação da idéia acima sobre herança: primeiro cria uma instância da classe do pai => muda o original __proto__ de instância, conectado ao prototype da classe do filho => muda o __proto__ da classe do filho prototype para o prototype da classe do pai.

A herança de implementação com o método acima pode perfeitamente resolver a restrição no baixo nível do JS.

Cópia rasa e profunda

let a = {
    age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2

A partir do exemplo acima, nós podemos ver que se você assinar um objeto para uma variável, então os valores dos dois vão ter a mesma referência, um muda o outro muda adequadamente.

Geralmente, nós não queremos que tal problema apareça durante o desensolvimento, portanto podemos usar a cópia rasa para resolver esse problema.

Cópia rasa

Primeiramente podemos resolver o problema através do Object.assign:

let a = {
    age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1

Certamente, podemos usar o spread operator (...) para resolver o problema:

let a = {
    age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // 1

Geralmente, a cópia rasa pode resolver a maioria dos problemas, mas precisamos da cópia profunda quando encontrado a seguinte situação:

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = {...a}
a.jobs.first = 'native'
console.log(b.jobs.first) // native

A cópia rasa resolve apenas o problema na primeira camada. Se o objeto contém objetos, então ele retorna para o topico inicial que os dois valores compartilham a mesma referência. Para resolver esse problema, precisamos introduzir a cópia profunda.

Cópia profunda

O problema pode geralmente ser resolvido por JSON.parse(JSON.stringify(object))

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

Mas esse método também tem seus limites:

  • ignora undefined
  • incapaz de serializar função
  • incapaz de resolver referência circular de um objeto
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

Se um objto é uma referência circular como o exemplo acima, você vai encontrar o método JSON.parse(JSON.stringify(object)) ele não pode fazer a cópia profunda desse objeto:

Quando lidando com uma função ou undefined, o objeto pode não ser serializado adequedamente.

let a = {
    age: undefined,
    sex: Symbol('male'),
    jobs: function() {},
    name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}

No caso acima, você pode perceber que o método ignora a função e undefined.

A maioria dos dados conseguem ser serializados, então esse método resolve a maioria dos problemas, e como uma função embutida, ele tem uma performance melhor quando lidando com a cópia profunda. Certamente, você pode usar the deep copy function of lodash quando sues dados contém os três casos acima.

Se o objeto que você quer copiar contém um tipo embutido mas não contém uma função, você pode usar MessageChannel

function structuralClone(obj) {
  return new Promise(resolve => {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
  });
}

var obj = {a: 1, b: {
    c: b
}}

// preste atenção que esse método é assíncrono
// ele consegue manipular `undefined` e referência circular do objeto
(async () => {
  const clone = await structuralClone(obj)
})()

Modularização

Com o Babel, nós conseguimos usar a ES6 modularização:

// arquivo a.js
export function a() {}
export function b() {}
// arquivo b.js
export default function() {}

import {a, b} from './a.js'
import XXX from './b.js'

CommonJS

CommonJS é uma aspecto único do Node. É preciso Browserify para o CommonJS ser usado nos navegadores.

// a.js
module.exports = {
    a: 1
}
// ou
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1

No código acima, module.exports e exports podem causar confusão. Vamos dar uma olhada na implementação interna:

var module = require('./a.js')
module.a
// esse é o empacotador atual de uma função a ser executada imediatamente, de modo que não precisamos bagunçar as variáveis globais.
// O que é importante aqui é que o módulo é apenas uma variável do Node.
module.exports = {
    a: 1
}
// implementação básica
var module = {
  exports: {} // exporta em um objeto vázio
}
// Esse é o por que o exports e module.exports tem usos similares.
var exports = module.exports
var load = function (module) {
    // to be exported
    var a = 1
    module.exports = a
    return module.exports
};

Vamos então falar sobre module.exports e exports, que tem uso similar, mas um não atribui um valor para exports diretamente. A tarefa seria um no-op.

A diferença entre as modularizações no CommonJS a no ES6 são:

  • O antigo suporta importes dinamico, que é require(${path}/xx.js); o último não suporta isso ainda, mas existem propostas.
  • O antigo usa importes síncronos. Desde de que usado no servidor os arquivos são locais, não importa muito mesmo se o import síncrono bloqueia a main thread. O último usa importe assíncrono, porque ele é usado no navegador em que os arquivos baixados são precisos. O processo de renderização seria afetado muito se assíncrono importe for usado.
  • O anterior copia os valores quando exportando. Mesmo se o valor exportado mudou, os valores importados não irão mudar. Portanto, se os valores devem ser atualizados, outro importe precisa acontecer. Contudo, o último usa ligações em tempo real, os valores importados são importados no mesmo endereço de memória, então o valor importado muda junto com os importados.
  • Em execução o último é compilado para require/exports.

AMD

AMD é apresentado por RequireJS.

// AMD
define(['./a', './b'], function(a, b) {
    a.do()
    b.do()
})
define(function(require, exports, module) {
    var a = require('./a')  
    a.doSomething()   
    var b = require('./b')
    b.doSomething()
})

A diferença entre call apply bind

Primeiro, vamos falar a diferença entre os dois antigos.

Ambos call e apply são usados para mudar o que o this se refere. Seu papel é o mesmo, mas a maneira de passar os parâmetros é diferente.

Além do primeiro parâmetro, call também aceita uma lista de argumentos, enquanto apply aceita um único array de argumentos.

let a = {
  value: 1
}
function getValue(name, age) {
  console.log(name)
  console.log(age)
  console.log(this.value)
}
getValue.call(a, 'yck', '24')
getValue.apply(a, ['yck', '24'])

simulação para implementar call e apply

Consideramos implementar eles a partir das seguintes regras:

  • Se o primeiro parâmetro não foi passado, então o primeiro será o padrão window;

  • Mude a referência do this, que faz um novo objeto capaz de executar a função. Então vamos pensar assim: adicione a função para um novo objeto e então delete ele depois da execução.

Function.prototype.myCall = function (context) {
  var context = context || window
  var fn = Symbol('fn')
  // Adiciona uma propriedade ao `context`
  // getValue.call(a, 'yck', '24') => a.fn = getValue
  context[fn] = this
  // pega os parâmentros do `context`
  var args = [...arguments].slice(1)
  // getValue.call(a, 'yck', '24') => a.fn('yck', '24')
  var result = context[fn](...args)
  // deleta fn
  delete context[fn]
  return result
}

O exemplo acima é a idéia central da simulação do call, e a implementação do apply é similar.

Function.prototype.myApply = function (context) {
  var context = context || window
  var fn = Symbol('fn')
  context[fn] = this

  var result
  // Existe a necessidade de determinar se guarda o segundo parâmentro
  // Se o segundo parâmetro existir, espalhe ele
  if (arguments[1]) {
    result = context[fn](...arguments[1])
  } else {
    result = context[fn]()
  }

  delete context[fn]
  return result
}

A regra do bind é a mesma das outras duas, exceto que ela retorna uma função. E nós podemos implementar currying com o bind

vamos simular o bind:

Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  var _this = this
  var args = [...arguments].slice(1)
  // retorna uma função
  return function F() {
    // Nós podemos usar `new F()` porque ele retorna uma função, então precisamos determinar
    if (this instanceof F) {
      return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
  }
}

Implementação de Promise

Promise é a nova sintaxe introduzida pelo ES6, que resolve os problemas de callback hell.

Promise pode ser visto como um estado de máquina e o seu estado inicial é pending. Nós podemos mudar o estado para resolved ou rejected usando as funções resolve e reject. Uma vez que o state mudou, ele não pode mudar novamente.

A função then retorna uma instância da Promise, do qual é uma nova instância ao invés do anterior. E existe por que a especificação de estado da Promise que adiciona para o estado pending, outro estado não pode ser mudado, e multiplas chamadas a função then serão insignificantes se a mesma instância for retornada.

Para then, ele pode essencialmente ser visto como flatMap`:

// árvore de estados
const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';
// promise aceita um argumento na função que será executada imediatamente.
function MyPromise(fn) {
  let _this = this;
  _this.currentState = PENDING;
  _this.value = undefined;
  // Save o callback do `then`, apenas em cache quando o estado da promise for pending,
  // no máximo será cacheado em cada instância
  _this.resolvedCallbacks = [];
  _this.rejectedCallbacks = [];

  _this.resolve = function(value) {
    // execute assícronamente para garantir a ordem de execução
    setTimeout(() => {
      if (value instanceof MyPromise) {
        // se o valor é uma Promise, execute recursivamente
        return value.then(_this.resolve, _this.reject)
      }
      if (_this.currentState === PENDING) {
        _this.currentState = RESOLVED;
        _this.value = value;
        _this.resolvedCallbacks.forEach(cb => cb());
      }
    })
  }

  _this.reject = function(reason) {
    // execute assícronamente para garantir a ordem de execução
    setTimeout(() => {
      if (_this.currentState === PENDING) {
        _this.currentState = REJECTED;
        _this.value = reason;
        _this.rejectedCallbacks.forEach(cb => cb());
      }
    })
  }

  // para resolver o seguinte problema
  // `new Promise(() => throw Error('error))`
  try {
    fn(_this.resolve, _this.reject);
  } catch (e) {
    _this.reject(e);
  }
}

MyPromise.prototype.then = function(onResolved, onRejected) {
  const self = this;
  // especificação 2.2.7, `then` deve retornar uma nova promise
  let promise2;
  // especificação 2.2, ambos `onResolved` e `onRejected` são argumentos opcionais
  // isso deveria ser ignorado se `onResolved` ou `onRjected` não for uma função,
  // do qual implementa a penetrar a passagem desse valor
  // `Promise.resolve(4).then().then((value) => console.log(value))`
  onResolved = typeof onResolved === 'function' ? onResolved : v => v;
  onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

  if (self.currentState === RESOLVED) {
    return (promise2 = new MyPromise((resolve, reject) => {
      // especificação 2.2.4, encapsula eles com `setTimeout`,
      // em ordem para garantir que `onFulfilled` e `onRjected` executam assícronamente
      setTimeout(() => {
        try {
          let x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === REJECTED) {
    return (promise2 = new MyPromise((resolve, reject) => {
      // execute `onRejected` assícronamente
      setTimeout(() => {
        try {
          let x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }))
  }

  if (self.currentState === PENDING) {
    return (promise2 = new MyPromise((resolve, reject) => {
      self.resolvedCallbacks.push(() => {
         // Considerando que isso deve lançar um erro, encapsule eles com `try/catch`
        try {
          let x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });

      self.rejectedCallbacks.push(() => {
        try {
          let x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      })
    }))
  }
}

// especificação 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
  // especificação 2.3.1,`x` e  `promise2` não podem ser referenciados para o mesmo objeto,
  // evitando referência circular
  if (promise2 === x) {
    return reject(new TypeError('Error'));
  }

  // especificação 2.3.2, se `x` é uma Promise e o estado é `pending`,
  // a promisse deve permanecer, se não, ele deve ser executado.
  if (x instanceof MyPromise) {
    if (x.currentState === PENDING) {
      // chame a função `resolutionProcedure` novamente para 
      // confirmar o tipo de argumento que x resolve
      // Se for um tipo primitivo, irá ser resolvido novamente
      // passando o valor para o próximo `then`.
      x.then((value) => {
        resolutionProcedure(promise2, value, resolve, reject);
      }, reject)
    } else {
      x.then(resolve, reject);
    }
    return;
  }

  // especificação 2.3.3.3.3
  // se ambos `reject` e `resolve` forem executado, a primeira execução 
  // de sucesso tem precedência, e qualquer execução é ignorada
  let called = false;
  // especificação 2.3.3, determina se `x` é um objeto ou uma função 
  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    // especificação 2.3.3.2, se não conseguir obter o `then`, execute o `reject`
    try {
      // especificação 2.3.3.1
      let then = x.then;
      // se `then` é uma função, chame o `x.then`
      if (typeof then === 'function') {
        // especificação 2.3.3.3
        then.call(x, y => {
          if (called) return;
          called = true;
          // especificação 2.3.3.3.1
          resolutionProcedure(promise2, y, resolve, reject);
        }, e => {
          if (called) return;
          called = true;
          reject(e);
        });
      } else {
        // especificação 2.3.3.4
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // especificação 2.3.4, `x` pertence ao tipo primitivo de dados
    resolve(x);
  }
}

O código acima, que é implementado baseado em Promise / A+ especificação, pode passar os testes completos de promises-aplus-tests

Implementação do Generator

Generator é uma funcionalidade sintática adicionada no ES6. Similar a Promise, pode ser usado para programação assíncrona.

// * significa que isso é uma função Generator
// yield dentro de um bloco pode ser usado para pausar a execução
// next consegue resumir a execução
function* test() {
  let a = 1 + 2;
  yield 2;
  yield 3;
}
let b = test();
console.log(b.next()); // >  { value: 2, done: false }
console.log(b.next()); // >  { value: 3, done: false }
console.log(b.next()); // >  { value: undefined, done: true }

Como podemos dizer no código acima, a função com um * teria a execução da função next. Em outras palavras, a execução de função retorna um objeto. Toda chamada a função next pode continuar a execução do código pausado. Um simples implementação da função Generator é mostrada abaixo:

// cb é a função 'test' compilada
function generator(cb) {
  return (function() {
    var object = {
      next: 0,
      stop: function() {}
    };

    return {
      next: function() {
        var ret = cb(object);
        if (ret === undefined) return { value: undefined, done: true };
        return {
          value: ret,
          done: false
        };
      }
    };
  })();
}
// Depois da compilação do babel's, a função 'test' retorna dentro dessa:
function test() {
  var a;
  return generator(function(_context) {
    while (1) {
      switch ((_context.prev = _context.next)) {
        // yield separa o código em diversos blocos
        // cada chamada 'next' executa um bloco de código
        // e indica o próximo bloco a ser executado
        case 0:
          a = 1 + 2;
          _context.next = 4;
          return 2;
        case 4:
          _context.next = 6;
          return 3;
        // execução completa
        case 6:
        case "end":
          return _context.stop();
      }
    }
  });
}

Debouncing

Tendo você encontrado esse problema e seu dia-a-dia no desenvolvimento: como fazer uma computação complexa em um evento de scroll ou prevenir o "segundo clique acidental" no butão?

Esses requisitos podem ser alcançados com funcões debouncing. Especialmente para o primeiro, se uma computação complexa estiver sendo chamado em frequentes eventos de callbacks, existe uma grande chance que a página se torne lenta. É melhor combinar essas multiplas computações e uma, e apenas operar em determinado periodo de tempo. Desde que existe muitas bibliotecas que implementam debouncing, nós não construimos nosso próprio aqui e vamos pegar o código do underscore para explicar o debouncing:

/**
 * função underscore debouncing. Quando a função callback é chamada em série, a funcão vai executar apenas quando o tempo ideal é maior ou igual ao `wait`.
 *
 * @param  {function} func        função callback
 * @param  {number}   wait        tamanho do intervalo de espera
 * @param  {boolean}  immediate   quando definido para true, func é executada imadiatamente
 * @return {function}             retorna a função a ser chamada pelo cliente
 */
_.debounce = function(func, wait, immediate) {
    var timeout, args, context, timestamp, result;

    var later = function() {
      // compara now para o último timestamp
      var last = _.now() - timestamp;
      // se o tempo de intervalo atual é menor então o set interval é maior que 0, então reinicie o timer.
      if (last < wait && last >= 0) {
        timeout = setTimeout(later, wait - last);
      } else {
        // senão é o momento de executar a função callback
        timeout = null;
        if (!immediate) {
          result = func.apply(context, args);
          if (!timeout) context = args = null;
        }
      }
    };

    return function() {
      context = this;
      args = arguments;
      // obtendo o timestamp
      timestamp = _.now();
      // se o timer não existir então execute a função imediatamente
      var callNow = immediate && !timeout;
      // se o time não existe então crie um
      if (!timeout) timeout = setTimeout(later, wait);
      if (callNow) {
        // se a função imediata é precisa, use aplly para começar a função
        result = func.apply(context, args);
        context = args = null;
      }

      return result;
    };
  };

A implementação completa da ƒunção não é tão difícil.

  • Para a implementação de proteger contra clicks acidentais: enquanto eu começar o time e o time existir, não importa quantas vezes eu clicar o butão, a função de callback não será executada. Contudo quando o time termina, é setado para null, outro click é permitido.
  • Para a implementação da executação da função de atraso: toda chamada para a função debouncing vai disparar um tempo de intervalo equivalente entre a chamada tual e a última chamada. Se o intervalo é menor que o requerido, outro time será cirado, e o atraso é atribuido ao set interval menos o tempo anterior. Quando o tempo passa, a função de callback é executada.

Throttle

Debounce e Throttle possuem naturezas diferentes. Debounce é para tornar multiplas execuções na última execução, e Throttle é para tornar multiplas execuções em uma execução de intervalos regulares.

// Os dois primeiro parâmetros com debounce são a mesma função
// options: você pode passar duas propriedades
// trailing: o último tempo não é executado
// leading: o primeiro tempo não é executado
// As duas propriedades não coexistem, contudo a função não será executada
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // timestamp anterior
    var previous = 0;
    // Defina vázio se as opções não forem passadas
    if (!options) options = {};
    // Função Timer callback
    var later = function() {
        // se você definiu `leading`, então defina `previous` para zero
        // O primeiro if da seguinte função é usada
        previous = options.leading === false ? 0 : _.now();
        // O primeiro é prevenindo memory leaks e o segundo é julgado os seguintes timers quando configurado `timeout` para null
        timeout = null;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
    };
    return function() {
        // Obtenha o timestamp atual
        var now = _.now();
        // Deve ser verdado quando entrar pela primeira vez
        // Se você não precisa executar essa função na primeira vez
        // Defina o último timestamp para o atual
        // Então ele será maior que 0 quando o termo remanecente for calculado da próxima vez
        if (!previous && options.leading === false)
            previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        // Essa condição só será preenchida se definido para `trailing`
        // Essa condição só será preenchida no ínicio se não definido `leading`
        // Outro ponto, você deve pensar que essa condição não será preenchida se você ligar o timer
        // De fato, será assim até entrar porque o atraso do timer não é acurado
        // Isso é muito como se você setar a 2 segundos, mas ele precisa 2.2 segundos para disparar, então o tempo será preenchido nessa condição
        if (remaining <= 0 || remaining > wait) {
            // Limpe se existe um timer e ele chama a callback duas vezes
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            result = func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            // Julge se o timer e trailing forem definidos
            // E você não pode defirnor leading e trailing no mesmo instante
            timeout = setTimeout(later, remaining);
        }
        return result;
    };
};

Map、FlatMap e Reduce

O efeito do Map é para gerar um novo array, iterando sobre o array original, tomando cada elemento para fazer alguma transformação, e então append para um novo array.

[1, 2, 3].map((v) => v + 1)
// -> [2, 3, 4]

Map tem três parâmetros, nomeando o índice atual do elemento, o índice, o array original.

['1','2','3'].map(parseInt)
//  parseInt('1', 0) -> 1
//  parseInt('2', 1) -> NaN
//  parseInt('3', 2) -> NaN

O efeito do FlatMap é quase o mesmo do Map, mas o array original será substituído para um array multidimensional. Você pode pensar no FlatMap com um map e um flatten, que atualmente não é suportado nos navegadores.

[1, [2], 3].flatMap((v) => v + 1)
// -> [2, 3, 4]

Você pode alcançar isso quando você quer reduzir completamente dimensões de um array multidimensional:

const flattenDeep = (arr) => Array.isArray(arr)
  ? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
  : [arr]

flattenDeep([1, [[2], [3, [4]], 5]])

O efeito do Reduce é para combinar os valores em um array e pegar o valor final:

function a() {
    console.log(1);
}

function b() {
    console.log(2);
}

[a, b].reduce((a, b) => a(b()))
// -> 2 1

Async e await

A função async vai retornar uma Promise:

async function test() {
  return "1";
}
console.log(test()); // -> Promise {<resolved>: "1"}

Você pode pensar em async como uma função encapsuladora usando Promise.resolve().

await pode ser usado apenas em funcões async:

function sleep() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('finish')
      resolve("sleep");
    }, 2000);
  });
}
async function test() {
  let value = await sleep();
  console.log("object");
}
test()

O código acime vai exibir finish antes de exibir object. Porque await espera pela funcão sleep resolve, mesmo se a sincronização de código estiver seguida, ele não executa antes do código assíncrono ser executado.

A vantagem do async e await comparado ao uso direto da Promise mente em manipular a cadeia de chamada do then, que pode produzir código claro e acurado. A desvantagem é que uso indevido do await pode causar problemas de performance porque await bloqueia o código. Possivelmente o código assíncrono não depende do anterior, mas ele ainda precisa esperar o anterir ser completo, ocasionando perda de concorrência.

Vamos dar uma olhada em um código que usa await:

var a = 0
var b = async () => {
  a = a + await 10
  console.log('2', a) // -> '2' 10
  a = (await 10) + a
  console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1

Você pode ter dúvidas sobre o código acima, aqui nós explicamos o príncipio:

  • Primeiro a função b é executada. A variável a ainda é zero antes da execução do await 10, porque os Generators são implementados dentro do await e Generators matém as coisas na pilha, então nesse momento a = 0 é salvo
  • Porque await é uma operação assíncrona, console.log('1', a) será executada primeiro.
  • Nesse ponto, o código síncrono é completado e o código assíncrono é iniciado. O valor salvo é usado. Nesse instante, a = 10
  • Então chega a execução usual do código

Proxy

Proxy é uma nova funcionalidade desde o ES6. Ele costuma ser usado para definir operações em objetos:

let p = new Proxy(target, handler);
// `target` representa o objeto que precisamos adicionar o proxy
// `handler` operações customizadas no objeto

Proxy podem ser conveniente para implementação de data bindind e listening:

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      setBind(value);
      return Reflect.set(target, property, value);
    }
  };
  return new Proxy(obj, handler);
};

let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
  value = v
}, (target, property) => {
  console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // liga `value` para `2`
p.a // -> obtém 'a' = 2

Por que 0.1 + 0.2 != 0.3

Porque JS usa a precisão-dupla do IEEE 754 versão (64-bit). Toda linguagem que usa esse padrão tem esse problema.

Como nós sabemos, computadores usam binários para representar decimais, então 0.1 em binário é representado como

// (0011) representa o ciclo
0.1 = 2^-4 * 1.10011(0011)

Como nós chegamos a esse número binário? Podemos tentar computar ele como abaixo:

Computações binária em números flutuantes são diferentes daqueles em inteiros. Por multiplicação, apenas bits flutuantes são computados, enquanto bits do tipo inteiro são usados pelos binários para cada bit. Então o primeiro bit é usado como o bit mais significante. Assim sendo nós obtemos 0.1 = 2^-4 * 1.10011(0011)`.

0.2 é similar. Nós apenas precisamos passear na primeira multiplicação e obter 0.2 = 2^-3 * 1.10011(0011)

Voltando a precisão dupla pelo padrão IEE 754. Entre o 64 bits, um bit é usado para assinatura, 11 é usado para bits inteiros, e o outros 52 bits são floats. Uma vez que 0.1 e 0.2 são ciclos infinitos de binários, o último bit do float precisa indicar se volta (mesmo como o arredendomaneto em decimal).

Depois do arredondamento, 2^-4 * 1.10011...001 se torna 2^-4 * 1.10011(0011 * 12 vezes)010. Depois de adicionado esses dois binários obtemos 2^-2 * 1.0011(0011 * 11 vezes)0100, que é 0.30000000000000004 em decimal.

A solução nativa pra esse problema é mostrado abaixo:

parseFloat((0.1 + 0.2).toFixed(10))

Expressões Regulares

Metacaracteres

Metacaractere Efeito
. corresponde a qualquer caractere exceto de terminadores de linhas: \n, \r, \u2028 or \u2029.
[] corresponde a qualquer coisa dentro dos colchetes. Por exemplo, [0-9] corresponde a qualquer número
^ ^9 significa corresponder qualquer coisa que começa com '9'; [^9] significa não corresponder aos caracteres exceto '9' nos colchetes
{1, 2} corresponde 1 ou 2 caracteres digitais
(yck) corresponde apenas strings com o mesmo 'yck'
| corresponde a qualquer caractere antes e depois |
\ caracter de escape
* corresponde a expressão precedente 0 ou mais vezes
+ corresponde a expressão precedente 1 ou mais vezes
? o caractere antes do '?' é opcional

Bandeiras

Bandeira Efeito
i pesquisa insensível a maiúsculas e minúsculas
g corresponde globalmente
m multilinha

Caracteres Atalhos

Atalho Efeito
\w caracteres alfanuméricos, caracteres sublinhados
\W o oposto do acima
\s qualquer caractere em branco
\S o oposto do acima
\d números
\D o oposto do acima
\b inicio ou fim da palavra
\B o oposto do acima