数据类型
动态和弱类型
前文提到,JavaScript 是一种动态的、弱类型的脚本语言。其中,动态
指的是 JavaScript
中的变量与任何值的类型没有任何关联,任何变量都可以被赋予(和重新赋予)各种类型的值:
let foo = 42 // foo 现在是一个数值
foo = 'bar' // foo 现在是一个字符串
foo = true // foo 现在是一个布尔值
而 弱类型
,则意味着当操作涉及不匹配的类型时,它允许隐式类型转换,而不是抛出类型错误。
const foo = 42 // foo 现在是一个数值
const result = foo + '1' // JavaScript 将 foo 强制转换为字符串,因此可以将其与另一个操作数连接起来
console.log(result) // 421
隐式转换无疑是把双刃剑,它可以让代码变得更加简洁,但是也可能导致一些隐蔽的错误。因此,在开发中,我们应当注意避免隐式类型转换,并在必要时加以明确。
一、数据类型
JavaScript 语言规定了 8 种语言类型,分为 基本类型 和 引用类型 两大类,如下所示:
基本类型:·
String
、Number
、Boolean
、Symbol
、Undefined
、Null
、BigInt
引用类型:·
Object
基本类型也称为简单类型,由于其占据空间固定,其值存储在栈(Stack)中。 基础类型的值是不可变的,一旦创建,其值本身就不能改变。 这就是说,改变原始数据类型的值事实上是创建了一个新的值。
let x = 10 // 原始数据类型
x = x + 5 // 这实际上是创建了一个新值 (15),并将其赋给 x
console.log(x) // 输出 15
引用类型也称为复杂类型,由于其值的大小会改变,所以不能将其存放在栈中,否则会降低变量查询速度,因此,其值存储在堆 (heap) 中。 而存储在变量处的值,是一个指针,指向存储对象的内存处,即按址访问。
引用类型除 Object 外,还包括 Function
、Array
、RegExp
、Date
、Map
、Set
、WeakMap
、WeakSet
等。
栈内存和堆内存
- 栈内存:栈(stack)是一种后进先出(LIFO)的内存结构,主要用于存储函数调用的上下文信息。栈内存通常具有较小的大小和有限的空间,因为它需要快速访问以保持高效的函数调用和返回。因此,引用类型的数据不能存储在栈内存中,因为它们的大小可能会动态变化,并且在栈上的生命周期通常比较短暂。
- 堆内存:堆是一种动态分配内存的数据结构,允许在程序运行时灵活分配和释放内存。这种灵活性使得堆成为存储引用类型的理想选择,因为引用类型的大小和生命周期通常是不确定的。
1. String
String
用于表示文本数据,最大长度为 2^53 - 1 个字符。
JavaScript
字符串是不可变的。这意味着一旦字符串被创建,就不可能修改它。字符串方法基于当前字符串的内容创建一个新的字符串——例如: 使用 substring()
获取原始的子字符串。 使用串联运算符(+)
或 concat()
将两个字符串串联。
const str = 'hello world'
const subStr = str.substring(2, 6) // "llo "
const newStr = str + ' ' + subStr // "hello world llo "
String
类型有很多方法可以操作字符串,如下所示:
const str = 'hello world'
// 获取首字母
console.log(str.charAt(0)) // "h"
// 获取字符编码
console.log(str.charCodeAt(0)) // 104
// 转换为大写字母
console.log(str.toUpperCase()) // "HELLO WORLD"
// 转换为小写字母
console.log(str.toLowerCase()) // "hello world"
// 去除首尾空格
console.log(str.trim()) // "hello world"
// 替换字符串
console.log(str.replace('l', 'L')) // "heLLo world"
// 截取字符串
console.log(str.substr(2, 4)) // "llo "
substr 和 substring
- str.substr(start, length) 方法返回从 start 开始,长度为 length 的子字符串。
- str.substring(startIndex, endIndex) 方法返回从 startIndex 开始,到 endIndex 结束的子字符串。
要注意的是, String
的意义并非“字符串”,而是字符串的 UTF16
编码,我们字符串的操作 charAt
、charCodeAt
、length
等方法针对的都是 UTF16
编码。所以,字符串的最大长度,实际上是受字符串的编码长度影响的。
1.1 字符串编码
字符编码指的是将字符映射为计算机可以处理和存储的数字(通常是字节或字节序列)的规则。 字符串的编码方式有两种:UTF-16
和 UTF-8
。
UTF-16
是一种定长的编码方式,它使用 2 个字节(16 位)来表示字符,所以,对于中文字符,它需要 2 个字节来表示,而英文字符只需要 1 个字节。UTF-8
是一种变长的编码方式,它使用 1-6 个字节来表示字符,所以,对于中文字符,它需要 3 个字节来表示,而英文字符只需要 1 个字节。
JavaScript
采用 UTF-16
编码,所以,对于中文字符,它的长度是 2,而对于英文字符,它的长度是 1。
const str = '你好,世界'
console.log(str.length) // 6
console.log(str.charAt(0)) // "你"
console.log(str.charCodeAt(0)) // 20320
console.log(str.charCodeAt(1)) // 19990
console.log(str.charCodeAt(2)) // 31219
console.log(str.charCodeAt(3)) // 22235
console.log(str.charCodeAt(4)) // 30340
console.log(str.charCodeAt(5)) // 21013
TIP
- 字节(Byte):计算机存储和处理数据的基本单位之一,通常由 8 位(bit)组成,比如 01001000。每一位可以是 0 或 1。 一个字节能够表示 256 个不同的值(从 0 到 255)。在字符编码中,字节用来表示或编码字符的数据。
- 字符:最基本的文本单位,可以是一个字母、数字、符号、标点、空格或其他图形符号。字符在不同的语言和字符集(如 ASCII、Unicode)中都有各自的表示和编码方式。
- 字符编码方式: 将字符映射为计算机可以处理和存储的数字(通常是字节或字节序列)的规则。
- Unicode: 是一种字符编码标准,它将每一个字符都分配一个唯一的码位,使得每个字符都能用唯一的数字来表示。 UTF-8 和 UTF-16 是实现 Unicode 编码的不同方式或编码形式。
详情可参考 String
2. Number
Number
类型表示我们通常意义上的“数字”。这个数字大致对应数学中的有理数,当然,在计算机中,我们有一定的精度限制。
2.1 数值范围
JavaScript
的 Number
类型为双精度 IEEE 754 64 位浮点类型,其数值范围有以下限制:
- 整数范围:-2^53 ~ 2^53 - 1
- 浮点数范围:-2^1024 ~ 2^1024 - 1
- 整数和浮点数的范围是不一样的,整数范围比浮点数范围小很多。
console.log(Number.MAX_SAFE_INTEGER) // 9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 1) // 9007199254740992
console.log(Number.MAX_SAFE_INTEGER + 1 // Uncaught RangeError: Maximum safe integer size exceeded
你可以使用 Number.isSafeInteger()
检查一个数是否在安全的整数范围内。
console.log(Number.MAX_SAFE_INTEGER) // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER) // -9007199254740991
console.log(Number.isSafeInteger(10)) // true
console.log(Number.isSafeInteger(10.1)) // false
±(2^-1074 ~ 2^1024) 范围之外的值会自动转换:
- 大于
Number.MAX_VALUE
的正值被转换为 +Infinity。 - 小于
Number.MIN_VALUE
的正值被转换为 +0。 - 小于
-Number.MAX_VALUE
的负值被转换为 -Infinity。 - 大于
-Number.MIN_VALUE
的负值被转换为 -0。
console.log(Number.MAX_VALUE) // 1.7976931348623157e+308
console.log(Number.MIN_VALUE) // 5e-324
console.log(Number.MAX_VALUE + 1) // Infinity
console.log(Number.MIN_VALUE - 1) // -Infinity
2.2 数值精度
JavaScript 中的数值精度是指小数点后保留的有效数字位数。 JavaScript 中的数值精度是不确定的,因为浮点数的表示方式是以二进制形式存储的,而二进制数的表示范围是无限的,所以,JavaScript 无法精确表示无限循环小数。
console.log(0.1 + 0.2) // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3) // false
我们只需知道在处理浮点数时,由于计算机的存储机制,导致了浮点数的精度丢失。解决办法是使用 BigDecimal
等库,它可以提供更高的精度。
参考链接
2.3 数值进制
JavaScript 中的数值进制指的是数字的表示形式,JavaScript 默认十进制。
console.log(0b11111111) // 255
console.log(0o377) // 255
console.log(0xff) // 255
TIP
- 十进制:最常用,使用 0-9。
- 二进制:使用 0 和 1,以 0b 为前缀。
- 八进制:使用 0-7,以 0o 为前缀。
- 十六进制:使用 0-9 和 A-F,以 0x 为前缀。
2.4 数值运算
JavaScript 中的数值运算有以下几种:
- 加法运算:
+
- 减法运算:
-
- 乘法运算:
*
- 除法运算:
/
- 求余运算:
%
- 自增运算:
++
- 自减运算:
--
console.log(1 + (2 * 3) / 4 - 5) // 1.75
console.log(10 % 3) // 1
console.log(10 ** 2) // 100
console.log(2 ** 32) // 4294967296
console.log(2 ** 53) // 9007199254740992
console.log(2 ** 53 - 1) // 9007199254740991
console.log(2 ** 53 + 1) // 9007199254740993
console.log(2 ** 53 + 1000000) // Uncaught RangeError: Maximum safe integer size exceeded
2.5 数值转换
JavaScript 中的数值转换有以下几种:
- 转字符串:
String()
- 转数字:
Number()
- 转整数:
parseInt()
- 转浮点数:
parseFloat()
console.log(String(123)) // "123"
console.log(Number('123')) // 123
console.log(parseInt('123.456')) // 123
console.log(parseFloat('123.456')) // 123.456
拓展:关于浮点数
浮点型是一种用于表示带有小数的数据的数值类型,浮点数的“点”指的是小数点的位置可以在数值中动态移动,通常采用 IEEE 754 标准来表示。该标准规定了浮点数的表示方式,包括:
- 单精度浮点数(32 位):分为 1 位符号位、8 位指数位、23 位尾数位。
- 双精度浮点数(64 位):分为 1 位符号位、11 位指数位、52 位尾数位。
3. Boolean
Boolean 类型有两个值, true
和 false
,它用于表示逻辑意义上的真和假,同样有关键字 true
和 false
来表示两个值。
3.1 布尔上下文中的假值
布尔上下文是指任何需要布尔值的地方,如条件语句和逻辑运算符, 如下假值的特性使得它们在条件判断和逻辑运算中都表现为 false。
false
0
""
(空字符串)null
undefined
NaN
(Not a Number)
console.log(false || 0 || '' || null || undefined || NaN || 'hello') // hello
if (false || 0 || '' || null || undefined || NaN || 'hello') {
console.log('true')
} else {
console.log('false')
} // false
const values = [0, false, '', null, undefined, NaN, 'hello']
for (const value of values) {
if (!value) {
console.log(`${value} is treated as false`)
} else {
console.log(`${value} is treated as true`)
}
}
// 0 is treated as false
// false is treated as false
// '' is treated as false
// null is treated as false,
// undefined is treated as false
// NaN is treated as false
// hello is treated as true
4. Symbol
起因是为对象增加不影响之前属性的新属性,保证属性名独一无二。
// Symbol 可以具有字符串类型的描述,但是即使描述相同,Symbol 也不相等
Symbol('foo') !== Symbol('bar')
Symbol.for('foo') === Symbol.for('foo')
Symbol
作为属性名,遍历对象的时候,该属性不会出现在 for...in
、for...of
循环中,也不会被 Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。
另一个新的 API,Reflect.ownKeys()
方法可以返回所有类型的键名,包括常规键名和 Symbol
键名。
5. Undefined、Null
Undefined
类型表示未定义,它的类型只有一个值,就是 undefined
。 任何变量在赋值前是 Undefined
类型、值为 undefined,一般我们可以用全局变量 undefined
来表达这个值,或者 void 运算来把任意一个表达式变成 undefined
值。
但是,因为 JavaScript 的代码 undefined
是一个变量,而并非是一个关键字,(这是 JavaScript 语言公认的设计失误之一),所以,我们为了避免无意中被篡改,建议使用 void 0 来获取 undefined
值。 「void 0」的执行结果永远是「undefined」, 即使在某些老旧浏览器 或者在某个函数中 undefined
被重新赋值,我们仍然可以通过 「void 0」 得到真正的「undefined
」。
var obj = {}
obj.undefined = '轻语'
console.log(obj.undefined) // 轻语
// 在标准浏览器下作为全局作用域下,作为window的一个属性, undefined 不可修改;
//但对于一个普通对象,undefined可作为属性且可以修改。
function fn() {
var undefined = 100
alert(undefined) //chrome: 100, ie8: 100
}
fn()
// 不管是标准浏览器,还是老的 IE 浏览器,在函数内部 undefined 可作为局部变量重新赋值
Undefined
跟 null
有一定的表意差别,null
表示的是:“定义了但是为空”。所以,在实际编程时,我们一般不会把变量赋值为 undefined
,这样可以保证所有值为 undefined
的变量,都是从未赋值的自然状态。
Null
类型也只有一个值,就是 null
,它的语义表示空值,与 undefined
不同,null
是 JavaScript 关键字,所以在任何代码中,你都可以放心用 null
关键字来获取 null
值。
6. BigInt
BigInt 是一种内置对象,它提供了一种方法来表示大于 253 - 1
的整数。这原本是 Javascript 中可以用 Number
表示的最大数字。BigInt
可以表示任意大的整数。
可以用在一个整数字面量后面加 n
的方式定义一个 BigInt
,如:10n,或者调用函数 BigInt()
。
它在某些方面类似于 Number
,但是也有几个关键的不同点:
- 不能用于 Math 对象中的方法;
- 不能和任何
Number
实例混合运算,两者必须转换成同一种类型。 - 在两种类型来回转换时要小心,因为
BigInt
变量在转换成Number
变量时可能会丢失精度。
const bigInt = 10
console.log(typeof bigInt) // number
const bigInt = 10n
console.log(typeof bigInt) // bigint
const bigInt = BigInt(10)
console.log(typeof bigInt) // bigint
// BigInt
const x = BigInt(Number.MAX_SAFE_INTEGER) // 9007199254740991n
x + 1n === x + 2n // false,因为 9007199254740992n 和 9007199254740993n 不相等
// Number
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 // true,因为都等于 9007199254740992
BigInt 不能表示小数,但可以更精确地表示大整数。这两种类型都不能相互替代。如果 BigInt 值在算术表达式中与常规 number
值混合,或者它们相互隐式转换,则抛出 TypeError
。
7. Object
Object
类型是 JavaScript 中所有对象的基础类型,它是所有对象的父类型,包括函数、数组、正则表达式、日期等。
在 JavaScript
中,对象可以被看作是一组属性的集合。每个属性都有一个名称和一个值。属性的值可以是任何类型,包括函数、数组、正则表达式、日期等。
typeof {} // object
typeof [] // object
typeof new Date() // object
typeof new RegExp() // object
typeof Math // object
typeof JSON // object
二、数据类型判断
1. typeof
typeof 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 7 种:number
、boolean
、symbol
、string
、object
、undefined
、function
等。
typeof '' // string 有效
typeof 1 // number 有效
typeof Symbol() // symbol 有效
typeof true //boolean 有效
typeof undefined //undefined 有效
typeof null //object 无效
typeof [] //object 无效
typeof new Function() // function 有效
typeof newDate() //object 无效
typeof newRegExp() //object 无效
有些时候,typeof
操作符会返回一些令人迷惑但技术上却正确的值:
- 对于基本类型,除
null
以外,均可以返回正确的结果。 - 对于引用类型,除
function
以外,一律返回object
类型。 - 对于
null
,返回 object 类型。 - 对于
function
返回function
类型。
其中,null
有属于自己的数据类型 Null
, 引用类型中的 数组、日期、正则 也都有属于自己的具体类型,而 typeof
对于这些类型的处理,只返回了处于其原型链最顶端的 Object
类型,没有错,但不是我们想要的结果。
2. instance
instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。
const instance = (A, B) => {
const proto = A._proto_
while (proto) {
if (proto === B.prototype) {
return true
} else {
proto = proto._proto_
}
return false
}
}
[] instanceof Array;// true
{} instanceof Object;// true
newDate() instanceof Date;// true
function Person(){};
newPerson() instanceof Person;
[] instanceof Object;// true
newDate() instanceof Object;// true
newPerson instanceof Object;// true
虽然 instanceof
能够判断出 [ ] 是 Array 的实例,但它认为 [ ] 也是 Object 的实例,为什么呢?
从 instanceof
能够判断出 [ ].proto 指向 Array.prototype
,而 Array.prototype.**proto**
又指向了 Object.prototype
,最终 Object.prototype.**proto**
指向了 null,标志着原型链的结束。因此,[]、Array、Object 就在内部形成了一条原型链:
从原型链可以看出,[] 的 proto 直接指向 Array.prototype,间接指向 Object.prototype
,所以按照 instanceof
的判断规则,[] 就是 Object
的实例。依次类推,类似的 new Date()、new Person() 也会形成一条对应的原型链 。因此,instanceof
只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。
3. constructor
当一个函数 F 被定义时,JS 引擎会为 F 添加 prototype
原型,然后再在 prototype
上添加一个 constructor
属性,并让其指向 F 的引用。
当执行 var f = new F() 时,F 被当成了构造函数,f 是 F 的实例对象,此时 F 原型上的 constructor
传递到了 f 上,因此 f.constructor == F
可以看出,F 利用原型对象上的 constructor
引用了自身,当 F 作为构造函数来创建对象时,原型上的 constructor
就被遗传到了新创建的对象上, 从原型链角度讲,构造函数 F 就是新对象的类型。这样做的意义是,让新对象在诞生以后,就具有可追溯的数据类型。
同样,JavaScript 中的内置对象在内部构建时也是这样做的:
null
和undefined
是无效的对象,因此是不会有constructor
存在的,这两种类型的数据需要通过其他方式来判断。- 函数的
constructor
是不稳定的,这个主要体现在自定义对象上,当开发者重写prototype
后,原有的constructor
引用会丢失,constructor
会默认为 Object。 因此,为了规范开发,在重写对象原型时一般都需要重新给constructor
赋值,以保证对象实例的类型不被篡改。 :::
prototype
被重新赋值的是一个 { }
, { }
是 new Object()
的字面量,因此 new Object()
会将 Object 原型上的 constructor
传递给 { }
,也就是 Object
本身。
因此,为了规范开发,在重写对象原型时一般都需要重新给 constructor
赋值,以保证对象实例的类型不被篡改。
4. toString
toString() 是 Object
的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。
Object.prototype.toString.call('') // [object String]
Object.prototype.toString.call(1) // [object Number]
Object.prototype.toString.call(true) // [object Boolean]
Object.prototype.toString.call(Symbol()) //[object Symbol]
Object.prototype.toString.call(undefined) // [object Undefined]
Object.prototype.toString.call(null) // [object Null]
Object.prototype.toString.call(newFunction()) // [object Function]
Object.prototype.toString.call(newDate()) // [object Date]
Object.prototype.toString.call([]) // [object Array]
Object.prototype.toString.call(newRegExp()) // [object RegExp]
Object.prototype.toString.call(newError()) // [object Error]
Object.prototype.toString.call(document) // [object HTMLDocument]
Object.prototype.toString.call(window) //[object global] window 是全局对象 global 的引用
function _typeof(obj) {
var s = Object.prototype.toString.call(obj)
return s.match(/\[object (.*?)\]/)[1].toLowerCase()
}
三、数据类型转换
关于隐式类型转换
- JS 的类型设计本身就存在很多问题,把时间花在学习这些错误上,得不偿失。
===
在某种程度上直接或间接的绕开了本来就属于设计失误的类型转换上。- 显示类型转换需要多了解,隐式类型转换将常用的小技巧记下就够。
// 隐式类型转换
var a = '1'; +a = 1; // +可以将其他类型转为 number 类型
var a = 1; '' + a = '1'; || `${a}` = '1'; // 将 number 转成 string
实在有兴趣,可翻阅《You Don't Know JS》第六章类型转换。
四、关于包装对象
你也许有过疑问,上文中基础类型的变量,为什么会有方法可以调用? 比如:
let num = 10
console.log(num.toFixed(2)) // 10.00
let str = 'hello'
console.log(str.toUpperCase()) // HELLO
这是因为,JavaScript 中的基本类型的值,在执行某些方法时,会自动被转换为对应的包装对象。 在上面的代码中,num
是一个基本类型的值,执行 toFixed()
方法时,num
会被自动转换为 Number
对象,然后再执行 toFixed()
方法。
包装对象是一种特殊的对象,它包装了一个基本类型的值,并提供了一些方法来操作这个值,主要有以下几种:
- Boolean 对象
- Number 对象
- String 对象
- Symbol 对象
- BigInt 对象