Post Top Ad

Your Ad Spot

domingo, 28 de junio de 2020

Escriba coerción en JavaScript

En esta publicación de blog, examinamos el papel de la coerción de tipos en JavaScript. Vamos a profundizar relativamente en este tema y, por ejemplo, veremos cómo la especificación ECMAScript maneja la coerción.

Tabla de contenido:

¿Qué es la coerción de tipo?  

Cada operación (función, operador, etc.) espera que sus parámetros tengan ciertos tipos. Si un valor no tiene el tipo correcto para un parámetro, las dos opciones más comunes para una persona que llama son:
  • Pueden convertir explícitamente el valor para que tenga el tipo correcto. Por ejemplo, en la siguiente interacción, queremos multiplicar dos números escritos en cadenas:
    > Number('3') * Number('2')
    6
    
  • Pueden dejar que la operación haga la conversión por ellos:
    > '3' * '2'
    6
    
    Este tipo de conversión implícita se llama coerción .
Inicialmente, JavaScript no tenía excepciones, por lo que utiliza valores de coerción y error para la mayoría de sus operaciones:
// Coercion
assert.equal(3 / true, 3);

// Error values
assert.equal(1 / 0, Infinity);
assert.equal(Number('xyz'), NaN);
Sin embargo, también hay casos (especialmente cuando se trata de características más nuevas) donde arroja excepciones si un argumento no tiene el tipo correcto:
  • Acceso a propiedades de nullundefined:
    > undefined.prop
    TypeError: Cannot read property 'prop' of undefined
    > null.prop
    TypeError: Cannot read property 'prop' of null
    > 'prop' in null
    TypeError: Cannot use 'in' operator to search for 'prop' in null
    
  • Usando símbolos:
    > 6 / Symbol()
    TypeError: Cannot convert a Symbol value to a number
    
  • Mezclando bigints y números:
    > 6 / 3n
    TypeError: Cannot mix BigInt and other types, use explicit conversions
    
  • Valores de nueva llamada o función que no admiten esa operación:
    > 123()
    TypeError: 123 is not a function
    > (class {})()
    TypeError: Class constructor  cannot be invoked without 'new'
    
    > new 123
    TypeError: 123 is not a constructor
    > new (() => {})
    TypeError: (intermediate value) is not a constructor
    
  • Cambio de propiedades de solo lectura (solo tira en modo estricto):
    > 'abc'.length = 1
    TypeError: Cannot assign to read only property 'length' of string 'abc'
    > Object.freeze({prop:3}).prop = 1
    TypeError: Cannot assign to read only property 'prop' of object '#<Object>'
    

Cómo se utilizan las funciones de conversión de tipo interno en la especificación ECMAScript   

Las siguientes secciones describen las funciones internas más importantes utilizadas por la especificación ECMAScript para convertir los parámetros reales a los tipos esperados.
Por ejemplo, en TypeScript, escribiría:
function multiply(leftValue: number, rightValue: number) {
  // ···
}
En la especificación, esto se ve de la siguiente manera (traducido a JavaScript, para que sea más fácil de entender):
function multiply(leftValue, rightValue) {
  let lnum = ToNumeric(leftValue);
  let rnum = ToNumeric(rightValue);
  // ···
}

Conversión a tipos y objetos primitivos   

Siempre que se esperan tipos u objetos primitivos, se utilizan las siguientes funciones de conversión:
  • ToBoolean()
  • ToNumber()
  • ToBigInt()
  • ToString()
  • ToObject()
Estas funciones internas tienen análogos en JavaScript que son muy similares:
> Boolean(0)
false
> Boolean(1)
true

> Number('123')
123
Como ahora tenemos bigints además de números, la especificación a menudo se usa ToNumeric()donde se usaba anteriormente ToNumber()Siga leyendo para obtener más información.

Conversión a tipos numéricos   

  • ToNumeric()se usa cuando se espera un número o un bigint. La ejecución se reenvía a Type(result)::operation(···)(método de pensar del tipo de result).
  • ToInteger(x)se usa siempre que se espera un número sin fracción. El rango del resultado a menudo se restringe aún más después.
    • Utiliza ToNumber(x)y elimina la fracción (a similares Math.trunc()).
  • ToInt32()ToUint32()coaccionan números a enteros de 32 bits y son utilizados por operadores bit a bit (ver tabla a continuación).
    • ToInt32(): firmado, rango [−2 ^ 31 ^, 2 ^ 31 ^ −1] (se incluyen límites)
    • ToUint32(): sin signo (de ahí el U), rango [0, 2 ^ 32 ^ −1] (se incluyen límites)
Coerción de operadores bit a bit para números (los operadores BigInt no limitan el número de bits).
operandooperando izquierdooperando derechoresultado
<<ToInt32()ToUint32()Int32
firmado >>ToInt32()ToUint32()Int32
no firmado >>>ToInt32()ToUint32()Uint32
&^`|ToInt32()ToUint32()Int32
~-ToInt32()Int32

Conversión a claves de propiedad   

ToPropertyKey() devuelve una cadena o un símbolo y es utilizado por:
  • El operador de soporte []
  • Claves de propiedad calculadas en literales de objeto
  • El lado izquierdo del inoperador
  • Object.defineProperty(_, P, _)
  • Object.fromEntries()
  • Object.getOwnPropertyDescriptor()
  • Object.prototype.hasOwnProperty()
  • Object.prototype.propertyIsEnumerable()
  • Varios métodos de Reflect

Conversión a índices de matriz   

  • ToLength() se usa (directamente) principalmente para índices de cadena.
    • Función auxiliar para ToIndex()
    • Rango de resultado l: 0 ≤ l≤ 2 ^ 53 ^ −1
  • ToIndex() se utiliza para los índices de Typed Array.
    • Diferencia principal con ToLength(): lanza una excepción si el argumento está fuera de rango.
    • Rango de resultado i: 0 ≤ i≤ 2 ^ 53 ^ −1
  • ToUint32() se usa para los índices de matriz.
    • Rango de resultado i: 0 ≤ i<2 ^ 32 ^ −1 (se excluye el límite superior, para dejar espacio para el .length)

Conversión a elementos de matriz tipeada   

Cuando establece el valor de un elemento Typed Array, se utiliza una de las siguientes funciones de conversión:
  • ToInt8()
  • ToUint8()
  • ToUint8Clamp()
  • ToInt16()
  • ToUint16()
  • ToInt32()
  • ToUint32()
  • ToBigInt64()
  • ToBigUint64()

Intermedio: expresando algoritmos de especificación en JavaScript   

En el resto de la publicación, encontraremos varios algoritmos de especificación, pero "implementados" como JavaScript.
  • Especificación: si Tipo (valor) es Cadena
    • JavaScript: if (TypeOf(value) === 'string')(traducción muy flexible, definida a continuación)
  • Especificación: si IsCallable (método) es verdadero
    • JavaScript: if (IsCallable(method))(definido a continuación)
  • Especificación: deje que numValue sea ToNumber (valor)
    • JavaScript: let numValue = Number(value)
  • Especificaciones: deje que isArray sea IsArray (O)
    • JavaScript: let isArray = Array.isArray(O)
  • Especificación: si O tiene una ranura interna [[NumberData]]
    • JavaScript: if ('__NumberData__' in O)
  • Especificaciones: deje que la etiqueta sea Get (O, @@ toStringTag)
    • JavaScript: let tag = O[Symbol.toStringTag]
  • Especificación: Devuelve la concatenación de cadenas de "[objeto", etiqueta y "]".
    • JavaScript: return '[object ' + tag + ']';
Se omiten algunas cosas, por ejemplo, los shorthands ReturnIfAbrupt ? y !.
/**
 * An improved version of typeof
 */
function TypeOf(value) {
  const result = typeof value;
  switch (result) {
    case 'function':
      return 'object';
    case 'object':
      if (value === null) {
        return 'null';
      } else {
        return 'object';
      }
    default:
      return result;
  }
}

function IsCallable(x) {
  return typeof x === 'function';
}

Ejemplo de algoritmos de coerción   

ToPrimitive()  

La operaciónToPrimitive() es un paso intermedio para muchos algoritmos de coerción (algunos de los cuales veremos más adelante en esta publicación). Convierte valores arbitrarios en valores primitivos.
ToPrimitive()se usa a menudo en la especificación porque muchas operaciones (eventualmente) solo funcionan con valores primitivos. Por ejemplo, podemos usar el operador más ( +) para agregar números y concatenar cadenas, pero no podemos usarlo para concatenar matrices.
Así es como se ve la versión JavaScript de ToPrimitive():
function ToPrimitive(input: any,
  hint: 'default' | 'string' | 'number' = 'default') {
    if (TypeOf(input) === 'object') {
      let exoticToPrim = input[Symbol.toPrimitive]; // (A)
      if (exoticToPrim !== undefined) {
        let result = exoticToPrim.call(input, hint);
        if (TypeOf(result) !== 'object') {
          return result;
        }
        throw new TypeError();
      }
      if (hint === 'default') {
        hint = 'number';
      }
      return OrdinaryToPrimitive(input, hint);
    } else {
      // input is already primitive
      return input;
    }
  }
hint puede tener uno de tres valores:
  • 'number'significa: si es posible, inputdebe convertirse a un número.
  • 'string'significa: si es posible, inputdebe convertirse en una cadena.
  • 'default' significa: no hay preferencia por números o cadenas.
ToPrimitive()permite que los objetos anulen la conversión a primitiva vía Symbol.toPrimitiveSi un objeto no hace eso, se pasa a OrdinaryToPrimitive():
function OrdinaryToPrimitive(O: object, hint: 'string' | 'number') {
  let methodNames;
  if (hint === 'string') {
    methodNames = ['toString', 'valueOf'];
  } else {
    methodNames = ['valueOf', 'toString'];
  }
  for (let name of methodNames) {
    let method = O[name];
    if (IsCallable(method)) {
      let result = method.call(O);
      if (TypeOf(result) !== 'object') {
        return result;
      }
    }
  }
  throw new TypeError();
}

Claves de propiedad utilizadas por ToPrimitive()OrdinaryToPrimitive()  

Tres claves de propiedad son relevantes para la conversión a valores primitivos:
  • 'toString'se prefiere cuando hintindica que nos gustaría que el valor primitivo sea una cadena.
  • 'valueOf'se prefiere cuando hintindica que nos gustaría que el valor primitivo sea un número.
  • Symbol.toPrimitivees para personalizar la conversión a primitiva. Eso solo se hace dos veces en la biblioteca estándar:
    • Symbol.prototype[@@toPrimitive](hint)
      • Devuelve el símbolo envuelto.
      • Existe para admitir dos casos de uso:
        • Los símbolos tienen un método .toString()que devuelve una cadena.
        • Las instancias de Symbolno deben convertirse accidentalmente en cadenas.
    • Date.prototype[@@toPrimitive](hint)
      • Explicado con más detalle pronto.
Examinemos cómo hintinfluye qué clave de propiedad se usa:
  • Number()llamadas ToPrimitive()con hintset to 'number':
    > Number({valueOf() {return 1}, toString() {return 'a'}})
    1
    
  • String()llamadas ToPrimitive()con hintset to 'string':
    > String({valueOf() {return 1}, toString() {return 'a'}})
    'a'
    

¿Cómo ToPrimitive()especifican las personas que llaman hint?  

Estos son algunos ejemplos de cómo usan varias operaciones ToPrimitive():
  • hint === 'number'Las siguientes operaciones prefieren números:
    • ToNumeric()
    • ToNumber()
    • ToBigInt()BigInt()
    • Comparación relacional abstracta ( <)
  • hint === 'string'Las siguientes operaciones prefieren cadenas:
    • ToString()
    • ToPropertyKey()
  • hint === 'default'Las siguientes operaciones son neutrales con respecto al tipo del valor primitivo devuelto:
    • Comparación de igualdad abstracta ( ==)
    • Operador adicional ( +)
    • new Date(value)valuepuede ser un número o una cadena)
Como hemos visto, el comportamiento predeterminado es 'default'ser manejado como si lo fuera 'number'Solo las instancias de SymbolDateanulan este comportamiento.

Date.prototype[Symbol.toPrimitive]()  

Así es como las fechas manejan la conversión a valores primitivos:
Date.prototype[Symbol.toPrimitive] = function (
  hint: 'default' | 'string' | 'number') {
    let O = this;
    if (TypeOf(O) !== 'object') {
      throw new TypeError();
    }
    let tryFirst;
    if (hint === 'string' || hint === 'default') {
      tryFirst = 'string';
    } else if (hint === 'number') {
      tryFirst = 'number';
    } else {
      throw new TypeError();
    }
    return OrdinaryToPrimitive(O, tryFirst);
  };
La única diferencia con el algoritmo predeterminado es que se 'default'convierte 'string'(y no 'number'). Esto se puede observar si utilizamos operaciones que se establecen hinten 'default':
  • ==coacciona a una fecha en una cadena si el otro operando es un valor primitivo distinta undefinednullboolean:
    > const d = new Date('2222-03-27');
    > d == 'Wed Mar 27 2222 01:00:00 GMT+0100 (Central European Standard Time)'
    true
    
  • Si el primer operando de +es un número, podemos ver que el segundo operando fue forzado a una cadena porque el resultado se calculó mediante la concatenación de cadenas (no mediante la suma numérica):
    > 123 + d
    '123Wed Mar 27 2222 01:00:00 GMT+0100 (Central European Standard Time)'
    
Esta es la versión de JavaScript de ToString():
function ToString(argument) {
  if (argument === undefined) {
    return 'undefined';
  } else if (argument === null) {
    return 'null';
  } else if (argument === true) {
    return 'true';
  } else if (argument === false) {
    return 'false';
  } else if (TypeOf(argument) === 'number') {
    return Number.toString(argument);
  } else if (TypeOf(argument) === 'string') {
    return argument;
  } else if (TypeOf(argument) === 'symbol') {
    throw new TypeError();
  } else if (TypeOf(argument) === 'bigint') {
    return BigInt.toString(argument);
  } else {
    // argument is an object
    let primValue = ToPrimitive(argument, 'string'); // (A)
    return ToString(primValue);
  }
}
Observe cómo esta función se usa ToPrimitive()como un paso intermedio antes de convertir el resultado primitivo en una cadena (línea A).
ToString()se desvía de una manera interesante de cómo String()funciona: si argumentes un símbolo, el primero tira un TypeErrorrato mientras que el segundo no. ¿Porqué es eso? El valor predeterminado para los símbolos es que convertirlos en cadenas arroja excepciones:
> const sym = Symbol('sym');

> ''+sym
TypeError: Cannot convert a Symbol value to a string
> `${sym}`
TypeError: Cannot convert a Symbol value to a string
Ese valor predeterminado se anula en String()Symbol.prototype.toString()(ambos se describen en las siguientes secciones):
> String(sym)
'Symbol(sym)'
> sym.toString()
'Symbol(sym)'

String()  

function String(value) {
  let s;
  if (value === undefined) {
    s = '';
  } else {
    if (new.target === undefined && TypeOf(value) === 'symbol') { // (A)
      return SymbolDescriptiveString(value);
    }
    s = ToString(value);
  }
  if (new.target === undefined) {
    // Function call
    return s;
  }
  // New call
  return StringCreate(s, new.target.prototype); // simplified!
}
En la línea A, podemos ver qué sucede si String()se llama a una función y su argumento es un símbolo. También podemos ver que String()funciona de manera diferente si se llama a la función y si se llama a una nueva. La función auxiliar StringCreate()SymbolDescriptiveString()se muestran a continuación.
function StringCreate(value, prototype) {
  // Create a new String instance that has the given prototype
}
function SymbolDescriptiveString(sym) {
  assert.equal(TypeOf(sym), 'symbol');
  let desc = sym.description;
  if (desc === undefined) {
    desc = '';
  }
  assert.equal(TypeOf(desc), 'string');
  return 'Symbol('+desc+')';
}

Symbol.prototype.toString()  

Además String(), también puede usar el método .toString()para convertir un símbolo en una cadena. Su especificación se ve de la siguiente manera.
Symbol.prototype.toString = function () {
  let sym = thisSymbolValue(this);
  return SymbolDescriptiveString(sym);
};
function thisSymbolValue(value) {
  if (TypeOf(value) === 'symbol') {
    return value;
  }
  if (TypeOf(value) === 'object' && '__SymbolData__' in value) {
    let s = value.__SymbolData__;
    assert.equal(TypeOf(s), 'symbol');
    return s;
  }
}

Object.prototype.toString  

La especificación predeterminada para el .toString()aspecto es la siguiente:
Object.prototype.toString = function () {
  if (this === undefined) {
    return '[object Undefined]';
  }
  if (this === null) {
    return '[object Null]';
  }
  let O = ToObject(this);
  let isArray = Array.isArray(O);
  let builtinTag;
  if (isArray) {
    builtinTag = 'Array';
  } else if ('__ParameterMap__' in O) {
    builtinTag = 'Arguments';
  } else if ('__Call__' in O) {
    builtinTag = 'Function';
  } else if ('__ErrorData__' in O) {
    builtinTag = 'Error';
  } else if ('__BooleanData__' in O) {
    builtinTag = 'Boolean';
  } else if ('__NumberData__' in O) {
    builtinTag = 'Number';
  } else if ('__StringData__' in O) {
    builtinTag = 'String';
  } else if ('__DateValue__' in O) {
    builtinTag = 'Date';
  } else if ('__RegExpMatcher__' in O) {
    builtinTag = 'RegExp';
  } else {
    builtinTag = 'Object';
  }
  let tag = O[Symbol.toStringTag];
  if (TypeOf(tag) !== 'string') {
    tag = builtinTag;
  }
  return '[object ' + tag + ']';
};
Esta operación se usa si convierte objetos simples en cadenas:
> String({})
'[object Object]'
Por defecto, también se usa si convierte instancias de clases en cadenas:
class MyClass {}
assert.equal(
  String(new MyClass()), '[object Object]');
Puede configurar lo que viene después de " object" dentro de los corchetes:
class MyClass {}
MyClass.prototype[Symbol.toStringTag] = 'Custom!';
assert.equal(
  String(new MyClass()), '[object Custom!]');
Si llama Object.prototype.toStringdirectamente, puede acceder al comportamiento anulado:
> Object.prototype.toString.call(['a', 'b'])
'[object Array]'
> String(['a', 'b'])
'a,b'

ToPropertyKey()  

ToPropertyKey()es utilizado, entre otros, por el operador del soporte. Así es como funciona:
function ToPropertyKey(argument) {
  let key = ToPrimitive(argument, 'string'); // (A)
  if (TypeOf(key) === 'symbol') {
    return key;
  }
  return ToString(key);
}
Una vez más, los objetos se convierten en primitivos antes de trabajar con primitivos.
ToNumeric()es utilizado, entre otros, por el operador de multiplicación ( *). Así es como funciona:
function ToNumeric(value) {
  let primValue = ToPrimitive(value, 'number');
  if (TypeOf(primValue) === 'bigint') {
    return primValue;
  }
  return ToNumber(primValue);
}

ToNumber()  

ToNumber() funciona de la siguiente manera:
function ToNumber(argument) {
  if (argument === undefined) {
    return NaN;
  } else if (argument === null) {
    return +0;
  } else if (argument === true) {
    return 1;
  } else if (argument === false) {
    return +0;
  } else if (TypeOf(argument) === 'number') {
    return argument;
  } else if (TypeOf(argument) === 'string') {
    return parseTheString(argument); // not shown here
  } else if (TypeOf(argument) === 'symbol') {
    throw new TypeError();
  } else if (TypeOf(argument) === 'bigint') {
    throw new TypeError();
  } else {
    // argument is an object
    let primValue = ToPrimitive(argument, 'number');
    return ToNumber(primValue);
  }
}
La estructura de ToNumber()es similar a la estructura de ToString().

Operaciones que coaccionan   

Operador de adición ( +)   

Así es como se especifica el operador de adición de JavaScript:
function Addition(leftHandSide, rightHandSide) {
  let lprim = ToPrimitive(leftHandSide);
  let rprim = ToPrimitive(rightHandSide);
  if (TypeOf(lprim) === 'string' || TypeOf(rprim) === 'string') { // (A)
    return ToString(lprim) + ToString(rprim);
  }
  let lnum = ToNumeric(lprim);
  let rnum = ToNumeric(rprim);
  if (TypeOf(lnum) !== TypeOf(rnum)) {
    throw new TypeError();
  }
  let T = Type(lnum);
  return T.add(lnum, rnum); // (B)
}
Pasos de este algoritmo:
  • Ambos operandos se convierten en valores primitivos.
  • Si uno de los resultados es una cadena, ambos se convierten en cadenas y se concatenan (línea A).
  • De lo contrario, ambos operandos se convierten en valores numéricos y se agregan (línea B).

Comparación de igualdad abstracta ( ==)   

/** Loose equality (==) */
function abstractEqualityComparison(x, y) {
  if (TypeOf(x) === TypeOf(y)) {
    // Use strict equality (===)
    return strictEqualityComparison(x, y);
  }

  // Comparing null with undefined
  if (x === null && y === undefined) {
    return true;
  }
  if (x === undefined && y === null) {
    return true;
  }

  // Comparing a number and a string
  if (TypeOf(x) === 'number' && TypeOf(y) === 'string') {
    return abstractEqualityComparison(x, Number(y));
  }
  if (TypeOf(x) === 'string' && TypeOf(y) === 'number') {
    return abstractEqualityComparison(Number(x), y);
  }

  // Comparing a bigint and a string
  if (TypeOf(x) === 'bigint' && TypeOf(y) === 'string') {
    let n = StringToBigInt(y);
    if (Number.isNaN(n)) {
      return false;
    }
    return abstractEqualityComparison(x, n);
  }
  if (TypeOf(x) === 'string' && TypeOf(y) === 'bigint') {
    return abstractEqualityComparison(y, x);
  }

  // Comparing a boolean with a non-boolean
  if (TypeOf(x) === 'boolean') {
    return abstractEqualityComparison(Number(x), y);
  }
  if (TypeOf(y) === 'boolean') {
    return abstractEqualityComparison(x, Number(y));
  }

  // Comparing an object with a primitive
  // (other than undefined, null, a boolean)
  if (['string', 'number', 'bigint', 'symbol'].includes(TypeOf(x))
    && TypeOf(y) === 'object') {
      return abstractEqualityComparison(x, ToPrimitive(y));
    }
  if (TypeOf(x) === 'object'
    && ['string', 'number', 'bigint', 'symbol'].includes(TypeOf(y)) {
      return abstractEqualityComparison(ToPrimitive(x), y);
    }
  
  // Comparing a bigint with a number
  if ((TypeOf(x) === 'bigint' && TypeOf(y) === 'number')
    || (TypeOf(x) === 'number' && TypeOf(y) === 'bigint')) {
      if ([NaN, +Infinity, -Infinity].includes(x)
        || [NaN, +Infinity, -Infinity].includes(y)) {
          return false;
        }
      if (isSameMathematicalValue(x, y)) {
        return true;
      } else {
        return false;
      }
    }
  
  return false;
}
Las siguientes operaciones no se muestran aquí:
  • strictEqualityComparison()
  • StringToBigInt()
  • isSameMathematicalValue()
Ahora que hemos examinado más de cerca cómo funciona la coerción de tipos de JavaScript, concluyamos con un breve glosario de términos relacionados con la conversión de tipos:
  • En la conversión de tipos , queremos que el valor de salida tenga un tipo dado. Si el valor de entrada ya tiene ese tipo, simplemente se devuelve sin cambios. De lo contrario, se convierte en un valor que tiene el tipo deseado.
  • La conversión de tipo explícita significa que el programador usa una operación (una función, un operador, etc.) para activar una conversión de tipo. Las conversiones explícitas pueden ser:
    • Marcado : si un valor no se puede convertir, se genera una excepción.
    • Sin marcar : si un valor no se puede convertir, se devuelve un valor de error.
  • El tipo de conversión depende del lenguaje de programación. Por ejemplo, en Java, es una conversión explícita de tipo verificado.
  • La coerción de tipo es conversión de tipo implícita: una operación convierte automáticamente sus argumentos a los tipos que necesita. Se puede marcar o desmarcar o algo intermedio.
[Fuente: Wikipedia ]

No hay comentarios.:

Publicar un comentario

Dejanos tu comentario para seguir mejorando!

outbrain

Páginas