声明,本文为个人在阅读阮老师的es6标准规范过程中,对一些个人比较觉得比较重要的知识点整理而成,如果需要了解和学习更多的es6规范,请移步至阮一峰老师的es6个人网站查看,地址:https://es6.ruanyifeng.com/。
字符的扩展
codePointAt() 与 fromCodePoint()
codePointAt 方法主要用于 js 中的四个字节的字符处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
var s = ""; s.length // 2 s.charAt(0) // '' s.charAt(1) // '' s.charCodeAt(0) // 55362 s.charCodeAt(1) // 57271 var s = 'a'; s.codePointAt(0) // 134071 s.codePointAt(1) // 57271 s.charCodeAt(2) // 97 s.codePointAt(0).toString(16) // "20bb7" // 注意,下面是 2 不是 1 s.charCodeAt(2).toString(16) // "61" // codePointAt方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。 function is32Bit(c) { return c.codePointAt(0) > 0xFFFF; } // 数字转为16进制 var a = 134071 a.toString(16) "20bb7" |
fromCodePoint()
1 2 3 4 5 6 7 8 9 10 11 12 |
String.fromCharCode(0x20BB7) // "ஷ" String.fromCodePoint(0x20BB7) // "" String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y' // true var text = String.fromCodePoint(0x20BB7); for (let i = 0; i < text.length; i++) { console.log(text[i]); } // " " // " " for (let i of text) { console.log(i); } // "" |
模板字符串
大括号内部可以放入任意的JavaScript表达式,可以进行运算,以及引用对象属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var x = 1; var y = 2; `${x} + ${y} = ${x + y}` // "1 + 2 = 3" `${x} + ${y * 2} = ${x + y * 2}` // "1 + 4 = 5" var obj = {x: 1, y: 2}; `${obj.x + obj.y}` // 3 // 函数调用 function fn() { return "Hello World"; } `foo ${fn()} bar` // foo Hello World bar // 嵌套 const tmpl = addrs => ` <table> ${addrs.map(addr => ` <tr><td>${addr.first}</td></tr> <tr><td>${addr.last}</td></tr> `).join('')} </table> `; |
标签模板
模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
alert`123` // 等同于 alert(123) // 如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。 tag`Hello ${ a + b } world ${ a * b }`; // 等同于 tag(['Hello ', ' world ', ''], 15, 50); // 如何将各个参数按照原来的位置拼合回去 var total = 30; var msg = passthru`The total is ${total} (${total*1.05} with tax)`; function passthru(literals) { console.log(literals); // ["The total is ", " (", " with tax)"] console.log(arguments); // [["The total is ", " (", " with tax)"], 30 ,31.5] var result = ''; var i = 0; while (i < literals.length) { result += literals[i++]; if (i < arguments.length) { result += arguments[i]; } } return result; } console.log(msg) // "The total is 30 (31.5 with tax)" // 方法2, 用 扩展符 function passthru(literals, ...values) { var output = ""; for (var index = 0; index < values.length; index++) { output += literals[index] + values[index]; } output += literals[index] return output; } |
正则
字符串对象共有4个方法,可以使用正则表达式:match()、replace()、search()和split()。 ES6将这4个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。 String.prototype.match 调用 RegExp.prototype[Symbol.match] String.prototype.replace 调用 RegExp.prototype[Symbol.replace] String.prototype.search 调用 RegExp.prototype[Symbol.search] String.prototype.split 调用 RegExp.prototype[Symbol.split]
u 修饰符和 y 修饰符
ES6对正则表达式添加了u修饰符,含义为“Unicode模式”,用来正确处理大于\uFFFF的Unicode字符。也就是说,会正确处理四个字节的UTF-16编 码。
1 2 3 4 |
/^\uD83D/u.test('\uD83D\uDC2A') // false /^\uD83D/.test('\uD83D\uDC2A') // true |
y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配 就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var s = 'aaa_aa_a'; var r1 = /a+/g; var r2 = /a+/y; r1.exec(s) // ["aaa"] r2.exec(s) // ["aaa"] r1.exec(s) // ["aa"] r2.exec(s) // null // 没有找到匹配 'x##'.split(/#/y) // [ 'x##' ] // 找到两个匹配 '##x'.split(/#/y) // [ '', '', 'x' ] |
正则表达式的 exec 和 字符串的 match 方法对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
var str = "带我去百度errorCode=123456789,whqwherrorCode=12345;带我去百度"; var r1 = /errorCode=\d*/; // exec每次只找一个 r1.exec(str); // 永远都是 [ "errorCode=123456789", index: 5, input: "带我去百度errorCode=123456789,whqwherrorCode=12345;带我去百度", groups: undefined ] // 每次匹配成功后,正则的 lastIndex 属性都变为匹配后的下一个位置 var r2 = /errorCode=\d*/g; // r2.lastIndex // 0 r2.exec(str); // ["errorCode=123456789", index: 5, input: "带我去百度errorCode=123456789,whqwherrorCode=12345;带我去百度", groups: undefined ] // r2.lastIndex // 24 r2.exec(str); // [ "errorCode=12345", index: 30, input: "带我去百度errorCode=123456789,whqwherrorCode=12345;带我去百度", groups: undefined ] // r2.lastIndex // 45 r2.exec(str); // null // 也可以在匹配前指定r2的lastIndex(匹配的开始位置); // r2.lastIndex = 5; // 注意match多个和一个时返回数据的区别,有无 g 修饰符的区别 str.match(r1); // [ "errorCode=123456789", index: 5, input: "带我去百度errorCode=123456789,whqwherrorCode=12345;带我去百度", groups: undefined ] str.match(r2); // ["errorCode=123456789", "errorCode=12345"] var r3 = /^errorCode=\d*$/g; str.match(r3); // null |
先行断言,先行否定断言,后行断言,后行否定断言
JavaScript语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead),不支持后行断言(lookbehind)和后行否定 断言(negative lookbehind)。 目前,有一个提案,在ES7加入后行断言。V8引擎4.9版已经支持,Chrome浏览器49版打开”experimental JavaScript features“开关(地址栏键 入about:flags),就可以使用这项功能。 ”先行断言“指的是,x只有在y前面才匹配,必须写成/x(?=y)/。比如,只匹配百分号之前的数字,要写成/\d+(?=%)/。”先行否定断言“指的是,x只有 不在y前面才匹配,必须写成/x(?!y)/。比如,只匹配不在百分号之前的数字,要写成/\d+(?!%)/。
1 2 |
/\d+(?=%)/.exec('100% of US presidents have been male') // ["100"] /\d+(?!%)/.exec('that’s all 44 of them') // ["44"] |
上面两个字符串,如果互换正则表达式,就会匹配失败。另外,还可以看到,”先行断言“括号之中的部分((?=%)),是不计入返回结果的。 “后行断言”正好与”先行断言”相反,x只有在y后面才匹配,必须写成/(?<=y)x/。比如,只匹配美元符号之后的数字,要写成/(?<=$)\d+/。 ”后行否定 断言“则与”先行否定断言“相反,x只有不在y后面才匹配,必须写成/(?<!y)x/。比如,只匹配不在美元符号后面的数字,要写成/(?<!$)\d+/。
1 2 |
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill') // ["100"] /(?<!\$)\d+/.exec('it’s is worth about €90') // ["90"] |
上面的例子中,”后行断言”的括号之中的部分((?<=$)),也是不计入返回结果。 “后行断言”的实现,需要先匹配/(?<=y)x/的x,然后再回到左边,匹配y的部分。这种”先右后左”的执行顺序,与所有其他正则操作相反,导致了一些 不符合预期的行为。 首先,”后行断言“的组匹配,与正常情况下结果是不一样的。
1 2 |
/(?<=(\d+)(\d+))$/.exec('1053') // ["", "1", "053"] /^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3"] |
上面代码中,需要捕捉两个组匹配。没有”后行断言”时,第一个括号是贪婪模式,第二个括号只能捕获一个字符,所以结果是105和3。而”后行断 言”时,由于执行顺序是从右到左,第二个括号是贪婪模式,第一个括号只能捕获一个字符,所以结果是1和053。 其次,”后行断言”的反斜杠引用,也与通常的顺序相反,必须放在对应的那个括号之前。
1 2 |
/(?<=(o)d\1)r/.exec('hodor') // null /(?<=\1d(o))r/.exec('hodor') // ["r", "o"] |
上面代码中,如果后行断言的反斜杠引用(\1)放在括号的后面,就不会得到匹配结果,必须放在前面才可以。
字符串的转义,让其成为正则模式。
1 2 3 4 5 6 |
function escapeRegExp(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); } let str = '/path/to/resource.html?search=query'; escapeRegExp(str) // "\/path\/to\/resource\.html\?search=query" |
数值的扩展
- 添加 2 进制和 8 进制
ES6提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。
- 添加 Number.isFinite(), Number.isNaN(),Number.isInteger()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// es5 实现方式 (function (global) { var global_isFinite = global.isFinite; Object.defineProperty(Number, 'isFinite', { value: function isFinite(value) { return typeof value === 'number' && global_isFinite(value); }, configurable: true, enumerable: false, writable: true }); })(this); (function (global) { var global_isNaN = global.isNaN; Object.defineProperty(Number, 'isNaN', { value: function isNaN(value) { return typeof value === 'number' && global_isNaN(value); }, configurable: true, enumerable: false, writable: true }); })(this); (function (global) { var floor = Math.floor, isFinite = global.isFinite; Object.defineProperty(Number, 'isInteger', { value: function isInteger(value) { return typeof value === 'number' && isFinite(value) && value > -9007199254740992 && value < 9007199254740992 && floor(value) === value; }, configurable: true, enumerable: false, writable: true }); })(this); |
- Number.parseInt(), Number.parseFloat()
ES6将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。
- Number.EPSILON,Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER
1 2 3 4 5 6 7 8 |
function withinErrorMargin (left, right) { return Math.abs(left - right) < Number.EPSILON; } withinErrorMargin(0.1 + 0.2, 0.3) // true Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1 // true Number.MAX_SAFE_INTEGER === 9007199254740991 // true Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER // true Number.MIN_SAFE_INTEGER === -9007199254740991 // true |
- 安全整数和 安全整数和Number.isSafeInteger()
1 2 3 4 5 6 |
Number.isSafeInteger = function (n) { return (typeof n === 'number' && Math.round(n) === n && Number.MIN_SAFE_INTEGER <= n && n <= Number.MAX_SAFE_INTEGER); } |
Array的扩展
扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。Array.from方法则是还支持类似数组的对 象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此 时扩展运算符就无法转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// Array.from Array.from({length:5}).fill('1') // ["1", "1", "1", "1", "1"] // 简单的 Array.from 替代方法 const toArray = (() => Array.from ? Array.from : obj => [].slice.call(obj) )(); // Array.from 的第二个参数等同于 map 方法 Array.from({length:5},(x, index) => index); // [0, 1, 2, 3, 4] // Array.from()的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种Unicode字符, // 可以避免JavaScript将大 于\uFFFF的Unicode字符,算作两个字符的bug。 function countSymbols(string) { return Array.from(string).length; } // Array.of function ArrayOf(){ return [].slice.call(arguments); } // indexOf 返回索引,includes 返回 boolean,后者能正确判断 NaN [NaN].indexOf(NaN) // -1 [NaN].includes(NaN) // true // arr.some arr.some(el => el === value); [1,2,3].some(el => el === 3); true [1,3,3].some(el => el === 3); true // arr.every [1,3,3].every(el => el === 3); false [3,3,3].every(el => el === 3); true // 数组的空位,下面代码说明,第一个数组的0号位置是有值的,第二个数组的0号位置没有值。 0 in [undefined, undefined, undefined] // true 0 in [, , ,] // false |
函数默认参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
// 与解构赋值默认值结合使用 function foo({x, y = 5}) { console.log(x, y); } foo({}) // undefined, 5 foo({x: 1}) // 1, 5 foo({x: 1, y: 2}) // 1, 2 foo() // TypeError: Cannot read property 'x' of undefined function fetch(url, { body = '', method = 'GET', headers = {} }) { console.log(method); } fetch('http://example.com', {}) // "GET" fetch('http://example.com') // 报错 function fetch(url, { method = 'GET' } = {}) { console.log(method); } fetch('http://example.com') // "GET" // 通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。 如果非尾部的参数设置默认值,实际上 这个参数是没法省略的。 // 例一 function f(x = 1, y) { return [x, y]; } f() // [1, undefined] f(2) // [2, undefined]) f(, 1) // 报错 f(undefined, 1) // [1, 1] // 例二 function f(x, y = 5, z) { return [x, y, z]; } f() // [undefined, 5, undefined] f(1) // [1, 5, undefined] f(1, ,2) // 报错 f(1, undefined, 2) // [1, 5, 2] // 如果传入undefined,将触发该参数等于默认值,null则没有这个效果。 function foo(x = 5, y = 6) { console.log(x, y); } foo(undefined, null) // 5 null // 指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后, // length属性将失真。这是因为length属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后, // 预期传入的参数个数就不包括这个参数了。 (function (a = 5) {}).length // 0 (function (a, b, c = 5) {}).length // 2 // 同理,rest参数也 不会计入length属性。 (function(...args) {}).length // 0 // 如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。 (function (a = 0, b, c) {}).length // 0 (function (a, b = 1, c) {}).length // 1 |
箭头函数
箭头函数有几个使用注意点。
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。没有 new.target
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。
(4)不可以使用yield命令,因此箭头函数不能用作Generator函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function foo() { setTimeout(() => { console.log('id:', this.id); }, 100); } function bar() { setTimeout(function(){ console.log('id:', this.id); }, 100); } var id = 21; foo.call({ id: 42 }); // id: 42 bar.call({ id: 42 }); // id: 21 |
上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到100毫秒后。如果是普通函 数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所 以输出的是42。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function Timer() { this.s1 = 0; this.s2 = 0; // 箭头函数 setInterval(() => this.s1++, 1000); // 普通函数 setInterval(function () { this.s2++; }, 1000); } var timer = new Timer(); setTimeout(() = >console.log('s1: ', timer.s1), 3100); setTimeout(() = >console.log('s2: ', timer.s2), 3100); // s1: 3 // s2: 0 |
上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者 的this指向运行时所在的作用域(即全局对象)。所以,3100毫秒之后,timer.s1被更新了3次,而timer.s2一次都没更新。
this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块 的this。正是因为它没有this,所以也就不能用作构造函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function foo() { setTimeout(() = >{ console.log('args:', arguments); }, 100); } foo(2, 4, 6, 8) // args: [2, 4, 6, 8] // 上面代码中,箭头函数内部的变量arguments,其实是函数foo的arguments变量。 (function() { return [(() = >this.x).bind({ x: 'inner' })()]; }).call({x: 'outer'}); // ['outer'] // 上面代码中,箭头函数没有自己的this,所以bind方法无效,内部的this指向外部的this。 |
尾递归优化
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。 递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用 帧,所以永远不会发生“栈溢出”错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
function factorial(n) { if (n === 1) return 1; return n * factorial(n - 1); } factorial(5) // 120 // 上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。 // 如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。 function factorial(n, total) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5, 1) // 120 // 还有一个比较著名的例子,就是计算fibonacci 数列,也能充分说明尾递归优化的重要性 // 如果是非尾递归的fibonacci 递归方法 function Fibonacci (n) { if ( n <= 1 ) {return 1}; return Fibonacci(n - 1) + Fibonacci(n - 2); } Fibonacci(10); // 89 // Fibonacci(100) // Fibonacci(500) // 堆栈溢出了 // 如果我们使用尾递归优化过的fibonacci 递归算法 function Fibonacci2(n, ac1 = 1, ac2 = 1) { if (n <= 1) { return ac2 }; return Fibonacci2(n - 1, ac2, ac1 + ac2); // Fibonacci2(1, 1, 2); // Fibonacci2(2, 1, 3); } Fibonacci2(100) // 573147844013817200000 Fibonacci2(1000) // 7.0330367711422765e+208 Fibonacci2(10000) // Infinity |
重点:递归优化手段
1、中间变量改为函数参数
2、柯里化(currying)
3、改为循环,蹦床函数(trampoline)可以将递归执行转为循环执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
function sum(x, y) { if (y > 0) { return sum(x + 1, y - 1); } else { return x; } } sum(1, 100000) // Uncaught RangeError: Maximum call stack size exceeded(…) // 优化1 // 蹦床函数(trampoline)可以将递归执行转为循环执行。 function trampoline(f) { while (f && f instanceof Function) { f = f(); } return f; } function sum(x, y) { if (y > 0) { // 每次都返回一个函数 return sum.bind(null, x + 1, y - 1); } else { return x; } } trampoline(sum(1, 100000)) // 100001 // 优化2,从调用栈的角度出发自己实现 function tco(f) { var value; var active = false; var accumulated = []; return function accumulator() { accumulated.push(arguments); if (!active) { active = true; while (accumulated.length) { value = f.apply(this, accumulated.shift()); } active = false; return value; } }; } var sum = tco(function(x, y) { if (y > 0) { return sum(x + 1, y - 1) } else { return x } }); sum(1, 100000) // 100001 |
set 和 map
set
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var s = new Set(); [2, 3, 5, 4, 5, 2, 2].map(x => s.add(x)); for (let i of s) { console.log(i); } // 2 3 5 4 console.log(s.size); // 4 var items = new Set([1, 2, 3, 4, 5]); var array = Array.from(items); // 去除数组的重复成员 [...new Set(array)] function dedupe(array) { return Array.from(new Set(array)); } |
Set实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。
add(value):添加某个值,返回Set结构本身。
delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
has(value):返回一个布尔值,表示该值是否为Set的成员。
clear():清除所有成员,没有返回值。
Set结构的实例有四个遍历方法,可以用于遍历成员。
keys():返回键名的遍历器
values():返回键值的遍历器
entries():返回键值对的遍历器
forEach():使用回调函数遍历每个成员
由于Set结构没有键名,只有键值(或者说键名和键值是同 一个值),所以key方法和value方法的行为完全一致。
需要特别指出的是,Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用Set保存一个回调函数列表,调用时就能保证按照添加顺序调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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} let a = new Set([1, 2, 3]); let b = new Set([4, 3, 2]); // 并集 let union = new Set([...a, ...b]); // Set {1, 2, 3, 4} // 交集 let intersect = new Set([...a].filter(x => b.has(x))); // set {2, 3} // 差集 let difference = new Set([...a].filter(x => !b.has(x))); // Set {1} // 重新赋值 // 方法一 let set = new Set([1, 2, 3]); set = new Set([...set].map(val => val * 2)); // set的值是2, 4, 6 // 方法二 let set = new Set([1, 2, 3]); set = new Set(Array.from(set, val => val * 2)); // set的值是2, 4, 6 |
WeakSet
WeakSet结构与Set类似,也是不重复的值的集合。但是,它与Set有两个区别。
首先,WeakSet的成员只能是对象,而不能是其他类型的值。
其次,WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收 机制会自动回收该对象所占用的内存,不考虑该对象还存在于WeakSet之中。这个特点意味着,无法引用WeakSet的成员,因此WeakSet是不可遍历的。
WeakSet没有size属性,没有办法遍历它的成员
1 2 3 4 5 6 7 8 9 |
var ws = new WeakSet(); ws.add(1) // TypeError: Invalid value used in weak set ws.add(Symbol()) // TypeError: invalid value used in weak set var b = [3, 4]; var ws = new WeakSet(b); // Uncaught TypeError: Invalid value used in weak set(…) var a = [[1,2], [3,4]]; var ws = new WeakSet(a); // 可以 |
WeakSet结构有以下三个方法。
WeakSet.prototype.add(value):向WeakSet实例添加一个新成员。
WeakSet.prototype.delete(value):清除WeakSet实例的指定成员。
WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在WeakSet实例之中。
WeakSet不能遍历,是因为成员都是弱引用,随时可能消失,遍历机制无法保证成员的存在,很可能刚刚遍历结束,成员就取不到了。WeakSet的一 个用处,是储存DOM节点,而不用担心这些节点从文档移除时,会引发内存泄漏。
下面代码保证了Foo的实例方法,只能在Foo的实例上调用。这里使用WeakSet的好处是,foos对实例的引用, 不会被计入内存回收机制,所以删除实 例的时候,不用考虑foos,也不会出现内存泄漏。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const foos = new WeakSet() class Foo { constructor() { foos.add(this) } method() { if (!foos.has(this)) { throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!'); } } } |
Map
Object结构提供了“字符串—值”的对应,Map结构提供了“值—值”的对应.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
var m = new Map(); var o = {p: 'Hello World'}; m.set(o, 'content') m.get(o) // "content" m.has(o) // true m.delete(o) // true m.has(o) // false var map = new Map([ ['name', '张三'], ['title', 'Author'] ]); map.size // 2 map.has('name') // true map.get('name') // "张三" map.has('title') // true map.get('title') // "Author" // 字符串true和布尔值true是两个不同的键。 var m = new Map([ [true, 'foo'], ['true', 'bar'] ]); m.get(true) // 'foo' m.get('true') // 'bar' |
注意,只有对同一个对象的引用,Map结构才将其视为同一个键。这一点要非常小心。
1 2 3 4 5 6 7 8 9 10 11 |
var map = new Map(); map.set(['a'], 555); map.get(['a']) // undefined var map = new Map(); var k1 = ['a']; var k2 = ['a']; map.set(k1, 111).set(k2, 222); map.get(k1) // 111 map.get(k2) // 222 |
由上可知,Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题.
如果Map的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map将其视为一个键,包括0和-0。另外,虽然NaN不严格相 等于自身,但Map将其视为同一个键。
1 2 3 4 5 |
let map = new Map(); map.set(NaN, 123); map.get(NaN) // 123 map.set(-0, 123); map.get(+0) // 123 |
Map原生提供三个遍历器生成函数和一个遍历方法。
keys():返回键名的遍历器。
values():返回键值的遍历器。
entries():返回所有成员的遍历器。
forEach():遍历Map的所有成员。
需要特别注意的是,Map的遍历顺序就是插入顺序
Map 转数组和对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let 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'] function strMapToObj(strMap) { let obj = Object.create(null); for (let [k,v] of strMap) { obj[k] = v; } return obj; } |
WeakMap
WeakMap结构与Map结构基本类似,唯一的区别是它只接受对象作为键名(null除外),不接受其他类型的值作为键名,而且键名所指向的对象,不计 入垃圾回收机制。十分类似于 weakset 和 set 的区别。
WeakMap与Map在API上的区别主要是两个,一是没有遍历操作(即没有key()、values()和entries()方法),也没有size属性;二是无法清空,即 不支持clear方法。这与WeakMap的键不被计入引用、被垃圾回收机制忽略有关。因此,WeakMap只有四个方法可 用:get()、set()、has()、delete()。
Iterator(遍历器)和 和for…of循环
Iterator(遍历器)的概念
JavaScript原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6又添加了Map和Set。这样就有了四种数据集合,用户还可以 组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。 遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成 遍历操作(即依次处理该数据结构的所有成员)。
Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一 种新的遍历命令for…of循环,Iterator接口主要供for…of消费。
Iterator的遍历过程是这样的。 (1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。 (2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。 (3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。 (4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。 每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成 员的值,done属性是一个布尔值,表示遍历是否结束。
任何部署了Iterator接口的对象,都可以用for…of循环遍历。Map结构原生支持Iterator接口,配合变量的解构赋值,获取键名和键值就非常方便。
1 2 3 4 5 6 7 8 9 10 11 |
var map = new Map(); map.set('first', 'hello'); map.set('second', 'world'); for (let [key, value] of map) { console.log(key + " is " + value); } // first is hello // second is world 如果只想获取键名,或者只想获取键值,可以写成下面这样。 // 获取键名 for (let [key] of map) { // ... } // 获取键值 for (let [,value] of map) { // ... } |
发表评论