基本类型都是不可变类型(immutable),它们的值不可修改。
undefined
undefined 是全局对象的一个属性,undefined的初始值就是undefined。不可写,不可枚举,不可配置。 声明了一个变量但是没有给它赋值,那么它的值就是undefined。
let a
typeof a -> "undefined"
null
null 表示一个空对象。
typeof null -> "object" // bug,历史问题 参见[typeof null](https://2ality.com/2013/10/typeof-null.html)
let a = null
typeof a -> "object"
string
需要注意的是 javascript 采用 UTF-16 编码,所以 javascript 字符默认都是 Unicode 编码。 ES7以前是没有明确的最大长度的,ES7规定的最大长度为 2^53 - 1个 UTF-16 码元。 实际最大长度取决于不同浏览器的实现,基本上都远小于这个值。
详情参见:
The String Type Maximum String length RAM limit
'hello, world' // literal, primitive value
`hello world` // ES6 literal template, primitive value
String('hello, world') // non-constructor mode, returns primitive value
new String('hello, world') // constructor mode, returns object
'hello, '.concat('world') // auto wrap the primitive value and call object method
转义(Escape)
'\n' -> ↵ // 控制符转义
'\141' -> a // 八进制转义 latin-1(EASCII,非ASCII)
'\xFF' -> ÿ // 十六进制转义 latin-1
'\"do or do not\"' -> "do or do not" // 引号转义
'\u0061' -> a // Unicode 码点转义
'\uD83D\uDE00' -> 😀 // Unicode 补充平面字符代理对码元转义
'\u{1F601}' -> 😀 // Unicode 补充平面字符码点转义
Unicode
'你好'.charCodeAt(0) -> 20320 // 十进制 20320 转换为十六进制为 4F60
'你好' === '\u4F60\u597d' -> true
另外需要注意的一点是,Unicode 存在预组合字符,某些字符可能存在两种表示方式。如字符 ǎ
'\u0061\u030c' -> ǎ
'\u01ce' -> ǎ
这两种编码输出的字符虽然是一样但实际编码是不相等的,调用 normalize 方法对两个字符组合起来表示一个字符的转义序列做正规化处理后就是相等的了。
'\u0061\u030c' === '\u01ce' -> false
'\u0061\u030c'.length -> 2
'\u01ce'.length -> 1
'\u0061\u030c'.normalize() === '\u01ce' -> true
String 对象的 length 属性以及 charAt、codePointAt 方法等都是以 UTF-16 码元为处理单位(不是码点,也不是字节或字符)。
'a'.length -> 1 // a 按 UTF-16 编码规则表示为 '\u0061'
'😀'.length -> 2 // 😀 是单个字符,属于 Unicode 补充平面,实际用代理对'\uD83D\uDE00'表示,一个代理对包含了两个码元
所以遍历一个字符串内的字符的正确姿势是用 [Symbol.iterator] 方法而不是基于 length 属性做 for 循环。
const emojiMixedStr = '\ud83d\ude00hi ';
let iterator = emojiMixedStr[Symbol.iterator]();
let char = iterator.next();
while(!char.done && char.value !== ' ') {
console.log(char.value);
char = iterator.next();
}
// output
// 😀
// h
// i
for(let char in emojiMixedStr) { console.log(char) }
需要注意的 String 对象方法
trim 方法会移除字符串首尾的所有空白字符和换行符(latin-1、Unicode的都包含在内)。 replace 方法第一个参数传字符串时,只会替换字符串内的一个对应的字符串,不会替换所有
'\s\t\fhello, world\v\r\n'.trim()
'hello, world\u2028\u2029'.trim() // U+2028 line separator U+2029 paragraph separator
空白字符和换行符定义及细节请参见维基百科词条:
// TODO revise slice和subString方法作用都是截取字符串的一部分,都不包含结束索引位置的字符。 subString起始索引为负数或者大于字符串长度时自动当做0处理 slice起始索引为负数时会用字符串长度加上索引值得到新的索引
number
number 采用 IEEE-754实现,关于IEEE-754请参见我另一篇笔记(整数与浮点数)。在 bigint 出现之前,number 是 javascript 唯一的数字类型。
let a = Number('1') // non-constructor mode, typeof a === 'number'
let b = new Number('2') // constructor mode, typeof b === 'object'
typeof NaN -> 'number'
number 可以表示的安全正整数范围为 -(2 ^ 53 -1) 到 2 ^ 53 -1,所以当后端返回的整数超过这个范围时,需要用string类型。
Number.MAX_SAFE_INTEGER -> 9007199254740991
Number.MAX_VALUE -> 1.7976931348623157e+308
关于安全整数问题 因为 javascript 没有单独的整型,需要借助IEEE-754标准来表示整型数值。通过指数偏移,52位小数位可以表示2 ^ 53 -1个唯一整数。当指数超过52时,通过改变小数数位已经不能表示某些整数了。如指数提高到53时,能表示的整数如下:
$0\ 10000110100\ 0000000000000000000000000000000000000000000000000000_2$ $2^{53} \times (1 + 0) = 2^{53} + 0 = 9007199254740992_{10}$
$0\ 10000110100\ 0000000000000000000000000000000000000000000000000001_2$ $2^{53} \times (1 + 2^{-52}) = 2^{53} + 2 = 9007199254740994_{10}$
$0\ 10000110100\ 0000000000000000000000000000000000000000000000000010_2$ $2^{53} \times (1 + 2^{-51}) = 2^{53} + 4 = 9007199254740996_{10}$
$0\ 10000110100\ 0000000000000000000000000000000000000000000000000011_2$ $2^{53} \times (1 + 2^{-51} + 2^{-52}) = 2^{53} + 4 + 2 = 9007199254740998_{10}$
$0\ 10000110100\ 0000000000000000000000000000000000000000000000000100_2$ $2^{53} \times (1 + 2^{-50}) = 2^{53} + 8 = 9007199254741000_{10}$ … $0\ 10000110100\ 1111111111111111111111111111111111111111111111111111_2$ $2^{53} \times (1+(1 - 2^{-52})) = 2^{54} - 2 = 18014398509481982$
可以看出指数为53时数字以2为间隔增长,2^{53} - 2^{54}之间很多整数已经没有对应的表示了。随着指数的进一步扩大,数字之间的间隔也将进一步扩大。
所以出现了多个不同的十进制数的二进制表示相同的情况
9007199254740993 === 9007199254740992 // true
9007199254740999 === 9007199254741000
9007199254741001 === 9007199254741000 // true
当尝试以小于步长的值做累加时
let ns = Number.MAX_SAFE_INTEGER
ns += 1 // 9007199254740992
ns += 1 // 9007199254740992
...
ns += 1 // 9007199254740992
关于精度问题 因为采用IEEE-754标准的二进制格式来表示十进制数,所以同其他采用IEEE-754标准的二进制格式来表示十进制数的语言一样存在无法精确精确表示某些数字的问题
0.2 + 0.1 -> 0.30000000000000004
0.3 - 0.2 -> 0.09999999999999998
关于浮点数精度问题参见阿里 JavaScript 浮点数陷阱及解法
FLOATING POINT VISUALLY EXPLAINED
How numbers are encoded in JavaScript
Here is what you need to know about JavaScript’s Number type
// TODO 如何处理呢?
需要注意的 Number 对象方法
toFixed 四舍五入后小数点后保留指定位数
(12.345).toFixed(2) -> 12.35
(12.345).toFixed(5) -> 12.34500
(1.005).toFixed(2) -> 1.00 // 1.005 无法用浮点数精确表示 实际表示为1.00499999999999989341858963598E0
toPrecision 将数字转换为包含指定个有效数字的字符串形式的数
影响测量精度的数字称作有效数字,详细解释参见:
(12.345).toPrecision(2) -> '12'
(12.345).toPrecision(1) -> '1e+1'
(0.00012).toPrecision(1) -> '0.0001'
(0.00012).toPrecision(3) -> '0.000120'
bigint
前面提到了 number 无法表示大于 2 ^ 53 的整数,为了解决这个问题增加了 bigint 类型
BigInts cannot be serialized to JSON
9007199254740999n -> 9007199254740999n // number suffixed with n
BigInt("9007199254740999") -> 9007199254740999n
- Math 对象内置方法对BigInt无效
- BigInt 无法和 number 类型进行运算
- BigInt 只能和 BigInt 进行运算
- BigInt 强制转换为 number 类型会丢失精度
- parseInt parseFloat 会将 BigInt 类型的数字转换为 number 类型
BigInt: Arbitrary precision integers in JavaScript v8: bigint
boolean
0, -0, null, false, NaN, undefined 转换为 boolean 类型均为 false。 其他值如空数组,空对象,字符串的"false"等转换为 boolean 类型时值均为 true。 这个规则同样适用于条件判断语句。
Boolean('') -> false
Boolean(0) -> false
Boolean(-0) -> false
Boolean(NaN) -> false
Boolean(null) -> false
Boolean(undefined) -> false
Boolean(false) -> false
Boolean([]) -> true
Boolean({}) -> true
Boolean('false') -> true
Boolean(new Boolean(false)) -> true
symbol
javascript
symbol的值是全局唯一的,所以用于当做标识符(identifier)。 设计symbol是用于对象的键。symbol没有构造函数,所以无法通过new关键字创建symbol。
const hello = Symbol('hello')
Symbol('hello') === Symbol('hello') -> false // same description, different value
const hi = new Symbol('hello') // Uncaught TypeError: Symbol is not a constructor
{ [hello]: 'world', [hi]: 'world' }
alert(Symbol('hello')); // Uncaught TypeError: Cannot convert a Symbol value to a string
Symbol('hello').description -> 'hello'
Symbol.for("hello") // global symbol registry.return on found, create on absent
Symbol.keyFor(hello) -> 'hello'
使用symbol类型做对象的键时,symbol类型的键对 for..in 循环以及Object.keys、Object.getOwnPropertyNames方法不可见。 使用 Object.getOwnPropertySymbols 方法才会返回对象的symbol类型键,另外使用 Reflect.ownKeys 方法返回的键也会包含 symbol 类型的键
系统内置常用的symbol
- Symbol.toPrimitive // 别名 @@toPrimitive
当一个对象具有Symbol.toPrimitive属性方法时,做自动类型转换时会读取[Symbol.toPrimitive]属性方法的返回值
let foo = {
[Symbol.toPrimitive](hint) {
if (hint == 'number') {
return 0;
}
if (hint == 'string') {
return 'hello';
}
return true;
}
};
+foo -> 0 // hint 为 number
`${foo}` + ', world' -> 'hello, world' // hint 为 string
foo + '' -> 'true' // hint 为 default
js引擎在做自动类型转换时会判断隐含的目标类型,这个目标类型会当做hint参数传给 [Symbol.toPrimitive] 方法。 hint参数有三种可能的值,default、number、string。在一个对象没有通过 [Symbol.toPrimitive] 方法进行覆盖的情况下,default 等同于 number。 Symbol对象就添加了[Symbol.toPrimitive]方法,hint 参数被忽略了,该方法返回的永远都是 symbol 类型,所以symbol类型不能做自动类型转换。
`${Symbol('hello')}` + '' // Uncaught TypeError: Cannot convert a Symbol value to a string
Symbol('hello').toString() + ', world' -> 'hello, world'
- Symbol.iterator // 别名 @@iterator 对象的默认迭代器
const iterable1 = new Object();
iterable1[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
- Symbol.hasInstance // 别名 @@hasInstance
类型转换(Type conversion)
- 显示类型转换(Type castting)
- 隐式类型转换(Type coercion)
Number('1') // type castting
'1' + 2 // type coercion
我们应该尽可能地避免隐式类型转换。js 会将某个原始值或对象转换成三种类型的值 string、number、boolean。类型转换规则参见ECMA文档: Type Conversion
下面根据类型转换规则举例分析进行转换流程
+{} -> NaN
转换流程如下:
- 触发 toNumber 规则,Number({})
- {} 是一个对象 触发 toPrimitive 规则,hint 为 number
- 根据 toPrimitive 规则,查找 {} 是否定义了 @@toPrimitive 方法
- {} 没有定义 @@toPrimitive 方法,hint 为 number 时,继续触发 OrdinaryToPrimitive 规则
- 根据 OrdinaryToPrimitive 规则,hint 为 number 时,依次调用 valueOf toString 方法
- valueOf 返回 {} 仍然是一个对象,toString 返回 “[object Object]",所以 toPrimitive 的结果是”[object Object]”
- Number("[object Object]"),参数无法转化为有效数字,所以返回 NaN
对象可以通过 @@toPrimitive 方法来覆盖默认的转换规则。
let sayHi = {
[Symbol.toPrimitive](hint) {
if (hint == 'number') {
return 0;
}
if (hint == 'string') {
return 'hello';
}
return true;
}
};
+sayHi -> 0 // hint is number
`${sayHi}` + ', world' -> 'hello, world' // hint is string
sayHi + '' -> 'true' // no hint present
更多转换示例
// 根据转换规则表,对象类型的转换,不管是 toString 规则还是 toNumber 规则都会进一步触发 toPrimitive 规则
true + false -> 1 + 0 -> 1 // 布尔型的加法运算,遵循 toNumber 规则
"number" + 2 + 1 -> 'number' + '5' + '3' -> 'number21' // 字符串拼接,遵循 toString 规则
1 + 2 + "number" -> '1' + '2' + 'number' -> '12number' // 字符串拼接,遵循 toString 规则
'1' + 2 + 3 -> '1' + '2' + '3' -> '123' // 字符串拼接,遵循 toString 规则
"foo" + + "bar" -> "foo" + (+"bar") -> "foo" + "NaN" -> "footNaN" // 字符串拼接, +"bar" 遵循 toNumber 规则
[] + null + 1 -> '' + null + 1 -> 'null1' // [] 遵循 toPrimitive 原则
// +[] 遵循 toPrimitive 规则,[] 遵循 toPrimitive 规则,![] 遵循 toBoolean 规则
!+[]+[]+![] -> !(+[])+[]+(![]) -> !(+"") + '' + false -> true + '' + false -> 'truefalse'
6 / "6" -> 6 / 6 -> 1 // 除法,遵循 toNumber 规则
{}+[]+{}+[1] -> (+[])+{}+[1] -> 0 + '[object Object]' + '1' -> '0[object Object]1' // 第一个 {} 被解析成了代码块 ╮(╯▽╰)╭
({}+[])+{}+[1] -> ('[object Object]' + '') + '[object Object]' + '1' -> [object Object][object Object]1
// Date 对象覆盖了默认的 @@toPrimitive 方法,hint 为 default 和 string 时返回string类型,为number时返回number类型
new Date(0) - 0 -> 0 - 0 -> 0 // 减法,遵循 toNumber 规则
new Date(0) + 0 -> "Thu Jan 01 1970 08:00:00 GMT+0800 (China Standard Time)0" // 拼接, 遵循 toString 规则
// Symbol 类型无法进行隐式转换,Symbol 的 @@toPrimitve 方法永远返回 symbol 类型的值
Symbol('hello') + '' // 抛错
Symbol('hello').toString() + '' -> 'hello' // 显示转换,运算正常
比较运算时类型隐式转换规则 参见Abstract Relational Comparison
[1] > null -> 1 > 0 -> true // 遵循 toPrimitive 规则,hint 为 number
[] > {} -> 0 > NaN -> false // 遵循 toPrimitive 规则,hint 为 number
{} > [] -> "Uncaught SyntaxError: Unexpected token '>'" // 开始的 {} 被解析成了代码块
等式比较时类型隐式转换规则 参见Abstract Equality Comparison
[1,2,3] == [1,2,3] -> false // 类型相同,进行严格等式对比
'true' == true -> 'true' == !Number(true) -> false
false == 'false' -> 0 == NaN -> false
null == '' -> false // 触发最后一条规则直接返回 false
!!"false" == !!"true" -> true == true -> true
['x'] == 'x' -> 'x' == 'x' -> true
[] == 0 -> '' == 0 -> 0 == 0 -> true
[] == '' -> '' == '' -> true