JavaScript

O Engine é uma plataforma de desenvolvimento baseada em JavaScript e atualmente são disponibilizados dois runtimes que podem ser utilizados para executar os códigos desenvolvidos sobre a plataforma:

  • Ije: runtime proprietário da plataforma, baseado na especificação EcmaScript 3.
  • V8: runtime desenvolvido pela Google, com desempenho bem superior ao Ije e que segue as especificações mais recentes da linguagem.

O runtime Ije é o padrão do Engine e não pode ser substituído para a maioria dos contextos onde são executados códigos JavaScript. Há um projeto para que o V8 torne-se o runtime padrão, mas essa transição não será simples e os dois runtimes precisarão coexistir enquanto a transição não for totalmente concluída. O runtime Ije possui algumas incompatibilidades com a especificação da linguagem JavaScript e códigos desenvolvidos para esse runtime precisam ser revistos ou transpilados para que se tornem compatíveis com o V8.

Apesar dessa restrição de uso, hoje já é possível utilizar o V8 nos seguintes contextos:

  • Servidor HTTP: por meio da configuração Realm.runtime é possível indicar que os arquivos JavaScript de um diretório da Virtual File System ou que um conjunto de rotas HTTP devem utilizar o runtime V8. O Engine atualmente utiliza o V8 no contexto de execução do Manage.
  • ScriptRunner: uma sessão JavaScript Ije pode executar scripts no runtime V8 utilizando a opção runtime do construtor da classe ScriptRunner.
  • IDE: o runtime utilizado nas guias iDBC sql pode ser selecionado no menu Ferramentas.

É importante ressaltar que um código executado no runtime V8 somente poderá importar módulos e scripts que tenham sido compatibilizados com esse runtime. Como a maioria dos códigos não foram revistos, entre eles os scripts de definição de classes de dados (x-model e x-class), a aplicação prática do V8 ainda é muito restrita.

Runtime Ije

O runtime JavaScript padrão do Engine segue a especificação EcmaScript 3 com as seguintes limitações:

  • O operador switch não é suportado.
  • Representações literais de octais (0nn), hexadecimais (0xnn) e binários (0bnn) não são suportadas.
  • Strings são codificadas em WIN1252 em vez de UCS-2 (Unicode).
  • Ausência da classe global RegExp e de expressões regulares literais.

Além das limitações citadas, foram adicionados os seguintes comportamentos que diferem da especificação:

  • Chaves de registros do cache local quando representadas por valores primitivos ou objetos do tipo Number, podem ter ser os seus campos acessados como propriedades. Exemplo:
    • (-1).iname; // => 'Anonymous'
  • Datas também podem ser representadas por valores primitivos, de forma similar ao tipos String, Number e Boolean. O operador typeof desses valores retorna 'date', comportamento não existente na especificação padrão da linguagem.
  • Os operadores + e - são sobrecarregados para datas. Datas podem ser somadas ou subtraídas de um intervalo de dias. A soma ou substração de duas datas retorna um number que representa um intervalo de dias entre as duas datas, sendo a parte fracionária uma razão do tempo por 24 horas. Exemplos:
    • new Date(2019, 0, 1, 0, 0, 0) - 31; // 'Dom Dez 01 2018 00:00:00 GMT-0300'
    • new Date(2019, 0, 1, 12, 0, 0) - new Date(2018, 11, 31, 0, 0, 0); // -1.5
  • String(null) retorna '' em vez de 'null'.
  • Number('') retorna NaN em vez de 0.
  • Comparação não estrita == de null com '', 0 e false retorna true em vez de false.
  • O valor null é tratado como uma instância de Object, permitindo que o seus métodos possam ser utilizados. O código null.toString() é válido no runtime Ije.
  • Acessar uma propriedade inexistente em um objeto Function gera o erro “Undefined property” em vez de retornar undefined.
  • include e includeOnce são declarações com o objetivo de incluir scripts e são tratados como palavras reservadas. Os comentários //include e //includeOnce são interpretados pelo runtime Ije de forma especial e se comportam como se o código não estivesse comentado. Esses operadores são obsoletos e foram substituídos pelas funções globais __include() e __includeOnce() com finalidade equivalente, mas ainda hoje são mantidos para fins de compatibilidade.
  • Uma operação de divisão onde o divisor é zero gera uma exceção.
  • A pilha de chamadas de funções durante a captura de uma exceção deve ser obtida pela função global getStackTrace() em vez da propriedade Error.prototype.stack. Ela deve ser chamada em um bloco catch e leva em consideração apenas a pilha do último erro lançado.
  • Para o runtime Ije, as declarações const e let são equivalentes a var. Essas declarações não fazem parte da especificação EcmaScript 3 e foram introduzidas posteriormente no runtime Ije a fim de permitir o uso das validações e facilitadas introduzidas pelas IDEs modernas. O Engine não realiza nenhum tipo de validação de alteração das declarações const, nem implementa o escopo de bloco definido pela especificação. Por esse último motivo, é recomendado que seja utilizada uma ferramenta de validação estática, como o Eslint no-shadow, garantindo que não haja duas declarações do mesmo identificador em escopos sobrepostos. Ver variable shadowing para mais detalhes.
  • As strings podem ser multiline sem a necessidade do caractere \ no final da linha, de forma similar ao recurso templates literals das especificações mais recente da linguagem.
  • Propriedades são definidas automaticamente em um objeto se houver métodos no protótipo com nomes iniciados com get ou set. Propriedades definidas dessa forma podem ser ter a sua presença testada pelo operator in, no entanto não são iteradas por um for...in. Durante a iteração, os getters e setters são relacionados como propriedades, comportamento diferente do Object.defineProperty(), onde apenas a propriedade pode ser enumerável. Exemplo:
    function Test() {
    }
    Test.prototype.getName = function () {
        return 'Test';
    };
    const test = new Test();
    test.name; // => 'Test'
    'name' in test; // => true
    'getName' in test; // => true
    const props = [];
    for (var name in test) {
        props.push(name);
    }
    props; // => ['getName']
    
  • Não é realizado o hoisting das variáveis declaradas via var.
  • As atribuições de variáveis deixam o valor atribuído na pilha e algumas funções, como o executeScript(), podem fazer uso desse comportamento. No V8, o valor na pilha após uma atribuição é undefined, sendo necessário ler explicitamente a variável atribuída para obter o mesmo comportamento do Ije. Por exemplo: o código var x = 10 executado no Ije precisaria ser reescrito para var x = 10; x para ter o mesmo comportamento no V8.
  • O método Array.prototype.indexOf() realiza uma comparação não estrita no runtime Ije, exceto quando o argumento for um Object e o retorno de toString() for "[object Object]". Por exemplo, o código ['5', '10'].indexOf(10) retorna 1 no Ije e -1 no V8.
  • No runtime Ije, o método Array.prototype.sort() ordena os elementos pelo valor numérico dos elementos sempre quando isso for possível. Por sua vez, a especificação da linguagem determina que a ordenação deve ser realizada com base na representação textual dos elementos. Exemplo:
    const ar = [1, 5, 10];
    ar.sort();
    ar // => [1, 5, 10] (Ije)
    ar // => [1, 10, 5] (V8)
    
  • Datas não são ajustadas para uma data adjacente quando seus valores passam do limite lógico. Ao invés disso, um erro é gerado. Por exemplo, new Date(2024, 0, 32) não gera a data Feb-01-2024, mas sim um erro de data inválida. O mesmo vale para os métodos que alteram componentes da data, como setMinutes(70), por exemplo, que ao invés de alterar para 10 minutos da hora seguinte, também gera erro de data inválida.

As classes nativas da linguagem foram estendidas com os seguintes métodos e propriedades:

  • Object.prototype.toHtmlString()
  • Object.prototype.toSqlString()
  • Object.prototype.toString(format)
  • Object.prototype.deleteProperty()
  • String.prototype.compare()
  • Date.prototype.compare()
  • Array.prototype.indexOf(searchElement, compareFunction, binarySearch)
  • Array.prototype.sorted
  • Math.isZero()
  • Math.decimalRound()
  • isNumber()
  • __include() e __includeOnce()
  • _()

Algumas dessas extensões são particularmente problemáticas, pois elas são incompatíveis com a especificação atual da linguagem. São elas:

  • Object.prototype.toString()

    • ECMAScript: Object.prototype.toString()
    • Ije: Object.prototype.toString(format)
      • O conceito de formatação não existe na linguagem. Apesar de ser definido na classe * Object*, apenas as classes *Date* e *Number* fazem uso do argumento de formatação.
  • Array.prototype.indexOf()

    • ECMAScript: Array.prototype.indexOf(searchElement[, fromIndex])
    • Ije: Array.prototype.indexOf(searchElement, compareFunction, binarySearch)
  • String.prototype.replace()

    • ECMAScript: String.prototype.replace(regexp|substr, newSubstr|function)
    • Ije: String.prototype.replace(substr, newSubstr)
      • Expressões regulares e funções de substituição não são suportadas.
  • String.prototype.match()

    • ECMAScript: String.prototype.match(regexp)
    • Ije: String.prototype.match(pattern)
      • O padrão não é descrito por uma expressão regular, e sim por meio de um formato legado e proprietário onde a expressão .. é equivalente ao % do operador LIKE da linguagem SQL.
  • String.prototype.repeat(count)

    • ECMAScript: quantidades negativas ou Infinity geram um erro.
    • Ije: quantidades negativas ou Infinity retornam uma string vazia.
  • Date.prototype.toDateString()

    • ECMAScript: new Date().toDateString(); // => 'Mon Feb 18 2019'
    • Ije: new Date().toDateString(); // => '18/02/2019'
      • Utiliza o padrão DD/MM/YYYY, diferente da especificação.
  • Date.prototype.toTimeString()

    • ECMAScript: new Date().toTimeString(); // => '10:57:46 GMT-0300 (Horário Padrão de Brasília)'
    • Ije: new Date().toTimeString(); // => '10:57:46'
      • O retorno não inclui o fuso horário local.
  • Date.prototype.toUTCString()

    • ECMAScript: new Date().toUTCString(); // => '"Mon, 18 Feb 2019 13:58:35 GMT"'
    • Ije: new Date().toUTCString(); // => 'Seg Fev 18 13:58:35 2019'
      • O retorno não inclui o fuso horário GMT.

Recursos das especificações mais recentes do ECMAScript

Por uma questão estratégica, optou-se por não evoluir o runtime Ije para acompanhar as novas especificações da linguagem JavaScript. A evolução do Engine passa pela substituição desse runtime pelo V8, que além de ser um runtime mais aderente a especificação da linguagem, possui sofisticadas implementações de compilação sob demanda que geram um desempenho muito superior ao do runtime atual.

Apesar desse direcionamento, foram incorporadas algumas APIs das especificações mais atuais do JavaScript com base nos seguintes critérios:

  • Funcionalidades que possam ser implementadas por meio de polyfills compatíveis com o ECMAScript 3.
  • Melhorias que são necessárias para o tratamento de conteúdos binários.
  • Funcionalidades que tornem mais simples a migração para o runtime V8.

Segue a relação das APIs disponíveis que não fazem parte da especificação ECMAScript 3.

ECMAScript 5.1:

ECMAScript 2015 (6th Edition):

ECMAScript 2016:

ECMAScript 2017:

ECMAScript 2019:

ECMAScript 2020:

Web APIs:

  • TextEncoder
  • TextDecoder: a implementação disponível no sistema não tem suporte às opções TextDecoderOptions e TextDecodeOptions. Ela também é restrita às codificações “utf-8”, “windows-1252” e “iso-8859-1”, sendo essa última um rótulo alternativo ao “windows-1252”.

Runtime V8

O V8 é um runtime JavaScript moderno que implementa a maioria das funcionalidades das especificações recentes do ECMAScript. Abaixo relacionamos algumas melhorias que justificam a migração para o V8:

  • O V8 usa um sofisticado pipeline de execução híbrido, sendo o código inicialmente interpretado para depois ser compilado sob demanda. O runtime Ije utiliza apenas um interpretador, não ocorrendo em nenhum momento uma compilação do código JavaScript para código de máquina. Em códigos onde não há I/O, o V8 chega a ser duas ordens de grandeza mais rápido que o Ije.
  • Ao seguir de forma mais fiel a especificação ECMAScript, o V8 simplificará o uso de códigos de terceiros. Hoje eles quase sempre precisam ser compatibilizados para funcionarem no Engine, inibindo o seu uso. É importante ressaltar que códigos JavaScript podem ser dependentes da plataforma, como o Browser ou o Node.js, e esses códigos ainda precisarão ser adaptados à plataforma do Engine, apenas que com um esforço menor que com o atual runtime Ije.
  • Strings com Unicode eliminarão a necessidade de tratamentos adicionais na integração com sistemas e códigos de terceiros, além de possibilitar o suporte ao tratamento de textos de línguas não latinas.
  • A sintaxe de Classes possibilita a definição de classes de uma forma mais convencional e simples do que a equivalente via protótipo.
  • Arrow functions tornam mais simples a definição de eventos e uso de funções de alta ordem, como Array.prototype.map.
  • Template literals simplificam a geração de códigos HTML e XML, ao permitir a interpolação de valores em vez da concatenação manual de strings.
  • Atribuição via desestruturação simplifica o uso funções que retornam um array ou um objeto.
  • Parâmetros pré-definidos de funções eliminam o esforço de tratar manualmente os parâmetros não informados e tornam a IDE mais produtiva, ao sugerir o tipo dos parâmetros no escopo da função.

Essa é apenas uma pequena seleção das melhorias do V8 em relação ao runtime Ije. A relação de todas as funcionalidades implementadas no V8 por ser acompanhada no site http://kangax.github.io/compat-table/es2016plus/. O Engine atualmente utiliza a versão 10.8 do V8, a mesma versão utilizada no Chrome 108. Portanto, as funcionalidades existentes para essa versão do Chrome estão disponíveis no runtime V8 utilizado no Engine.

Migração para o V8

Ainda não foi definida uma estratégia de revisão dos códigos JavaScript para serem compatíveis com o V8. No entanto, códigos novos já podem ser desenvolvidos evitando comportamentos e APIs que não fazem parte da especificação e que não funcionarão corretamente no V8. Para isso, relacionamos abaixo boas práticas e alternativas das APIs incompatíveis:

  • Acesso aos campos de um registro por meio de propriedades de um Number não é suportado no V8. Alternativas:
    • se a chave estiver em um campo de um DataSet, utilize DataSet.prototype.val() ou uma das suas variações com tipo de retorno definido.
    • se a chave for definida de forma literal ou se for obtida de alguma outra fonte, crie uma instância de DBKey e utilize DBKey.prototype.val() ou uma das suas variações.
  • Para verificar se um valor é uma chave deve ser utilizado o método DBKey.isLike(). Chaves podem ser representadas como valores do tipo number ou como instâncias de DBKey. A classe DBKey é pouco utilizada no runtime Ije, pois há o comportamento especial do tipo number para obter os campos a partir das chaves dos registros. No V8 esse comportamento não existe e o uso do DBKey será mais frequente. Testar se um valor é uma chave verificando apenas se o tipo dele é number falhará no V8.
  • Strings multiline devem ser declaradas com o caractere de escape \ no final. Exemplo:
    const sql = 'Teste \
    ok';
    
  • Utilize incDate em vez de somar ou subtrair uma data de um número de dias. Para obter um intervalo entre duas datas, subtraia o resultado do getTime() delas.
  • Valores que potencialmente podem ser nulos devem ser verificados antes de serem concatenados em strings a fim de evitar que a string 'null' seja adicionada. O uso dos métodos DataSet.prototype.str() e DBKey.prototype.str() torna desnecessária essa verificação manual para valores obtidos de campos de registros.
  • As funções __include e __includeOnce devem ser utilizadas em vez das declarações obsoletas include e includeOnce.
  • Defina propriedades com getter e setter utilizando a função declareProperty(). Esta função tem uma API similar ao Object.defineProperty() e tem o objetivo de ser uma API compatível com os dois runtimes. No Ije, ela cria os getters e setters como propriedades do objeto, no V8 ela executa a função Object.defineProperty nativa.
  • A função Object.deleteProperty() não deve ser utilizada. Ela foi criada para contornar a ausência do operador delete nas versões antigas do Ije. Na versão atual o operador deve ser utilizado.
  • Sempre devem ser utilizadas comparações estritas, operador ===, quando comparados valores com null, false, '' ou 0, garantindo que o resultado da comparação seja igual nos dois runtimes.
  • A função de comparação sempre deve ser informada ao chamar o método Array.prototype.sort, evitando a divergência de comportamento da função de comparação padrão existente entre os runtimes Ije e V8.
  • Extensões introduzidas pelo runtime Ije nas classes nativas não devem ser utilizadas. Alternativas:
    • Object.prototype.toHtmlString() => htmlString.
    • Object.prototype.toString(format) => toFormattedString.
    • Object.prototype.deleteProperty() => operador delete.
    • Object.prototype.toSqlString() => toSqlString.
    • String.prototype.compare() => stringCompare.
    • String.prototype.replace: para substituir múltiplas ocorrências de uma substring, utilize replaceAll.
    • Date.prototype.compare() => dateCompare.
    • Date.prototype.toDateString() => toFormattedString(dt, 'dd/mm/yyyy').
    • Date.prototype.toTimeString() => toFormattedString(dt, 'hh:nn:ss').
    • Math.isZero(): isZero.
    • Math.decimalRound(): decimalRound.
    • Array.prototype.indexOf(searchElement, compareFunction, binarySearch): use apenas com um único argumento, o valor pesquisado, garantindo a compatibilidade com os dois runtimes. Para pesquisar utilizando uma função de comparação, utilize o método Array.prototype.findIndex.
    • Array.prototype.sorted: essa propriedade é utilizada principalmente para indicar que uma busca binária deve ser realizada pelo indexOf() em códigos críticos em relação ao desempenho. Esse é um conceito que não existe no V8 e ela não deveria ser mais utilizada em códigos novos. O seu uso normalmente pode ser substituído por um mapa de valores.
    • isNumber(): converta o valor para um Number e teste o resultado com a função isNaN(). Caso deseje manter o comportamento inalterado, utilize o módulo isNumber.
    • __include() e __includeOnce(): a função __includeOnce() é implementada no V8 e a __include() pode ser substituída por um eval(), mas preferencialmente devem ser utilizados módulos em vez de funções de injeção de scripts.
    • _(): essa é uma função legada de uma implementação de internacionalização não mais utilizada pelo Engine. No Ije ela retorna a própria string informada, portanto ela pode ser removida dos códigos sem prejuízo.
    • Função global getStackTrace(): deve ser utilizado o módulo @nginstack/engine/lib/error/getStackTrace informando como primeiro parâmetro o erro do qual se deseja obter a pilha. Esse módulo não retorna todas as propriedades existentes na implementação do runtime Ije, como source e tokenPosition, e elimina algumas informações retornadas pelo V8. Sempre que for possível, é preferível utilizar diretamente a propriedade Error.prototype.stack no V8.