JavaScript 的数据类型有下图所示
其中,前 7 种类型为基础类型,最后 1 种(Object)为引用类型,也是你需要重点关注的,因为它在日常工作中是使用得最频繁,也是需要关注最多技术细节的数据类型
在这里,我想先请你重点了解下面两点,因为各种 JavaScript 的数据类型最后都会在初始化之后放在不同的内存中,因此上面的数据类型大致可以分成两类来进行存储:
JavaScript 中的数据是如何存储在内存中的?
在 JavaScript 中,原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。
在 JavaScript 的执行过程中, 主要有三种类型内存空间,分别是代码空间
、栈空间
、堆空间
。其中的代码空间主要是存储可执行代码的,原始类型(Number、String、Null、Undefined、Boolean、Symbol、BigInt
)的数据值都是直接保存在“栈”中的,引用类型(Object)的值是存放在“堆”中的。因此在栈空间中(执行上下文),原始类型存储的是变量的值,而引用类型存储的是其在"堆空间"中的地址,当 JavaScript 需要访问该数据的时候,是通过栈中的引用地址来访问的,相当于多了一道转手流程。
在编译过程中,如果 JavaScript 引擎判断到一个闭包,也会在堆空间创建换一个“closure(fn)”
的对象(这是一个内部对象,JavaScript 是无法访问的),用来保存闭包中的变量。所以闭包中的变量是存储在“堆空间”中的。
JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。因此需要“栈”和“堆”两种空间。
题目一:初出茅庐
let a = {
name: 'lee',
age: 18
}
let b = a;
console.log(a.name); //第一个console
b.name = 'son';
console.log(a.name); //第二个console
console.log(b.name); //第三个console
这道题比较简单,我们可以看到第一个 console 打出来 name 是 'lee',这应该没什么疑问;但是在执行了 b.name='son' 之后,结果你会发现 a 和 b 的属性 name 都是 'son',第二个和第三个打印结果是一样的,这里就体现了引用类型的“共享”的特性,即这两个值都存在同一块内存中共享,一个发生了改变,另外一个也随之跟着变化。
你可以直接在 Chrome 控制台敲一遍,深入理解一下这部分概念。下面我们再看一段代码,它是比题目一稍复杂一些的对象属性变化问题。
题目二:渐入佳境
let a = {
name: 'Julia',
age: 20
}
function change(o) {
o.age = 24;
o = {
name: 'Kath',
age: 30
}
return o;
}
let b = change(a); // 注意这里没有new,后面new相关会有专门文章讲解
console.log(b.age); // 第一个console
console.log(a.age); // 第二个console
这道题涉及了 function
,你通过上述代码可以看到第一个 console
的结果是 30
,b
最后打印结果是 {name: "Kath", age: 30}
;第二个 console
的返回结果是 24
,而 a
最后的打印结果是 {name: "Julia", age: 24}
。
是不是和你预想的有些区别?你要注意的是,这里的 function
和 return
带来了不一样的东西。
原因在于:函数传参进来的 o,传递的是对象在堆中的内存地址值,通过调用 o.age = 24(第 7 行代码)确实改变了 a 对象的 age 属性;但是第 12 行代码的 return 却又把 o 变成了另一个内存地址,将 {name: "Kath", age: 30} 存入其中,最后返回 b 的值就变成了 {name: "Kath", age: 30}。而如果把第 12 行去掉,那么 b 就会返回 undefined
2. 数据类型检测
(1)typeof
typeof 对于原始类型来说,除了 null 都可以显示正确的类型
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof []); // object []数组的数据类型在 typeof 中被解释为 object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object null 的数据类型被 typeof 解释为 object
typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型,所以想判断一个对象的正确类型,这时候可以考虑使用 instanceof
(2)instanceof
instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
// console.log(undefined instanceof Undefined);
// console.log(null instanceof Null);
instanceof
可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型;typeof
也存在弊端,它虽然可以判断基础数据类型(null
除外),但是引用数据类型中,除了 function
类型以外,其他的也无法判断// 我们也可以试着实现一下 instanceof
function _instanceof(left, right) {
// 由于instance要检测的是某对象,需要有一个前置判断条件
//基本数据类型直接返回false
if(typeof left !== 'object' || left === null) return false;
// 获得类型的原型
let prototype = right.prototype
// 获得对象的原型
left = left.__proto__
// 判断对象的类型是否等于类型的原型
while (true) {
if (left === null)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
console.log('test', _instanceof(null, Array)) // false
console.log('test', _instanceof([], Array)) // true
console.log('test', _instanceof('', Array)) // false
console.log('test', _instanceof({}, Object)) // true
(3)constructor
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
这里有一个坑,如果我创建一个对象,更改它的原型,constructor就会变得不可靠了
function Fn(){};
Fn.prototype=new Array();
var f=new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
(4)Object.prototype.toString.call()
toString() 是 Object 的原型方法,调用该方法,可以统一返回格式为 “[object Xxx]” 的字符串,其中 Xxx 就是对象的类型。对于 Object 对象,直接调用 toString() 就能返回 [object Object];而对于其他对象,则需要通过 call 来调用,才能返回正确的类型信息。我们来看一下代码。
Object.prototype.toString({}) // "[object Object]"
Object.prototype.toString.call({}) // 同上结果,加上call也ok
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g) //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([]) //"[object Array]"
Object.prototype.toString.call(document) //"[object HTMLDocument]"
Object.prototype.toString.call(window) //"[object Window]"
// 从上面这段代码可以看出,Object.prototype.toString.call() 可以很好地判断引用类型,甚至可以把 document 和 window 都区分开来。
实现一个全局通用的数据类型判断方法,来加深你的理解,代码如下
function getType(obj){
let type = typeof obj;
if (type !== "object") { // 先进行typeof判断,如果是基础数据类型,直接返回
return type;
}
// 对于typeof返回结果是object的,再进行如下的判断,正则返回结果
return Object.prototype.toString.call(obj).replace(/^[object (\S+)]$/, '$1'); // 注意正则中间有个空格
}
/* 代码验证,需要注意大小写,哪些是typeof判断,哪些是toString判断?思考下 */
getType([]) // "Array" typeof []是object,因此toString返回
getType('123') // "string" typeof 直接返回
getType(window) // "Window" toString返回
getType(null) // "Null"首字母大写,typeof null是object,需toString来判断
getType(undefined) // "undefined" typeof 直接返回
getType() // "undefined" typeof 直接返回
getType(function(){}) // "function" typeof能判断,因此首字母小写
getType(/123/g) //"RegExp" toString返回
小结
判断 Target 的类型,单单用 typeof 并无法完全满足,这其实并不是 bug,本质原因是 JS 的万物皆对象的理论。因此要真正完美判断时,我们需要区分对待:
null
): 使用 String(null)
string / number / boolean / undefined
) + function
: - 直接使用 typeof
即可Array / Date / RegExp Error
): 调用toString
后根据[object XXX]
进行判断3. 数据类型转换
我们先看一段代码,了解下大致的情况。
'123' == 123 // false or true?
'' == null // false or true?
'' == 0 // false or true?
[] == 0 // false or true?
[] == '' // false or true?
[] == ![] // false or true?
null == undefined // false or true?
Number(null) // 返回什么?
Number('') // 返回什么?
parseInt(''); // 返回什么?
{}+10 // 返回什么?
let obj = {
[Symbol.toPrimitive]() {
return 200;
},
valueOf() {
return 300;
},
toString() {
return 'Hello';
}
}
console.log(obj + 200); // 这里打印出来是多少?
首先我们要知道,在 JS 中类型转换只有三种情况,分别是:
转Boolean
在条件判断时,除了 undefined,null, false, NaN, '', 0, -0,其他所有值都转为 true,包括所有对象
Boolean(0) //false
Boolean(null) //false
Boolean(undefined) //false
Boolean(NaN) //false
Boolean(1) //true
Boolean(13) //true
Boolean('12') //true
对象转原始类型
对象在转换类型的时候,会调用内置的 [[ToPrimitive]] 函数,对于该函数来说,算法逻辑一般来说如下
x.valueOf()
,如果转换为基础类型,就返回转换的值x.toString()
,如果转换为基础类型,就返回转换的值当然你也可以重写 Symbol.toPrimitive,该方法在转原始类型时调用优先级最高。
let a = {
valueOf() {
return 0
},
toString() {
return '1'
},
[Symbol.toPrimitive]() {
return 2
}
}
1 + a // => 3
四则运算符
它有以下几个特点:
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
1
转换为字符串,得到结果 '11'
true
转为数字 1
toString
转为字符串 1,2,3
,得到结果 41,2,3
另外对于加法还需要注意这个表达式 'a' + + 'b'
'a' + + 'b' // -> "aNaN"
+ 'b'
等于 NaN
,所以结果为 "aNaN"
,你可能也会在一些代码中看到过 + '1'
的形式来快速获取 number
类型。4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN
比较运算符
toPrimitive
转换对象unicode
字符索引来比较let a = {
valueOf() {
return 0
},
toString() {
return '1'
}
}
a > -1 // true
在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再比较值。
强制类型转换
强制类型转换方式包括 Number()、parseInt()、parseFloat()、toString()、String()、Boolean(),这几种方法都比较类似
Number()
方法的强制转换规则true
和 false
分别被转换为 1
和 0
;null
,返回 0
;undefined
,返回 NaN
;0X / 0x
开头的十六进制数字字符串,允许包含正负号),则将其转换为十进制;如果字符串中包含有效的浮点格式,将其转换为浮点数值;如果是空字符串,将其转换为 0
;如果不是以上格式的字符串,均返回 NaN;Symbol
,抛出错误;[Symbol.toPrimitive]
,那么调用此方法,否则调用对象的 valueOf()
方法,然后依据前面的规则转换返回的值;如果转换的结果是 NaN
,则调用对象的 toString()
方法,再次依照前面的顺序转换返回对应的值。Number(true); // 1
Number(false); // 0
Number('0111'); //111
Number(null); //0
Number(''); //0
Number('1a'); //NaN
Number(-0X11); //-17
Number('0X11') //17
Object 的转换规则
对象转换的规则,会先调用内置的 [ToPrimitive] 函数,其规则逻辑如下:
Symbol.toPrimitive
方法,优先调用再返回;valueOf()
,如果转换为基础类型,则返回;toString()
,如果转换为基础类型,则返回;var obj = {
value: 1,
valueOf() {
return 2;
},
toString() {
return '3'
},
[Symbol.toPrimitive]() {
return 4
}
}
console.log(obj + 1); // 输出5
// 因为有Symbol.toPrimitive,就优先执行这个;如果Symbol.toPrimitive这段代码删掉,则执行valueOf打印结果为3;如果valueOf也去掉,则调用toString返回'31'(字符串拼接)
// 再看两个特殊的case:
10 + {}
// "10[object Object]",注意:{}会默认调用valueOf是{},不是基础类型继续转换,调用toString,返回结果"[object Object]",于是和10进行'+'运算,按照字符串拼接规则来,参考'+'的规则C
[1,2,undefined,4,5] + 10
// "1,2,,4,510",注意[1,2,undefined,4,5]会默认先调用valueOf结果还是这个数组,不是基础数据类型继续转换,也还是调用toString,返回"1,2,,4,5",然后再和10进行运算,还是按照字符串拼接规则,参考'+'的第3条规则
'==' 的隐式类型转换规则
null
或者 undefined
,那么另一个操作符必须为 null
或者 undefined
,才会返回 true
,否则都返回 false
;Symbol
类型,那么返回 false
;string
和 number 类型,那么就会将字符串转换为 number
;boolean
,那么转换成 number
;object
且另一方为 string
、number
或者 symbol
,就会把 object
转为原始类型再进行判断(调用 object
的 valueOf/toString
方法进行转换)。null == undefined // true 规则2
null == 0 // false 规则2
'' == null // false 规则2
'' == 0 // true 规则4 字符串转隐式转换成Number之后再对比
'123' == 123 // true 规则4 字符串转隐式转换成Number之后再对比
0 == false // true e规则 布尔型隐式转换成Number之后再对比
1 == true // true e规则 布尔型隐式转换成Number之后再对比
var a = {
value: 0,
valueOf: function() {
this.value++;
return this.value;
}
};
// 注意这里a又可以等于1、2、3
console.log(a == 1 && a == 2 && a ==3); //true f规则 Object隐式转换
// 注:但是执行过3遍之后,再重新执行a==3或之前的数字就是false,因为value已经加上去了,这里需要注意一下
'+' 的隐式类型转换规则
'+' 号操作符,不仅可以用作数字相加,还可以用作字符串拼接。仅当 '+' 号两边都是数字时,进行的是加法运算;如果两边都是字符串,则直接拼接,无须进行隐式类型转换。
undefined
、null
或布尔型,则调用 toString()
方法进行字符串拼接;如果是纯对象、数组、正则等,则默认调用对象的转换方法会存在优先级,然后再进行拼接。undefined
、null
、布尔型或数字,则会将其转换成数字进行加法运算,对象的情况还是参考上一条规则。1 + 2 // 3 常规情况
'1' + '2' // '12' 常规情况
// 下面看一下特殊情况
'1' + undefined // "1undefined" 规则1,undefined转换字符串
'1' + null // "1null" 规则1,null转换字符串
'1' + true // "1true" 规则1,true转换字符串
'1' + 1n // '11' 比较特殊字符串和BigInt相加,BigInt转换为字符串
1 + undefined // NaN 规则2,undefined转换数字相加NaN
1 + null // 1 规则2,null转换为0
1 + true // 2 规则2,true转换为1,二者相加为2
1 + 1n // 错误 不能把BigInt和Number类型直接混合相加
'1' + 3 // '13' 规则3,字符串拼接
整体来看,如果数据中有字符串,JavaScript 类型转换还是更倾向于转换成字符串,因为第三条规则中可以看到,在字符串和数字相加的过程中最后返回的还是字符串,这里需要关注一下
null 和 undefined 的区别?
Undefined
和 Null
都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined
和 null
。undefined
代表的含义是未定义, null
代表的含义是空对象(其实不是真的对象,请看下面的注意!)。一般变量声明了但还没有定义的时候会返回 undefined
,null
主要用于赋值给一些可能会返回对象的变量,作为初始化。其实 null 不是对象,虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
undefined
来作为一个变量名,这样的做法是非常危险的,它会影响我们对 undefined 值的判断。但是我们可以通过一些方法获得安全的 undefined
值,比如说 void 0
。阅读量:717
点赞量:0
收藏量:0