堆栈内存篇之数据类型与类型转换
前言
堆栈内存篇将以JavaScript的数据类型为引,然后一步步刨析堆栈内存,以此来了解JS的运行机制。在这个过程中,我们也会学习到一些经常出现的面试知识点,比如this指向、闭包等。
数据类型分类
JS的数据类型分为两大类,一类是基本数据类型,一类是引用数据类型。两种数据类型最大的区别在于:基础数据类型是按值访问的(实际操作的是值本身);而引用数据类型是按引用访问的(实际操作的是地址)。
基本数据类型一共有七种:String
,Boolean
,Number
,Bigint
,Symbol
,Null
,Undefinded
引用数据类型有很多种,常见的有:Object
,Array
,Function
,Date
,RegExp
等。
引用数据类型可以统称为一种,即Object
,因为在JS中,一切都是基于对象的。所以也有一些书籍在介绍JS的数据类型分类时,是介绍为一共有八种数据类型,包括七种基本数据类型和一种复杂数据类型的。
我们在平常使用的过程中可能会疑惑:为什么String
,Boolean
,Number
是基本数据类型,但却拥有其他引用类型一样的特点,比如:它们的实例同样拥有属性方法。这是因为这三个基本数据类型是极其特殊的,它们被称为原始值包装类型。在每次用到某个原始值的属性或方法时,后台都会创建一个相对应的对象,以此来暴露那些属性方法,对于用户来说,就像在操作引用数据类型一样。
let str1 = 'this is a test'
let str2 = str1.substring(2)
比如以上的代码,当执行到第二句的时候。后台执行了以下三步:1.创建一个String类型的实例;2.调用实例上的特定方法;3.销毁创建的实例
可以注意到这里创建的实例对象在执行完语句之后就被销毁了,这也是引用类型和原始值包装类型的不同之处:引用类型实例化的对象在离开作用域时才被销毁,而原始值包装类型自动实例化的对象只存在于访问它的那行代码的执行期间,当这行代码运行结束,该对象就被销毁了。这意味着:在运行时给和原始值包装类型的变量添加属性和方法是无效的
let str1 = 'test test'
str1.name = 'str1'
console.log(str1.name) // undefined
类型检测
typeof
typeof
操作符是类型检测最基础的方法,对一个变量使用typeof
操作符时,将会返回其对应的数据类型
console.log(typeof 'string') // string
console.log(typeof 123) // number
console.log(typeof true) // boolean
console.log(typeof 123n) // bigInt
console.log(typeof Symbol()) // symbol
console.log(typeof a) // undefined
console.log(type null) // object
console.log(typeof new Object()) // object
console.log(typeof new Array()) // objecct
console.log(typeof new Function()) //function
注意:基本数据类型中除了null
之外都会返回对应的数据类型,而null
返回的是object
,这是因为null
表示的是一个空对象,因此是一个对象。而引用数据类型中除了function
之外都返回object
,因为function
虽然也是对象,但因为其特殊性,所以就把它和其他对象区分开来。
instanceof
由上可知,当对引用数据类型使用typeof
时,除了function
之外,其他的都是返回object
,这就不能使用它来区分Array
,Date
等类型了。
而instanceof
的功能就是用来判断一个对象是否为另一个构造函数(类)的实例。
语法为object instanceof constructor
。该运算符是通过判断对象的原型链上是否存在着构造函数的原型对象(本文不着重介绍关于原型链和原型对象的细节),当结果为是时返回true
,否则返回false
let a = []
a instanceof Array // true
a instanceof Object // true
a instanceof String // false
且该修饰符不局限于JS自带的引用数据类型,它也可用于自定义的构造函数
function Foo() {} // 自定义的构造函数
let bar = new Foo()
bar instanceof Foo // true
bar instanceof object // true
constructor
我们知道原始值包装类型使用起来与引用数据类型并没有多大不同,其在被访问其属性和方法时也会进行实例化一个对象。因此使用字面量方式和构造函数创建的基本数据类型也有着不同
let a = 123
let b = Number(321)
a instanceof Number // false
b instanceof Number // true
当遇到这种问题时,就可以使用constroctor
来进行检测
let a = 123
let b = Number(321)
console.log(a.constructor === Number)// true
console.log(b.constructor === Number)// true
但这个方法也不是最优解,毕竟constructor
,__proto__
等属性是可能会被改写的
Object.prototype.toString.call()
该方法是目前最常用也是最准确的方式。
每一个对象都有着一个toString
方法,当该方法没有被重写时,其执行结果返回[object type]
,其中type
表示对象的类型。而许多数据类型为了实现特定的功能都对其进行了重写,比如Array
,String
,Date
等,所以在进行类型检测时,都会使用Object.prototype.toString.call(object)
的方式,通过改写其this
的绑定,来调用Object
原型上的toString
let a = [1,2,3,4]
console.log(a.toString()) // "1,2,3,4"
console.log(Object.prototype.toString.call(a)) // "[object Array]"
类型转换
在一些情况下,数据类型之间会发生自动转换。比如在流程控制语句之中,+
性运算符,==
运算符等。JS也提供了一些函数方法用于主动转换类型。
转为布尔类型
JS提供了Boolean()
转型函数来进行其他类型到布尔类型的转变。
下表为不同类数据类型与布尔类型的转换规则:
数据类型 | 转为true | 转为false |
---|---|---|
Boolean | true | false |
String | 非空字符串 | 空字符串 |
Number | 非0数值 | 0、NaN |
Object | 任意对象 | null |
Undefined | N/A | undefined |
转为数字类型
JS提供了三个函数来将非数值转换为数值:Number()
,parseInt()
,parseFloat()
。
Number()
是主要的转型函数,可用于任何数据类型。而后两个函数主要用于将字符串转为数值。
参数数据类型 | Number() | parseInt() | parseFLoat() |
---|---|---|---|
布尔值 | true 转换为 1,false 转换为 0 | 返回NaN | 返回NaN |
数值 | 直接返回 | 直接返回 | 直接返回 |
null | 返回 0 | 返回NaN | 返回NaN |
undefined | 返回 NaN | 返回 NaN | 返回 NaN |
字符串 | 只包含数值字符(前面带正负号的数、浮点型、有效的十六进制)转化成相应的数值;空字符串返回0;包含上述情况之外的字符返回NaN | 与Number()不同点:空字符串返回NaN;只要第一个字符是数值字符、加号、减号,则会依次检测每个字符(即后面包含其他字符也会返回前面的数值) | 与parseInt()基本相同,但只有第一个小数点有效 |
对象 | 调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用 toString()方法,再按照转换字符串的规则转换。 | 调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用 toString()方法,再按照转换字符串的规则转换。 | 调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用 toString()方法,再按照转换字符串的规则转换。 |
// 1.布尔值
Number(true) // 1
parseInt(true) // NaN
parseFloat(true) // NaN
// 2.数值
Number(123) // 123
parseInt(123) // 123
parseFloat(123.321) //123.321
// 3.null
Number(null) // 0
parseInt(null) // NaN
parseFloat(null) // NaN
// 4.undefined
Number(undefined) // NaN
parseInt(undefined) // NaN
parseFloat(undefined) // NaN
// 5.字符串
let str = '123abc'
Number(str) // NaN
parseInt(str) // 123
parseFloat(str) // 123
let str1 = ''
Number(str1) // 0
parseInt(str1) // NaN
parseFloat(str1) // NaN
// 6.对象
let a = [1,2,3]
a.valueOf() // [1,2,3]
a.toString() // "1,2,3"
Number(a) // NaN,因为包括逗号
parseInt(a) // 1
parseFloat(a) // 1
Number()
和其他两个函数最主要的区别在于处理字符串时不同。比如转换空字符串时,Number()
返回的是0,而其他两个则返回NaN
。而且当第一个字符为数字,且后面包括其他字符时,Number()
则直接返回NaN
,其他两个则会忽略其他字符。
而因为转换字符串时的区别所以也间接导致转换对象时的不同。
转为字符串类型
JS提供了toString()
和String()
来将其他类型转换为字符串。
之前我们介绍类型转换时,已经介绍过了toString()
,则是每一个对象都有的方法,但可能经过了重写。而且还有一个小操作:在对数值调用该方法时,可以传入一个参数来以什么进制来输出数值的字符串表示。
但并不是所有对象都有toString()
,比如null
,undefined
。此时就只能使用String()
转型函数。当不确定一个值是否为null
或undefined
时,使用该方法将会遵循以下规则:1.如果值有toString()
,则直接调用 2.如果值为null
,则返回'null'
3.如果值为undefined,则返回'undefined'
let value1 = 10;
let value2 = true;
let value3 = null;
let value4;
console.log(String(value1)); // "10"
console.log(String(value2)); // "true"
console.log(String(value3)); // "null"
console.log(String(value4)); // "undefined"
加性操作符
当两个操作数都为数值时,则进行加法运算。
当其中一个操作数为字符串时,若另一个也为字符串则进行拼接,否则则将另一个操作数转换为字符串,然后拼接。
console.log(1 + 1) // 2
console.log(1 + '1') // 11
console.log('1' + { a:123 }) // 1[object Object]
等于与不等于操作符
当任意一个操作数为布尔类型,则将其转为数值再进行比较。
当一个操作数为字符串,另一个为数值时,则将字符串转为数值再进行比较。
当一个操作数为对象,另一个不是,则使用对象的valueOf()
取值,然后再进行比较。
如果两个操作数都是对象,则比较它们是不是同一个对象。
null
等于undefined
,两者都不能转为其他类型。
当任意一个操作数为NaN
,则相等操作符返回 false,不相等操作符返回 true。因为即使两个操作数都是 NaN,相等操作符也返回 false,因为按照规则,NaN 不等于 NaN。
流程控制语句
在JS的流程控制语句中,比如if
,while
,do-while
之中。其条件(可能是表达式或数据类型)的结果不必都为布尔类型,因为在内部会自动将其值转换为布尔类型(调用Boolean()
方法)
堆栈内存
上面我们介绍了数据类型分为基本数据类型和引用数据类型,其最大的区别在于访问方式。那为什么会有这种不同呢?
首先我们需要知道JS的运行的内存主要分为栈内存和堆内存。栈内存即执行栈,是任务主要的执行环境。但因为栈的空间有限,不能用来直接保存对象,所以就使用了堆内存用来保存真正的对象,然后在栈上保存指向堆内存中对应的地址,该地址即为引用。
因此可知:基本数据类型是保存在栈上的,操作时是对实际的值进行操作;而引用数据类型是保存在堆上的,对其进行操作时实际上是通过地址(引用)来对堆内存对应区域内的对象进行操作。
其示意图如下,关于执行上下文的内容将在下一篇文章进行介绍。