ES6-新增数据结构

209 阅读6分钟

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的参数传进去

valueidxparseInt
101以0进制数处理,结果是1
212以1进制数处理,转化为10进制,无法转化
32无法转化
43无法转化

用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用法:

  1. 在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

  1. 在普通对象中,指代对象原型
let proto = {
    y: 20,
    z: 40
}
let obj = {
    x: 10,
    foo(){
        console.log(super.y)
    }
}
Object.setPrototypeOf(obj, proto);
obj.foo(); //2
  1. 在静态方法中,指向父类(少用)

修饰器模式

定义:为对象添加新的功能,而不改变原有的结构和功能

语法:@ + 自定义关键字,修饰和它相邻的下一个方法

@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();

主体逻辑在一部分,埋点逻辑在另外一部分,复用性更强

设计模式总原则:开放封闭原则(多拓展开放,对修改封闭)