22

es6 重点梳理第一篇(含字符,函数、正则、数值、Set 和 Map)

 5 years ago
source link: http://liguixing.com/archives/1389
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

声明,本文为个人在阅读阮老师的es6标准规范过程中,对一些个人比较觉得比较重要的知识点整理而成,如果需要了解和学习更多的es6规范,请移步至阮一峰老师的es6个人网站查看,地址: https://es6.ruanyifeng.com/

字符的扩展

codePointAt() 与 fromCodePoint()

codePointAt 方法主要用于 js 中的四个字节的字符处理

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

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表达式,可以进行运算,以及引用对象属性。

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)。

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编 码。

/^\uD83D/u.test('\uD83D\uDC2A')
// false 
/^\uD83D/.test('\uD83D\uDC2A') 
// true

y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配 就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。

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 方法对比

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+(?!%)/。

/\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+/。

/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill')  // ["100"] 
/(?<!\$)\d+/.exec('it’s is worth about €90')                // ["90"]

上面的例子中,”后行断言”的括号之中的部分((?<=$)),也是不计入返回结果。 “后行断言”的实现,需要先匹配/(?<=y)x/的x,然后再回到左边,匹配y的部分。这种”先右后左”的执行顺序,与所有其他正则操作相反,导致了一些 不符合预期的行为。 首先,”后行断言“的组匹配,与正常情况下结果是不一样的。

/(?<=(\d+)(\d+))$/.exec('1053') // ["", "1", "053"] 
/^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3"]

上面代码中,需要捕捉两个组匹配。没有”后行断言”时,第一个括号是贪婪模式,第二个括号只能捕获一个字符,所以结果是105和3。而”后行断 言”时,由于执行顺序是从右到左,第二个括号是贪婪模式,第一个括号只能捕获一个字符,所以结果是1和053。 其次,”后行断言”的反斜杠引用,也与通常的顺序相反,必须放在对应的那个括号之前。

/(?<=(o)d\1)r/.exec('hodor')  // null 
/(?<=\1d(o))r/.exec('hodor')  // ["r", "o"]

上面代码中,如果后行断言的反斜杠引用(\1)放在括号的后面,就不会得到匹配结果,必须放在前面才可以。

字符串的转义,让其成为正则模式。

function escapeRegExp(str) {  
    return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); 
}
let str = '/path/to/resource.html?search=query';
escapeRegExp(str) 
// "\/path\/to\/resource\.html\?search=query"

数值的扩展

  1. 添加 2 进制和 8 进制

ES6提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。

  1. 添加 Number.isFinite(), Number.isNaN(),Number.isInteger()
// 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);
  1. Number.parseInt(), Number.parseFloat()

ES6将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。

  1. Number.EPSILON,Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER
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
  1. 安全整数和 安全整数和Number.isSafeInteger()
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方法转为数组,而此 时扩展运算符就无法转换。

// 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

函数默认参数

// 与解构赋值默认值结合使用 
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函数。

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。

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,所以也就不能用作构造函数。

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)。但对于尾递归来说,由于只存在一个调用 帧,所以永远不会发生“栈溢出”错误。

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)可以将递归执行转为循环执行

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

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保存一个回调函数列表,调用时就能保证按照添加顺序调用。

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属性,没有办法遍历它的成员

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,也不会出现内存泄漏。

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结构提供了“值—值”的对应.

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结构才将其视为同一个键。这一点要非常小心。

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将其视为同一个键。

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 转数组和对象

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接口,配合变量的解构赋值,获取键名和键值就非常方便。

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) {  // ... }

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK