JavaScript 进阶营

ES6(2) map和set数据结构 + ( class类 )

2018-03-12  本文已影响101人  woow_wu7

(一) set 数据结构

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

(1) 基础知识

const s = new Set();

[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
  console.log(i);
}
// 2 3 5 4


----------------------------------------


var a = [1,2,2,3,4,5,2];

var s = new Set(a);    // 注意这里得到的s是set结构,并不是数组,要得到数组可以用 [...s]
console.log(s)     
// Set(5) {1, 2, 3, 4, 5}


-------------------------------------------

// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]

// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);        // 接受数组的对象作为参数。

items.size // 5

// 例三
function divs () {
  return [...document.querySelectorAll('div')];         // 接受类似数组的对象作为参数。
} 

const set = new Set(divs());
set.size // 56

// 类似于
divs().forEach(div => set.add(div));
set.size // 56     
// 去除数组的重复成员
[...new Set(array)]
var ss = new Set([{},{},NaN,NaN,2,2,3,4]);
console.log(ss)

// Set(6) {{…}, {…}, NaN, 2, 3, …}

(2) set实例的属性和方法

属性:

Set.prototype.constructor:构造函数,默认就是Set函数。
Set.prototype.size:返回Set实例的成员总数。

操作方法:------- ( 用于操作数据 )

add(value):添加某个值,返回 Set 结构本身。
delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
has(value):返回一个布尔值,表示该值是否为Set的成员。
clear():清除所有成员,没有返回值。

遍历方法:------- ( 用于遍历成员 )

keys():返回键名的遍历器
values():返回键值的遍历器
entries():返回键值对的遍历器
forEach():使用回调函数遍历每个成员

let set = new Set(['red', 'green', 'blue']);

for (let x of set) {
  console.log(x);
}
// red
// green
// blue

Array.from方法可以将 Set 结构转为数组。

const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);

数组的map和filter方法也可以间接用于 Set

let set = new Set([1, 2, 3]);
set = new Set([...set].map(x => x * 2));
// 返回Set结构:{2, 4, 6}

let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x % 2) == 0));
// 返回Set结构:{2, 4}





(二) for...of 和 for..in

for...of 用在数组中是数组的每个元素值
for...in 用在数组中是数组的每个元素值的 下标
for...of 可以用于Set结构 (变量就是values)
for...in 不能用在Set结构
https://www.cnblogs.com/enjoymylift/p/5997416.html






(三) map数据结构

JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键 (对象中的 属性 或者说 键名 都是字符串)。这给它的使用带来了很大的限制。

(1) map数据结构类似于对象,键的范围比对象更强大

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')     // o是map结构的键,o是一个对象,不是字符串
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false


// 上面代码:

// 使用 Map 结构的set方法,将对象o当作m的一个键,

// 然后又使用get方法读取这个键,

// 接着使用delete方法删除了这个键。

(2) 作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组

(1)

const map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);

map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"

// 上面代码在新建 Map 实例时,就指定了两个键name和title。



---------------------------------------------------------------------------------

(2)

Map构造函数接受数组作为参数,实际上执行的是下面的算法。


const items = [
  ['name', '张三'],
  ['title', 'Author']
];

const map = new Map();

items.forEach(
  ([key, value]) => map.set(key, value)
);    -----------注意这种写法,forEach的参数是一个函数,函数的参数是一个数组------------


foreach循环中的参数问题,数组参数

(3) 事实上,不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构,都可以当作Map构造函数的参数。这就是说,Set和Map都可以用来生成新的 Map。


const set = new Set([
  ['foo', 1],
  ['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1

const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3


const map = new Map();

map
.set(1, 'aaa')
.set(1, 'bbb');

map.get(1) // "bbb"
const map = new Map();

map.set(['a'], 555);
map.get(['a']) // undefined


上面代码的set和get方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的

因此get方法无法读取该键,返回undefined。


---------------------------------------------------------------------


同理,同样的值的两个实例,在 Map 结构中被视为两个键。

const map = new Map();

const k1 = ['a'];
const k2 = ['a'];

map
.set(k1, 111)
.set(k2, 222);

map.get(k1) // 111
map.get(k2) // 222

上面代码中,变量k1和k2的值是一样的,但是它们在 Map 结构中被视为两个键。
let map = new Map();

map.set(-0, 123);
map.get(+0) // 123

map.set(true, 1);
map.set('true', 2);
map.get(true) // 1

map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3

map.set(NaN, 123);
map.get(NaN) // 123

(4) map数据结构的属性和方法

(1) size
(2) set
(3) get
(4) has
(5) delete
(6) clear


(7)遍历方法:

 keys():返回键名的遍历器。
 values():返回键值的遍历器。
 entries():返回所有成员的遍历器。
 forEach():遍历 Map 的所有成员。
  const map = new Map();
    map.set(+0, 123);
    map.set(true, 111);
    map.set(NaN, '111222');
    map.set(NaN, '555555');   // NaN在map结构中,是同一个键,后面的覆盖前面的, 所以size是3,而不是4
    let bbb = map.size

    console.log(bbb) // 3
let map = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');
const m = new Map();
m.set(undefined, 'nah');
m.has(undefined)     // true

m.delete(undefined)
m.has(undefined)       // false
let map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2
map.clear()
map.size // 0
const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

for (let key of map.keys()) {      // map.keys()
  console.log(key);
}
// "F"
// "T"

              // console.log( [...map.keys()] );       
              // ['F', 'T']

              // console.log(map.keys())           -----------map.keys()返回的是map结构
              // MapIterator {"F", "T"}            -----------iterator是迭代器的意思    


for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {   ----------这里的map.entries()返回的map结构,item是数组
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {   ------------------和map.entries在forEach中得到的结果一样
  console.log(key, value);
}
// "F" "no"

(5) map结构转换为数组结构

const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

[...map.keys()]
// [1, 2, 3]

[...map.values()]
// ['one', 'two', 'three']

[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]

[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
const map0 = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');

const map1 = new Map(     // filter方法
  [...map0].filter(([k, v]) => k < 3)
);
// 产生 Map 结构 {1 => 'a', 2 => 'b'}

const map2 = new Map(     // map方法
  [...map0].map(([k, v]) => [k * 2, '_' + v])
    );
// 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}

(6) Map和其他类型的数据转换

(1) Map转数组

   const map = new Map()     
      .set(true, 7)                          // 可以采用链式写法
      .set({foo: 3}, ['abc']);

    map.set(['a'], 'a');                 // map的键,不仅仅只是字符串
    map.set({'b': 'b'}, 'b')
    let c = [...map]                     // 用扩展运算符将map结构转换为数组结构
    console.log(c)




    除了用...展开运算符,还可以用Map数据结构的map方法:

    map方法的函数参数分别是 value, key , map-----------map.forEach( (value, key, map) => {} )

    --------和数组中的map方法不一样
    --------数组中是array.forEach( (value, index, array) => {} )
    --------数组中是array.forEach( ([a, b, c,...], index, array) => {} ) 多重数组时

    const arr = [];
    map.forEach( ( value, key, map )  => {
        console.log(key,'key')
        console.log(value,'value')
        console.log(map,'map')
        arr.push([key, value])
    })
    console.log(arr,'arr---------')

(2) 数组转换为Map

const items = [
  [true, 7],
  [{foo: 3}, ['abc']]
]

const map = new Map();

items.forEach(
  ([key, value]) => map.set(key, value)
);




-----------------------------
new Map([
  [true, 7],
  [{foo: 3}, ['abc']]
])

(3) Map转化为对象


    const map = new Map()
      .set('yes', true)
      .set('no', false)
    const a = {}
    for(let [key, value] of map) {
      a[key] = value
    }    
    console.log(a)
  

// Object {yes: true, no: false}

(4) 对象转化为Map

const obj = {a: 'yes', b: 'no'}
const map = new Map()
for( let i of Object.keys(obj) ) {
  map.set(i, obj[i])
}
console.log(map)








(二) class类

(1) 概念

ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

class Point {
  // ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true
class Bar {
  doStuff() {
    console.log('stuff');
  }
}

var b = new Bar();
b.doStuff() // "stuff"
let methodName = 'getArea';

class Square {
  constructor(length) {
    // ...
  }

  [methodName]() {
    // ...
  }
}

上面代码中,Square类的方法名getArea,是从表达式得到的。

(2) 严格模式

(3) constructor 方法

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。

class Point {
}

// 等同于
class Point {    // 在类中,如果没有显式定义constructor方法,则会自动添加一个空的constructor方法。
  constructor() {}
}
//定义类
class Point {

  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }

}

var point = new Point(2, 3);

point.toString() // (2, 3)

point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true

point.hasOwnProperty('toString') // false      
// ----toString不是类本身的属性,而是原型上的属性

point.__proto__.hasOwnProperty('toString') // true

(4) class表达式

函数一样,类也可以使用表达式的形式定义。

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};
let inst = new MyClass();
inst.getClassName() // Me    ---- 取得getClassName方法

Me.name // ReferenceError: Me is not defined    ---- Me只在 Class 内部有定义。

上面代码使用表达式定义了一个类。

需要注意的是,这个类的名字是MyClass而不是Me,Me只在 Class 的内部代码可用,指代当前类

(5) 类不存在变量提升(hoist)

new Foo(); // ReferenceError
class Foo {}

上面代码中,Foo类使用在前,定义在后,这样会报错



(6) class的静态方法 ( 重要 ) -----------------static------------------

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'            // -----static静态方法,直接通过类来调用

var foo = new Foo();
foo.classMethod()                       // -----static静态方法,不被实例所继承

// TypeError: foo.classMethod is not a function


// 上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,
// 可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。

// 如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

class Foo {
  static bar () {
    this.baz();                 // ---- 在类的 static 静态方法中,this指向类,而不是类的实例
  }
  static baz () {               // ---- static静态方法不被实例所继承
    console.log('hello');
  }
  baz () {
    console.log('world');
  }
}

Foo.bar() // hello         // 所以输出 hello 而不是 world, 静态方法直接通过类来调用

const mm = new Foo();
mm.baz() // word           // baz()函数前没有加static,该方法就可以被实例所继承

上面代码中,静态方法bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz。

另外,从这个例子还可以看出,静态方法可以与非静态方法重名。
class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

Bar.classMethod() // 'hello'      //---- 父类的静态方法可以被子类继承 !!!
class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}

Bar.classMethod() // "hello, too"

(7) class的静态属性 和 实例属性

(1) class的静态属性

静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

class Foo {
}

Foo.prop = 1;
Foo.prop // 1

// 上面的写法 为Foo类定义了一个静态属性prop。

// 目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。
class MyClass {
  static myStaticProp = 42;

  constructor() {
    console.log(MyClass.myStaticProp); // 42
  }
}

// 老写法
class Foo {
  // ...
}
Foo.prop = 1;



// 新写法
class Foo {
  static prop = 1;
}
(2) 类的实例属性

类的实例属性可以用等式,写入类的定义之中。

class MyClass {
  myProp = 42;      // 类的实例属性,在实例上可以读取这个属性

  constructor() {
    console.log(this.myProp); // 42
  }
}

let mm = new MyClass ()

// ------注意这里一定要new MyClass(),new命令才能执行构造函数,不然构造函数没有调用

-------------------------------------------------------------------


class b {
  name = 'wang'     -------------------------------实例属性,实例可以读取
  static c() {
    this.d();
  }
  static d() {
      console.log(
        'class的static静态方法,不被实例所继承,直接通过类调用,
        如果static静态方法中有this,那么this指向类,而不是指向实例'
      )
  }
  d() {
    console.log('该函数d 可以被实例所继承,因为不是static静态方法')
  }
};
b.age = 20;       ----- ----------------------------静态属性,子类可以继承
b.age     // 20 
const oo = new b()
console.log ( oo.name )     // wang        ---------实例读取类中的实例属性
以前的写法:

class ReactCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {       // 在constructor中定义 实例属性 state
      count: 0
    };
  }
}


---------------------------------------------------------


新的写法:可以不在constructor中定义了

class ReactCounter extends React.Component {
  state = {
    count: 0
  };
}









(三) class的继承

Class 可以通过 extends 关键字实现继承

class Point {
}

class ColorPoint extends Point {
}


// 上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。

// 但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。

(1) super关键字

constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

// 上面代码中,constructor方法和toString方法之中,都出现了super关键字,

// 这里表示父类的构造函数,用来新建父类的this对象。
class Point { /* ... */ }

class ColorPoint extends Point {
  constructor() {
  }
}

let cp = new ColorPoint(); // ReferenceError


// 上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError      -----在调用super()函数前,使用this会报错
    super(x, y);
    this.color = color; // 正确                -----在调用super()函数后,使用this正常
  }
}

let cp = new ColorPoint(25, 8, 'green');

cp instanceof ColorPoint // true       ------- cp 是否是 ColorPoint 的实例
cp instanceof Point // true            ------- instance是实例的意思


Object.getPrototypeOf(ColorPoint) === Point   
// true

------Object.getPrototypeOf(子类) === (父类)      值是true
// 因此 Object.getPrototypeOf() 可以用来从 子类 上获取 父类 
// 同时,该方法也可以用来判断该子类是否继承自 某一个父类

(2) Object.getPrototypeOf()

Object.getPrototypeOf方法可以用来从子类上获取父类。

在上面的代码中已经分析了

Object.getPrototypeOf(ColorPoint) === Point       ----表示 ColorPoint类 继承了 Point类
// true

(3) 再谈 super关键字

super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

(1) super作为函数使用

super作为函数调用时,代表父类的构造函数,用来新建父类的this对象。ES6 要求,子类的构造函数必须执行一次super函数。

class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A() // A
new B() // B

new.target指向当前正在执行的函数。

可以看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。

也就是说,super()内部的this指向的是B。
class A {}

class B extends A {
  m() {
    super(); // 报错        ----super() 作为函数时,只能用在子类的构造函数当中
  }
}

(2) super作为对象使用

super作为对象时:

class A {
  p() {      // 类中定义的方法,实际上是定义在类的 prototype 原型对象 上的
    return 2;
  }
}

// A.prototype.p()   -------结果是2,说明p()方法在A类的prototype上

class B extends A {
  constructor() {
    super();        // ---------super()最为函数,用在子类的constructor中,表示父类的构造函数 
    console.log(super.p()); // 2      // ----super作为对象,在普通函数中,指向父类的原型对象
  }
}

let b = new B();


// 上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。

// 这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。
class A {
  constructor() {
    this.p = 2;         // p 属性是生成在A类的实例上的,并不在A的prototype上
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
b.m // undefined


// 上面代码中,p是父类A实例的属性,super.p就引用不到它。



----------------------------------------------------------------

如果属性定义在父类的原型对象上,super就可以取到。

class A {}
A.prototype.x = 2;

class B extends A {
  constructor() {
    super();
    console.log(super.x) // 2
  }
}

let b = new B();
class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  m() {
    super.print();
  }
}

let b = new B();
b.m() // 2


// 上面代码中,super.print()虽然调用的是A.prototype.print()

// 但是A.prototype.print()内部的this指向子类B,导致输出的是2,而不是1。

// 也就是说,实际上执行的是super.print.call(this)。---------指向执行时所在的对象

(3) super作为对象,用在静态方法中

如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。


class Parent {
  static myMethod(msg) {
    console.log('static', msg);
  }

  myMethod(msg) {
    console.log('instance', msg);
  }
}

class Child extends Parent {
  static myMethod(msg) {    // 静态方法,不能被实例使用,通过类直接调用,被子类所继承
    super.myMethod(msg);    // super对象用在静态方法中,指向父类,而不是父类的原型对象

  }

  myMethod(msg) {          // super对象用在普通函数中,这里是实例函数,指向父类的原型对象
    super.myMethod(msg);   
  }
}

Child.myMethod(1); // static 1    // 通过 ( 类.方法 ) 调用的是静态方法

// 通过 Child类 直接调用的方法,是静态方法myMethod,super对象在静态方法中,super指向父类Parent, 
// Parent.myMethod又是通过类直接调用的方法,是静态方法,所以输出:static 1 



var child = new Child();
child.myMethod(2); // instance 2


(4) 注意,使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

class A {}

class B extends A {
  constructor() {
    super();
    console.log(super); // 报错
  }
}




(5) 类的 prototype 属性和__proto__属性

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
class A {
}

class B extends A {
}


B.__proto__ === A // true 
// 子类的__proto__属性,总是指向父类


B.prototype.__proto__ === A.prototype // true
// 子类的prototype属性的__proto__属性,总是指向 父类的prototype属性
上一篇 下一篇

猜你喜欢

热点阅读