Set()
JS中的数组在底层实际上是用对象模拟的,严格意义来说JS是没有数组的
而ES6新增的Set数据结构,类似数组,但是成员唯一,不能有重复的值,经常用set进行数组去重,然后再转成数组,提升开发效率
去重最简单的方法
let set = new Set([1,2,3,4,5,6,7]);
console.log([...set])
let set = new Set([undefined, undefined, null, null, 5, '5', true, 1, NaN, NaN, {}, {}]);
console.log(set);
可以看到两个相同的undefined、null、NaN都是会被去重的,ES6基本上已经修复了NaN的bug
常见的可以隐式转化的值不会被认为重复
传参必须要是具有迭代器的数据结构
let set = new Set([5,6]);
console.log(set);
size相当于数组的length,指的是长度
add 添加
添加没有对数据类型进行限制,对象也可
返回的是一个新的set实例,意味着可以链式调用
let set = new Set();
var x = {id: 1},
y = {id: 2};
set.add(x).add(y);
console.log(set);
delete 删除
和对象的delete不同,set上的delete上需要点调用的,返回布尔值
set.delete(y);
clear 清空
要注意的是,如果先打印后清空,打印出来的是空,和对象不同
var obj = {a: 1, b: 2};
console.log(obj); //{a: 1, b: 2}
delete obj.a;
console.log(obj); //{b: 2}
console.log(set); //空,尽管clear在后面
set.clear();
说明这些操作是实时的,会影响操作之前的set
has 是否包含
遍历方法:keys、values、entries、forEach
let set = new Set([1,2,3,4,5,6,7]);
console.log(set.keys());
具有迭代器对象,就可以用for of
set结构没有键名,虽然可以看到顺序,打印出来keys和values是一样的,打印entries键名和键值一样
for(let i of set.entries()){
console.log(i);
}
for(let values of set){
console.log(values);
}
直接循环set也可以打印键值,底层调用的是values方法,所以循环一般这么调用就好了,不会用keys和values
拓展
内容翻倍
let set = new Set([1,2,3,4,5,6,7]);
let set1 = new Set([...set].map(value => value * 2));
[...set]先用拓展运算符变成数组,然后该怎么处理就怎么处理
法2:
let set1 = new Set(Array.from(set, value => value * 2));
映射出一个新的结构:set本身并没有这些map……方法,需要转一下
var arr = [1,2,3,4];
var arr1 = arr.map(parseInt); //[1, NaN, NaN, NaN]
console.log(arr1);
parseInt可以传两个参数,第一个参数是要转换的值,第二个参数是转换的进制
map循环数组时前两个参数value,idx会自动作为parseInt的参数传进去
| value | idx | parseInt |
|---|---|---|
| 1 | 0 | 1以0进制数处理,结果是1 |
| 2 | 1 | 2以1进制数处理,转化为10进制,无法转化 |
| 3 | 2 | 无法转化 |
| 4 | 3 | 无法转化 |
用set类型,处理方式一模一样
let set1 = new Set([...set].map(parseInt)); //Set(2){1, NaN}
并集、交集、差集:
let a = new Set([1,2,3]);
let b = new Set([4,2,3]);
let union = new Set([...a, ...b]);
let intersect = new Set([...a].filter(x => b.has(x)));
let difference = new Set([...a].filter(x => !b.has(x)))
console.log(union, intersect, difference); //Set(4){1, 2, 3, 4}; Set(2){2, 3}; Set(1){1}
set PK arr
let set = new Set();
let arr = new Array();
let obj = {'t': 1};
增
set.add(obj);
arr.push({'t': 1});
查
//let arr_exist = set.has({'t': 1}); //false
let arr_exist = set.has(obj);
let arr_exist = arr.find(item => item.t);
改(一样)
set.forEach(item => item.t ? item.t = 2 : '');
arr.forEach(item => item.t ? item.t = 2 : '');
删
set.forEach(item => item.t ? set.delete(item) : '');
let index = arr.findIndex(item => item.t);
arr.splice(index, 1);
优势不是很明显,操作略显,值上唯一的,数据更安全
Map()
类似对象,依旧是键值的存在,但是键值是一一对应的关系,而不是像对象,键名只能是字符串
var m = {};
var x = {id: 1},
y = {id: 2};
m[x] = 'foo';
m[y] = 'bar';
console.log(m); //{[object Object]: "bar"}
不能实现真正意义上的键值一一对应,键名会隐式转换成字符串,所以会出现覆盖的问题
let m = new Map();
let x = {id: 1},
y = {id: 2};
m.set(x, 'foo');
m.set(y, 'bar');
console.log(m);
参数同样是要具备iterator的数据类型,同时要有键值对,所以传入的双元的数组
let m = new Map([
['name', 'zhangsan'],
['sex', 'male']
]);
console.log(m); //Map(2){"name" => "zhangsan", "sex" => "male"}
模拟算法
var items = [
['name', 'zhangsan'],
['sex', 'male']
];
let m = new Map();
items.forEach(([key, value]) => m.set(key, value));
键名的问题:
m.set([5], 555);
console.log(map.get([5])); //undefined,引用值不一样,未指定指针
这样才行
var arr = [5];
m.set(arr, 555);
console.log(map.get(arr));
键名相同会覆盖
map.set(-0, 123);
console.log(map.get(+0)); //123
map的处理方式和全等一样
console.log(+0 === -0); //true
console.log(Object.is(+0, -0)); //false
true和'true'、undefined和null在Map里不一样;NaN和NaN一样
方法
map和set方法基本上一样,不同的就是存值和取值,并且因为set没有键名,没有必要用keys/values
map结构本质上遍历的是entries方法
for(let [key, value] of m){
console.log(key, value);
}
console.log(m[Symbol.iterator] === m.entries)
map和对象/数组互转
map转换为数组也和set一样[...myMap],数组转成map就直接传数组参数就可以了
map转成对象,键名要是字符串
const myMap = new Map();
myMap.set(true, 7)
.set('a', 'abc');
function strMapToObj(strMap){
let obj = Object.create(null);
for(let [key, val] of strMap.entries()){
obj[key] = val;
}
return obj
}
console.log(strMapToObj(myMap));
对象转成map
function objToStrMap(obj){
let map = new Map();
for(let key of Object.keys(obj)){
map.set(key, obj[key]);
}
return map;
}
console.log(objToStrMap({true: 7, no: false}));
map PK array
let map = new Map();
let arr = new Array();
增
map.set('t', 1);
arr.push({'t': 1});
查
let map_exist = map.has('t');
let arr_exist = arr.find(item => item.t);
改
map.set('t', 2);
arr.forEach(item => item.t ? item.t = 2 : '');
删
map.delete('t');
let index = arr.findIndex(item => item.t);
arr.splice(index, 1);
显然map更方便
map set object
let item = {t: 1};
let map = new Map();
let set = new Set();
let obj = {};
增
map.set('t', 1);
set.add(item);
obj['t'] = 1;
查
console.log({
map_exist: map.has('t'),
set_exist: set.has(item),
obj_exist: 't' in obj,
obj_exist1: obj.hasOwnProperty('t');
});
改
map.set('t', 2);
item.t = 2;
obj['t'] = 2;
删
map.delete('t');
set.delete(item);
delete obj['t'];
结论:优先使用map,对数据唯一性有要求的话用set
WeakMap WeakSet
严格版的map和set,基本操作一样,但是不存在遍历方法,成员只能是对象,其他值不行
垃圾回收回收的是引用
var o1 = {
o2: {
x: 1
}
}
var o3 = o1; //o3持有对o1的引用
o1 = 1; //o1被重新赋值
var o4 = o3.o2; //o4拿o2的引用,o2不被释放,引用次数累加
o3 = '123'; //o3被重新赋值,然后整个o3就没有了
o4 = null; //o4释放,所有引用都被释放了
当引用被访问和修改的时候,没有手动释放,就会一直持有引用
WeakMap和WeakSet是弱引用,垃圾回收会不考虑他们的引用,不会被算入引用次数中,不可预测会被怎么回收,会随时消失,很不稳定,最好不要用
proxy 代理
代理模式:在目标之前设置了一个拦截层,要想访问到目标,就要通过拦截,起到控制和授权的作用
例:明星经纪人
let star = {
name: 'lisi',
age: '23',
phone: 'star 666666'
}
let agent = new Proxy(star, {
get: function(target, key){ //读取操作
if (key === 'phone') { //拦截,不允许直接获得明星的电话
return 'agent: 5453354';
}
if (key === 'price') {
return 12000;
}
return target[key];
},
set: function(target, key, value){ //赋值操作
if (value < 10000) {
throw new Error('价格太低');
}else{
target[key] = value;
return true;
}
},
has: function(target, key){
console.log('请联系agent:5453354');
if (key === 'customPrice') {
return target[key];
}else{
return false;
}
}
});
console.log(agent.phone);
console.log(agent.price);
console.log(agent.name);
agent.customPrice = 15000;
console.log(agent.customPrice);
console.log('customPrice' in agent);
has无法拦截for in循环
for(let key in agent){
console.log(agent[key]); //可以打印属性
}
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同
对应的操作是等效的
console.log(obj.a);
console.log(Reflect.get(obj, 'a'));
obj.b = 10;
Reflect.set(obj, 'b', 10);
console.log('a' in obj);
Reflect.has(obj, 'a');
把Object上的一些方法移到Reflect上,把get、has等操作命令变成了一种函数的行为
Object.defineProperty()可以变成Reflect.defineProperty()
class 类
class是保留字,有时候会故意写错,写成ckass
类是一种语法糖,只是把以前的写法换了一种方式,提高可读性和维护性
其实就是构造函数及其原型的另一种写法,本质上就是函数(typeof检验)
在类中,默认使用严格模式
不可枚举
function Person(name = 'zhangsan', age = '18'){
this.name = name;
this.age = age;
}
Person.prototype.say = function(){
console.log(`my name is ${this.name}, my age is ${this.age}.`);
}
Object.assign(Person.prototype, {
eat: function(){
console.log('I can eat');
},
drink: function(){
console.log('I can drink');
}
}) //三种方法都可枚举
var person = new Person();
console.log(Object.getPrototypeOf(person));
class内部定义的方法都是不可枚举的,以前都是可枚举的
class Person{
constructor(name = 'zhangsan', age = '18'){
// 实例化的属性配置:私有属性
this.name = name;
this.age = age;
//改变this指向,原本默认return this
return Object.create(null);
} //不要加逗号,class是一个方法
// 公有属性和方法
say(){
console.log(1);
}
eat(){
console.log('I can eat');
}
}
console.log(new Person());
console.log(Object.getPrototypeOf(person));
console.log(Object.keys(Person.prototype)); //[] 类内部的方法不可枚举
没有添加构造器的话,会自动添加,直接打印空类不会报错
声明,不可提升,必须要用new方式执行
class有以下声明方法,执行必须通过new的方式执行类
class Person{} //函数声明
let Person = class{} //函数表达式
new Person(); //执行
let Person = class{
say(){console.log(1)};
}(); //立即执行
Person.say(); //报错,必须要new,ES5构造函数是可以不new执行的
//修正,一般不这么写
let person = new class{
constructor(name = 'zhangsan', age = '18'){
this.name = name;
this.age = age;
}
say(){console.log(1)};
}('lisi', '19'); //立即执行
person.say();
函数声明可以提升,但是class函数不会提升,会存在暂时性死区,和let一样
console.log(new Person());//报错
class Person{}
方法私有化
私有属性就是构造器上通过this绑定的属性,公有属性的方法是原型上的方法
直接在class函数内部声明一个变脸,会自动挂到构造函数上,还是私有属性(ES2017)
class Person{
a = 1;
}
new Person();
会自动变成
class Person{
constructor(){
this.a = 1;
}
}
一般来说属性都在构造器里私有,方法都是公有的,要想让公有方法私密化(公有属性的私有方法)
法1:Symbol
const eat = Symbol();
class Person{
constructor(name, age){
this.name = name;
this.age = age;
}
say(){
console.log(1);
}
[eat](){
console.log('I can eat');
}
}
console.log(new Person().eat()); //无法访问
法2:直接在外面定义
class Person{
constructor(name, age){
this.name = name;
this.age = age;
}
say(baz){
children.call(this, baz);
}
}
function children(baz){
return this.bar = baz;
}
static 静态属性和方法
静态方法是不会被实例继承的,而是通过类直接调用的
class Person{
static a(){
console.log(a);
};
}
console.log(Person.a);
静态属性
class Person{
static a = 1;
}
这样定义属性(static a = 1)也可以,但是有兼容性问题,是比较新的做法,一般不会这么做
比较多的做法是:
class Person{}
Person.a = 1;
取值函数和存值函数
对象中定义
var obj = {
get a(){
console.log(a);
},
set b(val){
console.log(2);
}
}
obj.a;
类当中也可
class Person{
get a(){
console.log(a);
}
set b(val){
console.log(2);
}
}
let person = new Person();
person.a;
person.b = 4; //2
get和set不要认为是一种方法,是一种取值和存值的操作,就当属性那样访问就好了
继承extends
ES6当中的继承非常容易,以前要来回倒腾原型链
super用法:
- 在constructor中,以函数的方式执行
class Parent{
constructor(name = 'zhangsan'){
this.name = name;
}
static a(){
console.log(1);
};
}
//派生类
class Child extends Parent{
constructor(name = 'lisi', age = '18'){
//this.age = age; 这里的this的指向是有问题的
super(name); //加工父级构造器,拿到基于父级实例,过了一遍之后返回实例
this.age = age;
this.type = 'child';
}
}
console.log(new Child());
console.log(new Child().a()); //报错
console.log(Parent.a); //1
console.log(new Child().age);
super必须在构造器内部,使用this之前要先用super把父级的整个实例继承过来,才可以用this
- 在普通对象中,指代对象原型
let proto = {
y: 20,
z: 40
}
let obj = {
x: 10,
foo(){
console.log(super.y)
}
}
Object.setPrototypeOf(obj, proto);
obj.foo(); //2
- 在静态方法中,指向父类(少用)
修饰器模式
定义:为对象添加新的功能,而不改变原有的结构和功能
语法:@ + 自定义关键字,修饰和它相邻的下一个方法
@testable
class Person{
constructor(name, age){
this.name = name;
this.age = age;
}
@readonly
say(){
console.log(1);
}
eat(){
console.log('I can eat');
}
}
function readonly(target, name, descriptor){
console.log(target, name, descriptor); // Person、属性名say、say属性描述符
descriptor.writable = false;
}
function testable(target){
console.log(target); // Person
}
let person = new Person();
person.say();
target就是当前要修饰的对象:整个Person
通过参数添加一系列功能,实现业务和逻辑相分离。
埋点分析:每做一个数据操作,就有数据处理对应的动作,类似控制台日志。
//埋点代码,记录相应函数执行所对应的结果
let log = (type) => {
return function(target, name, descriptor){
let src_method = descriptor.value; //descriptor.value就是对应的函数
descriptor.value = (...arg) => { //动态的值,不能用src_method替代
src_method.apply(target, arg);
console.log(type);
}
}
}
class AD{
//逻辑代码
@log('show')
show(){
console.log('ad is show');
}
@log('click')
click(){
console.log('ad is click');
}
}
let ad = new AD();
ad.show();
ad.click();
主体逻辑在一部分,埋点逻辑在另外一部分,复用性更强
设计模式总原则:开放封闭原则(多拓展开放,对修改封闭)