你不一定了解的js数据类型(二)

260 阅读8分钟

上篇文章聊了js两种数据类型的区别以及基本数据类型,这篇文章主要聊一下js的引用数据类型、包装对象、js的强转换、隐性数据类型转换。

一、引用数据类型

在ECMAScript中,引用类型是一种数据结构,用于将数据和功能组织在一起。

除了对象,引用数据类型还有很多:

Array 数组
Date 日期
RegExp 正则
Function 函数

除了上面的数据类型,还有三个比较特殊的(基本数据类型的包装对象):

包装对象

Boolean
Number
String

大家可能遇到过一道面试题:

let num1 = 3
let num2 = new Number(3)
console.log(num === num2)  // false

这个Number()就是基本数据类型的number的包装对象:

typeof(num1) // number
typeof(num2) // object

这两个不是一个东西,当然也不相等了,那么我们就来搞清楚,这个包装对象到底是干什么的?

let str = 'wanghuahua'
let str1 = str.substr(0, 4)
console.log(str1) // wang
console.log(String.substr) // undefined

刚才调用substr方法截取了一下str这个字符串,str是基本数据类型,打印String的substr方法是undefined,这说明String是没有substr方法的,但是为什么能够截取呢?

这中间有一个计算机的操作,我们把它称作‘加工厂’,那么这个加工厂进行了哪些操作呢?(重点)

1、原材料进厂:把基本数据类型转成包装对象

因为基本数据类型是没有方法的,如果你想操作基本数据类型,那就必须转成包装对象

2、加工:调用substr的相关方法

包装对象是可以加方法和属性,所以这个时候包装对象会调用方法进行操作

3、成品出厂:包装对象转成基本数据类型

ECMAScript规范中提供了toPrimitive原则,所以这个地方会遵从toPrimitive原则进行转换。

上篇文章说到了,这个js弱类型语言把相当多的操作都交给了计算机,这个地方就是一个明显的栗子,这些操作就都交给了计算机,相对来说增加了计算机的负担。

包装对象的由来,其实也没有那么复杂,就是为了处理基本数据类型的数据,加了中间的一层。

刚才在说成品出厂的时候,聊到了toPrimitive,toPrimitive就是我们常说的js强制转换:

二、js强制转换

所谓强制转换,就是人为的进行转换:

(一)toString() 转成字符串

let num2 = new String('hhh')
console.log(num2.toString()) // hhh

每个对象都有一个 toString()方法,当对象被表示为文本值时或者当以期望字符串的方式引用对象时,该方法被自动调用。

有时候会有一些非常恶心的面试题,这儿也列举几个不常见的:

console.log(null.toString()) // 报错
console.log(undefined.toString()) // 报错
console.log(true.toString()) // 'true'
let a = 4
console.log(a.toString()) // 4
console.log('hhh'.toString()) // 'hhh'
console.log(Symbol().toString()) // 报错

除此之外,toString()还接收数字参数,来代表转换的进制:

二进制:.toString(2);

八进制:.toString(8);

十进制:.toString(10);

十六进制:.toString(16);

(二)valueOf() 返回原始值

let num2 = new String('hhh')
console.log(num2.valueOf())  // hhh

JavaScript 调用 valueOf()方法用来把对象转换成原始类型的值(数值、字符串和布尔值)。但是我们很少需要自己调用此函数,valueOf 方法一般都会被 JavaScript 自动调用。

String => 返回字符串
Number => 返回数字
Date => 返回时间戳
Boolean => 返回Boolean的this值
Object => 返回this

上面两种方法都是object的方法,除了这两个方法之外,还有三个运算符:

(三)Number运算符 尽量搞成数字

null 转换为 0
undefined 转换为 NaN
true 转换为 1,false 转换为 0
字符串转换时遵循数字常量规则,转换失败返回NaN

(四)String运算符 全部转成字符串

刚才看toString的时候,null和undefined都会报错,但是String就不会报错,万物皆可转,(也不是那么绝对)但是String运算符没办法接收参数。

String(null) // null
string(undefined) // undefined

(五)Boolean运算符 全部转成true或者false

Boolean也很好理解,就是转成布尔值,只需要记住,一下几种转成false,其余的全部是true就行了。

undefined
null
-0
0或+0
NaN
''(空字符串)

除了这六种,其余全部都是true,什么空对象,空数组全部都是true,甚至这种:

Boolean(new Boolean(false)) // true

(六)ToPrimitive运算符 转换成原始值

除了上面五种方法,js还有一个相对综合了一下的ToPrimitive,它对基础数据类型是没用的,只有引用数据类型可以。

ToPrimitive(obj,type)

ToPrimitive运算符接受一个需要转换的对象值,以及一个可选择的type值,根据这个可选的type值,来进行转换:

type为string:

先调用obj的toString方法,如果为原始值,则return,否则进行第2步
调用obj的valueOf方法,如果为原始值,则return,否则进行第3步
抛出TypeError 异常

type为number:

先调用obj的valueOf方法,如果为原始值,则return,否则进行第2步
调用obj的toString方法,如果为原始值,则return,否则第3步
抛出TypeError 异常

type参数为空

该对象为Date,则type被设置为String
否则,type被设置为Number

三、隐式数据类型转换

因为js是所类型语言,所以相对语法非常宽松,字符串可以和数字运算,数字还能和布尔值运算,在运算过程中就产生了隐式数据类型转换,首先,隐式转换有下面三个原则:

1、遇到+(字符串连接符)就转成string类型:
2、转成number类型:
++/--(自增自减运算符) 
+ - * / %(算术运算符) 
> < >= <= == != === !=== (关系运算符)
3、遇到 !(逻辑非运算符),就转成boolean类型:

但是,这里面还有很多常见的坑,比如:

1、字符串拼接的+和算数运算符的+都是+,到底是哪个?

常见的面试题:

console.log(1 + 'true') // '1true'
console.log(1 + true) // 2
console.log(1 + null) // 1
console.log(1 + undefined) // NaN
console.log(null + undefined) //NaN

关于字符串拼接+和算数运算符+,有个原则,只要有一边是字符串,那这个+就是字符串拼接符,其余的所有的情况都是算数运算符。

console.log(1 + 'true') // '1true'

'true'是字符串,所以是字符串拼接,只要是字符串拼接,两边都会调用String运算符

console.log(1 + true) // 2 Number(1)+Number(true) 1+1 2
console.log(1 + null) // 1 Number(1)+Number(null) 1+0 1
console.log(1 + undefined) // Number(1)+Number(undefined) 0+undefined NaN
console.log(null + undefined) //Number(null)+Number(undefined) 0+undefined NaN

上面的没有一边是字符串,所以是运算符,如果被当做运算符,那么两边都会调用Number运算符

2、关系运算符,为什么2<'10','2'>'10'?

常见的面试题:

console.log(2>'10') // false
console.log('2'>'10') // true
如果关系运算符一边是字符串,那么会调用Number运算符,把两边转成数字,再比较
如果关系运算符两边都是字符串,会用charCodeAt()方法转成数字,再比较
console.log(2>'10') // Number(2)>Number(10) 2>10 false 
console.log('2'>'10') //2.charCodeAt()>10.charCodeAt() 50>49 true

'2'>'10'进行charCodeAt()的时候,会先比第一个字节,如果不一样,直接就返回结果,如果第一个一样会接着比第二个字节。就是2.charCodeAt()会先跟1.charCodeAt()进行比较。

3、引用数据类型在进行隐式转换的时候,会先转成string。

console.log([1,2] == '1,2') //true

在这个过程中[1,2]是引用数据类型,会先调用valueOf()方法
如果返回的还是引用数据类型,再调用toString()方法

4、逻辑非运算符和关系运算符,为啥[] == 0,![] == 0?(重点)

入门级别:

console.log([] == 0) // true
console.log(![] == 0) // true

为啥[]等于0,![]也等于0,这不是毁三观吗?

首先分析下[] == 0:
1、在讲引用数据类型阴性转换的时候,我们说了引用数据类型进行隐形类型转换必须先转成string
[].valueOf().toString() // ''

2、==数据关系运算符,两边必须都转成Number
Number('') // 0

所以true
然后分析下![]为啥也是0:
1、在讲Boolean运算符的时候说了
除了undefined、null、-0、0或+0、NaN、''(空字符串),其余全部都是true
所以[]是true,那么![]就是false

2、两边在转Number的时候:
Number(false) // 0

所以![] == 0也是true

进阶级别

[] == ![] //true
[] == [] // false

[]等于非[]竟然是true,[]等于他自己反倒是false?

[] == ![]
1、[]还是会转成string,就是'''
2、![]还是false
3、‘’和false都进行Number操作都是0

所以相等
[] == [](这是个坑)
这个在上一篇讲栈内存的时候说了,引用数据类型在栈中存的都是地址,所以这两个肯定不一样

高阶级别

{} == !{} // false
{} == {} // false

{}和[]都是引用数据类型,为啥他是false呢?

{} == !{}
1、{}还是会转成string,但是{}转成字符串是""[object Object]"
2、![]还是false
3、Number(""[object Object]")和Number(false)

所以不相等
{} = {}
同理,也是比较栈内存的地址,所以false

关于js的数据类型,主要的就是隐性数据类型转换,会经常出错,这个系列的两篇文章主要是聊了一下js的赋值和赋址的区别、强转换以及隐性转换我们经常出现的问题。

个人的微信公众号:小Jerry有话说,平时会发一些技术文章和读书笔记,欢迎交流。

后面会持续更新一些js基础的文章,有兴趣的可以点个关注。