有生之年系列

总结一下子数据类型,基础中的基础。

有哪些数据类型?

js中有两类数据类型,可分为

  • 原始类型Primitive Value
  • 引用类型Reference Value

JS之数据类型 - 图1
这是大类,在此基础上分出很多 面试题 不,知识点来。

数据类型举例

基础内容略。

注意这部分引用子《《JavaScript高级程序设计》第4版

  • 3.3 string部分,提到了转义,有个表格,略过没有整理。觉得不是很必要
  • 3.6 Symbol,我单独在下面整理
  • 3.9 类型转换这里,我觉得有了ts,这块知识是毒瘤,就没有整理。
  1. // Number
  2. console.log(Infinity,Number.MAX_VALUE, NaN)
  3. // 如果对数学运算比较严格,应该去使用 Math第三方库
  4. // BigInt 123n 是单独的类型
  5. typeof 1n === 'bigint'

Symbol

JS悟道作者直接说 Symbol是多余的东西,哈哈哈

Symbol 是原始值,符号是唯一的不可变的,目的是创建唯一记号,这让对象的key可以是非字符串,有字符串和数字的地方就可以有symbol。

这个和私有属性没有关系,也很容易拿到Symbol

  1. // Symbol
  2. let n=Symbol('n')
  3. // 多次返回不同的值,这样就可以重写对象的属性,不用担心会覆盖,比较安全
  4. typeof n // 'symbol' 是简单值
  5. // 每次不等,证明唯一
  6. Symbol() === Symbol() // false
  7. // Symbol.for 可以全局重用,定义同一个符号
  8. var a=Symbol.for('aa');
  9. var b=Symbol.for('aa') // 查到了就复用
  10. a===b // true
  11. // 反解key
  12. Symbol.keyFor(a)//'aa'
  13. // Symbol 能遍历得到吗?
  14. var o={[a]:1}
  15. // object中很容易拿到符号key
  16. Object.getOwnPropertySymbols(o) // [Symbol(aa)]
  17. // 万能的
  18. Reflect.ownKeys(o)`

还有一些其他属性,实在记不住。

对symbol来说,更重要的是 常用内置符号well-known symbol ,可以暴露js内部行为,这就可以让我们重写、覆盖一些行为。 我觉得目前只需要知道symbol能重写js行为 就够了,没啥应用场景。

es6 允许显示指定 toPrimitive Symbol覆盖原有行为,也就是可以改变把对象转成原始值。重写 valueof , toString 。(这一点《重学前端》也有提及。)

  1. class Bar {
  2. constructor() {
  3. this[Symbol.toPrimitive]=function(hint){
  4. switch(hint){
  5. case 'number':
  6. return 3;
  7. case 'string':
  8. return 'stringbar';
  9. case 'default':
  10. default:
  11. return 'defaultbar';
  12. }
  13. }
  14. }
  15. }
  16. let bar = new Bar();
  17. console.log(3 + bar); // "3default bar"
  18. console.log(3- bar); // 0
  19. console.log(String(bar)); // "string bar"

Object

基本api略过,参考《JS对象

Array

基本api略过。参考《JS之数组

高程4讲 [].sort时候并没有提及多种排序算法,等到数据结构与算法时候再说吧。

Math

为什么 0.1+0.2 不等于0.3

有问题的举例如下:

  1. 0.1+0.2
  2. 0.28*100
  3. 1-0.9
  4. 19.9*100
  5. #4.661*1855正常结果是8646.155,四舍五入就是8646.16
  6. #但因为精度缺失,变成了8646.154999999999,导致四舍五入是8646.15

一句话描述:浮点数误差,用IEEE 754 的都有这个问题,包括Python。

底层原理稍微复杂,我引用一篇文章 JavaScript精度丢失问题

要解决这个问题,建议使用第三方 math.js https://mathjs.org/docs/datatypes/numbers.html#roundoff-errors

A solution is to limit the precision just below the actual precision of 16 digits in the displayed output 解决方案是将精度限制在显示输出中16位的实际精度以下

比如

  1. // math.format(math.evaluate(.1+.2),{precision:14})
  2. function calc(string){
  3. return math.format(math.evaluate(string),{precision:14})
  4. }
  5. calc('.1+.2')
  6. calc('.28*100')

发现计算 4.661*1855还是有问题~~ 或者使用原生 `parseFloat((0.1+0.2).toFixed(10));` ~~ 既然如此,那就 math.js 一把梭

点击查看【codepen】

操作符

普通的不提,《也谈js里的位操作符、位运算》单独摘出去了。

Map

如果存储 key/value ,js中使用对象足够。两者大部分功能一致,略有差异。

  1. // 传递可迭代内容
  2. var a = new Map([['a',1]]);
  3. // key 的部分可以是任意内容,比如对象
  4. var a=new Map([[{a:1},1]])

Map特点:

  • Key约束不同。object的key智能是 number/string/symbol,而map可以是任意内容
  • 顺序和迭代不同。因为传入的是可迭代的内容,天生就有顺序、可迭代。

那怎么选?

  • 用哪个都行,影响不大
  • 在乎内存和性能需要注意
  • 内存占用。但同等内存,map比object存得多,说是多50%
  • 插入性能。如果大量操作map性能更好
  • 查找速度。差不多
  • 删除性能。大量删除map性能好

WeakMap

弱映射。weak是指js垃圾回收对待key的方式。基础使用和 Map 没区别。

但 key是弱弱的拿着,不是正式引用,不会影响系统垃圾回收,这也意味着没有迭代、没有统计。

  1. const a=new WeakMap()
  2. a.set({},'val')

这里的代码,key是空数组,没法再拿到它,代码执行完就GC了,a也就变成了空映射。

另一个显著区别,WeakMap 的 key 只能是对象。这样确保是通过引用来取值。

应用场景,vue3 里在对数据做响应式处理使用用到了 WeakMap,设定双向缓存。
应用场景,深拷贝解决循环应用问题。可以见下面的实战部分。

Set 和 WeakSet

和Map、WeakMap 很像。
Set 一个广泛使用的场景是数组去重。

实战

两者类型在存储上不同

  • Primitvie 是在 Stack 内存中,被引用或者拷贝,会创建新的变量,彼此独立
  • Reference 是 Heap 内存中,存储的是引用地址,多个引用指向同一个地址,彼此共享

1 面试1

基础面试题,就是 a=b={} a.b=1,b.a会不会变?
会变,引用是引用值

2 面试题2

  1. let a = {
  2. name: 'Julia',
  3. age: 20
  4. }
  5. function change(o) {
  6. debugger;
  7. o.age = 24; // 修改的依然是引用
  8. o = {
  9. name: 'Kath',
  10. age: 30
  11. }
  12. return o; // return 把o变成了另一个内存地址
  13. }
  14. let b = change(a);
  15. console.log(b.age); // 30 {name: "Kath", age: 30}
  16. console.log(a.age); // 24 {name: "Julia", age: 24}

实战:类型检测

如何判断这个变量的类型?

简单说还是 typeof 判断整体类型, instanceof 判断具体类型。

typeof

判断基础类型

  1. typeof Symbol() // symbol
  2. typeof 1n // bigint
  3. typeof console.log // function
  4. typeof null // object

规则:

  • 返回类型字符串
  • 不能是null,null有问题
  • 如果是可调用对象,有 [[call]] ,比如 箭头函数、构造函数、生成器、普通函数会返回 function
  • 其他都是 object
  • 如果是未声明变量,返回 'undefined'

line5 为啥是object,,因为 null 的二进制表达式 000000,前三位数字是0说明是 object ,且无法被 [[call]] 因此不是 function 。这是一个bug,如果要判断是否为 null,直接三等即可。

规范指路 12.5.6.1

instanceof

new一个对象,得到的新对象就是 原型链 prototype chain 继承的对象,通过 instanceof 能判断是否是被new的构造函数,进而判断出新对象的数据类型

  1. var Car=function(){}
  2. var bmw = new Car()
  3. let a=new String('2')
  4. let b='3'
  5. console.log(bmw instanceof Car) // true
  6. typeof a // object
  7. a instanceof String //true
  8. typeof b // string
  9. b instanceof String // false

这里没啥好说的,注意new出来的是 object,对照一下最上面的思维导图就懂了。

实现一个 instanceof 函数?

思路:

  • 判断是否是基础类型
  • 如果是引用类型,顺着原型查找
  • 使用 Object.getProrotypOf
  1. function instanceof(a,b){
  2. // 如果左边是基础类型,就算了
  3. if(typeof left !== 'object' || left === null) return false;
  4. // 左侧的原型对象
  5. let proto = Object.getPrototypeOf(left)
  6. while(true){
  7. if(proto === null) return false // 说明走到null上了
  8. if(proto === right.prototype) return true;
  9. proto = Object.getPrototyeOf(proto)
  10. }
  11. }

上面两种方式都比较麻烦,有更优的解

Object.prototype.toString

toString 是object的原型方法,返回 [Object Xxx] 的字符串。
对于非object,需要call来调用,为了统一,统一加上 call

  1. Object.prototype.toString.call({}) // "[object Object]"
  2. Object.prototype.toString.call(function(){}) // "[object Function]"

做个总结

  1. function getType(obj){
  2. let type = typeof obj
  3. if(type !== 'object') return type
  4. return Object.prototype.toString.call(obj)
  5. .replace(/^\[object (\S+)\]$/, '$1')
  6. }

数据类型转换

除了显式转换,还有隐式转换这一说法。我觉得有了ts之后,这块知识更多是理论知识,意义不大是知识毒瘤,不值得花费大力气。

但有几个词很有意思:偏字符串、偏数值、无偏好。

显式转换,或者强制类型转换就是用 Number()、parseInt()、parseFloat()、toString()、String()、Boolean()

如果是对象,依次:

  • [Symbol.toPromitive] 方法
  • valueof 方法
  • 如果上一步是NaN,调用 toString 方法

隐式

以前整理的,我就不删了。

基本就 +==

  • 两者类型相同,不需要转换
  • 某一个为 null undefined ,另一边也得是同样的,否则false
  • 如果有 Symbol,返回false
  • 如果都是 string 或者 number,会统一转number ''==0 true
  • 如果有一个是boolean,会转成number 1==true true
  • 如果一边是object,另一边是 string number symbol,会把object的 valueOf/toSting 法法转换
    • 具体用哪个呢?
    • 如果倾向于转换成number,会 valueof 优先
    • 如果倾向于转换成是string,值调用 toString
  1. [] == 0 // true 对象先 valueof 不行,就toString 变成'' 在转number
  2. [] == '' // true

加号 + 特殊

  • 左右两侧都是数字,数学运算
  • 两侧都是字符串,直接拼接
  • 字符串和 undefined null boolean ,会调用 toString 进行字符串拼接 ''+undefined
  • 字符串和 对象、数组、正则等,会调用对象的默认转换方法,再进行拼接 primitive > valueof>tostring
  • 数字和 undifined null boolean number,会都变为数字
  • 字符串和数字,字符串拼接
undfined null boolean number string object
undefined null NaN ‘’和toString ‘’和toString
boolean NaN 转数字相加
number Number Number 数字相加 直接拼接
string ‘’和toString ‘’和toString 直接拼接 ‘’和p>v>t
object ‘’和toString

(这里应该整理比较好?)

  • 对象作为 操作数 的时候,会优先调用 valueOf 方法 {}+10===10
  • 其他情况下会优先使用 toString 10+{} === 1-[object Object]

object的转换规则
刚才也提到了就是 p>v>t

  • [Symbol.toPrimitive] 方法
  • valueOf 方法,如果能转换成基础类型就停止
  • toString 方法,如果能转换成基础类型就停止
  • 如果都没有返回基础类型,会报错
    1. {}+10 // 10
    2. let obj = {
    3. [Symbol.toPrimitive]() {
    4. return 200;
    5. },
    6. valueOf() {
    7. return 300;
    8. },
    9. toString() {
    10. return 'Hello';
    11. }
    12. }
    13. console.log(obj + 200); // 400

    实战:深拷贝

    JS对象

-1 参考链接

本知识点的梳理,受到了他们的启发: