ES6学习笔记三 (Set、Map、改进的数组功能、Promise与异步编程)

第七章 Set集合与Map集合

Set集合是一种无重复元素的列表,开发者们一般不会逐一读取数组中的元素,也不大可能逐一访问Set集合中的每个元素,通常的做法是检测给定的值在某个集合中是否存在
Map集合内含多组键值对,集合中每个元素分别存放着可访问的键名和他对应的值,Map集合经常被用于缓存频繁取用的数据。

1. ES5中使用对象模拟Set、Map集合的问题

对象的键值会被自动转化为字符串

var map = Object.create(null);
map[5] = "foo";
console.log(map["5"]);  // foo

var key1 = {};
var key2 = {};
map[key1] = "foo";    // key被转换为 [object Object]

console.log(map[key2]);  // foo

2. ES6中的Set集合

在Set集合中不会对所存值进行强制的类型转换,引擎内部使用Object.is()方法检测两个值是否一致
Set具有的基本方法: add, has, delete, clear
size属性可以获取集合中目前的元素数量。

let set = new Set();
/************ add 添加元素 ****************/
set.add(5);
set.add("5");
console.log(set.size);                      // 2

set.add("5");                               // 重复 - 本次调用直接被忽略
console.log(set.size);                      // 2

let key1 = {};
let key2 = {};
set.add(key1);
set.add(key2);
console.log(set.size);                      // 4

/************ has 检测存在 ****************/
console.log(set.has(key1));                 // true
console.log(set.has(5));                    // true
console.log(set.has(6));                    // false

/************ delete 移除元素 ****************/
set.delete(5);                 
console.log(set.has(5));                    // false
console.log(set.size);                      // 3

/************ clear 清除元素 ****************/
set.clear();                 
console.log(set.size);                      // 0

2.1 Set转数组

let set = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
let array = [...set];
console.log(array);    // [1,2,3,4,5]

/*****************************************/

function eliminateDuplicates(item){
    return [...new Set(item)];
}

let numbers = [1,2,3,4,5,5,5,5];
let noDuplicates = eliminateDuplicates(numbers);

console.log(noDuplicates);      // [1, 2, 3, 4, 5]

2.2 forEach方法

Set的forEach方法的回调函数接收一下三个参数:

  • Set集合中下一次索引的位置
  • 与第一个参数一样的值
  • 被遍历的Set集合本身
    我们可以看到第一二个参数的值是一模一样的,这是因为为了和数组的forEach方法保持一致,避免分歧过大。
let set = new Set([1, 2]);
set.forEach(function(value, index, ownerSet){
    console.log(index + " " + value);
    console.log(index === value);
    console.log(set === ownerSet);
});

// 输出

1 1
true
true
2 2
true
true

forEach方法的第二个参数也和数组一样,如果需要在回调函数中使用this,则可以将它作为第二个参数传入forEach()函数。当然,也可以使用箭头函数。

let set = new Set([1, 2]);
let processor = {
    output(value) {
        console.log(value);
    },
    process(dataSet) {
        dataSet.forEach(function (value) {
            this.output(value);
        }, this);
    },
    processArrow(dataSet) {
        dataSet.forEach(value => this.output(value));
    }
}

processor.process(set);    // 1 2
processor.processArrow(set);    // 1 2

请记住,尽管Set更适合用来跟踪多个值,而且又可以通过forEach()方法操作集合中的每一个元素,但是你不能像访问数组元素那样直接通过索引来访问集合中的元素,如有需要,请先转换为数组。

2.3 Weak Set 集合

将对象存储在Set的实例与存储在变量中一样,只要Set实例中的引用存在,垃圾回收机制就不能释放该对象的内存空间,于是之前提到的Set类型可以被看做是一个强引用的Set集合。

let set = new Set();
let key = {};

set.add(key);
console.log(set.size);      // 1

// 移除原始引用
key = null;
console.log(set.size);      // 1

// 重新取回原始引用
key = [...set][0];

大部分情况下这种代码运行良好,但是有时候你希望某个对象的其他所有引用都不再存在时,让Set集合中的这些引用随之消失。例如,在页面中通过JS记录了一些DOM,但是这些DOM可能被另一端脚本移除,而你又不希望自己的代码保留这些DOM元素的最后一个引用(这个情景被称为内存泄漏)。
为了解决这个问题,ES6引入了Weak Set集合。Weak Set集合只存储对象的弱引用,并且不可以存储原始值;集合中的弱引用如果是对象唯一的引用,则会被回收并释放相应内存。
Weak Set只有addhasdelete三个方法。

let set = new WeakSet(),
    key = {};

set.add(key);
console.log(set.has(key));      // true  

set.delete(key);
console.log(set.has(key));      // false

// 以下皆报错
// TypeError: Invalid value used in weak set
set.add(5);    
set.add("5");
set.add(true);
两类Set的主要区别

最大区别是Weak Set保存的是对象的弱引用,可惜我们没有办法用代码来验证,例如下面的代码

let weakSet = new WeakSet(),
    set = new Set(),
    key = {};

set.add(key);
weakSet.add(key);

console.log(set.size);      // 1
console.log(weakSet.size);  // undefined

key = null;
console.log(set.size);      // 1
console.log(weakSet.size);  // undefined

这是因为WeakSet没有size属性。所以说,我们可以看到WeakSet和Set的差别还有下面这几点:

  • WeakSet中,add、has、delete三个方法传入非对象参数都会报错
  • WeakSet不可迭代,不能被用于for-of
  • WeakSet不暴露任何迭代器(例如keys、values方法),所以无法通过程序本身来检测其中的内容
  • 不支持forEach方法
  • 不支持size属性

Weak Set集合的功能看似受限,其实这是为了让它能够正确地处理内存中的数据。总之,如果你只需要跟踪对象引用,你更应该使用Weak Set集合而不是Set集合
Set类型可以用来处理列表中的值,但是不适用于处理键值对这样的信息结构。ES6中添加了Map集合来解决类似的问题。

ES6中的Map集合

ES6中的Map类型是一种存储着许多键值对的有序列表,其中的键名和对应的值支持所有的数据类型。键名的等价性判断是通过调用Object.is()方法实现的。

Map集合支持的方法

  • set
  • get
  • has(key)
  • delete(key)
  • clear()
let map = new Map();
map.set("name", "NowhereToRun");
map.set("age", "24");

console.log(map.size);          // 2

console.log(map.has("name"));   // true
console.log(map.get("name"));   // NowhereToRun

map.delete("name");
console.log(map.has("name"));   // false
console.log(map.get("name"));   // undefined
console.log(map.size);          // 1

map.clear();
console.log(map.size);          // 0    

let key1 = {};
let key2 = {};
map.set(key1, 1);
map.set(key2, 2);
console.log(map.size);          // 2
console.log(map.get(key1));     // 1

Map集合初始化方式

由于Map集合可以接受任意数据类型的键名,为了确保它们在被存储到Map集合中之前不会被强制转换为其他数据类型,因而只能将他们放在数组中,因为这是唯一一种可以准确地呈现键名类型的方式。

let map = new Map([["name", "NowhereToRun"], ["age", "24"]]);
console.log(map.size);          // 2
console.log(map.has("name"));   // true
console.log(map.get("name"));   // NowhereToRun
console.log(map.has("age"));    // true
console.log(map.get("age"));    // 24

Map集合的 forEach() 方法

let map = new Map([["name", "NowhereToRun"], ["age", "24"]]);
map.forEach(function (value, key, ownerMap) {
    console.log(key + '  ' + value);
    console.log(ownerMap === map);
})

// name  NowhereToRun
// true
// age  24
// true

遍历过程中,会按照键值对插入Map集合的顺序将相应信息传入forEach()方法的毁掉函数,而在数组中,会按照数值型索引值的顺序依次传入回调函数。

Weak Map集合

键名必须是对象,否则会报错;
集合中保存的是对象的弱引用,如果在弱引用之外不存在其他的强引用,会被垃圾回收;但是只有集合的键名遵从这个规则,键名对应的键值如果是一个对象,则保存的是对象的强引用,不会触发垃圾回收

支持的方法:

  • set
  • get
  • has
  • delete

和Weak Set一致,不支持size属性。从而无法验证集合是否为空,同样,(在键名对象销毁后)由于没有键对应的引用,因而无法通过get()方法获取到相应的值。(就是无脑相信引擎已经给他干掉了)

Weak Map的用处
  1. 一般用来存dom
let map = new WeakMap();
document.querySelector(".element");

map.set(element, "original");

let value = map.get(element);
console.log(value);                 // original  

// 移除element元素  
element.parentNode.removeChild(element);
element = null;

// 此时Weak Map集合为空
(但是此时用Chrome输出map会输出这些的信息,像是没回收)
  1. 存私有对象数据

ES5中实现

var Person = (function () {
    var privateData = {};
    var privateId = 0;

    function Person(name) {
        Object.defineProperties(this, "_id", { value: privateId++ });

        privateData[this._id] = {
            name: name
        };
    }

    Person.prototype.getName = function(){
        return privateData[this._id].name;
    };

    return Person;
}())

这种方法最大的问题是,如果不主动管理,由于无法获知对象实例何时被销毁,因此privateData中的数据就永远不会消失。而使用WeakMap可以解决这个问题

var Person = (function () {
    var privateData = new WeakMap();

    function Person(name) {
        privateData.set(this, {name: name});
    }

    Person.prototype.getName = function(){
        return privateData.get(this).name;
    };

    return Person;
}())

只要对象实例销毁,相关信息也被销毁,从而保证了信息的私有性。

什么时候使用WeakMap

只用对象作为集合的键名?WeakMap : Map
需要forEach()||size||clear() ? Map : WeakMap

第十章 改进的数组功能

Array.of()

出这个Api的目的是规范化使用构造函数创建数组时的怪异行为。

let items = new Array(2);
console.log(items.length); // 2
console.log(items[0]);  // undefined
console.log(items[1]);  // undefined  

items = new Array('2');
console.log(items.length); // 1
console.log(items[0]);  // '2'
console.log(items[1]);  // undefined

items = new Array(1, 2);
console.log(items.length); // 2
console.log(items[0]);  // 1
console.log(items[1]);  // 2

items = new Array(3, '2');
console.log(items.length); // 2
console.log(items[0]);  // 3
console.log(items[1]);  // '2'

如果给Array构造函数传入一个数值型的数,那么数组的length属性会被设为该值;如果传入多个值,无论是否是数值型,都会变成数组的元素。

Array.of方法,无论参数是什么类型,无论参数有多少个,Array.of()总会创建一个包含所有参数的数组。

let items = Array.of(1, 2);
console.log(items.length); // 2
console.log(items[0]); // 1
console.log(items[1]); // 2

items = Array.of(2);
console.log(items.length); // 1
console.log(items[0]); // 2

items = Array.of('2');
console.log(items.length); // 1
console.log(items[0]); // '2'

Array.from

处理类数组转换。例如一个处理数组类型转换的函数:

// 旧方法:
Array.prototype.slice.call(arrayLike);
// 新方法:
Array.from(arrayLike);

映射转换

如果想进一步转换,可以提供给一个函数作为Array.from的参数。同时,第三个函数也可指定,用以确定this指向。

function translate(){
    return Array.from(arguments, (value) => value + 1);
}

translate(1,2,3); // [2, 3, 4]

用Array.from转换可迭代对象

Array.from除了可以处理类数组对象,还可以处理可迭代对象。

为所有数组添加的新方法

findfindIndex

这两函数都可接收两参数,第一个为回调函数;为一个为可选参数,确定回调函数中this的值。

let numbers = [25, 30, 35, 40, 999];
console.log(numbers.find(n => n > 33)); // 35
console.log(numbers.findIndex(n => n > 33)); // 2

如果在数组中根据某个条件查找匹配元素,那么find和findIndex是最好的选择;如果只想查找与某个值匹配的元素,indexOf和lastIndexOf即可。

fill

使用指定的值填充一至多个数组元素。第一个参数为数值,第二个为起始位置,第三个不传默认为终点,传则为终止点(不包含这个位置)。二三个参数都可接收负数。

let numbers = [25, 30, 35, 40, 999];
numbers.fill(1, 2); // numbers: [25, 30, 1, 1, 1];
numbers.fill(5);    // numbers: [5, 5, 5, 5, 5]
numbers.fill(6, 1, 3);// numbers: [5, 6, 6, 5, 5]

copyWithin

由定型数组引出的方法。

定型数组

为了方便高性能算术运算使用。所谓定型数组,就是将任何数字转换为一个包含数字比特的数组,随后就可以通过我们熟悉的JavaScript数组方法来进一步处理。

第十一章 Promise与异步编程

背景不多说,一搜一大堆,记一下平时容易疏忽的点。

  1. 众所周知的Promise的三个状态“pending”、“fulfilled”、“rejected”。由内部属性[[PromiseState]]来标记。这个属性不暴露在Promise对象上,所以不能以编程的方式检测Promise的状态,只有当Promise的状态改变时,通过then方法来采取特定的行动。

  2. Promise的对象都有then方法,接收两个参数,不再赘述。如果一个对象实现了上述的then方法,那这么对象我们称之为thanable对象。所有的Promise都是thenable对象,但并非所有thenable对象都是Promise。

  3. Promise的catch方法,相当于只给其传入拒绝处理程序的then方法。

  4. 每次调用then方法或catch方法都会创建一个新任务,当Promise被解决(resolved)时执行。这些任务最终会被加入到一个为Promise量身定制的独立队列中,这个任务队列的具体细节对于理解如何使用Promise而言不重要,通常你只要理解任务队列是如何运作的就可以了。

  5. Promise执行顺序。

let promise = new Promise(resolve => {
    console.log('in promise');
    resolve(1);
})
console.log('Hi!');
promise.then(e => console.log(e));

// in promise
// Hi!
// 1
  1. 创建已处理的Promise
let promise = Promise.resolve('test');
promise.then((msg) => {
    console.log(msg);
});
// test

let promise = Promise.reject('test');
promise.then(msg => {
    console.log(msg)
}).catch(msg => {
    console.log('catch', msg);
});
// catch test

如果向Promise.resolve()和Promise.reject()方法传入一个Promise,那么这个Promise会被直接返回。

let promise2 = new Promise((resolve, reject) => {
    setTimeout(reject(123), 30000);
})

let promise = Promise.resolve(promise2);
promise.then(msg => {
    console.log(msg)
}).catch(msg => {
    console.log('catch', msg);
});

// 立刻输出 catch 123
  1. 非Promise的Thenable对象
    Promise.resolve()和Promise.reject()都可以接收非Promise的Thenable对象作为参数。如果传入一个非Promise的Thenable对象,则这些方法会创建一个新的Promise,并在then()函数中被调用。
    拥有then方法,并接resolve和reject这两个参数的普通对象就是非Promise的Thenable对象。
let thenable = {
  then: (resolve, reject) => {
    resolve(42);
  }
}

let p1 = Promise.resolve(thenable);
p1.then(value => {
  console.log(value); // 42
})

也可以reject

let thenable = {
  then: (resolve, reject) => {
    reject(42);
  }
}

let p1 = Promise.resolve(thenable);
p1.then(value => {
  console.log(value);
}).catch(value => {
  console.log('error', value);  // error, 42
})

如果不确定某个对象是不是Promise对象,那么可以根据预期的结果将其传入Promise.resolve()方法中或Promise.reject()方法中,如果它是Promise对象,则不会有任何变化

  1. 执行器错误
let promise = new Promise((resolve, reject) => {
    throw new Error('explosion!');
})

promise.catch((err) => {
    console.log(err.message); // "explosion"
})

// 等价于

let promise = new Promise((resolve, reject) => {
    try {
        throw new Error('explosion!');
    } catch(e){
        reject(e);
    }   
})

promise.catch((err) => {
    console.log(err.message); // "explosion"
})

执行器会捕获所有抛出的错误,但只有当拒绝处理程序存在时才会记录执行器中抛出的错误,否则错误会被忽略掉
在早期的时候,开发人员使用Promise会遇到这种问题,后来,JavaScript环境提供了一些捕获已拒绝Promise的钩子函数来解决这个问题。

  1. 全局的Promise拒绝处理
    浏览器通过触发两个事件来识别未处理的拒绝。
  • unhandledrejection 在一个时间循环,当Promise被拒绝,并且没有提供拒绝处理程序时被调用。
  • rejectionhandled 在一个时间循环,当Promise被拒绝,拒绝处理程序执行时触发 。

事件接受一个有以下属性的事件对象作为参数:

  • type 事件名称(“unhandledrejection”或“rejectionhandled”)
  • promise 被拒绝的Promise对象
  • reason 来自Promise的拒绝值

所有参数可参考下图


unhandledrejection
let rejected;

window.addEventListener("unhandledrejection", event => {
  console.log(event, -new Date());
  console.log(event.type); // unhandledrejection
  console.log(event.reason.message); // Explosion!
  console.log(rejected === event.promise); // true
})

window.addEventListener("rejectionhandled", event => {
  console.log(event, -new Date());
  console.log(event.type);
  console.log(event.reason.message);
  console.log(rejected === event.promise);
})

rejected = Promise.reject(new Error("Explosion!"));

setTimeout(() => {
  rejected.catch(e => {
    console.log('处理错误', e);
  })
}, 5000)

函数输出如下图:


1是Promise.reject后没有添加处理程序,unhandledrejection内监听到,
2是命令行输出的报错信息(一开始是红色,在catch添加后变成黑色)
3是rejected.catch中输出的log
4是Promise.reject被catch后rejectionhandled输出的信息

注意,这里的setTimeout很重要,因为rejectionhandled 是监听的在一个时间循环的之前未处理之后已处理的错误。如果直接catch错误,两个事件都不会触发。(这也符合常理,因为错误被我们立刻捕获到了)

  1. 跟踪未处理拒绝
    对于错误日志,错误监控,我们需要监听这两个事件,并做一些什么处理。这里的handleRejection只是个例子,随便打了console,实际应用中按需修改。
let handleRejection = (a, b) => {
  console.log(a, b);
}

let possiblyUnhandedRejections = new Map();

// 如果一个拒绝没有被处理,则将它添加到Map集合中
window.addEventListener("unhandledrejection", event => {
  console.log(event);
  possiblyUnhandedRejections.set(event.promise, event.reason);
})

window.addEventListener("rejectionhandled", event => {
  console.log(event);
  possiblyUnhandedRejections.delete(event.promise);
})

setInterval(() => {
  possiblyUnhandedRejections.forEach((reason, promise) => {
    console.log(reason.message ? reason.message : reason);

    // 做一下什么来处理这些拒绝
    handleRejection(promise, reason);
  });

  possiblyUnhandedRejections.clear();

}, 6000)

rejected = Promise.reject(new Error("Explosion!"));
  1. Promise.all
    传入值是一个可迭代对象,全部成功才返回,返回值是一个数组,按顺序返回。一个失败则直接返回,返回的为失败的信息,不再是数组。

  2. Promise.race
    一个被完成即返回。

let p1 = new Promise(resolve => setTimeout(() => {
  resolve(1)
}, 100));
let p2 = new Promise(resolve => resolve(2));
let p3 = Promise.resolve(3);

let p4 = Promise.race([p1, p2, p3]);
p4.then(value => {
  console.log(value); // 2
})

第一个被拒绝也直接返回拒绝(下面这段代码执行结果和书上说明的不一致,Promise.reject或Promise.resolve并没有比new Promise后直接返回执行的快,书上说的是后者有一个编排过程,但似乎没有发现)

let p1 = new Promise(resolve => setTimeout(() => {
  resolve(1)
}, 100));
let p2 = new Promise((resolve, reject) => {
  resolve(2)
});
let p3 = Promise.reject(3);

let p4 = Promise.race([p1, p2, p3]);
p4.then(value => {
  console.log('then', value); // then 2
}).catch(value => {
  console.log('catch', value);
})
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,976评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,249评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,449评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,433评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,460评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,132评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,721评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,641评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,180评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,267评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,408评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,076评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,767评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,255评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,386评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,764评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,413评论 2 358

推荐阅读更多精彩内容

  • 1.Set 基本用法 ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。Set本...
    雨飞飞雨阅读 1,854评论 0 7
  • 强大的for-of循环 ES6不会破坏你已经写好的JS代码。目前看来,成千上万的Web网站依赖for-in循环,其...
    Awe阅读 7,519评论 2 7
  • 笔记,完全摘录自阮一峰 set数据结构 ES6提供的,新的数据结构,类似于数组。 特点:成员值唯一,没有重复的值 ...
    布蕾布蕾阅读 3,324评论 1 6
  • 1、Set 1.1概念:ES6提供了新的数据结构Set,它类似数组,但是成员的值都是唯一的。 eg:const s...
    Kris_lee阅读 2,523评论 0 3
  • “ 啪”一声有东西落在身上,惊醒了正做黄粱美梦的我。睁眼就瞧见开心果抱着她的粉红小兔笑颜如花的望着我,见我醒来...
    田埂上的菊阅读 224评论 1 4