JavaScript中的令人头晕的类型转换

159 阅读9分钟

强制类型转换

toString

  • 这个方法用于处理非字符串转化为字符串的强制类型转换
  • 对于普通对象来说,除非自定义,否则继承Object原型上toString()【Object.prototype.toString()】返回的是内部属性[[Class]]的值,如"[object Object]"
  • 数组默认对toString()方法进行了重新,将所有单元字符串化再用","连接起来
const a = [1,2,3];
a.toString();// "1,2,3"

JSON

  • JSON.stringify() 如果在对象中遇到undefined,function,symbol是会自动忽略,在数组中则会返回null(为了保证单元位置不变)
JSON.stringify(undefined); // undefined
JSON.stringify(function () {}); //undefined
JSON.stringify([1, undefined, function () {}]);// "[1,null,null]"
JSON.stringify({a:4,b:function (){}});// "{"a",2}"
  • 对包含循环引用的对象执行JSON.stringify()会报错
  • 当通过JSON.stringify()将对象转化为字符串时,如果目标对象有toJSON()方法,会自动调用toJSON()方法,方法中this指向这个对象,很多人认为toJSON返回的是JSON字符串化后的值,其实不是这样的,除非我们想要对字符串进行字符串化(当然,应该没有这种需求)。toJSON()方法返回的应该是一个合适的值,可以是任何类型,然后再对其进行字符串化。也就是说,返回的值应该是一个能够被字符串化的值。
const a = {
  name: "zf",
  job: ["web", "java"],
  toJSON() {
    return this.job;
  },
};
console.log(JSON.stringify(a)); //["web","java"]
  • 可以向JSON.stringify()传如第二个可选参数replacer,它可以是数组或者是函数,用于指定对象序列化过程中,哪些属性需要处理。
  1. 如果replacer是一个数组,那么它必须是一个字符串数组,其中包含序列化要处理的对象的属性名,除此之外的属性会被忽略。
const A = {
  b: 23,
  c: "54",
  d: [3, 4, 5],
};
console.log(JSON.stringify(A, ["b", "c", "d"])); //{"b":23,"c":"54","d":[3,4,5]}

  1. 如果replacer是一个函数,他会对对象本身调用一次,然后对对象中的每个属性各调用一次,每次传递两个参数,键和值,如果要忽略某个键就返回undefined,否则返回指定的值,如果值为数组,遍历进入数组,键为0,1,2,值为元素。
const A = {
  b: 23,
  c: "54",
  d: [3, 4, 5],
};
console.log(
  JSON.stringify(A, (k, v) => {
    if (k !== "1") {
      return v;
    } else {
      return "我是四";
    }
  })
); // {"b":23,"c":"54","d":[3,"我是四",5]}

ToNumber(如果要将非数字的值当做数字来使用,比如数字运算)

  • true转化为1,false转化为0,undefined转化为NaN,null转化为0,如果转化失败就为NaN
  • 对象(包括数组)会首先被转化为相应的基本类型值,如果返回的是非数字的基本类型值,再强制转化为数字
  • 为了将值转化为相应的基本类型值,抽象操作ToPrimitive检查该值是否有valueOf()方法,如果有并且可以返回基本类型值,就使用该值进行强制类型转化,如果没有就使用toString()方法的返回值来进行(如果存在)转化
  • 如果对象是由Object.create(null)创建的,对象的原型指向null,并且没有valueOf()toString()方法,因此无法进行强制类型转换

ToBoolean

  • javaScript有两个关键字true,false,分别代表boolean类型的真和假。
  • 假值 javaScript中的值可以分为两类
    • 可以被强制类型转化为false的值
    • 可以被强制转化为true的值

这些就是假值:undefined,null,false,+0,-0,NaN,""

假值的强制转化结果为false,可以认为假值以外的所有都是真值。

隐式类型转换

虽然隐式类型转换会让代码晦涩难懂,利用合理的话也会减少冗余,让代码更简洁

字符串和数字之间的隐式类型转换

    const a = "32";
    const b = 54;
    // a + a = "3232";
    // a + b = "3254";
    // b + d = 57;
    const arr1 = [1, 2];
    const arr2 = [3, 4];
    // arr1 + arr2 = "1,23,4";
  • 根据ES5规范,如果某个操作数是字符串或者能转换为字符串的话,+操作符进行拼接操作,数组的valueOf()操作无法得到一个基本类型值,于是它转而调用toString(),因此上面的两个数组变成了"1, 2"和"3, 4",+拼接为"1, 23, 4";
  • 运算符会将字符串强制转化为数字,因为数字才有这个操作符,数组会先调用toString()转化为字符串,然后再转化为数字。
const a = "3.14";
const b = a - 0;
console.log(b); // 3.14
const c = [4];
const d = [2];
console.log(c - d); // 2

布尔值到数字的隐式类型转换

console.log(false == 0); // true
console.log(false == 1); // false
console.log(true == 0); // false
console.log(true == 1); // true

console.log(false == 3); // false
console.log(true == 3); // false

console.log(false == -3); // false
console.log(true == -3); // false

示例

// 用于判断参数是否只有一个为真值
function onlyOne() {
    let sum = 0;
    for (let i = 0; i < arguments.length; i++) {
        if (arguments[i]) {
            sum += arguments[i];
            console.log(sum);
        }
    }
    return sum == 1;
}

隐式类型转换为布尔值

  • 下面的情况布尔值会隐式强制类型转换
    • if()语句中的条件判断表达式
    • for语句中条件判断表达式
    • while循环中的条件判断表达式
    • 三目运算符中的条件判断表达式
    • 逻辑运算|| ,&& 作为条件判断表达式的部分

nullundefined之间的相等比较

  • null == undefined (true),也就是说,在==中,null和undefined是一回事,可以互相进行隐式类型转换:
const a = null;
const b;
a == b; // true
b == null // true
a == false; //false
b == false; // false
a == ""; // false
b == ""; //false
a == 0; // false
b == 0; // false

对象与非对象之间的相等比较

  • 关于对象(对象、函数、数组)和基本类型(字符串、数字、布尔值)之间的比较
    • 如果Type(x)是字符串或者数组,Type(y)是对象,则返回x==ToPrimitive(y)的结果
    • 如果Type(x)是对象,Type(y)是字符串或者数组,则返回ToPrimitive(x)==y的结果
const a = 42;
const b = [42];
a == b; // true
// [42]会首先调用ToPrimitive抽象操作,返回"42",变成"42" == 42,然后又变成42 == 42
  • "拆封"封装对象如(new String("abc")), 返回其中的基本类型"abc"。 == 中的ToPrimitive强制类型转换也会发生这样的情况
const a = null;
const b = Object(a);
a == b; // false
a === b; // false
  • 有一些值不是这样,原因是==算法中其他优先级更高的规则
const a = null;
const b = Object(a);
a == b; //false

const c = undefined;
const d = Object(c);
c == d; // false

const e = NaN;
const f = Object(e);
e == f; // false
// 因为没有对象的封装对象,所以null和undefined不能被封装,Object(null)和Object()均返回一个常规对象
// NaN能够封装为数字类型对象,但拆封后NaN!=NaN

比较少见的情况

  • 返回其他数字
Number.prototype.valueOf = function () {
  return 3;
};
new Number(2) == 3; // true
// 2 == 3不会有这种问题,而Number(2)涉及ToPrimitive转换

  • 如果让 a == 2 && a == 3同时成立
let i = 2;
Number.prototype.valueOf = function() {
    return i++;
}
let a = new Number(454);
if (a == 2 && a == 3) {
    console.log("good!"); // good
}
  • 假值的相等比较(容易造成混乱的情况)
console.log("0" == false); // true
console.log(0 == false); // true
console.log([] == false); // true
console.log("" == false); // true

console.log(0 == ""); // true
console.log(0 == []); // true
console.log("" == []); // true
// 极端情况
[] == ![]; // true
// 首先根据ToBoolean规则,进行值的显示类型转换,所以[] == ![]变成了[] == false

数组进行ToPrimitive是会转化为字符,[null]会直接转化为""

2 == [2]; //true
"" == [null]; //true
0 == "\n"; //true
  • 安全运用隐式类型转换

    • 如果两边的值其中有true或者false,千万不要使用==
    • 如果两边值中有[],"",或者0,尽量不要使用==
  • 抽象关系比较

  1. 示例一

比较双方首先调用ToPrimitive,结果如果出现非字符串,就根据ToNumber规则将双方转化为数字进行比较,如果都是字符串,就从头比较字符编码。

const a = ["123"];
const b = ["21"];
console.log(a < b); // true
const c = ["123"];
const d = 21;
console.log(c < d); // false
  1. 示例二

在JavaScript中<=应该是不大于的意思,>=是不小于的意思,a<=b处理为!(a>b)同理a>=b处理为!(a<b)

const a = { b: 42 };
const b = { b: 43 };
console.log(a < b); //false
console.log(a == b); //false
console.log(a > b); //false

console.log(a >= b); //true
console.log(a <= b); // true

[] == ![] 的结果为什么是true

在JavaScript中,console.log([] == ![]) 之所以输出 true,是因为这个表达式涉及到了JavaScript中的类型转换和比较操作。

  • [] 是一个空数组,它是一个对象。
  • ![] 是对空数组应用逻辑非操作符 !。在JavaScript中,任何对象(包括数组,因为数组是特殊的对象)在逻辑非操作符 ! 的作用下都会被转换为布尔值 true(因为对象总是被认为是“真值”),然后逻辑非操作符会将其取反,所以 ![] 的结果是 false

接下来,我们来看比较操作 [] == ![],左边是[](一个对象),右边是false(一个Boolean),所以 会尝试将对象转换为基本类型。对于对象到基本类型的转换,JavaScript会使用对象的valueOf方法(如果它返回的不是基本类型,则忽略其结果)和toString方法(如果valueOf的结果不是基本类型,则使用toString的结果)。

对于数组,valueOf 方法通常返回数组对象本身(不是一个基本类型),但 toString 方法会返回数组的字符串表示,即空数组 [] 的字符串表示是 ""(空字符串)。

因此,在比较 [] == ![] 时,JavaScript会尝试将 [] 转换为基本类型以与 false 进行比较。由于 [] 的 valueOf 方法不返回基本类型,所以会使用 toString 方法,得到空字符串 ""

然后,JavaScript会尝试将 false 转换为可以与空字符串比较的值。在JavaScript中,false 在与字符串进行比较时会被转换为数字 0(因为 false 的数字表示为 0),而空字符串 "" 在与数字进行比较时也会被转换为数字 0(空字符串的数字表示也是 0)。

最后,我们得到的是 0 == 0,这是一个真值,所以 [] == ![] 的结果是 true

总结

在规范的开发中,尽量减少使用隐式类型转换,因为隐式类型转换很多时候会造成难以理解的现象,开发中用===代替==,做Boolean判断的时候,如果目标值不是Boolean,最好用!!做一次Boolean类型转换。

const res = !!目标值 ? '返回1' : '返回2'