四千字详解JS中的变量类型与类型转换

JS中的变量类型与类型转换

NOTE:文末有几个题目,看完这篇文章来检测一下吧

一、JS中的变量类型

Everything in JavaScript is either a primitive or object

  • 原始类型:Bumber、String、boolean、Null、Undefined、Symbol、BigInt
  • 引用类型:Object

其中,引用数据类型可统称为一种,在JS中万物皆对象,其他的如Function、Array、Date等都是基于Object的

二、拓展

1.包装对象

使用原始类型的变量时,为什么我们可以像使用引用数据类型一样直接调用属性和方法呢?这就涉及到"包装对象"。

  • 定义

所谓"包装对象",指的是数值、字符串、布尔值分别相对应的NumberStringBoolean三种原始值类型。这三个原生对象可以把原始类型的值变成(包装成)对象。

比如

js 复制代码
"abc".length  //3
  • 自动转换

在Number、String、Boolean类型调用属性或方法的时候JavaScript 引擎自动将其转为包装对象,在这个对象上调用其属性或方法。调用结束后,这个临时对象就会被销毁。

2、Number类型中的几个特殊值

NaN

直译为 not a number。不过事实上NaN为Number类型,更贴切地可以描述为不是一个有效的数字。

js 复制代码
Number("a") //NaN
1/"a"  //NaN
0/0    //NaN

NaN的特殊行为:

  • 如果 NaN 涉及数学运算(但不涉及位运算),结果通常也是 NaN。(参见下面的反例。)
  • NaN 是任何关系比较(>, <, >=, <=)的操作数之一时,结果总是 false
  • NaN 不等于(通过 ==、!=、===和 !==)任何其他值------包括与另一个 NaN 值。
js 复制代码
NaN === NaN; // false
Number.NaN === NaN; // false
isNaN(NaN); // true
isNaN(Number.NaN); // true
Number.isNaN(NaN); // true

Object.is(NaN,NaN)  //true
Infinity

Infinity表示"无穷",用来表示两种场景。一种是一个正的数值太大,或一个负的数值太小,无法表示;另一种是非0数值除以0,得到Infinity。此外,其也有正负之分

js 复制代码
1**1024 //Infinity
1/0     //Infinity
-1/0    //-Infinity

3、null与undefined

js 复制代码
typeof(null)  //"object"
typeof(undefined) //"undefined"

JavaScript的最初版本是这样区分的:null是一个表示"无"的对象,转为数值时为0;undefined是一个表示"无"的原始值,转为数值时为NaN。

用法区别:

**null表示"没有对象",即该处不应该有值。**典型用法是:

复制代码
(1) 作为函数的参数,表示该函数的参数不是对象。
(2) 作为对象原型链的终点。

**undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。**典型用法是:

javascript 复制代码
(1)变量被声明了,但没有赋值时,就等于undefined。
(2) 调用函数时,应该提供的参数没有提供,该参数等于undefined。
(3)对象没有赋值的属性,该属性的值为undefined。
(4)函数没有返回值时,默认返回undefined。

二、显示类型转换

首先需要明确一点,JS中只存在三种类型转换

  • to string
  • to boolean
  • to number

然后具体到不同的数据类型向这三种类型转换,如原始数据类型{string, number, boolean, undefined, null} +和引用数据类型{object, ... }

1 to string的原始类型转换

原始数据类型 <value>通过String(<value>) 进行to string的类型转换规则表:

原始类型 类型转换规则
string 无需转换
number ""
boolean "true" or "false"
null "null"
undefined "undefined"
js 复制代码
String(1)     // 1
String(NaN)   //"NaN"
String(Infinity) // Infinity

Note: number和boolean都为原始数据类型, 没有toString()方法, 但是仍然可以以点语法的形式调用toString(), javascript会自动将<number | boolean>.toString()解析为String(<number | boolean>), 进行显式类型转换

2 to number的原始类型转换

原始数据类型 <value> 通过Number(<value>) 进行to number的类型转换规则表:

原始类型 类型转换规则
string - ("", "<空白符>", "<多个空白符>") -> 0 - "<number>" -> <number> - "<16进制数>" -> <16 进制数> -> 10 十进制数 -其他字符串 → NaN
number 无需转换
boolean true -> 1, false -> 0
null 0
undefined NaN

3 to boolean的原始类型转换

原始数据类型 <value> 通过Boolean(<value>) 进行to number的类型转换规则:

  • 转换为false
    • ""
    • 0 || -0 || NaN
    • null
    • undefined
  • 转换为true
    • 非空字符串
    • 非 0 和非NaN 的数值

对象类型到boolean转换见4.1

NOTES:但这里有个特殊情况,即具有 [[IsHTMLDDA]] 属性的对象(例如document.all),其Boolean值为false,typeof为undefined

ECMA规范文档说明

If is an Object and has an [[IsHTMLDDA]] internal slot, return false.argumentargument

js 复制代码
Boolean(document.all)   // false

这个了解即可,

4 对象 to {string, number, boolean}的 To Primitive类型转换

ES5版本 , 当我们显式或者隐式地将对象转换为原始值(primitive value)的时候, 通常是默认调用 对象自身或者原型链上的toString() 或者valueOf() 方法, 将其转换原始值. 或者可以自定义覆盖对象的这两个方法来控制对象的to primitive行为, 不过不建议这样做.

ES6 为开发者提供了官方的[Symbol.toPrimitve] 接口来自定义对象的to primitive操作, 当对象发生显式或者隐式类型转换操作的时候, 会自动调用其预先定义的[Symbol.toPrimitive] 方法, 同时会忽略对象自身的toString()valueOf()方法.

在下面的内容, 将重点讨论ES6版本中, 对象发生类型转换时的方法调用逻辑. 主要划分为一下几块内容进行分析:

  • 对象 to boolean
  • 对象 to string | number
    • 对象有自定义的[Symbol.toPrimitive] 方法
      • 对象 to string
      • 对象 to number
    • 对象没有自定义的[Symbol.toPrimitive]方法
      • 对象 to string
      • 对象 to number

4.1 对象 to boolean

关于对象转为布尔值的机制很简单, 一般情况下, 对象to boolean都是直接转换为true, 而且不会调用对象的[Symbol.toPrimitive], toString, valueOf 这三个方法.

不过需要注意的是, 在<你不知道的JavaScript_中卷>4.2.3 节中提到, document.all对象在现代浏览器中转为布尔值的时候为true,而在老版本的IE浏览器中为false. 这是一种极特殊情况,在vscode中使用这个对象的时候,会提示已经被弃用了,所以不用在意.

这里需要注意的点是空对象

其中,只有null表示空对象,故下列变量中的obj1obj2转换为boolean后均为 true

js 复制代码
let obj1 = {};
let obj2 = new Object();
 
let con1 = obj1.constructor;
let con2 = obj2.constructor;
 
console.log(con1);//ƒ Object() { [native code] }
console.log(con2);//f Object() { [native code] }

4.2 对象to string | number

对于对象发生类型转换时, 主要设计三个方法[Symbol.toPrimitive], toString, valueOf 我们需要明确调用时机顺序。

在ES5的时候, 对象的类型转换是通过内置或者自定义的toString, valueOf 方法进行to primitive类型转换的.

比如下面的代码中, 是对象 to string的类型转换.

js 复制代码
const obj = {
  toString() {
    console.log('to string called') // to string called
    return 1
  },

  valueOf() {
    console.log('value of called')
  },
}

let res = String(obj)
console.log(res) // "1"
console.log(typeof res) // string

对象<obj> to string的类型转换的步骤为:

  • 调用对象<obj>的toString()方法, 没有则去原型链上查找
    • 如果toString返回值为原始值, 对返回值进行原始值 to string的类型转换, 转换后的结果即为对象 to string的类型转换结果
    • 如果toString返回值为对象, 那么将调用<obj>的valueOf()方法
      • 如果返回值为原始值, 对返回值进行原始值to string的类型转换, 转换后的结果即为对象 to string的类型转换结果
      • 如果返回值为对象, 则报错 ❌

对象<obj> to string是先调用toString(), 并在返回值为对象的时候, 再调用valueOf().


而对象<obj> to number的时候, 调用的顺序相反, 不过其他逻辑基本相同.

对象<obj> to number的类型转换的步骤为:

  • 调用对象<obj>的valueOf()方法, 没有则去原型链上查找

    • 如果valueOf返回值为原始值, 对返回值进行原始值 to number的类型转换(2.2 节), 转换后的结果即为对象 to string的类型转换结果

    • 如果valueOf返回值为对象, 那么将调用<obj>toString ()

      方法

      • 如果返回值为原始值, 对返回值进行原始值to number的类型转换, 转换后的结果即为对象 to number的类型转换结果
      • 如果返回值为对象, 则报错 ❌

在es5的时候, 对象到原始值的转换结果依赖其toString()valueOf()方法, 这两个方法可以自定义,也可以是原型方法. 一些JavaScript的内置对象会有自己的内置toString()valueOf()方法, 位于其原型上, 汇总如下:

类型 toString valueOf
object "[object <type>]" 指向自身
function 函数的字符串形式 指向自身
array "arr0,arr1,..." 或者 ""(数组为空) 指向自身
date 包含本地时间信息的字符串 从1970年 1 月 1 日开始至今的毫秒数
regexp 正则表达式的字符串表示形式 指向自身
error 错误名+错误信息: "<err>.name:<err>.message" 指向自身

总结:只有Date重写了自身的valueOf()方法,其余都是返回自身

除了Number、Boolean、String、Array、Date、RegExp、Function这几种实例化对象之外,其他对象返回的都是该对象的类,都是继承的Object.prototype.toString方法。

js 复制代码
var obj = new Object({});
obj.toString(); // "[object Object]"

Math.toString(); // "[object Math]"

在ES6, 开发者可以通过官方提供的[Symbol.toPrimitive]接口去定义对象 to primitive 的行为. 需要注意的是 , 如果自定义了对象的[Symbol.toPrimitive]的方法, 那么, 当对象发生 to primitive类型转换的时候, 那么只会调用[Symbol.toPrimitive]方法, 而无视ES5中提供的toString(), valueOf()方法.

比如下面将对象显式转换为string的代码中, 只有[Symbol.toPrimitive]()会被调用: 如果返回结果为对象, 则直接报错(不会去调用toString或者是valueOf); 如果返回结果为原始值, 则将将该原始值进行to string操作, 作为最终的对象 to primitive结果.

js 复制代码
const obj = {
  [Symbol.toPrimitive] () {
    console.log("to primitive called") // to primitive called
    // return {} error!!!
    return 1
  }

  toString() {
    console.log("to string called")
  }

  valueOf() {
    console.log("value of called")
  }

}

String(obj) // 对象 to primitive(string)

常用总结

八种假值

  • undefined
  • null
  • NaN
  • false
  • '' (empty string)
  • 0
  • -0
  • 0n (BigInt(0))

注意:" "(包含空格的字符串转换为布尔值为true)

js 复制代码
!""  //true
!" " //false

三、隐式类型转换

注:symbol无法参与计算

对于某些运算符, 当A <operator> B的时候, 如果AB类型不一致, 那么将会触发隐式类型转换, 这些运算符汇总如下:

  • 宽松相等运算符==, !=
  • 关系运算符(>, <, <=, >=)
  • 逻辑运算符(&&, ||, !)
  • if, while, for, ? : + (condition)中的条件表达式(condition)
  • 加性运算符 +
  • 算数运算符(-, *, /, %)
  • 一元 +, - 操作

1. 一元加号、减号

+即强制转换为Number类型。直接参考显示类型转换中的to number

-+类似,不过会将结果取负号

javascript 复制代码
+"1"  // 1
+true //1
+""   //0
+"1c" //NaN
+undefined //NaN
+null //0
+{a:1} //NaN

-"1"  // -1
-""   // -0
+"1c" //NaN
-undefined //NaN
-null //-0
-{a:1} //NaN

2. 加法

分为两种:数值相加字符串拼接

在求值时,它首先将两个操作数强制转换为基本类型。然后,检查两个操作数的类型:

  • 如果有一方是字符串,另一方则会被转换为字符串,并且它们连接起来。
  • 如果双方都是 BigInt,则执行 BigInt 加法。如果一方是 BigInt 而另一方不是,会抛出 TypeError
  • 否则,双方都会被转换为数字,执行数字加法。
js 复制代码
1 + "1"  // "11"

1+{a:1}  // "1[object Object]"
//1.{a:1}转换为 "1[object Object]"
//1 + "[object Object]" ,出现字符串,进行字符串拼接

3. 减法

注意和加法区分。减法不涉及字符串拼接,故需要都转换为Number类型

  • 如果有一个操作数为string,boolean,null,undefined中一种,则在后台调用Number()将其转化为数值,再进行减法运算。
  • 如果有一个操作数是对象 ,则调用对象的valueOf()方法取得表示该对象的数值。如果得到的值是NaN,减法结果是NaN。如果对象没有valueOf()方法,调用它的toString()方法,并将得到的字符串转化为数值.(参考上文一元加号的运算法则)
js 复制代码
[]-1 	//-1  过程[]->''->0  即相当于 0-1
[0]-1   //-1
[0,1]-1 //NaN 过程[0,1]->"0,1"->NaN 即相当于 NaN-1

{}-1    //NaN

4.相等判断

4.1宽松相等(==)

注意:特例NaN不等于自身

详细比较过程MDN文档与ECMAScript规范有一些初入,这里参考ECMASCript

ECMAScript® 2023 Language Specification (ecma-international.org)

  1. If Type(x) is Type(y), then a. Return IsStrictlyEqual(x,y )

  2. If x is null and is undefined, return true

  3. If x is undefined and is null, return true

  4. NOTE: This step is replaced in section B.3.6.2.(即下列步骤)

    a. If is an Object, has an [[IsHTMLDDA]] internal slot, and is either null or undefined, return true

    b. If is either null or undefined, is an Object, and has an [[IsHTMLDDA]] internal slot, return true

  5. If x is a Number and y is a String, return ! IsLooselyEqual(x, ! ToNumber(y))

  6. If x is a String and y is a Number, return ! IsLooselyEqual(! ToNumber(x),y )

  7. If x is a BigInt and y is a String, then a. Let n be StringToBigInt(y). b. If n is undefined, return false. c. Return ! IsLooselyEqual(x,n )

  8. If x is a String and y is a BigInt, return ! IsLooselyEqual(y,x )

  9. If x is a Boolean, return ! IsLooselyEqual(! ToNumber(x), y)

  10. If x is a Boolean, return ! IsLooselyEqual(x, ! ToNumber(y))

  11. If x is either a String, a Number, a BigInt, or a Symbol and y is an Object, return ! IsLooselyEqual(x, ? ToPrimitive(y))

  12. If x is an Object and y is either a String, a Number, a BigInt, or a Symbol, return ! IsLooselyEqual(? ToPrimitive(x),y )

  13. If x is a BigInt and y is a Number, or if x is a Number and y is a BigInt a. If x is not finite or y is not finite, return false b. If ℝ(x) = ℝ(y), return true; otherwise return false

  14. Return false.

补充一些名词的解释

[\[IsHTMLDDA\]\] internal slot](https://link.juejin.cn?target=https%3A%2F%2F262.ecma-international.org%2F14.0%2F%23sec-IsHTMLDDA-internal-slot "https://262.ecma-international.org/14.0/#sec-IsHTMLDDA-internal-slot"):拥有IsHTMLDDA作为属性。如document.all ℝ():表示转换为数学上的值(+∞ 到 -∞),-0与+0相同,均为0。但-infinity与infinity不同

浅浅地翻译一下,大意为(有些地方稍作修改和调整)

js 复制代码
1、若 Type(x) 与 Type(y) 相同, 则进行严格相等(===)的比较
2、若 x 为 null 且 y 为 undefined, 返回 true。
3、若 x 为 undefined 且 y 为 null, 返回 true。
4、这里只需记住document.all 与 null或undefined ,返回true即可。
5、若 Type(x) 为 Number 且 Type(y) 为 String,返回比较 x == ToNumber(y) 的结果。
6、若 Type(x) 为 String 且 Type(y) 为 Number,返回比较 ToNumber(x) == y 的结果。
7、若 Type(x) 为 String 且 Type(y) 为 BigInt,
	使用与 BigInt() 构造函数相同的算法将字符串转换为 BigInt。如果转换失败,返回 false,然后再进行比较。
8、和第7步类似(顺序交换即可) 
9、若 Type(x) 为 Boolean, 返回比较 ToNumber(x) == y 的结果。
10、若 Type(y) 为 Boolean, 返回比较 x == ToNumber(y) 的结果。
11、若 Type(x) 为 String 或 Number,且 Type(y) 为 Object,返回比较 x == ToPrimitive(y) 的结果。
12、若 Type(x) 为 Object 且 Type(y) 为 String 或 Number, 返回比较 ToPrimitive(x) == y 的结果。
13、BigInt与Number类型比较
	a.如果Number类型的值为无穷值(Infinity或-Infinity),直接返回false
    b.如果两个值转换为数学上的值相同,返回true。(数学上-0与+0均为0)
14、返回 false

ECMAScript® 2023 Language Specification# R

4.2严格相等(===)

类型必须相同。

特例:NaN不等于自身。

js 复制代码
null == undefined		//true
0 == false				//true 	

null===undefined      //false
0 === false			  //false
4.3 Object.is()

全等的缺陷

js 复制代码
NaN === NaN //false
-0 === +0   //true

解决:使用Object.is()

js 复制代码
Object.is(NaN,NaN) //true
Object.is(-0,+0)   //false

Object.is()与===的区别

js 复制代码
//实现Object.is()
//可观察Object.is()与===的区别
function equal(a,b) {
      // 出现0、+0、-0
      if(a===0||b===0){
        return 1/a === 1/b
      }
      // 均为NaN
      if(a!==a&&b!==b){
        return true
      }

      return a===b
}

四、几个面试题

最后来几个面试题来测试一下吧

题目一

js 复制代码
[] == !{}

1、! 运算符优先级高于==,故先进行!运算。
2、!{}运算结果为false,结果变成 [] == false比较。
3、根据4.1中的规则(引用类型与基本类型比较),等式左边ToPrimitive([]) == ''。
   按照上面规则进行原始值转换,[]会先调用valueOf函数,返回this。
   不是原始值,继续调用toString方法,x = [].toString() = ''。
   故结果为 '' == 0比较。
5、等式左边为string,右边为number,等式左边x = ToNumber('') = 0。
   所以结果变为: 0 == 0,返回true,比较结束。

题目二

js 复制代码
let result = 100 + true + 21.2 + null + undefined + "Tencent" + [] + null + 9 + false;
//'NaNTencentnull9false'
js 复制代码
//拆解
100 + true  //100 + 1 = 101
101 + 21.2 // 122.2
122.2 + null // 122.2 + 0 
122.2 + undefined //NaN
NaN + "Tencent" // "NaNTencent"
"NaNTencent" + [] // "NaNTencent" + ""
"NaNTencent" + null // "NaNTencent" + "null"
"NaNTencentnull" + 9 // "NaNTencentnull9"
"NaNTencentnull9" + false // "NaNTencentnull9false"

题目三

js 复制代码
let arr= []
arr[0] = 1
arr["0"] = 2
arr["1"] = 3
arr[-1] = 4
arr[{}] = 5

console.log(arr);

浏览器打印结果

0: 2

1: 3

-1:4

[object Object]: 5

length: 2

解析:

  • 知识点1:在js中,数组底层也是通过键值对存储的。当我们以索引形式为数组添加元素时,会发生以下过程。自动将传入的索引值转换为CanonicalNumericIndexString (规范数字索引字符串)
    1. 将索引转换为数值类型
    2. 再将索引转换为字符串类型
    3. 最终添加至数组

​ 故 arr[0]与arr["0"]事实上等价

  • 知识点2:数组也是一个对象,可以添加属性。数组的索引范围为 [0,2^32-1) ,而向对象中添加属性时,若不在此范围,就相当于向其身上添加属性。

参考文章及资料

undefined与null的区别 - 阮一峰的网络日志 (ruanyifeng.com)

JS加法运算全解析 - 简书 (jianshu.com)

最全的javascript类型转换规则精简总结 - 掘金 (juejin.cn)

ECMAScript® 2023 Language Specification (ecma-international.org)

相关推荐
passerby60618 分钟前
完成前端时间处理的另一块版图
前端·github·web components
掘了15 分钟前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅18 分钟前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅40 分钟前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅1 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊1 小时前
jwt介绍
前端
爱敲代码的小鱼2 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte2 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc