likes
comments
collection
share

JavaScript数据类型深度了解

作者站长头像
站长
· 阅读数 14

装箱拆箱

一般来说,基本数据类型是不该有属性和方法的,但是当我们需要进行字符串的检索或切割的时候我们会用到一些方法,比如:indexOf,slice等方法。 那么问题来了,既然说了不该有属性和方法,但是实际上我们是调用了这些方法的,这些方法是从何而来呢?这里我们就需要引入两个新的概念:装箱和拆箱

在引用类型之中有着几个较为特殊的类型,我们通常称之为基本包装类型。它们分别是BooleanNumberString。从它们的名字中我们就可以看出来,它们的名字就只是大小写的区别而已。而装箱和拆箱这两个操作和它们也确实有着不小的联系。

1.装箱: 将基本数据类型转化为引用类型的操作称之为装箱。装箱又分为隐式装箱显式装箱

隐式装箱:

let num = 4
num.test = 2
num.toString() // "4"
console.log(num.test) // undefined

上面那句代码实际上是这么执行的:

  1. 创建 Number 类型的一个实例;(装箱过程,后台操作,对开发不可见)
  2. 在实例上调用指定的toString方法;(方法调用,开发指定的方法调用)
  3. 返回函数操作结果后,将该实例丢弃。(自动拆箱,后台操作,对开发不可见) 值得注意的是,装箱操作中这个基本类型的对象是临时的,它只存在于调用那行代码的那一瞬间,执行之后就被销毁了,所以就算往基本类型上添加属性也无法被识别。

显式装箱:与隐式装箱相比,显式装箱就很容易理解了。

let num = new Number(4)
num.test = 2
num.toString() // "4"
console.log(num.test) // 2

因为现在的num是用new操作符创建的一个实例,也没有返回值之后就销毁,因此现在的num是一个对象自然就能添加属性并使用.

Symbol 函数无法使用 new 来调用,但我们也可以利用装箱机制来得到一个 Symbol 对象,比如用函数的 call 方法来强迫产生装箱。

let symbolObj = (function () { return this }).call(Symbol('a'))
console.log(typeof symbolObj) // 'object'
console.log(symbolObj.description) // 'a'
console.log(symbolObj instanceof Symbol) // true

2.拆箱: 将引用类型转化为基本数据类型的操作称之为拆箱。 一般情况下拆箱是根据toString()和valueOf()方法来实现。

String和Number这两种数据类型的自动拆箱操作一般是先把对象转成基本数据类型,再从数据类型根据toString()valueOf()转成相应的String或者Number,如果转换的对象不存在这两种方法或者没有返回基本数据类型则会报错。

let num = new Number(123);
let str = new String('123');
console.log(typeof num); // object
console.log(typeof str); // object
// 拆箱
console.log(typeof num.valueOf()); // number 
console.log(typeof num.toString()); // string 
console.log(typeof str.valueOf()); // string 
console.log(typeof str.toString()); // string 

3.原理:

js引擎内部有着ToPrimitive()函数

ToPrimitive(input, PreferredType?)

这个函数接收两个对象,第一个参数是要转变的对象,第二个参数是可选参数,它是指期待被转成的类型。 当PreferredType被标志为Number,这个函数在转换时会执行一下几个步骤:

  1. 如果输入的值是原始值,则直接返回,否则走到第二步
  2. 如果输入的值是一个对象,则会调用对象的和valueOf()方法返回这个值,如果返回的值为原始值则直接返回,否则走到第三步
  3. 如果输入的值是一个对象,则会调用对象的和toString()方法返回这个值,如果返回的值为原始值则直接返回,否则抛出异常

PreferredType被标志为String时第二、三步转换的方法顺序进行替换,但是如果不传PreferredType,则当转变对象为Date类型时设为String,其他情况设为Number

(在数据类型转换中,经过拆箱完成之后所得到的的类型如果不是与标志相同的类型,则会调用ToString()ToNumber()或者ToBoolean()这几个操作转成自己需要的类型。)

数据类型转换

数据类型转换分为显式转换(强制转换)和隐式转换(自动转换)。

1.显式转换: 这种转换类型我们一般是转成以下三种类型:NumberStringBoolen,转换使用的函数也正好对应它们的类型,分别为Number()String()Boolen()这几个函数可以使得基本数据类型或者某些引用类型转成我们想要的基本数据类型。

1.1转成Number类型

// Boolen类型转成Number类型
Number(true) // 1
Number(false) // 0

// String类型转成Number类型
Number('') // 0 (空字符串转成Number类型为0)
Number('aaaa') // NaN (非数字字符串转成Number类型为NaN)
Number('321564') // 321564(字符串数字转成Number类型为相应的数字)
Number('321aaacc') // NaN(非纯数字字符串转成Number类型为NaN)

// Undefined类型转成Number类型
Number(undefined) // NaN

// Null类型转成Number类型
Number(null) // 0

//对象转成Number类型时一般为NaN
Number({a:100}) // NaN
Number({a:'a'}) // NaN

// 空数组或者单个数值的数组可以成功转成Number类型
Number([]) // 0
Number([123]) // 123
Number(['123']) // 123
Number([123,1]) // NaN
Number(['123','1']) // NaN

1.1.1对象转Number类型原理 由对象转为基础类型,我们自然而然能想到上文的拆箱操作,其实强转为Number类型使用的正是这样的一个操作,不过PreferredType是确定标志为Number了,在转换成原始值之后按照既定的转换的规则进行转换就可以了。

1.2转成String类型

// 原始类型转为String类型最为简单,基本就是该类型的值加上双引号就行了
String(123) // "123"
String("abc") // "abc"
String(true) // "true"
String(false) // "false"
String(undefined) // "undefined"
String(null) // "null"

// 对象和数组转为String类型就有点不太一样(和JSON序列化不同)
String({}) // "[object Object]"
String({a:"11"}) // "[object Object]" (对象一律为 [object Object])
String([]) // "" 
String([1,2,3]) // "1,2,3" 
String([1,2,3,"a","b","c"]) // "1,2,3,a,b,c"
String([1,2,3,"a","b","c",[1,2,3]]) // 
"1,2,3,a,b,c,1,2,3"(数组一律为 相当于去掉了数组的括号的字符串)

1.2.1对象转String类型原理 原理同上,PreferredType是确定标志为String

1.3转成Boolean类型

// 对于Boolen类型的强转,我们只需记住几个会转为false类型的值就行,因为除此之外的其他值转为Boolean类型都为true
Boolean(+0) // false
Boolean(-0) // false
Boolean(0) // false
Boolean("") // false
Boolean(NaN) // false
Boolean(undefined) // false
Boolean("undefined") // true(这是String类型不是Undefined类型)
Boolean(null) // false

// 因为除了这些值固定为false之外其他的值都为true,包括任何对象和任何数组,所以这样可能会导致出现一些坑
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true (因为new Boolean(false)是一个对象,基本类型的包装对像,还记得吧)

2.隐式转换: 隐式转换在js的学习中是很有意思的一个知识点,也是必须要了解的一个知识点,因为如果没有学好这个在写代码的时候很容易掉坑里。

2.1原因: 平时在写代码的时候我们进行比较操作或者加减乘除四则运算操作或多或少都会触发JavaScript的隐式类型转换机制。 举个例子:

let num = 0;
    num
      ? console.log("num为0")
      : console.log("num不为0")

从显式转换中我们知道将0转为Boolen类型的话是false,所以这里直接用num当做条件就可以进行判断了,因为JavaScript的隐式类型转换机制会帮我们将num转为false再进行判断。

2.2算术运算符转换规则 在转换的时候要进行进行ToPrimitive()处理。

2.1.1加法规则

  1. 对于"+"运算符来说,有String类型的值则进行toString()操作(PreferredType标志为String),即全部值转为String类型然后相加,也就是字符串相加

  2. 其他条件下都进行ToNumber()操作(Date类型除外,拆箱中有讲过原因)

  3. 进行ToNumber()操作的时候如果返回值为原始类型就停止转换,否则还会进行ToString()操作(相当于进行了String类型的强制转换)

eg1:
console.log(1+'2'+false+{}+[1,2,3,]+null+undefined) //"12false[object Object]1,2,3nullundefined"

因为上式中'2'String类型,所以其他的值全部被隐式转成了字符串类型然后才相加。

eg2:
console.log([123] + 1) // 1231

上式中首先因为没有字符串,我们就可以知道要先进行ToNumber()操作。但是[123]valueOf()为对象,所以接着进行ToString()操作,进行完ToString()操作之后得到一个String类型的原始值,所以接下来就自然要全部转为String之后再进行相加。

如果还是不太清楚,我们可以再专门为此举个例子:

let a = {
   valueOf: function() { return 111 },
   toString: function() { return '111' }
}
let b = {
    valueOf: function() { return {} },
    toString: function() { return '111' }
}
console.log(1+a) // 112
console.log(1+a) // "1111"

上面的例子我们就很清楚明白了,因为avalueOf()方法返回的是原始类型,而且这个原始类型是Number格式的,所以就用了数字的加法,但是b中因为valueOf()方法返回的是对象,所以调用了toString()方法,而toString()方法返回的是String类型的值,因此结果是字符串拼接的结果。

2.1.2其他规则 因为加法比较特殊,所以我们需要单独来说,至于- * / %等这些运算符用于运算时就是把两边的值转为Number类型然后分别进行运算,该是什么结果就是什么结果。

console.log(30-"a") // NaN
console.log(30*"a") // NaN
console.log(30/"a") // NaN
console.log(30%"a") // NaN
console.log(30-"30") // 0 Number
console.log(30*"30") // 900 Number
console.log(30/"30") // 1 Number
console.log(30%"30") // 0 Number

2.3比较运算符转换规则 我们常用的比较运算符有可以分成两种来分析:

  1. 比较类型的运算符(===!==

    这两个运算符在进行比较的时候会先进行类型比较。如果类型不一样则返回false(他们不会发生隐式类型转换,所以得到的结果往往都会是预期结果)

  2. 不比较类型的运算符(>,<,==等) 这类运算符比较难缠,其中使用和面试时出现最多的又以==为主。 其实虽然==运算符涉及隐式转换,但是并不是那么难,只要我们把握好它的规律加上前面我们学习的内容,可以轻而易举地学习,解决大部分这种问题.

undefined==null结果为true,且它们与其他值比较皆为false 因为规范中有提到:

要比较相等性之前,不能将 null 和 undefined 转换成其他任何值,并且规定null 和 undefined 是相等的。

  1. null 和 undefined都代表着无效的值,所以才会出现这么个结果。(反正很多自己无法解释的东西要么是自己知道的太少,要么就是规范中已经定义好了)
undefined == null; // true
undefined == 10; // false

  1. 如果双方都是对象,则比较它们的引用:
let a = [10];
let b = [10];
a==b; // false

3.如果双方是同一基础类型,则直接比较其值

"str" == "str1" ;//false

4.如果一方是基础类型,一方是引用类型,则一起转为Number类型然后比较

let a = [10]
a==10 // true

5.如果双方都是基础类型,则一起转为Number类型然后比较

"0" == false // true

2.4 Symbol的显式和隐式转换

在谈这一块之前我们需要先明确的一点:Symbol 类型是一种特殊的数据类型,其主要特点是不可变且唯一。 所以该类型的显式和隐式转换和其他基本类型是不一样的。

Symbol 类型在进行隐式类型转换时会产生一个错误 (TypeError),无法直接转换为数字或字符串。但是,我们仍然可以使用显式转换将 Symbol 类型转换为字符串。

  1. 显式转换(将 Symbol 转换为字符串)
const symbolType = Symbol("symbolValue"); 
const symbolString = symbolType .toString(); 
console.log(symbolString); // Symbol(symbolValue)
console.log(typeof symbolString); // string
  1. Symbol 不能进行隐式转换(Symbol 类型不能直接与其他数据类型进行算术运算,也不能用于字符串拼接)
const symbolType = Symbol("symbolValue"); 
const  num = symbolType + 10 // 报错:TypeError: Cannot convert a Symbol value to a number
const str = symbolType + '' // 报错: TypeError: Cannot convert a Symbol value to a string

思考题: 验证分析++[[]][+[]]+[+[]]==10(提示:将一个值转为Boolen类型我们可以直接在它的前面加!!,如果我们想要将其转为Number类型,则需要在它前面加+,但是要注意不要使这个符号变成拼接字符串的符号或者运算符号) 如:+[123] === 123; // true