JS笔记(二):数据类型
阅读原文时间:2023年07月08日阅读:1

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] = valuemap.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

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器