镇楼图
![](https://img2023.cnblogs.com/blog/2146310/202212/2146310-20221229210109957-867043963.jpg)
Pixiv:torino
三、数据类型
原始类型
原始类型像是string、symbol、number之类的都只能存储原子值,而不能像对象一样随意扩展。但是为了提供额外功能,采取了轻量的对象包装器使得可以提供功能。这些对象包装器后的对象名字和原本的原始类型一样,但是首字母改成了大写,如String、Number、BigInt之类的
let a = new String("123");
let b = new Boolean(false);
let c = new Number(123);
console.log(typeof a);
//Object,但并不推荐使用
//返回类型并不是原本类型
//类型校验时可能会出bug
在非严格模式下,number、boolean、number都是可以像对象一样添加属性的,但这样违反了只能存储原子值的特性,因此严格模式是禁止这样操作的。至于对象包装器得到的方法这里不再说明,我写在了另一篇blog中
Number类型
JS采用了IEEE 754即双精度浮点数存储,无法安全地表示\([-2^{53}-1,2^{53}-1]\)以外的整数
■分隔符不影响表述
JS的数字类型可以用_
来分隔位数以更好地说明语义
let billion1 = 1000000000;
let billion2 = 10_0000_0000;
■科学计数法
let a = 1.2e9;
let b = -0.8E-5;
//e后面的数字必须是整数
■十六进制
console.log(0xff);
console.log(0xAC);
//0x开头,后面字母可大写可小写
■八进制
console.log(0o7654);;
■二进制
console.log(0b_1100_0100);
■小数点的情况
console.log(.123);//十进制若整数位为0可忽略
console.log(123.);//忽略小数点后的数则为0
console.log(123..toString());
//小数点结尾数字调用方法时第一个.会当作小数点
//因此需要两点来调用方法
String类型
在JS中,字符串采用UTF-16编码,绝大多数字符都可以被表述
■跨行字符串
模板字面量另外一个特性就是允许字符串跨行
let String = `123
456
789`;
■模板函数
模板函数利用模板字面量的特性,允许一个相当灵活的输入
let sum = (str,a,b) => {
return a+b;
};
//第一个参数接收模板字面量
console.log("3 + 5 = "+sum`${3} + ${5} = `);
//调用时参数在内部嵌入
■转义字符
有转义字符,也有防止转义字符生效的一个标签函数
String.raw`Str`会将Str不作转移返回
console.log(String.raw`123\n456\n789`);//\n不转义
//关于raw的更多用法详细参考MDN
■获取字符串长度
字符串为内置对象之一,有相当多的属性、方法,这里会列举一些常用的,其余属性方法建议学完JS后再翻MDN学习
console.log("John".length);//4
■访问字符串内的某个字符
[]与charAt方法的区别在于无法找到时的情况,[]无法找到返回undefined,charAt无法找到返回空字符串
let str = "Hello World";
console.log(str[0]);//采用类似于数组索引的方式
console.log(str.charAt(0));//用charAt方法
console.log(str[100]);//undefined,建议采用这种方式
console.log(str.charAt(100));//''
另外也有一种方法at,它可以支持负数提供更多的灵活性
let str = "Hell World";
console.log(str.at(-2));//倒数第二位
■for of循环
对于字符串可以采用另外一种循环形式
let str = "String";
for(let char of str){
console.log(char);//S,t,r,i,n,g
}
■字符串是不可变的常量
let str = "string";
str[0] = 'S';//错误
str = "String";//必须通过重新赋值
■改变大小写
String提供String.prototype.toUpperCase方法和String.prototype.toLowerCase方法进行转换
注:xxx.prototype.xxx方法是指某个具体值可用的方法,而不需要加String前缀
console.log("sTRING".toUpperCase());//STRING
console.log("sTRING".toLowerCase());//string
■查找子串位置
String.prototype.indexOf(substr[, pos])方法会从pos位开始寻找substr子串,如果存在则返回位置,如果不存在则返回-1表示未找到。pos位忽略则默认从头开始
console.log("Hello World".indexOf("World"));//6
console.log("Hello World".indexOf("World",2));//6
//返回值并不是pos的相对位置,而是字符串所在的绝对位置
console.log("Hello World".indexOf("World",8));//-1,未找到
也有另外一个类似的方法String.prototype.lastIndexOf(substr [, pos]),它会从最后往前搜寻,及返回最后一次出现substr的位置,pos代表了从末尾开始往前的位置,比如2代表倒数第3个字符开始
■判断是否包含子串
String.prototype.includes(substr [, pos])会从pos位开始判断是否包含substr,pos默认为0
String.prototype.startsWith(substr [, pos])会从pos位判断开头是否为substr,pos默认为0
String.prototype.endsWith(substr [, len])会截取len长度的子串判断结尾是否为substr,len默认为字符串长度
■获取子串
String.prototype.slice(begin [, end])会获取从begin到end位置之间的子串,end默认为字符串长度。若参数为负数会视为字符串长度减去参数,如-2即倒数第二位
String.prototype.substring(begin [, end])基本和slice方法一致
slice与substring的区别:1.当end > begin时的处理substring依然生效,而slice失效。2.substring不支持负数的参数会当作0。3.在substring中超过字符串长度的数会当作超过字符串的长度。可以简单理解为substring是参数受约束的slice
■拼接字符串
String.prototype.concat(str2 [, ... , strN])将会拼接参数中的字符串,建议直接使用+运算符代替方法
■重复n次返回
String.prototype.repeat(n)将字符串重复n次返回
console.log("10".repeat(3));//101010
■删除多余空白符
String.prototype.trim()将会删除字符串两端多余的空白符
String.prototype.trimStart()会删除左端多余的,String.prototype.trimEnd()会删除右端多余的
■包装类基础方法(没什么用)
String.prototype.toString()与String.prototype.valueOf()效果一致
■填充字符串
String.prototype.padStart(len [, str])、String.prototype.padEnd(len [, str])可以将字符串开头或结尾填充字符串str直到字符串长度到达len(多余部分会被截断),str默认为空格
console.log("123".padEnd(6,"*/"));//123*/*
'abc'.padStart(1); // "abc"
//长度低于原本长度时只会返回原本字符串
■split方法
String.prototype.split([separator[, limit]])将会以sparator为分隔符,返回一个子串的数组,若指定limit元素数量,则返回的数组最多有limit个多余的将会舍弃。其中separator如果是空字符将会返回非常奇怪的结果
let myString = "Hello World. How are you doing?";
let splits = myString.split(" ", 3);
console.log(splits);
Array类型
数组是一种有序集合,其内的元素按照固定顺序排列
■创建数组
let arr1 = new Array();
let arr2 = new Array("Apple", "Pear", "etc");
let arr3 = new Array(10);//创建长度为10的空数组
let arr4 = [];
let arr5 = ["Apple", "Orange", "Plum"];
for(i of arr5){
console.log(i);
}
■索引数组元素
let arr = [false, 1, "2", {3:3}];
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
//JS的数组可以接受任意类型包括自身的类型
console.log(arr[3]);
arr[3] = " 3 ";
console.log(arr.at(-2));//倒数第二个元素
//数组只能采用[]而不能采用对象的.
//at方法可以接受负数进行索引
■添加数组元素
let arr = [0,1,2];
arr[3] = 3;
//与Object一样
for(let i = 0;i < arr.length;i++){
//可以用length属性获取长度
console.log(arr[i]);
}
■pop、push、shift、unshift方法
JS没有栈、队列的结构但数组提供了方法可以实现
Array.prototype.pop()、Array.prototype.shift()分别用于删除数组尾、数组头的元素,并返回被删除的元素
Array.prototype.push(e1 [, ... , eN])、Array.prototype.unshift(e1 [, ... , eN])分布用于添加数组尾、数组头的元素,且可以添加多个,并返回添加后数组的长度
就性能而言pop/push是要比shift、unshift更快
■内部实现
Array的本质依然是Object可以用Object的一些操作,但Array是被优化后的数组,使用对象的特征可能会让优化关闭。for-in循环适用于通常的对象,Array也可使用但效率上不如for-of高,for-of仅限于可迭代对象
let arr = [];
arr.name = "John";
arr.atk = 20;
arr[125] = 125;
//可以使用但不推荐
■length属性
Array的length属性自动更新的逻辑是对最大元素下标+1且length属性是可被覆写的,因此不正常使用Array,length可能会有不正确的结果
let arr = [];
arr[100] = 100;
console.log(arr.length);//101
arr.length = 114514;//可被覆写
console.log(arr.length);
■toString
返回逗号相隔的字符串
let arr = [1,2,3];
console.log(String(arr));
■通用处理数组方法
如果要删除数组元素,使用对象的delete会导致length不发生变化,有一方法Array.prototype.splice(start [, count] [, item1, ... , itemN])可以同时实现添加、删除操作。
start指定要做修改的位置,且支持负数、超出长度等比较宽泛的输入
count指定从start开始含start要删除的元素数量,如果未指定则将会删除start及之后所有的元素
item(i)表示要插入的元素,可插入任意个元素
返回被删除的元素的数组
let arr = [0,1,2,3,4,5,6];
console.log(arr.splice(3));//[3,4,5,6]
console.log(arr);//[0,1,2]
arr.splice(3,0,3,4,5);
console.log(arr);//[0,1,2,3,4,5]
■合并数组、获取子数组
Array.prototype.concat( [a1 , ... , aN] )将会合并多个数组返回一个合并后的数组
Array.prototype.slice( [start] [, end] )将返回从start位置到end位置的子数组,start默认为0,end默认最后元素的位置。slice方法允许负数以及超出数组范围的输入
let arr = [0,1].concat([2,3],[4,5]);
console.log(arr);
console.log(arr.slice(-2));
let arr = [1, 2];
let arrayLike = {
0: "something",
length: 1
};
alert( arr.concat(arrayLike) );
alert( arr.concat([3, 4], true, "false") );
//也接受类数组
■forEach方法
// 箭头函数
forEach((element) => { /* … */ })
forEach((element, index) => { /* … */ })
forEach((element, index, array) => { /* … */ })
// 回调函数
forEach(callbackFn)
forEach(callbackFn, thisArg)
// 内联回调函数
forEach(function(element) { /* … */ })
forEach(function(element, index) { /* … */ })
forEach(function(element, index, array){ /* … */ })
forEach(function(element, index, array) { /* … */ }, thisArg)
forEach提供了一种更简洁的循环语法,其中接受的方法callbackFn但方法至少存在代表数组的元素element。可以有代表数组的下标index,可以有代表数组本身的array。thisArg是指定this值,当函数内出现this时可能会指向不明需要参数辅助。下面会有很多方法均采用forEach的参数形式,下面不再说明这种特殊的语法形式
["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
alert(`${item} is at index ${index} in ${array}`);
});
■判断是否包含元素
Array.prototype.includes(element [, start])从start位置开始判断数组是否包含元素element,start默认为0,返回布尔值
Array.prototype.indexOf(element [, start])从start位置开始查找第一个出现element的位置,若未找到返回-1
Array.prototype.lastIndexOf(element [, len])开始查找最后出现element的位置,len指定长度,若未找到返回-1
console.log( [NaN].indexOf(NaN) ); // -1
console.log( [NaN].includes(NaN) );// true,只有includes方法可以处理NaN
■find方法
Array中的find、findLast方法可以判断第一个和最后一个满足某个函数条件的元素,而这个函数必须是返回布尔值的函数表示是否满足。findIndex、findLastIndex功能一直但不返回元素而返回元素的索引。它的语法形式和forEach方法相同,之后也会遇到相当多以函数作为输入的方法
let arr = [
{atk: 2000,id: 1},
{atk: 1500,id: 2},
{atk: 1200,id: 3}
];
console.log(arr.find(i => i.atk < 1500).id);
//获取atk<1500的第一个元素的id值
■filter方法
filter是find的拓展,find只能寻找单个元素,filter则会返回所有满足的子数组(浅拷贝),它可能比find更加常用
let arr = [
{atk: 2000,id: 1},
{atk: 1500,id: 2},
{atk: 1200,id: 3}
];
console.log(arr.filter(i => i.atk < 2000).forEach(console.log));
//获取atk<2000的所有元素
■map方法
map即映射,提供一种从数组元素映射到新数组元素的方法(语法和forEach相同)
console.log([1,2,3,4,5].map(i => i*2));
■sort方法
关于JS的排序采用原地算法,返回排序后的数组。sort默认是先将元素转成字符串,然后根据UTF-16进行比较,用户也可自行制定比较器,其中这个函数至少有a,b两个元素,返回true表示a>b,反之false
const items = [
{ name: 'Edward', value: 21 },
{ name: 'Sharpe', value: 37 },
{ name: 'And', value: 45 },
{ name: 'The', value: -12 },
{ name: 'Magnetic', value: 13 },
{ name: 'Zeros', value: 37 }
];
// sort by value
items.sort((a, b) => a.value - b.value);
另外关于排序算法的稳定性,在ES10后要求sort排序是稳定的,但此之前是不稳定的
const students = [
{ name: "Alex", grade: 15 },
{ name: "Devlin", grade: 15 },
{ name: "Eagle", grade: 13 },
{ name: "Sam", grade: 14 },
];
console.log(students.sort((firstItem, secondItem) => firstItem.grade - secondItem.grade));
■reverse方法
Array.prototype.reverse()可以将数组逆序。(注:sort、reverse均会改变数组本身)
let arr = [1, 2, 3, 4, 5];
console.log(arr.reverse());
■join方法
Array.prototype.join( [separator] )是String.prototype.split方法的逆方法,会将所有元素结合成一个字符串并返回,也可以加上分隔符separator。
let arr = ['Bilbo', 'Gandalf', 'Nazgul'];
let str = arr.join(';');
alert( str ); // Bilbo;Gandalf;Nazgul
■reduce、reduceRight方法
它类似于一个累加器,将数组所有元素作用于一个数值
// 箭头函数
reduce((previousValue, currentValue) => { /* … */ } )
reduce((previousValue, currentValue, currentIndex) => { /* … */ } )
reduce((previousValue, currentValue, currentIndex, array) => { /* … */ } )
reduce((previousValue, currentValue) => { /* … */ } , initialValue)
reduce((previousValue, currentValue, currentIndex) => { /* … */ } , initialValue)
reduce((previousValue, currentValue, currentIndex, array) => { /* … */ }, initialValue)
// 回调函数
reduce(callbackFn)
reduce(callbackFn, initialValue)
// 内联回调函数
reduce(function(previousValue, currentValue) { /* … */ })
reduce(function(previousValue, currentValue, currentIndex) { /* … */ })
reduce(function(previousValue, currentValue, currentIndex, array) { /* … */ })
reduce(function(previousValue, currentValue) { /* … */ }, initialValue)
reduce(function(previousValue, currentValue, currentIndex) { /* … */ }, initialValue)
reduce(function(previousValue, currentValue, currentIndex, array) { /* … */ }, initialValue)
其中reducer函数有四个参数
1.previousValue:必要参数,累加器的上一返回值
2.currentValue:必要参数,正在处理的数组元素
3.currentIndex:可选参数,正在处理的数组元素的索引
4.array:可选参数,数组本身
累加器的初始值界定比较复杂
如果指定了初始值initialValue,那么初始值为initialValue,且从第一个元素开始;如果未指定初始值,初始值为第一个元素,且从第二个元素开始
而reduceRight则恰好是从末尾开始循环
let arr = [1,2,3,4,5];
console.log(arr.reduce( (sum,i) => sum + i))
■类型判断
数组是基于对象的,无法判断array类型,需要用专门方法判断Array.isArray(value)
console.log(Array.isArray([]));
■测试数组
some方法用于检测数组是否存在元素满足某个函数,every方法用于检测数组是否任意元素满足某个映射,返回布尔值
console.log([2,3,5,7,11,13].every((i) => {
//判断数组是否都为素数
for(let j = 2;j <= Math.sqrt(i);j++){
if(!(i%j))return false;
}
return true;
}));
■fill操作
Array.prototype.fill(value [, start] [, end])会填充数组从start开始至end(不包括end)所有值为value,用于初始化可能是非常不错的选择。start默认为0,end默认为数组长度
let arr = new Array(100).fill(0);
//创建长度100且所有元素设为0
■扁平化数组
数组当然有可能存在数组嵌套数组的情况,JS提供了方法用于去除这种情况
Array.prototype.flat( [depth] )用于扁平化数组,其中depth是扁平化的结构深度,默认为1
var arr = [0, 1, [2, 3, [4, 5, [6, 7, [8, 9]]]]];
console.log(arr.flat(Infinity));
//可以使用特殊数值Infinity来彻底扁平化
flatmap方法是结合了map与flat方法的混合,它会经过map映射后再flat扁平化一层深度
let arr1 = ["it's Sunny in", "", "California"];
arr1.map(x => x.split(" "));
// [["it's","Sunny","in"],[""],["California"]]
arr1.flatMap(x => x.split(" "));
// ["it's","Sunny","in", "", "California"]
扁平化一般是用不到的,会在一些特殊情况下用到。
■部分复制方法
Array.prototype.copyWithin(target [, start] [, end])将会从start到end(不包括end)的元素从该数组的target位置开始复制。可以接受负数参数。start默认0,end默认数组的长度
console.log([0,1,2,3,4,5].copyWithin(0,-3));
可迭代对象(Iterable Object)
JS中绝大多数特殊类型的本质均是Object,但是对象中存在一类特殊对象可迭代对象,它支持更多特殊的功能,但目前而言可知道的是它可以支持普通对象无法支持的for-of循环。在常用的内建对象中Array、String、Map、Set类型均是可迭代对象。
博主编写了一个关于自然数的推演
let Peano = {
//集论下自然数的表达
lang: 0,//代表的自然数
setExpress: "Ø",//集论的表述方法
[Symbol.iterator]() {
return {
lang: this.lang,
setExpress: this.setExpress,
next() {
//推演规则
if (this.lang != 0) {
this.setExpress = `${this.setExpress}∪{${this.setExpress}}`;
return {
done: false,
value: {
lang: this.lang++,
setExpress: this.setExpress,
},
};
} else {
return {
done: false,
value: {
lang: this.lang++,
setExpress: this.setExpress,
},
};
}
},
};
},
};
for (let num of Peano) {
console.log(num);
if(num.lang >= 20)break;
}
迭代器简单而言只有两类构件。1.可迭代协议,即定制迭代器的值,它的属性是@@iterator,JS中用Symbol.iterator可访问。至于为什么是Symbol因为迭代器因为可能需要多次使用,必须保证同名且不同。2.迭代器协议,至少具备next方法且返回IteratorResult对象。IteratorResult存在属性done以判断是否完成迭代,存属性value用于传递每次迭代产生的值(与迭代器实际保持的值不同)
迭代实质上就是不断执行next方法在没用其他特殊手段的情况下直到done属性为true了才停止,它是有可能达成无限迭代的。如上面的例子next方法并没有设计返回done为true的情况,如果下面代码不刻意写break就是死循环。博主刻意写了无穷迭代,因为自然数本身就是无穷的,可以封装特殊代码以指定迭代的范围
let Peano = {
//集论下自然数的表达
lang: 0,//代表的自然数
setExpress: "Ø",//集论的表述方法
print(len = 0) {
for (let num of Peano) {
console.log(num);
if(num.lang >= len)break;
}
},
[Symbol.iterator]() {
return {
lang: this.lang,
setExpress: this.setExpress,
next() {
//推演规则
if (this.lang != 0) {
this.setExpress = `${this.setExpress}∪{${this.setExpress}}`;
return {
done: false,
value: {
lang: this.lang++,
setExpress: this.setExpress,
},
};
} else {
return {
done: false,
value: {
lang: this.lang++,
setExpress: this.setExpress,
},
};
}
},
};
},
};
Peano.print(20);
//一个简单的迭代对象的语法
let iterableObject = {
data1: value1,
//...迭代器内的数据
[Symbol.iterator]() {
return {
//...可以接受其他数据或方法
//一般需要接受迭代器内的数据
next() {
//至少存在next方法
//...
return {done: v1, value: v2};
//不管作什么样的处理需要返回IterableResult
//done表示是否完成迭代
//value即迭代后产生的值
//如for-of循环产生的值便由next决定
}
}
}
}
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
this.current = this.from;
return this;
},
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
for (let num of range) {
alert(num);//num是由next返回的value决定的
}
迭代器的语法也可以显式调用
let str = "Hello";
while (true) {
//与for-of功能相同
//但相比于for-of更加灵活
let result = str[Symbol.iterator]().next();
if (result.done) break;
console.log(result.value);
}
类数组(Array-like)
类数组是指不具备迭代的特性,但是具有length属性以及像数组那样的下标索引
let arrayLike = {length: 0};
for(let i = 0;i < 10;i++){
arrayLike[i] = i;
arrayLike.length++;
}
只有具有类数组的形式以及可迭代对象的功能才是数组,JS提供了一种方法可以将任何可迭代对象、类数组转换为真正的数组
■Array.from方法
Array.from(obj [, mapFn ( element [,index] ){...}] [, thisArg])可以将可迭代对象或类数组转换为真正的数组
console.log(Array.from("123456789",x => x*2));
■Array.of方法
Array.of(e1 [, ... , eN])通过元素来创建数组,它可以用来浅COPY数组
function ArrayCopy(arr){
return Array.of(arr).flat();
}
Map类型
Map是映射,用Object的观点来说是键到值的关系,但Object的键只能是String、Symbol类型,因为Object中的关系主要是key作为语义辅助,value才是值。Map则是两个值之间的关系,如果具备集论基础应该马上能理解之间的差异。
■Map的特点
Map同样是键值对的集合,但与Object不同的是Map的键可以接受任何类型。Map中的key是唯一的。Map是可迭代对象,迭代值是键值对[key, value]的数组。
Map在键值比较上采用了零值相等,导致了NaN与NaN相等(虽然实际上并不相等)
Object存在原型,导致了一些隐藏的键,Map不存在这样的问题
Map键的顺序是按照创建顺序,而Object是先按数字排序再按创建顺序
Map再频繁增删键值对时相比于Object性能更优
性能上Map内部是哈希表,因此查找的时间复杂度<O(N)
■创建Map
const map = new Map();
//创建空Map
const map2 = new Map([[1,"1"],[2,"2"]]);
//创建Map
//参数接受带键值对的数组或可迭代对象
■添加键值对
Map.prototype.set(key, value)可以添加键值对,虽然可以使用Object的语法map[key] = value
或map.key = value
的形式,但它是不被允许的,它并不会真正地添加数据到Map中。由于方法返回Map可以使用链式编程的方法
const plot = ["plot1", "plot2"];
const result = ["result1", "result2"];
map.set(plot, result)
.set(plot[0], result[1])
.set(plot[1], result[0]);
■只读属性size
Map的size与Array的length不同,size是一个只读属性,它不能被修改
console.log(map.size);
■删除键值对
Map.prototype.delete(key)可以删除键值为key的键值对。如果key存在删除成功返回true,删除失败返回false。
Map.prototype.clear()会删除所有键值对,返回undefined
map.delete(plot);
map.clear();
■判断是否存在键
Map.prototype.has(key)用于判断key是否存在于Map中
console.log(map2.has(1));
■获取键关联的值
Map.prototype.get(key)可获取Map中key相关的值,若不存在key返回undefined
console.log(map2.get(2));
■迭代方法
Map.prototype.keys()会返回一个包含所有key的迭代器对象
Map.prototype.values()会返回一个包含所有value的迭代器对象
Map.prototype.entries()会返回key-value组成数组的迭代器对象(即对Map用for-of循环)
Map.prototype.forEach(function( [value] [, key] [, map] ){...}, thisMap)与Array的forEach类似,这里不多赘述了
const map3 = new Map();
for(let i = 1;i < 10;i++){
map3.set(i, "String:" + i);
}
for(let i of map3.keys()){
console.log(i)
}
for(let i of map3.values()){
console.log(i)
}
for(let i of map3){
//等价于map3.entries()
console.log(i)
}
■Object与Map相互转换
Object与Map相似,Object提供了两个方法用于实现Map、Object的相互转换
Object.entries(obj)会将obj转成key-value的键值对数组,从而可以转成Map
Object.fromEntries(iter)可以将可迭代对象转成Object,但其中可迭代对象的元素是一个二元素的数组,第一个子元素作为key,第二个子元素作为value
let map = new Map([[1,"a"],[2,"b"],[3,"c"]]);
let obj = Object.fromEntries(map);
console.log(obj);
console.log(Object.entries(obj));
Set类型
Set集合类型即元素必须上必须保证唯一性,不可出现重复。但实际情况可能会带来一定的困扰,如对象类型,对象的不同只看OID而非对象的内容。Set在值比较上与Map相同,认为NaN等于NaN
■创建Set
let set = new Set();
//创建空Set
let set2 = new Set([1,2,3,4,5]);
//参数必须是可迭代对象
■添加元素
console.log(set.add(1).add(2));
//和Map的set方法一致也支持链式语法
console.log(set.size);
//同Map为只读属性
■删除元素
Set.prototype.delete(value)、Set.prototype.clear()
■判断元素存在
Set.prototype.has(value)判断是否存在元素value,但对象元素大部分情况下都无法判定
■迭代方法
Set.prototype.keys()、Set.prototype.values()方法完全相同
Set.prototype.entries()返回一个[value,value]元素的数组的迭代器
Set.prototype.forEach( function( [value] [, key] [, set] ) [, thisSet] )但其中value和key其实是一样的。这样的设计主要是为了使得Map与Set兼容
let Set3 = new Set([1,2,3,4,5]);
for(let i of Set.keys()){
console.log(i)
}
弱引用对象(WeakRef)
像大部分高级语言一样,JS内置了垃圾回收功能以管理内存
其中最主要的概念是可达性(Reachability),简单来说就是判定能不能从根对象开始访问到,如果访问不到就开始回收避免内存泄漏。如有对象Obj下有很多属性a、b、c。如果此时无法访问Obj,那么即使能从Obj开始去访问到a、b、c,但不能从根部去访问,那么也会被清理掉。
但实际开发时可能会遇到某种情况是某个对象存在多种访问方式,手动去除其中一个访问方式不能保证完全无法访问。需求是要能够清理一个访问方式其他就能被连带清理。如游戏中失去一个buff,希望仅通过清理一个方式就能完成清理,而非要清理所有方式
let Buff1 = {id: 1, effect: 1};
let ExistBuff = new Set();
//存在的buff集合
ExistBuff.add(Buff1);
//失去Buff后设置null
//问题,依然可访问Buff1
Buff1 = null;
console.log(Array.from(ExistBuff.keys()));
JS提供了弱引用在某些时候应用可以完成一些性能与代码的优化,即弱引用对象不会去阻止GC回收,GC在判定没有任何强引用时就会回收,此时相关的弱引用就会失效
■创建弱引用
new WeakRef(obj)
//根据对象obj创建弱引用对象
let Buff1 = new WeakRef({id: 1, effect: 1});
let ExistBuff = new Set();
ExistBuff.add(Buff1);
//可以发现Buff1的指向确实消失
//但仍然会返回一个空WeakRef
//关于这个问题在WeakSet可以得到解决
Buff1 = null;
console.log(Array.from(ExistBuff.keys()));
■deref方法
WeakRef.prototype.deref()会返回弱引用的目标对象,如果这个对象被回收则返回undefined
let a = new WeakRef({id: 1});
let b = a;
但WeakRef并不建议使用,在https://github.com/tc39/proposal-weakrefs/blob/master/README.md中介绍了WeakRef比较大的缺陷
WeakMap类型
WeakMap中key是弱引用的,但它的key必须是Object类型。为了这种弱引用,性能上不如Map,赋值、搜索均是O(n)而非Map的O(1)。由于受到垃圾回收的开始时间无法确定,WeakMap以及下面的WeakSet均是不可迭代的
■创建WeakMap
new WeakMap();
new WeakMap([iter]);
//与Map一致
■增删元素
WeakMap.prototype.set(key, value);
//可链式
WeakMap.prototype.delete(key);
//返回布尔值表明删除是否成功
■根据key获取value
WeakMap.prototype.get(key);
■判断key的存在性
WeakMap.prototype.has(key);
WeakSet类型
WeakSet和WeakMap的特性类似,也只能接受Object类型,同样保持了弱引用。关于其方法博主简单列出不作多余说明了。
new WeakSet( [iter] )
WeakSet.prototype.add(value)
WeakSet.prototype.delete(value)
WeakSet.prototype.has(value)
let Buff1 = {id: 1, effect: 1};
let ExistBuff = new WeakSet();
ExistBuff.add(Buff1);
//weakset成功解决了问题
//查询更加方便
Buff1 = null;
setTimeout(console.log(ExistBuff), 100000);
小记:对于上面代码首先毫无疑问,key被清理内存后不管是WeakMap还是WeakSet都会完全清理元素而不会像WeakRef还保留弱引用的空对象。其次如果读者尝试运行这段代码了,setTimeout是不起作用的,不管后面等待时间写多长,这点博主不能做出解释。之所以要写setTimeout来输出,直接输出会发现它(Buff1)仍然在weakset里,读者可能疑惑它真的被删除了吗?请读者不断F5刷新查看,会呈现一种薛定谔的状态,它大部分情况确实是存在weakset中的,但有的时候它真的不在了,如下图。它到底在不在?我在一篇文章找到了答案,因为弱引用涉及到了GC,而GC启动时间是不固定的,导致了直接输出仍观察到它的存在,除非GC完成后再输出才观察到不存在。
Object迭代方法
之前介绍了下Object.assign(target, ...sources)、Object.entries(obj)、Object.fromEntries(iterable)方法,它还有很多方法,之后会一一列举,这里简单介绍迭代相关的方法
entries和fromEntries方法是键值对(二元数组的结构)与Object之间的一种转换方法,已经见诸于Map、Array
Object.keys(obj)会返回obj对象中所有的key(String类型)的数组,之前有说过Object奇特的排序,这点可以被清晰看到
const object1 = {
a: 'somestring',
b: 42,
3:2,
2:5,
c: false
};
console.log(Object.keys(object1));
//优先正整数升序其次才按创建顺序
//不建议只有数字的字符串
Object.values(obj)会返回obj对象中所有value的数组,同样会遵循奇特的排序
■对Symbol的处理
Object的entries、keys、values方法以及循环均会忽略Symbol(但fromEntries并不会忽略)
Object.getOwnPropertySymbol(obj)会返回obj对象所有Symbol属性的数组,这样也能对Symbol属性做处理
解构赋值
■数组解构
数组解构左侧[]内可以是任意值,右侧必须为可迭代对象。它提供了一种更加灵活的定义变量方法
let Name = {};
[Name.firName,Name.surName] = "Bernard Suits".split(" ");
console.log(Name);
/*相当于
Name.firName="Bernard",Name.surName="Suits"
*/
let user = {
name: "John",
age: 30
};
for (let [key, value] of Object.entries(user)) {
console.log(`${key}:${value}`);
}
//交换变量的技巧
let a = 1;
let b = 2;
[a,b] = [b,a];
■默认值
let [a,b,c] = new Set([1,2]);
console.log(c);
//此时c收不到
//默认为undefined
■剩余模式
右侧可迭代对象的数量可能是不定的,JS提供了一种语法在[]内最后使用...
用于接收不定数量的值
let vedio = ["xxx","tag1","tag2","tag3"];
//视频有一个title和不定数量的tag
let [title,...tag] = vedio;
console.log(tag);
//tag被接收为数组
let [a,...b] = [1];
console.log(b);
//若接收不到默认值为[]而非undefined
■修改默认值
let [id = "id",name = prompt("请输入名字")] = ["xxx"];
//可以针对单个变量修改默认值
//这个默认值可以是非常赋值的表达式
//当无法接收时会触发
■对象解构
左侧除了[]形式外还有{},它是针对对象来解构的,而非可迭代对象。它采取一种key-value对应的方式。由于这种形式导致它不能对Symbol或某些不符合变量声明规则的字符串生效
let obj = {
length: 4,
width: 3,
height: 5
};
let {width, height, length} = obj;
//它会根据变量名去匹配key
//而非[]形式去匹配顺序
console.log(width);
console.log(length);
console.log(height);
■对象解构指定变量名
变量名不一定完全按照其原本命名,可以存在映射关系。但它依然不能像数组解构那样灵活指定任意的变量值
let obj = {
length: 4,
width: 3,
height: 5
};
let {width: w, height: h, length: l} = obj;
console.log(w);
console.log(l);
console.log(h);
■对象解构修改默认值
let obj = {
width: 3
};
let {width: w, height: h = 5, length = 4} = obj;
console.log(w);
console.log(length);
console.log(h);
■对象解构剩余模式
它和数组解构类似,但剩余的返回不是数组而是对象
let obj = {
length: 4,
width: 3,
height: 5
};
let {width: w, ...r} = obj;
console.log(w);
console.log(r);
■一个错误识别
let obj = {
length: 4,
width: 3,
height: 5
};
let w,r;
({width: w, ...r} = obj);
//在没有任何关键词时必须用()表明是完整的赋值语句
//否则会报错
console.log(w);
console.log(r);
■嵌套解构
可以混用数组解构、对象解构以编写一种更精确的解构
let options = {
size: {
width: 100,
height: 200
},
items: ["Cake", "Donut"],
extra: true
};
let {
size: {
width,
height
},
items: [item1, item2],
title = "Menu"
} = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
alert(item1); // Cake
alert(item2); // Donut
■函数参数解构赋值
可以对函数的参数实行解构赋值实现更灵活的参数
let options = {
title: "My menu",
items: ["Item1", "Item2"]
};
function showMenu({title = "Untitled", width = 200, height = 100, items = []} = {}) {
alert( `${title} ${width} ${height}` );
alert( items );
}
showMenu(options);
showMenu();
//由于有初始值{}正常应为showMenu({});
JSON
JSON(JavaScript Object Notation)可用来序列化JS的对象。最初是针对JS来实现的,但JSON已经作为重要的数据格式,其他领域也经常见到。
JSON大体与JS的Object类似
■类型仅支持Object、Array、String、Number、Boolean、Null
■字符串必须使用双引号,不能使用单引号
■Object的属性名必须用字符串(即双引号)
■不支持Function、Symbol、undefined,若存在均会被忽略
■不存在循环引用
JSON只有两个方法,用于Object与JSON字符串相互转换
JSON.parse(value [, (key, value)=>{...}] );
JSON.parse可以将JSON字符串转成Object,其中第二个reviver参数可以将解析值经过reviver函数后返回新的解析值。调用reviver时this值为当前层次。此外JSON可能出现嵌套的情况,嵌套的情况下,它会从里至外依次调用reviver,但这之间会出现问题,最外层调用reviver时属性名会变成空字符串。
let json = '{"one": 1, "two": 2, "three": 3}';
console.log(JSON.parse(json,(key,value)=>{console.log(`key:${key},value:${value}`);return value;}))
//输出结果最外层的{}也会执行reviver
//若return undefined等其他均会被忽略
//由于reviver的特殊
//需要对特殊类型数据做相应处理
let json = "[1,2,3,4,5]";
function reviver(key, value) {
if (typeof value == "object") {
return value;
}else{
return value * 2;
}
}
console.log(JSON.parse(json,reviver));
JSON.stringify方法可以将object转成JSON格式的字符串。replacer参数如果是函数,则会将转换值时先通过函数映射。replacer参数如果是数组,则会限定需要序列化的属性,只有属性名在数组内才会序列化。replacer参数若是null或未提供,则所有属性(前提需要满足JSON数据格式要求)都会序列化。space参数若是数字,则会添加空格(上限为10)。space参数若是字符串,则会添加相应字符串(上限为10)。space参数若是null或未提供,则不添加
JSON.stringify(value[, replacer [, space]])
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: ["john", "ann"]
};
meetup.place = room;
room.occupiedBy = meetup;
//存在循环引用
console.log(meetup);
console.log(JSON.stringify(meetup));
toJSON方法可以设置在对象内部,当序列化到此对象时会直接执行toJSON方法,在Date中内置了toJSON方法,调用stringify相当于调用toJSON方法。如下也可以自定义toJSON方法对不同对象实现不同的序列化
let gameRoom = {
id: 10003,
width: 640,
height: 480,
toJSON(){
return this.id;
}
}
let struct = {
player: "Player1",
gameRoom
}
console.log(JSON.stringify(struct));
参考资料
[1] 《JavaScrpit DOM 编程艺术》
[2] MDN
[3] 现代JS教程
[4] 黑马程序员 JS pink