原始类型的方法
对象包装器
为什么能对一个基本类型调用其方法?因为JS引擎内部先帮我们创建了对象,调用完方法后,会自动销毁对象,只留下原本的基本类型。
给字符串添加一个属性?
得到的是一个 undefined
。
原因:访问属性时,创建了一个对象包装器,但使用完后会销毁。因此后面在访问,没有属性值。
let s = 's'
s.name = 'jack'
console.log(s.name) // undefined
数字类型
写法和其他进制
- 科学计数法
1e3
即 1000,e代表1后多少个0。1e-3
,即 1 / 1000。 - 二进制,
0b
开头。 16进制,0x
开头。八进制,0o
开头。
toString(base = 10)
数字转字符串,默认10进制。
数字.调用
整数时, 100
JS认为该语法省略了小数部分,因此对JS来说是 100.
,因此调用需要 100..toString()
四舍五入
- Math.floor 向下取整
- Math.ceil 向上取整
- Math.round 四舍五入
精确度问题
IEEE754,用64位(8字节)存储一个数字,52位用于存储数字,11位存储小数点的位置,1位用作符号(正负)。所以最多存储64位,超出的话,就无限循环了,二进制里面是0舍1入。
解释
- 符号位S,1.0 正数,-1.0 负数。 0代表正数,1代表负数。
- 指数位E,即指数的位数,转为二进制后,看科学计数法以2为底的次方。比如1000,转为二进制是
1000..toString(2)
是1111101000
,科学计数法为1.111101 * 2^9,所以指数为9。- 指数偏移量,因为只有11位用于存储小数点,2进制下11位满打满算是
11111111111
,转换为10进制就是2047
(可以通过parseInt(11111111111, '2')
得到 )。科学计数法下指数可以是正数,也可以是负数,中间值1023**,**所以[0,1022]表示负数,[1024,2047]表示正数。上面1000的二进制下的偏移量是 1023 + 9 = 1032,转为二进制位为10000001000
- 指数偏移量,因为只有11位用于存储小数点,2进制下11位满打满算是
- 尾数位M, 上面1000二进制,再科学计数法后的小数部分是
111101
,然后补齐到52位(用0补),即1111 0100 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
所以1000在计算机内存里面存储的是 符号位+指数偏移量+小数部分
0 + 10000001000 + 1111010000000000000000000000000000000000000000000000 =>
0100000010001111010000000000000000000000000000000000000000000000 // 正好64位
这个网站可以验证十进制数在内存中的存储是什么样的http://www.binaryconvert.com/convert_double.html
0.1 + 0.2 != 0.3
运算时,0.1和0.2先转换为二进制,本身2个数就会无限循环,相加后后依旧无限循环,因此会溢出。
解决方法
- 使用专业的数字处理库,比如
number.js
- 通过10或100来缩小误差
- 通过
toPrecision
来取需要的精度。 - 通过
toFixed
来四舍五入,但有时并不能满足你的需要,因为二进制精度问题,导致其四舍五入也会出问题
toFixed的误差
如下图
理想是4.6,但是这里是4.5。可以通过精度取舍来查看4.55到底是什么样子的。
可以看到。因为精度问题,内部实际上是4.549999,所以四舍五入后是4.5
isNaN
注意
- NaN不等于自身。但如果使用
Object.is
则相等,因为判断的是内存。 - 0 === -0,计算时是相等的。但是如果使用
Object.is
不相等,因为在内存中正负的第一位是不同的。正是0,1是负。
isFinite
判断是否是无穷
一些数学方法
parseInt(str, radix)
可以将字符串转换为整数,默认是10进制。parseFloat(str)
可以将字符串转为小数。Math.random
返回[0, 1)
,注意右边是开区间。Math.max
&Math.min
Math.pow(n, power)
以n为底的power次幂。
测试题
给定max和min,取一个在其范围内的小数(不包括max)。
分析:假设取100 ~ 200(不包括200)之间的小数。
Math.random
会产生[0, 1)的之间的一个数。所以(200-100) * [0,1)
会得到[0, 99.99...]
- 然后加上
min = 100
,可以得到[100, 199.99...]
的数,满足条件了。
结果:
function random(min, max) {
return min + (max - min) * Math.random()
}
给定max和min,取在其范围内的整数(包括min, max)
分析:假设取100 ~ 200,也包括100和200。
Math.random
产生[0, 1)之间的随机数。Math.floor
可以向下取整。- 我们可以算
[100, 201)
的取值范围,取到一个数,然后向下取整。 - 构造这个范围
100 + Math.random() * (200 - 100)
function randomInt(min, max) {
// 这里 用max+1-min就可以得201 - 100 = 101
// 101 再乘以 [0, 1)可以得到[0, 101)
// min + [0, 101) => [100, 201)中的一个随机数,然后向下取整200.999... => 200。
// 这样每个整数的概率都是均等的
return Math.floor(min + Math.random() * (max + 1 - min))
}
字符串类型
引号
双引号,单引号,反引号都可以包裹。其中反引号支持模板变量,如下:
const name = 'Jack'
log(`${name} is boy`)
反引号的优点
可跨行书写
这样会在某些 eslint
校验一行代码长度时通过。
`this
is
name
`
特殊字符
常用的如换行符 \n
,特殊符号需要转义输出它,如引号 \'
unicode字符
以 \uXXXX
开头,必须跟着4个十六进制数字。在一些汉字转码会用到,比如我的名字 小民
,不想让人一眼看出来。
const name = '小民'
const unicode = escape(name) // "%u5C0F%u6C11"
// 这里把%替换为\,然后在控制台输入就可以看到对应的汉字
// 同理转回汉字
const backtoStr = unescape("%u5C0F%u6C11") // 小民
以 \u{X...XXXXXX}
1个或6个十六进制组成的符号,比如
"\u{1F60D}"; // "😍"
字符串长度
具有length属性
访问字符
可以通过下标访问,甚至可以通过 for of
来迭代。
不可变性
不能改动字符串中的某个字符,可以自己截取拼接。
一些常用方法
- str.indexOf,这个匹配不到的话返回值是
-1
,可以使用~
运算符方便判断~1
=>0
- str.includes
- str.lastIndexOf 用的不多
- startsWith
- endsWith
- slice(start, [, end]) 截取字符串,不包括
end
字符串比较
实际比较的是编码。通过 codePointAt
获取字符对应的码。
数组
是对象,但是比较特殊,是有序集合,JS引擎对它做了优化,内存中存储的位置是连续的。
声明方法
new Array()
[]
字面量声明
常见方法
pop和push,shift和unshift
pop从末位删除一个,push从末位添加一个。
shift从头部删除一个,unshift从头部添加一个。
数组有2种常用的数据结构
- 队列:先进先出。
FIFO
- 栈:后进先出。
LIFO
F:first I:in O:out
数组内部
数组也是对象,因此可以通过 key
访问成员,它的 key
是数字, 0, 1, 2
。
因此可以给数组添加属性。
注意
- 不要跳跃式给数组分配一个很大的索引,会导致数组中间出现空洞。
- 不要给数组添加属性,JS引擎优化会失效。
性能
pop/push
相比 shift/unshift
,性能更好,因为在末端删除或添加,不会影响前面的元素重新赋予新的下标。如果是在头部添加或删除,会导致数组每个元素下标全部增加或减少。
其他方法
splice
splice(startIndex[, deleteCount, addElement1, addElement2..., addElementN)
很厉害的方法,可以实现数组的删除,添加,插入。
slice
slice([start], [end])
通用用于复制数组,复制的不包含end。
concat
arr.concat(arr1, arr2...)
拼接数组arr1, arr2等等,得到一个新的数组。
遍历相关方法
forEach
forEach(function(item, index, array){...})
搜索相关
indexOf/lastIndexOf & includes
前2者查找到返回对应位置index,找不到则是-1。
后者返回布尔值。
find/findIndex
前者返回找到的元素本身,后者返回元素的下标(找不到返回-1)。
filter
筛选,为每个元素执行一个函数,过滤出返回值为 true
的元素,形成一个新的数组。
转换数组
map
sort
对原数组进行排序(操作了原数组)。
默认按照字符串进行排序。所以
var arr = [1,2,15]
arr.sort(); // arr => [1,15,2]
所以sort接收一个比较函数
arr.sort((firstItem, secondItem) => {return 正数 | 负数 | 0}); // 正数代表大于 负数代表小于 0不变
reverse
split & join
str.split
将字符串按照一定规则拆成数组, arr.join
则按照一定规则将数组合并为字符串。
reduce/reduceRight
神方法,可以模拟数组其他方法,实现一些奇思妙想
https://segmentfault.com/a/1190000021737914 reduce25种妙用
实现减少循环次数
// 可以同时模拟map和filter
const arr = [1,2,3,4,5]
const arr2 = arr.map(item => item * 2) // [2,4,6,8,10]
const res = arr2.filter(item > 2) // [4,6,8,10]
// 可以看到上面map和filter,各1层循环了。
// 使用reduce 减少一层
arr.reduce((accu, item) =>{
const double = item * 2
if(double > 2) {
return [...accu, double]
} else {
return [...accu]
}
}, []) // [4,6,8,10] 只有1层reduce的循环
实现斐波那契
function fibonacci(n = 2) {
const arr = [...new Array(n).keys()] // => [0,1,2] 下标拿出来当元素
return arr.reduce((accu, item, index) => {
// index > 1的时候才能开始计算前2个数相加的和。不然 1 - 2 = -1了,数组成员不存在
// index <= 1时,直接返回斐波那契最基础的2个数[0,1]
return index > 1 ? [...accu, (accu[index - 1] + accu[index - 2])] : accu
}, [0,1])
}
redux中的compose函数原理
redux中的compose做的事是:
compose(fn1, fn2, fn3, fn4) 其实就是 fn1(fn2(fn3(fn4())))。前面的写法明显更清晰。
let n = 10
function fn1(x) {
return x + 1
}
function fn2(x) {
return x + 2
}
function fn3(x) {
return x + 3
}
function fn4(x) {
return x + 4
}
fn1(fn2(fn3(fn4(n)))) // 20
不直观。所以改下。
const compose = (...funs) => funs.reduce((accu, cur) => (...args) => accu(cur(...args)))
// 核心思想
// compose接受一系列函数,返回一个新的函数fn,fn接收一个或多个参数(此时fn接收的参数应该就是funs最右边的函数所接受的参数,所以除了最右边的函数可以接收多个参数意外,其他函数都只接受1个参数)
some/every
返回一个布尔值,前者只要数组中有一个满足条件即可,后者需要每一个都满足条件。
fill
填充(更像是替换)一个数组 fill(value, start, end)
不包括end。
copyWithin
array.copyWithin(targetIndex, startIndex, endIndex)
。把数组的一部分 复制 一下,到数组本身指定的位置(targetIndex)。
flat
array.flat(depth)
数组扁平化,参数默认是0只抹平一层,如果是很深的多维数组,传递depth即可。
var a = [1,2,[[[3,4,[5,[6]]]]]]
a.flat(5) // [1,2,3,4,5,6]
flatMap
先map,然后走flat(1)
var a = [1,2,3]
a.map(item => [item * 2]) // [[2], [4], [6]]
a.flatMap(item => [item * 2]) // [2,4,6]
一些数组训练
字符串转驼峰
var str = 'hello-world-jack'
function camelize(str) {
return str.split('-').map((item,index) => index === 0 ? item : `${item[0].toUpperCase() + item.slice(1)}`).join('')
}
洗牌算法
给定一个数组,随机排序,保证每个乱序的机会是均等的。
核心思想:逆向遍历数组,然后从数组中随机抽取一个元素,将该随机元素和当前遍历的index做交换位置。
为什么要逆向,而不是正向遍历? 逆向,我们可以在取随机数的时候,从数组剩余的0到(n-1)中取一个随机数,每次都能从剩余(已交换好位置的都已经放在数组尾部了)。正向的话,从 n 到 array.length - 1中取,有点麻烦。比如数组长度为10,已经遍历到第五个了。怎么从剩余的(5 到 10位置)中取一个随机的数字呢。逆向的话,就不用担心这个问题。
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
let randomIndex = Math.floor(Math.random() * (i + 1));
[array[i], array[randomIndex]] = [array[randomIndex], array[i]]; // 技巧,数组结构,调换2个元素位置。
}
console.log(array);
}
可迭代对象
Symbol.iterator
自己定义(或内置)对象的一个方法,会返回一个迭代器(具有next方法的对象)。
next方法返回的结果格式必须是 {done: Boolean, value: any}
, 当done = true时,代表迭代结束。
// 自定义一个例子
var obj = { from: 1, to: 5 }; // 希望for of打印1,2,3,4,5
obj[Symbol.iterator] = function () {
return {
current: this.from,
to: this.to,
next() {
if (this.current <= this.to) {
return {
done: false,
value: this.current++,
};
} else {
return {
done: true,
};
}
},
};
};
for (let v of obj) {
console.log(v);
}
字符串可迭代
显示调用迭代器
var s = "hello";
var sIterrator = s[Symbol.iterator]();
while (true) {
let res = sIterrator.next();
if (res.done) {
break;
} else {
console.log(res.value);
}
}
可迭代和类数组
这是2个概念。
具有 length
和索引的对象,称为类数组。
具有 Symbol.iterator
迭代器的对象,称为可迭代对象。(没有则一定不可以for of)。
var a = {
'0': 'hello',
'1': 'world',
length: 2
}
for(let v of a) {}; //Uncaught TypeError: a is not iterable
Array.from
这是一个全局方法,接收一个可迭代对象或类数组,从中获取一个真正的数组,然后可以调用一些数组方法了。
// 上面的代码片段,我们把a转换为一个真正的数组
var a = {
'0': 'hello',
'1': 'world',
length: 2
}
var arr = Array.from(a)
for(let v of arr) {console.log(v)} // hello world
Map和Set映射
Map
不同于对象的是,允许任何类型的键,比如对象不能以对象作为键,对象会把对象的键转为 [object, Object]
,然后多个对象键,都一样了,后者覆盖前者。
方法,属性
- new Map() 创建
- map.set(k, v) 根据建存储值
- map.get(k)
- map.has(k)
- map.delete(k)
- map.clear() // 清空map
-
map支持链式调用
map.set(1, 1).set(2, 2).get(2)
Map的迭代
可以使用for of(说明map是一个可迭代对象)。
map.keys() 获取所有键
- map.values() 获取所有值
- map.entries() 获取键值对
- map.forEach 方法,类似数组的。
顺序,按照插入Map时的顺序
从对象创建map
new Map(some)
可以接受一个带有键值对,或者其他可迭代对象来进行初始化。使用 Object.entries
// 如果你有一个对象,想快速变成map。
// 利用Object.entries 获取键值对构成的数组,然后给new Map 初始化
let obj = {name: 'jack', age: '18'}
let map = new Map(Object.entries(obj))
从map创建对象
反之,可以利用已有的Map或具有键值对的数组来创建一个普通对象。使用 Object.formEntries
let prices = Object.fromEntries([
["banana", 1],
["orange", 2],
["meat", 4],
]);
console.log(prices); // {banana: 1, orange: 2, meat: 4}
let map = new Map().set('jack', 'boy').set('tom', 'boy').set('lili', 'gril');
console.log(Object.fromEntries(map)); // {jack: "boy", tom: "boy", lili: "gril"}
Set
一个特殊的类型集合,值的集合,没有键。 每一个值只能出现一次,所以通常拿来过滤数组重复项。
方法和属性
- new Set(iterable) 接受一个可迭代对象,一般是数组。
- set.add(v) 添加一个值,返回set本身
- set.delete(v) 删除一个值,成功为true,失败为false。
- set.has(v)
- set.clear() 清空set
-
迭代
可使用for of(说明set是一个可迭代对象)或者forEach来遍历。也支持如下
set.keys() 返回的不是下标哦,是所有值
- set.values() 和keys作用相同,为了兼容map
- set.entries() 键值对相同
小结
可以使用Array.from 来Map Set
转换为数组。
可以使用Array.fromEntries 来把 Map 转为普通对象。(Set不行哦)WeakMap & WeakSet
不同于Map
和Set
的地方只有2个。
- 只接受引用类型作为键(Map),作为值(Set)。
- 随时可能被垃圾回收,因此不支持遍历相关方法,不支持size属性,因为这些都是不确定的,你调用的时候,可能已经都没了。所以查询的话,使用
has
,获取值的话使用get
。
举一个例子
var obj = {}
var arr = []
arr[0] = obj
obj = null // 看似释放了obj,但由于arr[0]还持有引用,所以不会内存没有释放。
同理,如果使用了 Map
也会造成无法释放。
let john = { name: "John" };
let map = new Map();
map.set(john, "...");
john = null; // 覆盖引用
console.log(map.keys()); // {{name: 'Join'}} 尽然还能访问的到,说明没有被内存回收
所以,改用 WeakMap
就会自动释放掉不用的。
let john = { name: "John" };
let map = new WeakMap();
map.set(john, "...");
john = null; // 覆盖引用
console.log(map.get(john)); // undefined 这里不能用keys
Object.keys & values & entries
三个方法返回的都是真正的可迭代的数组。
三者和for in 一样,忽略Symbol作为键的属性。
解构赋值
一种语法,更方便的从数组或对象中取需要的值。
var a = ['jack', 'lili']
var [first, second] = a // first => jack, second => lili
数组解构
可通过额外,丢弃不需要的值
let [a,,c] = ['a', 'b', 'c'] // a = 'a' c = 'c'
右侧可以是任何可迭代对象
let [a, b, c] = 'abc'
let [one, two] = new Set([1, 2])
let [i, k] = new Map([ ['i', 1], ['k', 2] ])
配合Object.entries和map
for(let [key, value] of Object.entries({a: 1, b: 2})){
console.log(key, value)
}
技巧:交换变量值
let guest = 'Jane'
let admin = 'Pete'
[guest, admin] = [admin, guest]
剩余…
通过 ...
操作符获取剩余部分(一个数组),需要在最后一个参数位置。
let [a, b, ...c] = [1,2,3,4,5] // c = [3,4,5]
对象解构
通常左侧包含的是右侧对象响应属性的一个模式(就是key)
let {a, b} = {a: 1, b: 2}
key也支持改名
let {a: one, b: two} = {a: 1, b: 2} // one: 1 two: 2
支持默认值
let {a = 100, b = 300} = {a: 1, b: undefined} // b: 300哦,不是undefined
改名和默认值结合
let {a: one = 100} = {} // one: 100
…剩余模式
嵌套解构
支持对复杂对象提取深层数据,前提是对应的上需要的key部分
let options = {
size: {
width: 100,
height: 200
},
items: ['cake', 'donut'],
extra: true
}
let {size: {width, height}, items: [one, two], extra} = options
// width => 100 height => 200 one => cake two => donut extra => true
智能函数参数(重要技巧)!
写公用函数的时候,可能会有多个参数,参数可能有的是可选,有的是必填,如果可选在前面,可能会这样
function t(a, b) {
a = a || 1
console.log(a+b)
}
t(undefined, 2) // 3
如果参数偏多,传递复杂,也容易记不住。
我们可以将所有参数作为对象传入,然后函数将这个对象进行解构成多个变量来使用
function t({ a, b }) { // 改造1:函数解构对象值获取需要的变量
a = a || 1;
console.log(a + b);
}
t({ b: 2 }); // 将所有参数作为一个对象传入
日期和时间
创建
使用new Date()
创建,支持多种参数格式,如下:
- 毫秒数
- 日期字符串,如2020-01-01
年月日时分秒毫秒,逗号隔开,如`new Date(1999, 7, 18)
获取日期相关
getFullYear 获取年份(4位数)
- getMonth 获取月份,从0开始算,return 0-11。
- getDate 获取日期,return 1 - 31.
- getHours, getMinutes, getSeconds, getMilliseconds 获取时,分,秒,毫秒
-
设置日期相关
通常不太会用到,跟获取对应
setFullYear 等等
- …
自动校准
这个场景会多点。// 创建时
new Date(2013, 0, 32) // 实际上是 2013年2月1日。因为32号不存在,往后进一天就是2月1号了。
获取几天之后是哪一天
这个平时会较多。要考虑到是30天的月份还是31天的月份,亦或是2月的28天还是29天(闰年)。利用校准的特性我们可以:
同理获取多少分钟,多少秒以后是什么日期,我们响应的调用let date = new Date() // 代码书写时是4月8号
date.setDate(date.getDate() + 23) // 4月8号+23天
console.log(date) // 2021年5月1日。
setSeconds
setMinutes
获取当前时间戳
通常我会new Date().getTime()
,但因为比较常用,提供了Date.now()
方法。Date.parse
我用的较少,接收一个YYYY-MM-DDTHH:mm:ss.sssZ
的字符串,返回对应时间戳这里的T是分隔符,Z是时区
let ms = Date.parse('2012-01-26T13:51:50.417-07:00');
JSON
不受语言限制,一种描述数据的规范。
键名都是双引号,不能是单引号或反引号。值如果是字符串,也只能是双引号。
JSON.stringify
将对象转换为JSON!一直以为是将对象字符串化。
但是这个函数也可用于原始数据类型,即 JSON.stringify(1)
得到 1
。
限制
支持的数据格式
因为JSON是通用的数据规范,所以不会支持JS才有的特定对象,如函数属性(方法), Symbol
类型的属性,存储 undefined
的属性。
不能有循环引用
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: ["john", "ann"]
};
meetup.place = room; // meetup 引用了 room
room.occupiedBy = meetup; // room 引用了 meetup
JSON.stringify(meetup); // Error: Converting circular structure to JSON
如何应对上述限制
这里看下完整语法, JSON.stringify(value, replacer, space)
可以传入第二个参数。
replacer接受一个转换的属性数组,或一个函数 fn(key, value)
。
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup 引用了 room
};
room.occupiedBy = meetup; // room 引用了 meetup
// 数组,这里数组只接受title和participants的话,会丢失name,place, number。我们只要排除导致循环引用的key就行了,也就是occupiedBy
// alert( JSON.stringify(meetup, ['title', 'participants']) );
// 所以改进下
alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) );
// 属性数组形式有点麻烦,我们用函数试下
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup 引用了 room
};
room.occupiedBy = meetup; // room 引用了 meetup
alert( JSON.stringify(meetup, function replacer(key, value) {
alert(`${key}: ${value}`);
// 这里,排除occupiedBy即可。
return (key == 'occupiedBy') ? undefined : value;
}));
自定义toJSON
如果对象有提供 toJSON
方法,使用stringify
时,会自动调用它。
let room = {
number: 23,
toJSON() {
return this.number;
}
};
let meetup = {
title: "Conference",
room
};
alert( JSON.stringify(room) ); // 这里调用了room.toJSON 23
alert( JSON.stringify(meetup) ); // 这里嵌套对象,也会去调用room的toJSON
JSON.parse
将JSON字符串,转化为对象。
直接看下完整语法吧, JSON.parse(string, reviver)
reviver
第二个参数也很少用到,一个可选函数,接收 key value
可以对值做你需要的转换。
var str = '{"a":1}';
var res = JSON.parse(str, (key, value) => {
if (key === "a") {
return value * 3;
}
return value;
});
console.log(res); // {a: 3}
循环引用的解决
通过 stringify
的第二个参数 replacer
,对每个处理的值,如果是对象,则存储其value(是个引用),每次存储都判断下存储的数组中是否已经有同一个对象引用,有的话