Por que a binding é mais lenta que um fechamento?

Um cartaz anterior perguntou Function.bind vs Encerramento em Javascript: como escolher?

e recebeu essa resposta em parte, o que parece indicar que o bind deve ser mais rápido que um encerramento:

A passagem de escopo significa que, quando você está tentando obter um valor (variável, object) que existe em um escopo diferente, portanto, a sobrecarga adicional é adicionada (o código fica mais lento para execução).

Usando bind, você está chamando uma function com um escopo existente, para que a passagem do escopo não ocorra.

Dois jsperfs sugerem que o bind é na verdade muito, muito mais lento que um encerramento .

Este foi postado como um comentário ao acima

E eu decidi escrever meu próprio jsperf

Então, por que a binding é muito mais lenta (70 +% no cromo)?

Uma vez que não é mais rápido e fechamentos podem servir para o mesmo propósito, deve ser evitado?

Atualização do Chrome 59: Como previ na resposta abaixo, o bind não é mais lento com o novo compilador de otimização. Aqui está o código com detalhes: https://codereview.chromium.org/2916063002/

Na maioria das vezes isso não importa.

A menos que você esteja criando um aplicativo onde .bind é o gargalo, eu não me incomodaria. A legibilidade é muito mais importante do que o desempenho absoluto na maioria dos casos. Eu acho que usar o .bind nativo geralmente fornece código mais legível e de fácil manutenção – o que é uma grande vantagem.

No entanto, sim, quando é importante – .bind é mais lento

Sim, .bind é consideravelmente mais lento que um encerramento – pelo menos no Chrome, pelo menos da maneira atual que é implementada na v8 . Eu pessoalmente tive que mudar o Node.JS para problemas de desempenho algumas vezes (mais geralmente, encerramentos são meio lentos em situações de desempenho intensivo).

Por quê? Porque o algoritmo .bind é muito mais complicado do que envolver uma function com outra function e usar .call ou .apply . (Curiosidade, ele também retorna uma function com toString setado para [function nativa]).

Existem duas maneiras de ver isso, do ponto de vista da especificação e do ponto de vista da implementação. Vamos observar os dois.

Primeiro, vamos examinar o algoritmo de binding definido na especificação :

  1. Deixe Alvo ser o valor deste.
  2. Se IsCallable (Target) for false, lance uma exceção TypeError.
  3. Seja A uma nova lista interna (possivelmente vazia) de todos os valores de argumentos fornecidos após thisArg (arg1, arg2 etc), em ordem.

(21. Chame o método interno [[DefineOwnProperty]] de F com argumentos “arguments”, PropertyDescriptor {[[Get]]: lançador, [[Set]]: lançador, [[Enumerable]]: false, [[Configurable] ]: false} e false.

(22. Retorno F.

Parece muito complicado, muito mais do que apenas um envoltório.

Em segundo lugar, vamos ver como isso é implementado no Chrome .

Vamos verificar o FunctionBind no código-fonte da v8 (engine JavaScript chrome):

 function FunctionBind(this_arg) { // Length is 1. if (!IS_SPEC_FUNCTION(this)) { throw new $TypeError('Bind must be called on a function'); } var boundFunction = function () { // Poison .arguments and .caller, but is otherwise not detectable. "use strict"; // This function must not use any object literals (Object, Array, RegExp), // since the literals-array is being used to store the bound data. if (%_IsConstructCall()) { return %NewObjectFromBound(boundFunction); } var bindings = %BoundFunctionGetBindings(boundFunction); var argc = %_ArgumentsLength(); if (argc == 0) { return %Apply(bindings[0], bindings[1], bindings, 2, bindings.length - 2); } if (bindings.length === 2) { return %Apply(bindings[0], bindings[1], arguments, 0, argc); } var bound_argc = bindings.length - 2; var argv = new InternalArray(bound_argc + argc); for (var i = 0; i < bound_argc; i++) { argv[i] = bindings[i + 2]; } for (var j = 0; j < argc; j++) { argv[i++] = %_Arguments(j); } return %Apply(bindings[0], bindings[1], argv, 0, bound_argc + argc); }; %FunctionRemovePrototype(boundFunction); var new_length = 0; if (%_ClassOf(this) == "Function") { // Function or FunctionProxy. var old_length = this.length; // FunctionProxies might provide a non-UInt32 value. If so, ignore it. if ((typeof old_length === "number") && ((old_length >>> 0) === old_length)) { var argc = %_ArgumentsLength(); if (argc > 0) argc--; // Don't count the thisArg as parameter. new_length = old_length - argc; if (new_length < 0) new_length = 0; } } // This runtime function finds any remaining arguments on the stack, // so we don't pass the arguments object. var result = %FunctionBindArguments(boundFunction, this, this_arg, new_length); // We already have caller and arguments properties on functions, // which are non-configurable. It therefore makes no sence to // try to redefine these as defined by the spec. The spec says // that bind should make these throw a TypeError if get or set // is called and make them non-enumerable and non-configurable. // To be consistent with our normal functions we leave this as it is. // TODO(lrn): Do set these to be thrower. return result; 

Podemos ver um monte de coisas caras aqui na implementação. Ou seja, %_IsConstructCall() . É claro que isto é necessário para cumprir a especificação - mas também torna mais lento que um simples envoltório em muitos casos.


Em outra nota, chamar .bind também é um pouco diferente, as notas de especificação "Objetos de function criados usando Function.prototype.bind não têm uma propriedade prototype ou [[Code]], [[FormalParameters]] e [[Scope] ] propriedades internas "