JavaScript 类型转换

类型转换:type casting,值从一种类型转换为另一种类的操作。

类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时,在 JavaScript 中统称为强制类型转换。 强制类型转换可具体分为隐式强制类型转换显示强制类型转换

var a = 42;
var b = a + "";    // 隐式强制类型转换
var c = String(a); // 显式强制类型转换

JavaScript 中的强制类型转换总是返回基本类型值,如字符串、数字和布尔值。

1. 抽象操作

抽象操作:仅供 JavaScript 内部使用的操作。

在 JavaScript 中进行不同类型之间的操作时,JavaScript 会首先把这些类型转换为相同的类型在进行操作,这些通常是发生在内部中的操作,比如字符串的转换、数字的转换、布尔值的转换等等,每种类型两两之间都会有定义好的转换规则与抽象操作,本篇主要介绍 ToString、ToNumber、ToBoolean、ToPrimitive 这些常用的抽象操作。

1.1 ToPrimitive(input [, PreferredType])

当对象 {} + {}[] + [] 进行类似的操作时,都会调用 ToPrimitive 将对象都会被转换为原始值,然后,再进行操作。

ToPrimitive 会接收两个值,第一个参数是 input 即需要转换的值,第二个是转换的首选类型 preferredType(优先转换为该类型)。

由于所有的对象在布尔上下文中均为 true,所以对于对象就不存在 to-boolean 的转换,只有字符串和数值的转换。

1.1.1 ToPrimitive 转换规则

第一步,判断 input 是否是原始值

  • i. 原始值,直接返回 input,结束转换;
  • ii. 对象值,继续执行第二步;

第二步,确定对象转换类型 hint

  • i. 如果 preferredType 值是 string,那么 hint = string,表示优先转换为字符串;
  • ii. 如果 preferredType 值是 number,那么 hint = number,表示优先转换为数字;
  • iii. 如果 preferredType 未传递,则默认为 default(可通过定义 @@toPrimitive 修改默认值),default 会在后续确定。

第三步,判断 input 对象是否拥有 @@toPrimitive 方法

  • i. 如果 input[Symbol.toPrimitive] 存在,则传入 hint 调用 input[Symbol.toPrimitive](hint) 该方法。执行结果如果是原始值,返回调用结果;否则抛出 TypeError,结束转换。
  • ii. 不存在该方法,执行第四步。

第四步,转换对象为基本类型

  • i. 如果 hint 值是 default,则修改默认值为 number;
  • ii. 如果 hint 值是 string,依次执行第五步、第六步、第七步;
  • iii.如果 hint 值是 number,执行第六步、第五步、第七步;

第五步,hint 值是 string,执行 input 的 toString 方法

  • i. 如果 toString() 的结果是基本类型,那么返回该结果,结束转换;

第六步,hint 值是 number,执行 input 的 valueOf 方法

  • i. 如果 valueOf() 的结果是基本类型,那么返回该结果,结束转换;

第七步,抛出错误结束转换

如果程序执行到此步骤,说明无法转换到最终的结果,抛出 TypeError,结束转换。

1.1.2 ToPrimitive 代码实现

// 获取类型
const getType = (obj) => {
  return Object.prototype.toString.call(obj).slice(8, -1);
};
// 是否为原始类型
const isPrimitive = (obj) => {
  const types = ["String", "Undefined", "Null", "Boolean", "Number"];
  return types.indexOf(getType(obj)) !== -1;
};
const ToPrimitive = (input, preferredType) => {
  // 如果input是原始类型,那么不需要转换,直接返回
  if (isPrimitive(input)) {
    return input;
  }
  let hint = "",
    exoticToPrim = null,
    methodNames = [];
  // 当没有提供可选参数preferredType的时候,hint会默认为"default";
  if (!preferredType) {
    hint = "default";
  } else if (preferredType === "string") {
    hint = "string";
  } else if (preferredType === "number") {
    hint = "number";
  }
  exoticToPrim = input.toPrimitive;
  // 如果有toPrimitive方法
  if (exoticToPrim) {
    // 如果exoticToPrim执行后返回的是原始类型
    if (typeof (result = exoticToPrim.call(O, hint)) !== "object") {
      return result;
      // 如果exoticToPrim执行后返回的是object类型
    } else {
      throw new TypeError("TypeError exception");
    }
  }
  // 这里给了默认hint值为number,Symbol和Date通过定义@@toPrimitive方法来修改默认值
  if (hint === "default") {
    hint = "number";
  }
  return OrdinaryToPrimitive(input, hint);
};
const OrdinaryToPrimitive = (O, hint) => {
  let methodNames = null,
    result = null;
  if (typeof O !== "object") {
    return;
  }
  // 这里决定了先调用toString还是valueOf
  if (hint === "string") {
    methodNames = [input.toString, input.valueOf];
  } else {
    methodNames = [input.valueOf, input.toString];
  }
  for (let name in methodNames) {
    if (O[name]) {
      result = O[name]();
      if (typeof result !== "object") {
        return result;
      }
    }
  }
  throw new TypeError("TypeError exception");
};

1.1.3 Symbol.toPrimitive

Symbol.toPrimitive 是内建的 symbol,被用来给转换方法命名,作为对象的属性名,属性值则是一个接收 hint 参数的函数,函数具体是转换对象为原始类型实现,如下:

obj[Symbol.toPrimitive] = function(hint) {
  // 返回一个原始值
  // hint = "string"、"number" 和 "default" 中的一个
}

比如,通过给 user 对象添加 Symbol.toPrimitive 属性的函数,实现自定义的原始类型转换:

const user = {
  name: "John",
  money: 1000,
  [Symbol.toPrimitive](hint) {
    console.log(`hint: ${hint}`);
    return hint == "string" ? `{name: "${this.name}"}` : this.money;
  }
};

console.log(+user); // hint: number -> 1000
console.log(user + 500); // hint: default -> 1500

1.1.4 toString()/valueOf()

如果对象没有定义 Symbol.toPrimitive,那么 JavaScript 在转换的时候就会尝试调用这两个方法:

  • hint 是 string,则先调用 toString(),再调用 valueOf();如果 toString() 返回的是原始值,则以该返回值作为结果,否则继续判断 valueOf() 的返回值;
  • hint 是 number,则先调用 valueOf(),再调用 toString();如果 valueOf() 返回的是原始值,则以该返回值作为结果,否则继续判断 toString() 的返回值;
const user = {
  name: "John",
  money: 1000,

  // hint="string"
  toString() {
    return `{name: "${this.name}"}`;
  },

  // hint="number" 或 "default"
  valueOf() {
    return this.money;
  },
};

console.log(+user); // 1000
console.log(user + "1000"); // 1500

1.2 ToString

抽象操作 ToString 用于将非字符串强制转换为字符串,转换规则如下:

1.2.1 undefined

"undefined"

1.2.2 null

"null"

1.2.3 boolean

true => "ture",false => "false"

1.2.4 number
数字的转换遵循通用的规则,如下:

  • 如果 numberNaN,则转换为 "NaN"
  • 如果 number 是 +0 或 -0,则都转换为 "0"
  • 如果 number 是 +∞,则转换为 "Infinity"
  • 如果 number 是 非十进制的数字,则默认转换为十进制的数字字符串
  • 如果 number 的十进制数超过 21 位,则会转换为指数形式的字符串

具体参考:https://262.ecma-international.org/12.0/#sec-numeric-types-number-tostring

1.2.5 symbol

symbol 无法转换为字符串,会直接抛出错误,throw TypeError

1.2.6 BigInt

  • 如果 number 是 +0n 或 -0n,则都转换为 "0"
  • 如果 number 是负数,则转换为带符号的字符串
  • 其他情况下,均默认转换为十进制的数字字符串

1.2.7 object
先调用 ToPrimitive 方法,将 object 转换为基本类型的值,再继续调用 ToString 转换为字符串类型。

var a = [1, 2, 3];
a.toString(); // "1,2,3",数组的 `toString()` 是重新定义的,将所有单元字符串化后在用 `,` 连接起来

1.3 ToNumber

抽象操作 ToNumber 用于将非数字强制转换为数字类型。

1.3.1 undefined

NaN

1.3.2 null

0

1.3.3 boolean

true => 1false => 0

1.3.4 string

  • 如果字符串中只包含数字(包括 Infinity),那么就转换为对应的数字。

  • 如果字符串中只包含十六进制格式,那么就转换为对应的十进制数字。

  • 如果字符串为空(包括空格),那么转换为 0。

  • 如果字符串包含上述之外的字符,那么转换为 NaN。

1.3.5 symbol

symbol 无法转换为数字,会直接抛出错误,throw TypeError

1.3.6 BigInt

BigInt 无法转换为数字,会直接抛出错误,throw TypeError

1.3.7 object

先调用内部的 ToPrimitive 方法,将 object 转换为基本类型的值,再继续调用 ToNumber 转换为数字类型。

1.4 ToBoolean

抽象操作 ToBoolean 用于将非布尔值强制转换为布尔值。

1.4.1 undefined

false

1.4.2 null

false

1.4.3 number

如果数字是 0NaN,则转换为 false,其他转换为 true

1.4.4 string

如果是一个空的字符串,即 length 属性值是 0,则转换为 false,其他转换为 true

1.4.5 symbol

true

1.4.6 BigInt

如果值是 0n,则转换为 false,其他转换为 true

1.4.7 object

true

2. 显示强制类型转换

显式强制类型转换就是手动地将一种值转换为另一种值。

常用地显式类型转换方法有 NumberStringBooleanparseIntparseFloattoStringvalueOf 等等。

2.1 字符串和数字的显示转换

字符串和数字之间的转换是通过 String(..)Number(..) 两个内建函数,转换规则遵循 ToStringToNumber

2.2 日期显示转换为数字

一元运算符 + 可以将日期强制转换为数字,返回 Unix 时间戳,以 ms 为单位(1970 年 1 月 1 日 00:00:00 UTC 到当前时间)。

var timestamp = +new Date(); // 1628824218581

// 其他获取时间戳的方法(推荐)
var timestamp = new Date().getTime();
var timestamp = Date.now();

2.3 ~ 运算符

~ 运算符即字位操作 "非",也会触发强制类型转换。位运算符只适用于 32 位整数,通过抽象操作 ToInt32 进行强制转换为 32 位格式。
ToInt32 首先执行 ToNumber 强制类型转换,比如 "123" 会先被转换为 123,然后再执行 ToInt32。

~x 大致等同于 -(x + 1),通过此规律可以简化对 indexOf 的判断:

var a = "Hello World";
if (a.indexOf("lo") >= 0) {
  // true
  // 找到匹配!
}

// 简化
if (~a.indexOf("lo")) {
  // true
  // 找到匹配!
}

2.4 ~~ 运算

~~ 中的第一个 ~ 执行 ToInt32 并反转字位,然后第二个 ~ 再进行一次字位反转,即将所有字位反转回原值,最后得到的仍然是 ToInt32 的结果。

Math.floor(-49.6); // -50
~~-49.6; // -49

2.5 解析数字字符串

解析字符串中的数字(parseInt)和将字符串强制类型转换为数字(Number)的返回结果都是数字。但解析和转换两者之间有明显的差别:

var a = "42";
var b = "42px";

Number(a); // 42
parseInt(a); // 42
Number(b); // NaN
parseInt(b); // 42

parseInt 允许字符串中含有非数字字符,解析顺序从左到右,遇到非数字字符就停止解析。

Number 不允许存在非数字字符,不然就会转换失败返回 NaN。

parseInt(1 / 0, 19); // 18(1/0 得到 "Infinity",第一个是 I,以 19 为基数时值为 18,第二个 n 不是有效的,停止解析。)
parseInt(0.000008); // 0 ("0" 来自于 "0.000008")
parseInt(0.0000008); // 8 ("8" 来自于 "8e-7")
parseInt(false, 16); // 250 ("fa" 来自于 "false")
parseInt(parseInt, 16); // 15 ("f" 来自于 "function..")
parseInt("0x10"); // 16
parseInt("103", 2); // 2

2.6 转换为布尔值

通常建议使用 Boolean(a) 和 !!a 来进行显式强制类型转换。

3. 隐式强制类型转换

隐式类型转换一般是在涉及到运算符的时候才会出现的情况,比如将两个变量相加,或者比较两个变量是否相等。对于对象转原始类型的转换,会遵守 ToPrimitive 的规则。

3.1 字符串与数字之间的转换

对于 + 运算符,如果其中一个操作数是字符串或者可转换为字符串(操作数是对象,包括数组,首先对其调用 ToPrimitive 抽象操作该抽象操作再调用 [[DefaultValue]],以数字作为上下文),那么执行字符串拼接操作;否则执行数字加法。

var a = "42";
var b = 0;
var c = 42;

a + b; // "420"
c + b; // 42

var e = [1, 2];
var f = [3, 4];

e + f; // "1,23,4" // 数组通过 valueOf() 无法转换为基本类型值,所以会继续调用 toString()

对于 a + ""(隐式) 和 String(a)(显式) 转换字符串有一个细微的差别,根据 ToPrimitive 抽象操作规则,a + ""
调用 valueOf() 方法,然后再通过 ToString 抽象操作转换为字符串。而 String(a) 则会直接调用 ToString。

对于 - * / 运算符,在进行运算操作时,两边都会被强制转换为数字进行操作。

var a = [3];
var b = [1];
a - b; // 2

3.2 布尔值到数字的转换

布尔值转换为数字会被转换为 0 和 1,隐式转换的处理方法就是通过与数字进行操作。

var sum = 0;
sum + true + false;

3.3 隐式强制类型转换为布尔值

如下情况,非布尔值都会进行布尔值的隐式强制类型转换,并且遵从 ToBoolean 的抽象操作规则:

  • if (..)语句中的条件判断表达式;
  • for ( .. ; .. ; .. )语句中的条件判断表达式(第二个);
  • while (..) 和 do..while(..) 循环中的条件判断表达式;
  • ? :中的条件判断表达式;
  • 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)

4. 宽松相等(==)与严格相等(===)

宽松相等(loose equals)== 和严格相等(strict equals)=== 都用来判断两个值是否“相等”,比较的结果是布尔值。

两者的区别在于:== 允许在相等比较中进行强制类型转换,而 === 不允许,也就是说在比较的时候,两者都会检查操作数的类型,如果操作数类型不同,== 会先进行强制转换为相同的类型再进行值的比较,而 === 则直接返回 false。

进行 == 比较的时候,具体的类型转换如下:

4.1 字符串与数字的 == 比较

  • 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果
  • 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果

也就是说,如果两者其中之一是数字,那么就会把另外一个字符串转换为数字比较

var a = 42;
var b = "42";

a === b; // false,没有强制类型转换
a == b; // true,b 先转换为数字再进行比较

4.2 布尔值与其他类型的 == 比较

  • 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;
  • 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。

会先把布尔值转换为数字,再进行与其他值的比较。

var x = true;
var y = "42";
x == y; // false,先把 x 转换为数字,即 1。此时 x,y 类型仍不相等;再把 "42" 转换为数字 42,1 == 42,false。

4.3 null 和 undefined 的 == 比较

  • 如果 x 为 null,y 为 undefined,则结果为 true。
  • 如果 x 为 undefined,y 为 null,则结果为 true。

null 与 undefined 进行宽松比较时,结果总是为 true。其他类型的值与 null 和 undefined 进行比较,总会返回 false。

4.4 对象与对象的 == 比较

如果两个对象指向同一个值(引用)时,即视为相等,不发生强制类型转换;否则视为不相等。

4.5 对象与非对象的 == 比较

  • 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果;
  • 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPrimitive(x) == y 的结果。
var a = 42;
var b = [42];

a == b; // true,[42] 首先调用 ToPromitive 抽象操作,返回 "42",变成 "42" == 42,然后 又变成 42 == 42,最后二者相等。
var a = "abc";
var b = Object(a); // 同 new String(a)

a === b; // false
a == b; // true,b 通过 ToPromitive 转换为基本类型值 "abc"

4.6 安全使用 ==

如下是一些比较特殊容易出错的情况:

false == 0; // true
false == ""; // true
false == []; // true
"" == 0; // true
"" == []; // true
"" == [null]; // true
"" == [undefined]; // true
0 == []; // true
0 == "\n"; // true,空格,换行符等都会转换为0
[] == ![]; // true
2 == [2]; // true

在实际使用中,应该遵循如下原则

  • 如果两边的值中有 true 或者 false,千万不要使用 ==
  • 如果两边的值中有 []、"" 或者 0,尽量不要使用 ==
  • 最好使用 === 来避免不经意的强制类型转换

5. 抽象关系比较

在进行 >< 等比较的时候,首先双方会调用 ToPrimitive 转换为基本类型

5.1 转换出现非字符串,根据 ToNumber 规则将双方强 制类型转换为数字来进行比较

var a = [42];
var b = ["43"];
a < b; // true

5.2 转换后双方都是字符串,按字母顺序来进行比较

var a = ["42"];
var b = ["043"];
a < b; // false
var a = { b: 42 };
var b = { b: 43 };

a < b; // false,因为 a,b 转换后都是 [object Object]
a > b; // false,同上
a == b; // false,// a,b 指向的不是同一个引用

a <= b; // true // 根据规范a <= b被处理为 b < a,将 b < a 的结果反转,即为 true
a >= b; // true // 同上
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容