[你不知道的JavaScript(中)] 类型和值
类型
直到目前(ES2021),JS 一共有八种内置数据类型了,分别是
- 空值(null)
- 未定义(undefined)
- 数字(number)
- 布尔(boolean)
- 字符串(string)
- 符号(symbol)
- 大整数(bigint)
- 对象(object)
bigint 类型是 ES2020 新加的一个基础类型,用于解决 JS 以往超大数字精度丢失的问题,在不使用 bigint 类型的普通数字最大精度为Number.MAX_SAFE_INTEGER
,最小精度为Number.MIN_SAFE_INTEGER
,具体数值分别为9007199254740991(2^53-1)
和-9007199254740991 (-(2^53-1))
。超出了这个范围的话,精度会丢失。
const a = -9007199254740991 - 100;
console.log(a); // -9007199254741092
console.log(a === a - 1); // true 很奇怪对吧
bigint 就是为了解决这个问题的。想要创建 bigint 类型,有两种方式。
const first = 10000000000000000000000n // 数值后面加n
const second = const c = BigInt('100000000000000000000000000')
// 100000000000000000000000000n 这里如果需要创建超出最大安全数的值需要传入字符串数字,不然依然会超精度,因为参数本身精度超了
类型判断
typeof
除了 null 其他的值都能正确获得类型
const bool = true;
const num = 1;
const str = "hello";
const sym = Symbol("demo");
const obj = {};
const bigint = 1000000000000000000000000n;
console.log(typeof obj); // object
console.log(typeof bigint); // bigint
console.log(typeof undefined); // undefined
console.log(typeof null); // object <- 特殊的
console.log(typeof sym); // symbol
console.log(typeof str); // string
console.log(typeof num); // number
console.log(typeof bool); // boolean
null 值的类型检测 bug 已经存在 20 多年,如果修复说不定会引起更多的 bug,所以目测这玩意是不会再修复了。
所以我们判断 null 的的方法需要变通一下 !a && typeof a === 'object'
instanceof
这个是代表是否由某个对象构建出来的值,所以你会发现对基础数据类型的判定不管用。
console.log(obj instanceof Object); // true
console.log(bigint instanceof BigInt); // false
console.log(sym instanceof Symbol); // false
console.log(str instanceof String); // false
console.log(num instanceof Number); // false
console.log(bool instanceof Boolean); // false
基础类型的装箱一般是在.
操作符的时候。比如str.charAt(0)
Object.prototype.toString.call
这种方式可以准确的得到具体的类型,包括 object 的子类型 function、array 之类的
const arr = [];
const func = () => {};
const getType = (value) => Object.prototype.toString.call(value).slice(8, -1);
console.log(getType(arr)); // Array
console.log(getType(func)); // Function
console.log(getType(str)); // String
console.log(getType(num)); // Number
console.log(getType(bool)); // Boolean
console.log(getType(sym)); // Symbol
console.log(getType(bigint)); // Bigint
console.log(getType(null)); // Null
console.log(getType(obj)); // Object
console.log(getType(undefined)); // Undefined
值和类型
JavaScript 中变量是没有类型的,只有值才有。因为变量是可以随时改变为其他类型的值的。
undefined(未定义)和 undeclared(未声明)
在作用域中已经声明但是还未赋值的是 undefined,还未声明的是 undeclared。这两个是不一样的。
// undefined 可以使用这样的判断
let a;
if (a) {
// ...
}
// undeclared 不行,会报错
if (b) {
//Uncaught ReferenceError: b is not defined
// ...
}
但是 JS 很奇怪的一点就是对于这两种情况,使用 typeof 都会返回一样的结果
let a;
typeof a; // undefined
typeof b; // undefined
所以我们可以使用这个特性去判断一些值是否已经定义或者已经声明
if (typeof DEBUG === "undefined") {
// ...
}
值
数组
JS 的数组和其他语言不一样,可以容纳其他所有值,并且不需要预设大小。数组通过数字进行索引,但是因为在 JS 中数组也是对象,所以数组也能使用字符串属性,但是不算进数组长度。
const arr = [0, 1, 2, 3];
arr["are"] = "you ok?";
arr.length; // 4
同时如果字符串能强制类型转换变成 10 进制的话,会被当做数字索引看待。
const arr = [];
arr["13"] = 666;
arr.length; // 14
对于稀疏数组(含有空缺单元的数组), 数组的一些对象方法会跳过,而不是传入 undefined 执行,比如 map。forEach 等。
const arr1 = [0, , 2, 3, 4]
arr1.forEach(item => console.log(item)) // 0 2 3 4
类数组
有时候我们会需要将类数组(一组通过数字索引的值)转为真正的数组,比如DOM查询获取到的DOM列表,arguments对象。我们可以这样做
const arrayFrom = (value) => Array.prototype.slice.call(value)
或者直接用ES6新增的数组方法
const arrayFrom = Array.from
字符串
字符串有时候和数组有点像,因为他们都有length
、indexOf
、concat
方法,也可以通过数字进行索引取值。但是他们是不一样的,比如字符串数字索引只能取值而不能赋值
let arr = ['a', 'r', 'e']
let str = 'are'
arr[1] = 'u'
str[1] = 'u'
console.log(arr[1]) // u
console.log(str[1]) // r
为什么字符串明明是个基础类型,但是却能像对象那样通过.
操作符来使用方法对象呢?这是因为JS会自动帮我们进行装箱操作,我们调用的字符串方法都是String
函数提供的,包括其他基础数据类型比如数字。布尔等也是一样的道理。
数字
在bigint出现之前,JS只有一种数字,它已经包括了“整数”和带小数的十进制数。整数带引号是因为JS没有真正的整数,40.00
和40
是一样的,JS使用IEEE754标准来实现的,也称之为浮点数,使用的是双精度格式(64位二进制)。和string同理,字面量定义的数字也可以使用Number
里的方法。一般定义字面量都是十进制的。
let a = 666.66
a.toFixed(1) // 666.7 四舍五入保留一位小数并转为字符串
我们也可以通过指数的形式定义数字
const a = 1E3 // 1 * 10 ^ 3,也就是1000
也支持其他格式
const a = 0xf3 // 16进制 243
const b = 0o363 // 8进制 243
const c = 0b11110011 // 2进制 243
IEEE754标准带来的问题
所有使用这套标准的语言,都会有个坑
0.1 + 0.2 === 0.3 // false
简单来说,二进制浮点数的0.1和0.2并不是十分准确,0.1+0.2实际上等于0.30000000000000004(0数的我眼花,如有错漏勿怪,反正大概就是这么个意思),所以判断结果为false。
那么我们怎么判断这个呢,比较常见的是设置误差范围,也称之为机器精度,对于JS来说,这个数值是2^-52,在ES6中,这个值也被定义在Number.EPSILON
中,那么我们就可以这样子判断
function numbersCloseEnoughToEqual(val1, val2) {
return Math.abs(val1-val2) < Number.EPSILON
}
数字的一些极值
- 最大和最小呈现的数字:
Number.MAX_VALUE
和Number.MIN_VALUE
分别代表1.798e+308
和5e-324
- 最大和最小安全整数:
Number.MAX_SAFE_INTEGER
和Number.MIN_SAFE_INTEGER
分别代表2^53-1
和-2^53+1
特殊值
null和undefined
null和undefined这两个类型只有一个值,也就是他们本身,,他们之间有一丢丢区别。
null
: 空值undefined
: 没有值
在非严格模式下,undefined是可以被赋值的,这是很奇怪的用法,永远也别使用,只做了解即可。
undefined = 2
void运算符
undefined是一个内置的标识符,并且它的值就是undefined,除非被重新定义(看上面👆),我们可以通过void
运算符得到该值。表达式void xxx
没有返回值,因此返回结果就是undefined
。
const a = 666
console.log(void a) // undefined
特殊的数字
-
NaN
NaN的意思是不是一个数字(not a number),是数值运算失败之后返回的结果,比如
const a = 2 / 'hello' // NaN console.log(typeof a) // number 但是他仍然是一个数字...
NaN有一个很特殊的点,就是自身不相等,是唯一一个非自反的值,NaN != NaN
所以我们可以利用这个特性来判断是否NaN。
if (a != a) { // ... }
或者使用内置函数
isNaN
来判断,但是isNaN有个很蛋疼的点const a = 666 / 'hello' console.log(isNaN(a)) // true console.log(isNaN('hello')) // true...
很明显字符串不是NaN,也不是一个数字,但是依然返回了true。所以我们要以后都应该使用ES6新增的
Number.isNaN
。const a = 666 / 'hello' console.log(Number.isNaN(a)) // true console.log(Number.isNaN('hello')) // false
-
Infinity
const a = 1 / 0 // Infinity const b = -1 / 0 // -Infinity
在JS中
1 / 0
这种操作不会报错,而是返回Infinity
或者-Infinity
(即正的Number.POSITIVE_INFINITY和负的Number.NEGATIVE_INFINITY)。无穷是不可逆的,也就是如果计算结果得到的是一个无穷值,那么这个值再也不会转变为有穷值
-
零值
JS有两个零值
0
和-0
,加减不会得到负零,只有乘除之中才会得到负零。将
-0
字符串化会得到"0"
,但是反过来数字化却能得到正常的返回值。const a = -0 console.log(a.toString()) // '0' console.log(JSON.stringify(a)) // '0' const b = '-0' console.log(JSON.parse(b)) // -0 console.log(Number(b)) // -0
特殊等式
如上面所说,很多时候等式的结果不一定符合我们的直觉,比如NaN不等于自身,0和-0相等。在ES6中,加入了一个新的方法Object.is
来判断两个值是否绝对相等,用来处理以上的这些特殊情况。
const a = 666 / 'hello'
console.log(Object.is(a, NaN)) // true
console.log(Object.is(0, -0)) // false
当然,能用==
或者===
的时候别用Object.is
,因为前两者效率更加高,后者只用来处理特殊情况。
值和引用
JS的引用指向的是值,而不是变量,JS无法让一个变量指向另一个变量,只能让一个变量的值指另一个变量的值,也就是引用了同一个值。
JS对值的引用和赋值在用法上是没区别的, 完全是根据值来确定的。
const a = 111
const b = a // 赋值, 复制了一份a的值赋值给b
const obj = { a: 1 }
const obj1 = obj // 引用,obj1引用了obj的值,中间没有进行复制操作,所以这两个变量指向同一个值。
所以总的来说,对于基础类型(除对象以外的所有类型都是基础类型)总是通过复制的方式进行赋值/传递,而对于复杂类型(对象)总是通过引用的方式进行赋值/传递。所以引用指向的是值本身而不是变量,一个引用无法改变另一个引用的指向。
let arr = [1,2,3]
let cpArr = arr
arr = [4,5,6]
console.log(arr) // [4,5,6]
console.log(cpArr) // [1,2,3]
// 另外一个例子
function foo(arr) {
arr.push(666)
arr = [4,5,6]
}
const a = [1,2,3]
foo(a)
console.log(a) // [1,2,3,666] 而不是 [4,5,6]
对于引用还有另外一个点需要注意的是,既然引用的值是一样的,那么对这个值的修改会影响到所有引用该值的变量。
const a = [1,2,3]
const b = a
a.push(666)
console.log(a, b) // [1,2,3,666], [1,2,3,666]
转载自:https://juejin.cn/post/7010363442347376647