有生之年系列
总结一下子数据类型,基础中的基础。
有哪些数据类型?
js中有两类数据类型,可分为
- 原始类型Primitive Value
- 引用类型Reference Value
这是大类,在此基础上分出很多 面试题 不,知识点来。
数据类型举例
基础内容略。
注意这部分引用子《《JavaScript高级程序设计》第4版》
- 3.3 string部分,提到了转义,有个表格,略过没有整理。觉得不是很必要
- 3.6 Symbol,我单独在下面整理
- 3.9 类型转换这里,我觉得有了ts,这块知识是毒瘤,就没有整理。
// Number
console.log(Infinity,Number.MAX_VALUE, NaN)
// 如果对数学运算比较严格,应该去使用 Math第三方库
// BigInt 123n 是单独的类型
typeof 1n === 'bigint'
Symbol
JS悟道作者直接说 Symbol是多余的东西,哈哈哈
Symbol 是原始值,符号是唯一的不可变的,目的是创建唯一记号,这让对象的key可以是非字符串,有字符串和数字的地方就可以有symbol。
这个和私有属性没有关系,也很容易拿到Symbol
// Symbol
let n=Symbol('n')
// 多次返回不同的值,这样就可以重写对象的属性,不用担心会覆盖,比较安全
typeof n // 'symbol' 是简单值
// 每次不等,证明唯一
Symbol() === Symbol() // false
// Symbol.for 可以全局重用,定义同一个符号
var a=Symbol.for('aa');
var b=Symbol.for('aa') // 查到了就复用
a===b // true
// 反解key
Symbol.keyFor(a)//'aa'
// Symbol 能遍历得到吗?
var o={[a]:1}
// object中很容易拿到符号key
Object.getOwnPropertySymbols(o) // [Symbol(aa)]
// 万能的
Reflect.ownKeys(o)`
还有一些其他属性,实在记不住。
对symbol来说,更重要的是 常用内置符号well-known symbol
,可以暴露js内部行为,这就可以让我们重写、覆盖一些行为。 我觉得目前只需要知道symbol能重写js行为 就够了,没啥应用场景。
es6 允许显示指定 toPrimitive Symbol覆盖原有行为,也就是可以改变把对象转成原始值。重写 valueof
, toString
。(这一点《重学前端》也有提及。)
class Bar {
constructor() {
this[Symbol.toPrimitive]=function(hint){
switch(hint){
case 'number':
return 3;
case 'string':
return 'stringbar';
case 'default':
default:
return 'defaultbar';
}
}
}
}
let bar = new Bar();
console.log(3 + bar); // "3default bar"
console.log(3- bar); // 0
console.log(String(bar)); // "string bar"
Object
基本api略过,参考《JS对象》
Array
基本api略过。参考《JS之数组》
高程4讲 [].sort
时候并没有提及多种排序算法,等到数据结构与算法时候再说吧。
Math
为什么 0.1+0.2 不等于0.3
有问题的举例如下:
0.1+0.2
0.28*100
1-0.9
19.9*100
#4.661*1855正常结果是8646.155,四舍五入就是8646.16
#但因为精度缺失,变成了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位的实际精度以下
比如
// math.format(math.evaluate(.1+.2),{precision:14})
function calc(string){
return math.format(math.evaluate(string),{precision:14})
}
calc('.1+.2')
calc('.28*100')
发现计算 4.661*1855
还是有问题~~ 或者使用原生 `parseFloat((0.1+0.2).toFixed(10));` ~~ 既然如此,那就 math.js 一把梭
操作符
普通的不提,《也谈js里的位操作符、位运算》单独摘出去了。
Map
如果存储 key/value ,js中使用对象足够。两者大部分功能一致,略有差异。
// 传递可迭代内容
var a = new Map([['a',1]]);
// key 的部分可以是任意内容,比如对象
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是弱弱的拿着,不是正式引用,不会影响系统垃圾回收,这也意味着没有迭代、没有统计。
const a=new WeakMap()
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
let a = {
name: 'Julia',
age: 20
}
function change(o) {
debugger;
o.age = 24; // 修改的依然是引用
o = {
name: 'Kath',
age: 30
}
return o; // return 把o变成了另一个内存地址
}
let b = change(a);
console.log(b.age); // 30 {name: "Kath", age: 30}
console.log(a.age); // 24 {name: "Julia", age: 24}
实战:类型检测
如何判断这个变量的类型?
简单说还是 typeof
判断整体类型, instanceof
判断具体类型。
typeof
判断基础类型
typeof Symbol() // symbol
typeof 1n // bigint
typeof console.log // function
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的构造函数,进而判断出新对象的数据类型
var Car=function(){}
var bmw = new Car()
let a=new String('2')
let b='3'
console.log(bmw instanceof Car) // true
typeof a // object
a instanceof String //true
typeof b // string
b instanceof String // false
这里没啥好说的,注意new出来的是 object,对照一下最上面的思维导图就懂了。
实现一个 instanceof 函数?
思路:
- 判断是否是基础类型
- 如果是引用类型,顺着原型查找
- 使用 Object.getProrotypOf
function instanceof(a,b){
// 如果左边是基础类型,就算了
if(typeof left !== 'object' || left === null) return false;
// 左侧的原型对象
let proto = Object.getPrototypeOf(left)
while(true){
if(proto === null) return false // 说明走到null上了
if(proto === right.prototype) return true;
proto = Object.getPrototyeOf(proto)
}
}
Object.prototype.toString
toString 是object的原型方法,返回 [Object Xxx]
的字符串。
对于非object,需要call来调用,为了统一,统一加上 call
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call(function(){}) // "[object Function]"
做个总结
function getType(obj){
let type = typeof obj
if(type !== 'object') return type
return Object.prototype.toString.call(obj)
.replace(/^\[object (\S+)\]$/, '$1')
}
数据类型转换
除了显式转换,还有隐式转换这一说法。我觉得有了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
[] == 0 // true 对象先 valueof 不行,就toString 变成'' 在转number
[] == '' // 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 方法,如果能转换成基础类型就停止
- 如果都没有返回基础类型,会报错
{}+10 // 10
let obj = {
[Symbol.toPrimitive]() {
return 200;
},
valueOf() {
return 300;
},
toString() {
return 'Hello';
}
}
console.log(obj + 200); // 400
实战:深拷贝
见 JS对象
-1 参考链接
本知识点的梳理,受到了他们的启发:
- 拉钩教育 《JavaScript 核心原理精讲》章节1 章节2
- 《JavaScript高级程序设计》第4版 章节3.4
- 小黄书《你不知道的JavaScript》3.3
- 书籍《JavaScript悟道》
- 《JavaScript权威指南》第7版
- 公众号 如何写出一个惊艳面试官的深拷贝