计算机世界中定义的数据类型其实就是为了描述现实世界中存在的事实而定义的。比如我们用人来举例:
JS
中对应 Boolean
类型, true
表示是, false
表示非;JS
中对应 Number
类型,包含整数和浮点数,还有一些特殊的值,比如: -Infinity
表示负无穷大、 +Infinity
表示正无穷大、 NaN
表示不是一个数字;JS
中对应 String
类型;JS
中 null
和 undefined
都可以表示这个意思;JS
中对应 Symbol
类型,表示唯一且不可改变;Number
所表示的整数是有范围的,超出范围的数据就没法用 Number
表示了,于是 ES10
中提出了一种新的数据类型 BigInt
,能表示任何位数的整数;Boolean
、 Number
、 String
、 null
、 undefined
、 Symbol
和 BigInt
等7种类型都是 JavaScript
中的原始类型,还有一种是非原始类型叫做对象类型;比如:一个人是对象,这个人有名字、性别、年龄等;let person = {
name: 'bubuzou',
sex: 'male',
age: 26,
}
在回答这个问题之前,我们先看一下变量在内存中是如何存储的:
let name1 = 'bubuzou'
let name2 = name1.concat('.com')
console.log(name1) // 'bubuzou'
执行完上面这段代码,我们发现变量 name1
的值还是不变,依然是 bubuzou
。这就说明了字符串的不可变性。但是你看了下面的这段代码,你就会产生疑问了:
let name1 = 'bubuzou'
name1 += '.com'
console.log(name1) // 'bubuzou.com'
你说字符串是不可变的,那现在不是变了嘛? 其实这只是变量的值变了,但是存在内存中的字符串依然不变。这就涉及到变量在内存中的存储了。 在 JavaScript
中,变量在内存中有2种存储方式:存在栈中和存在堆中。那么栈内存和堆内存有啥区别呢?
栈内存:
堆内存:
了解完变量在内存中的存储方式有2种,那我们继续以上面那串代码为例,画出变量的存储结构图:
然后我们可以描述下当计算机执行这段代码时候的发生了什么?首先定义了一个变量 name1
并且给其赋值 bubuzou
这个时候就会在内存中开辟一块空间用来存储字符串 bubuzou
,然后变量指向了这个内存空间。然后再执行第二行代码 letname2=name1.concat('.com')
这里的拼接操作其实是产生了一个新字符串 bubuzou.com
,所以又会为这个新字符串创建一块新内存,并且把定义的变量 name2
指向这个内存地址。 所以我们看到其实整个操作 bubuzou
这个字符串所在的内存其实是没有变化的,即使在第二段代码中执行了 name1+='.com'
操作,其实也只是变量 name1
指向了新的字符串 bubuzou.com
而已,旧的字符串 bubuzou
依然存在内存中,不过一段时间后由于该字符串没有被变量所引用,所以会被当成垃圾进行回收,从而释放掉该块内存空间。
从而我们得出结论:原始类型的值都是固定的,而对象类型则是由原始类型的键值对组合成一个复杂的对象;他们在内存中的存储方式是不一样的,原始类型的值直接存在栈内存中,而对象类型的实际值是存在堆内存中的,在栈内存中保存了一份引用地址,这个地址指向堆内存中的实际值,所以对象类型又习惯被叫做引用类型。
想一个问题为什么引用类型的值要存储到堆内存中?能不能存到栈内存中呢?答案一:因为引用类型大小不固定,而栈的大小是固定的,堆空间的大小是可以动态变化的,所以引用类型的值适合存在堆中;答案二:在代码执行过程中需要频繁的切换执行上下文的时候,如果把引用类型的值存到栈中,将会造成非常大的内存开销。
当我们对两个变量进行比较的时候,不同类型的变量是有不同表现的:
let str1 = 'hello'
let str2 = 'hello'
console.log( str1 === str2 ) // true
let person1 = {
name: 'bubuzou'
}
let person2 = {
name: 'bubuzou'
}
console.log( person1 === person2 ) // false
我们定义了2个字符串变量和2个对象变量,他们都长一模一样,但是字符串变量会相等,对象变量却不相等。这是因为在 JavaScript
中,原型类型进行比较的时候比较的是存在栈中的值是否相等;而引用类型进行比较的时候,是比较栈内存中的引用地址是否相等。
如上几个变量在内存中的存储模型如图所示:
变量进行复制的时候,原始类型和引用类型变量也是有区别的,来看下面的代码:
let str1 = 'hello'
let str2 = str1
str2 = 'world'
console.log( str1 ) // 'hello'
letstr1='hello'
: 复制前,定义了一个变量 str1
,并且给其赋值 hello
,这个时候 hello
这个字符串就会在栈内存中被分配一块空间进行存储,然后变量 str1
会指向这个内存地址;letstr2=str1
:复制后,把 str1
的值赋值给 str2
,这个时候会在栈中新开辟一块空间用来存储 str2
的值;str2='world'
:给 str2
赋值了一个新的字符串 world
,那么将新建一块内存用来存储 world
,同时 str2
原来的值 hello
的内存空间因为没有变量所引用,所以一段时间后将被当成垃圾回收;console.log(str1)
:因为 str1
和 str2
的栈内存地址是不一样的,所以即使 str2
的值被改变,也不会影响到 str1
。然后我们继续往下,看下引用类型的复制:
let person1 = {
name: 'bubuzou',
age: 20
}
let person2 = person1
person2.name = 'bubuzou.com'
console.log( person1.name) // 'bubuzou.com'
原始类型进行复制的时候是变量的值进行重新赋值,而如上图所示:引用类型进行复制的时候是把变量所指向的引用地址进行赋值给新的变量,所以复制后 person1
和 person2
都指向堆内存中的同一个值,所以当改变 person2.name
的时候, person1.name
也会被改变就是这个原因。
先说一下结论,在 JavaScript
中,所有函数的参数传递都是按值进行传递的。看如下代码:
let name = 'bubuzou'
function changeName(name) {
name = 'bubuzou.com'
}
changeName(name)
console.log( name ) // 'bubuzou'
定义了一个变量 name
,并赋值为 bubuzou
,函数调用的时候传入 name
,这个时候会在函数内部创建一个局部变量 name
并且把全局变量的值 bubuzou
传递给他,这个操作其实是在内存里新建了一块空间用来存放局部变量的值,然后又把局部变量的值改成了 bubuzou.com
,这个时候其实内存中会有3块地址空间分别用来存放全局变量的值 bubuzou
、局部变量原来的值 bubuzou
、和局部变量新的值 bubuzou.com
;一旦函数调用结束,局部变量将被销毁,一段时间后由于局部变量新旧值没有变量引用,那这两块空间将被回收释放;所以这个时候全局 name
的值依然是 bubuzou
。
再来看看引用类型的传参,会不会有所不同呢?
let person = {
name: 'bubuzou'
}
function changePerosn(person) {
person.name = 'bubuzou.com'
}
changePerosn( person )
console.log( person.name ) // 'bubuzou.com'
引用类型进行函数传参的时候,会把引用地址复制给局部变量,所以全局的 person
和函数内部的局部变量 person
是指向同一个堆地址的,所以一旦一方改变,另一方也将被改变,所以至此我们是不是可以下结论说:当函数进行传参的时候如果参数是引用类型那么就是引用传递嘛?
将上面的例子改造下:
let person = {
name: 'bubuzou'
}
function changePerosn(person) {
person.name = 'bubuzou.com'
person = {
name: 'hello world'
}
}
changePerosn( person )
console.log( person.name ) // 'bubuzou.com'
如果 person
是引用传递的话,那就会自动指向值被改为 hello world
的新对象;事实上全局变量 person
的引用地址自始至终都没有改变,倒是局部变量 person
的引用地址发生了改变。
null
在 JavaScript
中自成一种原始类型,只有一个值 null
,表示无、空、值未知等特殊值。可以直接给一个变量赋值为 null
:
let s = null
undefined
和 null
一样也是自成一种原始类型,表示定义了一个变量,但是没有赋值,则这个变量的值就是 undefined
:
let s
console.log( s) // undefined
虽然可以给变量直接赋值为 undefined
也不会报错,但是原则上如果一个变量值未定,或者表示空,则直接赋值为 null
比较合适,不建议给变量赋值 undefined
。 null
和 undefined
在进行逻辑判断的时候都是会返回 false
的:
let a = null, b
console.log( a ? 'a' : b ? 'b' : 'c') // 'c'
null
在转成数字类型的时候会变成 0
,而 undefined
会变成 NaN
:
let a = null, b
console.log( +null ) // 0
console.log( + b ) // NaN
Symbol
值表示唯一标识符,是 ES6
中新引进的一种原始类型。可以通过 Symbol()
来创建一个重要的值,也可以传入描述值;其唯一性体现在即使是传入一样的描述,他们两者之间也是不会相等的:
let a = Symbol('bubuzou')
let b = Symbol('bubuzou')
console.log( a === b ) // false
那还是不是任意2个描述一样的 Symbol
都是不相等的呢?答案是否定的。可以通过 Symbol.for()
来查找或新建一个 Symbol
:
let a = Symbol.for('bubuzou')
let b = Symbol.for('bubuzou')
console.log( a === b ) // true
使用 Symbol.for()
可以在根据传入的描述在全局范围内进行查找,如果没找到则新建一个 Symbol
,并且返回;所以当执行第二行代码 Symbol.for('bubuzou')
的时候,就会找到全局的那个描述为 bubuzou
的 Symbol
,所以这里 a
和 b
是会绝对相等的。
居然可以通过描述找到 Symbol
, 那是否可以通过 Symbol
来找到描述呢?答案是肯定的,但是必须是全局的 Symbol
,如果没找到则会返回 undefined
:
let a = Symbol.for('bubuzou')
let desc = Symbol.keyFor( a )
console.log( desc ) // 'bubuzou'
但是对于任何一个 Symbol
都有一个属性 description
,表示这个 Symbol
的描述:
let a = Symbol('bubuzou')
console.log( a.description ) // 'bubuzou'
我们知道对象的属性键可以是字符串,但是不能是 Number
或者 Boolean
; Symbol
被设计出来其实最大的初衷就是用于对象的属性键:
let age = Symbol('20')
let person = {
name: 'bubuzou',
[age]: '20', // 在对象字面量中使用 `Symbol` 的时候需要使用中括号包起来
}
这里给 person
定义了一个 Symbol
作为属性键的属性,这个相比于用字符串作为属性键有啥好处呢?最明显的好处就是如果这个 person
对象是多个开发者进行开发维护,那么很容易再给 person
添加属性的时候出现同名的,如果是用字符串作为属性键那肯定是冲突了,但是如果用 Symbol
作为属性键,就不会存在这个问题了,因为它是唯一标识符,所以可以使对象的属性受到保护,不会被意外的访问或者重写。
注意一点,如果用 Symbol
作为对象的属性键的时候, forin
、 Object.getOwnPropertyNames
、或 Object.keys()
这里循环是无法获取 Symbol
属性键的,但是可以通过 Object.getOwnPropertySymbols()
来获取;在上面的代码基础上:
for (let o in person) {
console.log( o ) // 'name'
}
console.log (Object.keys( person )) // ['name']
console.log(Object.getOwnPropertyNames( person )) // ['name']
console.log(Object.getOwnPropertySymbols( person )) // [Symbol(20)]
JavaScript 中的数字涉及到了两种类型:一种是 Number 类型,以 64 位的格式 IEEE-754 存储,也被称为双精度浮点数,就是我们平常使用的数字,其范围是 2^{52} 到 -2^{52};第二种类型是 BigInt,能够表示任意长度的整数,包括超出 2^{52} 到 -2^{52} 这个范围外的数。这里我们只介绍 Number 数字。
对于一个常规的数字,我们直接写即可,比如:
let age = 20
但是还有一种位数特别多的数字我们习惯用科学计数法的表示方法来写:
let billion = 1000000000;
let b = 1e9
以上两种写法是一个意思, 1e9 表示 1 x 10^9;如果是 1e-3 表示 1 / 10^3 = 0.001。 在 JavaScript 中也可以用数字表示不同的进制,比如:十进制中的 10 在 二、八和十六进制中可以分别表示成 0b1010、 0o12 和 0xa;其中的 0b 是二进制前缀, 0o 是八进制前缀,而 ox 是十六进制的前缀。
我们也可以通过 toString(base)
方法来进行进制之间的转换, base
是进制的基数,表示几进制,默认是 10
进制的,会返回一个转换数值的字符串表示。比如:
let num = 10
console.log( num.toString( 2 )) // '1010'
console.log( num.toString( 8 )) // '12'
console.log( num.toString( 16 )) // 'a'
数字也可以直接调用方法,
10..toString(2)
这里的 2个.
号不是写错了,而是必须是2个,否则会报SyntaxError
错误。第一个点表示小数点,第二个才是调用方法。点符号首先会被认为是数字常量的一部分,其次再被认为是属性访问符,如果只写一个点的话,计算机无法知道这个是表示一个小数呢还是去调用函数。数字直接调用函数还可以有以下几种写法:
(10).toString(2) // 将10用括号包起来
10.0.toString(2) // 将10写成10.0的形式
10 .toString(2) // 空格加上点符号调用
Number
类型除了常规数字之外,还包含了一些特殊的数字:
NaN
:表示不是一个数字,通常是由不合理的计算导致的结果,比如数字除以字符串 1/'a'
; NaN
和任何数进行比较都是返回 false
,包括他自己: NaN==NaN
会返回 false
; 如何判断一个数是不是 NaN
呢?有四种方法:方法一:通过 isNaN()
函数,这个方法会对传入的字符串也返回 true
,所以判断不准确,不推荐使用:
isNaN( 1 / 'a')` // true
isNaN( 'a' ) // true
方法二:通过 Number.isNaN()
,推荐使用:
Number.isNaN( 1 / 'a')` // true
Number.isNaN( 'a' ) // false
方法三:通过 Object.is(a,isNaN)
:
Object.is( 0/'a', NaN) // true
Object.is( 'a', NaN) // false
方法四:通过判断 n!==n
,返回 true
, 则 n
是 NaN
:
let s = 1/'a'
console.log( s !== s ) // true
+Infinity
:表示正无穷大,比如 1/0
计算的结果, -Infinity
表示负无穷大,比如 -1/0
的结果。+0
和 -0
, JavaScript
中的数字都有正负之分,包括零也是这样,他们会绝对相等:console.log( +0 === -0 ) // true
console.log( 0.1 + 0.2 == 0.3 ) // false
有没有想过为什么上面的会不相等?因为数字在 JavaScript
内部是用二进制进行存储的,其遵循 IEEE754
标准的,用 64
位来存储一个数字, 64
位又被分隔成 1
、 11
和 52
位来分别表示符号位、指数位和尾数位。
比如十进制的 0.1
转成二进制后是多少?我们手动计算一下,十进制小数转二进制小数的规则是“乘2取整,顺序排列”,具体做法是:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数 部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,或者达到所要求的精度为止。
0.1 * 2 = 0.2 // 第1步:整数为0,小数0.2
0.2 * 2 = 0.4 // 第2步:整数为0,小数0.4
0.4 * 2 = 0.8 // 第3步:整数为0,小数0.8
0.8 * 2 = 1.6 // 第4步:整数为1,小数0.6
0.6 * 2 = 1.2 // 第5步:整数为1,小数0.2
0.2 * 2 = 0.4 // 第6步:整数为0,小数0.4
0.4 * 2 = 0.8 // 第7步:整数为0,小数0.8
...
我们这样依次计算下去之后发现得到整数的顺序排列是 0001100110011001100.... 无限循环,所以理论上十进制的 0.1 转成二进制后会是一个无限小数 0.0001100110011001100...,用科学计数法表示后将是 1.100110011001100... x 2^{-4} ,但是由于 IEEE754 标准规定了一个数字的存储位数只能是 64 位,有效位数是 52 位,所以将会对 1100110011001100.... 这个无限数字进行舍入总共 52 位作为有效位,然后二进制的末尾取舍规则是看后一位数如果是 1 则进位,如果是 0 则直接舍去。那么由于 1100110011001100.... 这串数字的第 53 位刚好是 1 ,所以最终的会得到的数字是 1100110011001100110011001100110011001100110011001101,即 1.100110011001100110011001100110011001100110011001101 x 2^{-4}。 十进制转二进制也可以用 toString 来进行转化:
console.log( 0.1.toString(2) ) // '0.0001100110011001100110011001100110011001100110011001101'
我们发现十进制的 0.1
在转化成二进制小数的时候发生了精度的丢失,由于进位,它比真实的值更大了。而 0.2
其实也有这样的问题,也会发生精度的丢失,所以实际上 0.1+0.2
不会等于 0.3
:
console.log( 0.1 + 0.2 ) // 0.30000000000000004
那是不是没办法判断两个小数是否相等了呢?答案肯定是否定的,想要判断2个小数 n1
和 n2
是否相等可以如下操作:
Number.EPSILON
还小,那么说明两数是相等的。Number.EPSILON 是 ES6 中的误差精度,实际值可以认为等于 2^{-52}。
if ( Math.abs( n1 - n2 ) < Number.EPSILON ) {
console.log( 'n1 和 n2 相等' )
}
toFixed(n)
对结果进行舍入, toFixed()
将会返回字符串,我们可以用 一元加 +
将其转成数字:let sum = 0.1 + 0.2
console.log( +sum.toFixed(2) === 0.3 ) // true
对数字进行操作的时候将常常遇到数值的舍入和字符串转数字的问题,这里我们巩固下基础。先来看舍入的:
Math.floor()
,向下舍入,得到一个整数:Math.floor(2.2) // 2
Math.floor(2.8) // 2
Math.ceil()
,向上舍入,得到一个整数:Math.ceil(2.2) // 3
Math.ceil(2.8) // 3
Math.round()
,对第一位小数进行四舍五入:
Math.round(2.26) // 2
Math.round(2.46) // 2
Math.round(2.50) // 3
Number.prototype.toFixed(n)
,和 Math.round()
一样会进行四舍五入,将数字舍入到小数点后 n
位,并且以字符串的形式返回:12..toFixed(2) // '12.00'
12.14.toFixed(1) // '12.1'
12.15.toFixed(1) // '12.2'
为什么 6.35.toFixed(1)
会等于 6.3
?因为 6.35
其实是一个无限小数:
6.35.toFixed(20) // "6.34999999999999964473"
所以在 6.35.toFixed(1)
求值的时候会得到 6.3
。
再来看看字符串转数字的情况:
Number(n)
或 +n
,直接将 n
进行严格转化:
Number(' ') // 0
console.log( +'') // 0
Number('010') // 10
console.log( +'010' ) // 10
Number('12a') // NaN
console.log( +'12a' ) // NaN
parseInt()
,非严格转化,从左到右解析字符串,遇到非数字就停止解析,并且把解析的数字返回:parseInt('12a') // 12
parseInt('a12') // NaN
parseInt('') // NaN
parseInt('0xA') // 10,0x开头的将会被当成十六进制数
parseInt()
默认是用十进制去解析字符串的,其实他是支持传入第二个参数的,表示要以多少进制的 基数去解析第一个参数:
parseInt('1010', 2) // 10
parseInt('ff', 16) // 255
如何判断一个数是不是整数?介绍两种方法:
Number.isInteger()
:
Number.isInteger(12.0) // true
Number.isInteger(12.2) // false
typeofnum=='number'&&num%1==0
function isInteger(num) {
return typeof num == 'number' && num % 1 == 0
}
除了原始类型外,还有一个特别重要的类型:引用类型。高程里这样描述他:引用类型是一种数据结构, 用于将数据和功能组织在一起。到目前为止,我们看到最多的引用类型就是 Object
,创建一个 Object
有两种方式:
new
操作符:
let person = new Object()
person.name = 'bubuzou'
person.age = 20
let person = {
name: 'bubuzou',
age: 20
}
除了 Object
外,在 JavaScript
中还有别的内置的引用类型,比如:
Array
数组Date
日期RegExp
正则表达式Function
函数他们的原型链的顶端都会指向 Object
:
let d = new Date()
console.log( d.__proto__.__proto__.constructor ) // ƒ Object() { [native code] }
先来看一个问题,为什么原始类型的变量没有属性和方法,但是却能够调用方法呢?
let str = 'bubuzou'
str.substring(0, 3) // 'bub'
因为 JavaScript
为了更好地操作原始类型,设计出了几个对应的包装类型,他们分别是:
Boolean
Number
String
上面那串代码的执行过程其实是这样的:
用代码体现一下:
let str = new String('bubuzou')
str.substring(0, 3)
str = null
原始类型调用函数其实就是自动进行了装箱操作,将原始类型转成了包装类型,然后其实原始类型和包装类型是有本质区别的,原始类型是原始值,而包装类型是对象实例:
let str1 = 'bubuzou'
let str2 = new String('bubuzou')
console.log( str1 === str2 ) // fasle
console.log( typeof str1 ) // 'string'
console.log( typeof str2 ) // 'object'
居然有装箱操作,那肯定也有拆箱操作,所谓的拆箱就是包装类型转成原始类型的过程,又叫 ToPromitive
,来看下面的例子:
let obj = {
toString: () => { return 'bubuzou' },
valueOf: () => { return 20 },
}
console.log( +obj ) // 20
console.log( `${obj}` ) // 'bubuzou'
在拆箱操作的时候,默认会尝试调用包装类型的 toString()
和 valueOf()
方法,对于不同的 hint
调用顺序会有所区别,如果 hint
是 string
则优先调用 toString()
,否则的话,则优先调用 valueOf()
。 默认情况下,一个 Object
对象具有 toString()
和 valueOf()
方法:
let obj = {}
console.log( obj.toString() ) // '[object Object]'
console.log( obj.valueOf() ) // {},valueOf会返回对象本身
Javascript
是弱类型的语音,所以对变量进行操作的时候经常会发生类型的转换,尤其是隐式类型转换,可能会让代码执行结果出乎意料之外,比如如下的代码你能理解其执行结果嘛?
[] + {} // '[object Object]'
{} + [] // 0
所以我们需要知道类型转换的规则,以下整理出一个表格,列出了常见值和类型以及转换之后的结果,仅供参考。
我们平时写代码的时候应该尽量让写出来的代码通俗易懂,让别人能阅读后知道你是要做什么,所以在对类型进行判断的时候应该尽量显示的处理。 比如将字符串转成数字,可以这样:
Number( '21' ) // 21
Number( '21.8' ) // 21.8
+'21' // 21
将数字显示转成字符串可以这样:
String(21) // '21'
21..toString() // '21'
显示转成布尔类型可以这样:
Boolean('21') // true
Boolean( undefined ) // false
!!NaN // false
!!'21' // true
除了以上之外,还有一些关于类型转换的冷门操作,有时候也挺管用的: 直接用一元加操作符获取当前时间的毫秒数:
+new
Date()
// 1595517982686
用 ~
配合 indexOf()
将操作结果直接转成布尔类型:
let str = 'bubuzou.com'
if (~str.indexOf('.com')) {
console.log( 'str如果包含了.com字符串,则会打印这句话' )
}
使用 ~~
对字符或数字截取整数,和 Math.floor()
有稍许不同:
~~21.1 // 21
~~-21.9 // -21
~~'1.2a' // 0
Math.floor( 21.1 ) // 21
Math.floor( -21.9 ) // -22
隐式类型转换发生在 JavaScript
的运行时,通常是由某些操作符或语句引起的,有下面这几种情况:
if(..)
语句中的条件判断表达式。for(..;..;..)
语句中的条件判断表达式(第二个)。while(..)
和 do..while(..)
循环中的条件判断表达式。?:
中的条件判断表达式。||
(逻辑或)和 &&
(逻辑与)左边的操作数(作为条件判断表达式)if (42) {
console.log(42)
}
while ('bubuzou') {
console.log('bubuzou')
}
const c = null ? '存在' : '不存在' // '不存在'
上例中的非布尔值会被隐式强制类型转换为布尔值以便执行条件判断。 需要特别注意的是 ||
和 &&
操作符。 ||
的操作过程是只有当左边的值返回 false
的时候才会对右边进行求值且将它作为最后结果返回,类似 a?a:b
这种效果:
const a = 'a' || 'b' // 'a'
const b = '' || 'c' // 'c'
而 &&
的操作过程是只有当左边的值返回 true
的时候才对右边进行求值且将右边的值作为结果返回,类似 a?b:a
这种效果:
const a = 'a' && 'b' // 'b'
const b = '' && 'c' // ''
-*/
会对非数字类型的会优先转成数字类型,但是对 +
操作符会比较特殊:String
类型,被识别为字符串拼接,并会优先将另一侧转换为字符串类型。Number
类型,另一侧为原始类型,则将原始类型转换为 Number
类型。Number
类型,另一侧为引用类型,将引用类型和 Number
类型转换成字符串后拼接。42 + 'bubuzou' // '42bubuzou'
42 + null // 42
42 + true // 43
42 + [] // '42'
42 + {} // '42[object Object]'
宽松相等( ==
)和严格相等( ===
)在面试的时候经常会被问到,而回答一般是 ==
是判断值是否相等,而 ===
除了判断值会不会相等之外还会判断类型是否相等,这个答案不完全正确,更好的回答是: ==
在比较过程中允许发生隐式类型转换,而 ===
不会。 那 ==
是怎么进行类型转换的呢?
1、 数字和字符串比,字符串将转成数字进行比较:
20 == '20' // true
20 === '20' // false
2、 别的类型和布尔类型比较,布尔类型将首先转成数字进行比较, true
转成数字 1
, false
转成数字 0
,注意这个是非常容易出错的一个点:
'bubuzou' == true // false
'0' == false // true
null == false // false,
undefined == false // false
[] == true // false
['1'] == true // true
所以写代码进行判断的时候一定不要写成 x==true
或 x==false
这种,而应该直接 if(x)
判断。
3、 null
和 undefined
: null==undefined
比较结果是 true
,除此之外, null
、 undefined
和其他任何结果的比较值都为 false
。可以认为在 ==
的情况下, null
和 undefined
可以相互的进行隐式类型转换。
null == undefined // true
null == '' // false
null == 0 // false
null == false // false
undefined == '' // false
undefined == 0 // false
undefined == false // false
4、 原始类型和引用类型比较,引用类型会首先进行 ToPromitive
转成原始类型然后进行比较,规则参考上面介绍的拆箱操作:
'42' == [42] // true
'1,2,3' == [1, 2, 3] // true
'[object Object]' == {} // true
0 == [undefined] // true
5、 特殊的值
NaN == NaN // false
+0 == -0 // true
[] == ![]
// true,![]的优先级比==高,所以![]先转成布尔值变成false;即变成[] == false,false再转成数字0,[]转成数字0,所以[] == ![]
0 == '\n' // true
JavaScript
中有 null
、 undefined
、 boolean
、 number
、 string
、 Symbol
等六种原始类型,我们可以用 typeof
来判断值是什么原始类型的,会返回类型的字符串表示:
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof 42 // 'number'
typeof "42" // 'string'
typeof Symbol() // 'symbol'
但是原始类型中有一个例外, typeofnull
会得到 'object',所以我们用 typeof
对原始值进行类型判断的时候不能得到一个准确的答案,那如何判断一个值是不是 null
类型的呢?
let o = null
!o && typeof o === 'object' // 用于判断 o 是否是 null 类型
undefined
和undeclared
有什么区别?前者是表示在作用域中定义了但是没有赋值的变量,而后者是表示在作用域中没有定义的变量;分别表示undefined
未定义、undeclared
未声明。
typeof
能够对原始类型进行判断,那是否也能判断引用类型呢?
typeof [] // 'object'
typeof {} // 'object'
typeof new Date() // 'object'
typeof new RegExp() // 'object'
typeof new Function() // 'function'
从上面的结果我们可以得到这样一个结论: typeof
对引用类型判断的时候只有 function
类型可以正确判断,其他都无法正确判断具体是什么引用类型。
我们知道 typeof
只能对部分原始类型进行检测,对引用类型毫无办法。 JavaScript
提供了一个操作符 instanceof
,我们来看下他是否能检测引用类型:
[] instanceof Array // true
[] instanceof Object // true
我们发现数组即是 Array
的实例,也是 Object
的实例,因为所以引用类型原型链的终点都是 Object
,所以 Array
自然是 Object
的实例。那么我们得出结论: instanceof
用于检测引用类型好像也不是很靠谱的选择。
我们可以使用 Object.prototype.toString.call()
来检测任何变量值的类型:
Object.prototype.toString.call(true) // '[object Boolean]'
Object.prototype.toString.call(undefined) // '[object Undefined]'
Object.prototype.toString.call(null) // '[object Null]'
Object.prototype.toString.call(20) // '[object Number]'
Object.prototype.toString.call('bubuzou') // '[object String]'
Object.prototype.toString.call(Symbol()) // '[object Symbol]'
Object.prototype.toString.call([]) // '[object Array]'
Object.prototype.toString.call({}) // '[object Object]'
Object.prototype.toString.call(function(){}) // '[object Function]'
Object.prototype.toString.call(new Date()) // '[object Date]'
Object.prototype.toString.call(new RegExp()) // '[object RegExp]'
Object.prototype.toString.call(JSON) // '[object JSON]'
Object.prototype.toString.call(MATH) // '[object MATH]'
Object.prototype.toString.call(window) // '[object RegExp]'