四千字详解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:拥有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)

相关推荐
qq_390161775 分钟前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test35 分钟前
js下载excel示例demo
前端·javascript·excel
Yaml41 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶1 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo1 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v1 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫1 小时前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web
贩卖纯净水.1 小时前
Chrome调试工具(查看CSS属性)
前端·chrome
栈老师不回家2 小时前
Vue 计算属性和监听器
前端·javascript·vue.js