gistfile1.txt 函数/** * Created by Administrator on 2017/11/12. */function log(x, y) { y = y||'world' //检查函数log的参数y有没有赋值,如果没有,则指定默认值为World。这种写法的缺点在于,如果参数y赋值
函数 /** * Created by Administrator on 2017/11/12. */ function log(x, y) { y = y||'world' //检查函数log的参数y有没有赋值,如果没有,则指定默认值为World。这种写法的缺点在于,如果参数y赋值了,但是对应的布尔值为false,则该赋值不起作用。就像上面代码的最后一行,参数y等于空字符,结果被改为默认值。 console.log(x,y) } log('hello') //hello world log('hello','china') //hello china log('hello','') //hello world // ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。简洁多了 function newLog(x, y = 'world' ) { console.log(x,y) } newLog('hello','') //hello world function fetch(url, {body = '', method = 'GET', headers = {}}) { console.log(method) } fetch('http://example.com',{}) //GET // fetch('http://example.com') //error function m1({x = 0, y = 0}={}) { console.log(x,y) } m1() //0 0 m1({x:2,y:2}) //不可以用等号 必须用冒号 2 2 m1({x:3}) //3 0 function m2({x,y}={x:2,y:2}) { console.log(x,y) } m2() //2 2 m2({x:3}) //3 undefined // 函数的 length 属性 length属性的返回值,等于函数的参数个数减去指定了默认值的参数个数 console.log((function (a) {}).length) // 1 console.log((function (a=5) {}).length) //0 console.log((function (a, b = 1, c = 3) {}).length) //1 // 这是因为length属性的含义是,该函数预期传入的参数个数。 // 如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。 console.log((function (a = 1, b, c) {}).length) //0 console.log((function (a, b = 1, c) {}).length) // 1 // 作用域 var a = 2 function f(x,y=x) { //调用函数f时,参数形成一个单独的作用域。在这个作用域里面,默认值变量x指向第一个参数x,而不是全局变量x console.log(y) } f(3) //3 // var aa = 1 // function ff(aa=aa) { //参数x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错”x 未定义“ // } // ff() //aa is not defined let foo = 'outer' function bar(func=()=>foo) { let foo = 'inner' console.log(func()) //函数bar的参数func的默认值是一个匿名函数,返回值为变量foo } bar() //outer //其实编辑器很智能 他能识别 要是一个值就会都会亮起来 var xx = 1 function fuza(xx,y=function () {xx=2}) { var xx = 3 y() console.log(xx) } fuza() //3 //ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中 function add(...values) { let sum = 0 for (var val of values){ sum += val } return sum } console.log(add(2,5,3)) //10 function sortNumbers() { return Array.prototype.slice.call(arguments).sort() } console.log(sortNumbers(2,3,4)) //[2,3,4] //第二种写法 const sortNumbers2 = (...numbers) => console.log(numbers.sort()) sortNumbers2(2,3,4,4) //[ 2, 3, 4, 4 ] // 注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。 // 报错 // function f(a, ...b, c) { // // ... // } // ES6 允许使用“箭头”(=>)定义函数。 // 由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。 // let getTempItem = id => {id:2,name:'xuliang'} //error let getTempItem = id => ({id:2,name:'xuliang'}) // 箭头函数的一个用处是简化回调函数。 // [1,2,3].map(function (x) { // return x*x // }) [1,2,3].map(x=>x*x) function fooo() { //setTimeout(code,millisec) 用于在指定的毫秒数后调用函数或计算表达式。 setTimeout(()=>{console.log('id:',this.id)},100) } var id = 100 //call([thisObj[,arg1[, arg2[, [,.argN]]]]]) // call 方法可以用来代替另一个对象调用一个方法。call 方法可将一个函数的对象上下文从初始的上下文改变为由 thisObj 指定的新对象。 // 如果没有提供 thisObj 参数,那么 Global 对象被用作 thisObj fooo.call({id:42}) //id: 42 // fooo({id:42}) //id undefined //setInterval(code,millisec[,"lang"]) setInterval() 方法会不停地调用函数,直到 clearInterval() 被调用或窗口被关闭。由 setInterval() 返回的 ID 值可用作 clearInterval() 方法的参数。 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) // s1: 3 setTimeout(()=>console.log('s2:',timer.s2),3100) //s2: 0 //Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。所以,3100毫秒之后,timer.s1被更新了3次,而timer.s2一次都没更新。 // 箭头函数可以让this指向固定化,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。 function foooo() { return ()=>{ return ()=>{ return ()=>{ console.log('id:',this.id) } } } } var fffff = fooo.call({id:1}) //这样调用也是一种方式 // var t1 = fffff.call({id:2})()() /** * Created by Administrator on 2017/11/13. */ // 箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(call、apply、bind)。但是,箭头函数并不适用于所有场合,所以现在有一个提案,提出了“函数绑定”(function bind)运算符,用来取代call、apply、bind调用。 // 函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。 // 双冒号运算符的运算结果,还是一个对象,因此可以采用链式写法。 // 尾递归 // 递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。 // 上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。 function factorial(n) { if (n===1) return 1 else return n*factorial(n-1) } // 如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。 function factorial2(n, total) { if(n===1) return total else return factorial2(n-1,n*total) } console.log(factorial2(5,1)) //120 // 还有一个比较著名的例子,就是计算 Fibonacci 数列,也能充分说明尾递归优化的重要性。 // 非尾递归的 Fibonacci 数列实现如下。 function Fibonacci(n) { if (n<=1){return 1} return Fibonacci(n-1)+Fibonacci(n-2) } console.log(Fibonacci(10)) // console.log(Fibonacci(100)) //// 堆栈真的溢出 function Fibonacci2(n, ac1 = 1, ac2 = 1) { //采用 ES6 的函数默认值变得简洁 可以所以调用时不用提供这个值 if(n<=1){return ac2} return Fibonacci2(n-1,ac1,ac2+ac1) } console.log(Fibonacci2(100)) //100 console.log(Fibonacci2(1000)) //1000 //“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存。 // 以下三种情况,都不属于尾调用。 // // // 情况一 // function f(x){ // let y = g(x); // return y; // } // // // 情况二 // function f(x){ // return g(x) + 1; // } // // // 情况三 // function f(x){ // g(x); // } // 尾递归优化只在严格模式下生效,那么正常模式下,或者那些不支持该功能的环境中,有没有办法也使用尾递归优化呢?回答是可以的,就是自己实现尾递归优化。 // // 它的原理非常简单。尾递归之所以需要优化,原因是调用栈太多,造成溢出,那么只要减少调用栈,就不会溢出。怎么做可以减少调用栈呢?就是采用“循环”换掉“递归”。 // // 下面是一个正常的递归函数。 数组 /** * Created by Administrator on 2017/11/13. */ // 扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列 console.log(...[1,2,3]) // 该运算符主要用于函数调用。 // 扩展运算符后面还可以放置表达式。 const arr = [...(true?['a']:[])] // 由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。 // 数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。 const a1 = [1, 2]; const a2 = a1; // a2[0] = 2; // a1 // [2, 2] // 扩展运算符提供了数组合并的新写法。 [...arr1, ...arr2, ...arr3] // 扩展运算符可以与解构赋值结合起来,用于生成数组。 [a, ...rest] = list // 扩展运算符还可以将字符串转为真正的数组。[...'hello'] // 扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。 let map = new Map([ [1,'one'], [2,'two'], [3,'three'] ]) let arre = [...map.keys()] console.log(arre) //[ 1, 2, 3 ] //变量go是一个 Generator 函数,执行后返回的是一个遍历器对象,对这个遍历器对象执行扩展运算符,就会将内部遍历得到的值,转为一个数组。 const go = function* () { yield 1 yield 2 yield 3 } console.log(...go()) //1 2 3 // 如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错。 const obj = {a:1,b:2} //原来这是对象 不是map // console.log(...obj.keys) //error // console.log(...obj) //error /** * Created by Administrator on 2017/11/13. */ // 扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列 console.log(...[1,2,3]) // 该运算符主要用于函数调用。 // 扩展运算符后面还可以放置表达式。 const arr = [...(true?['a']:[])] // 由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。 // 数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。 const a1 = [1, 2]; const a2 = a1; // a2[0] = 2; // a1 // [2, 2] // 扩展运算符提供了数组合并的新写法。 [...arr1, ...arr2, ...arr3] // 扩展运算符可以与解构赋值结合起来,用于生成数组。 [a, ...rest] = list // 扩展运算符还可以将字符串转为真正的数组。[...'hello'] // 扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。 let map = new Map([ [1,'one'], [2,'two'], [3,'three'] ]) let arre = [...map.keys()] console.log(arre) //[ 1, 2, 3 ] //变量go是一个 Generator 函数,执行后返回的是一个遍历器对象,对这个遍历器对象执行扩展运算符,就会将内部遍历得到的值,转为一个数组。 const go = function* () { yield 1 yield 2 yield 3 } console.log(...go()) //1 2 3 // 如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错。 const obj = {a:1,b:2} //原来这是对象 不是map // console.log(...obj.keys) //error // console.log(...obj) //error // Array.from() 可以通过以下方式来创建数组对象: // 伪数组对象(拥有一个 length 属性和若干索引属性的任意对象) // 可迭代对象(可以获取对象中的元素,如 Map和 Set 等) // 扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。Array.from方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。 let arrayLike = { "0":"a", "1":"b", "2":"c", length:3 } let arr2 = Array.from(arrayLike) console.log(arr2) //[ 'a', 'b', 'c' ] // 只要是部署了Iterator接口的数据结构,Array.from都能将其转为数组。 // Array.from('foo'); // 实际应用中,常见的类似数组的对象是DOM操作返回的NodeList集合,以及函数内部的arguments对象。Array.from都可以将它们转为真正的数组。 // var args = Array.from(arguments); // Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。 Array.from(arrayLike, x => x * x); // 等同于 Array.from(arrayLike).map(x => x * x); Array.from([1, 2, 3], (x) => x * x) // [1, 4, 9] // Array.from()的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种Unicode字符,可以避免JavaScript将大于\uFFFF的Unicode字符,算作两个字符的bug。 function countSymbols(string) { return Array.from(string).length; } // Array.of方法用于将一组值,转换为数组。 console.log(Array.of(3,19,18)) //[ 3, 19, 18 ] console.log(Array.of(3).length) //1 // Array.of基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一 console.log(Array.of(undefined)) //[ undefined ] // 数组实例的 copyWithin() console.log( [1,2,3,4,5].copyWithin(0,3) ) //[ 4, 5, 3, 4, 5 ] console.log( [1,2,3,4,5].copyWithin(0,3,4) ) //[ 4, 2, 3, 4, 5 ] // 数组实例的 find() 和 findIndex() ES6都不支持啊 // [1,4,-5].find( (n) => n<0 ) // 数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1 // [1,5,15,10].findIndex(function (value, index, arr) { // return value>9 // }) //另外,这两个方法都可以发现NaN,弥补了数组的IndexOf方法的不足。 console.log( [NaN].indexOf(NaN) ) //-1 // fill方法使用给定值,填充一个数组。 console.log( ['a','b','c'].fill(7) ) //[ 7, 7, 7 ] console.log( ['a','b','c'].fill(7,1,2) ) //[ 'a', 7, 'c' ] // 数组实例的 entries(),keys() 和 values() for(let index of ['a','b'].keys()){ console.log( index ) // 0 1 }for (let [index,elem] of ['a','b'].entries()){ console.log(index,elem) } // 数组实例的 includes() console.log( [1,2,3].includes(1) ) //true console.log( [1,'a',NaN].includes('a') ) //true // 另外,Map 和 Set 数据结构有一个has方法,需要注意与includes区分。 // Map 结构的has方法,是用来查找键名的,比如 Set 结构的has方法,是用来查找值的 // forEach方法 // [,'a'].forEach((x,i) => console.log(i)); // 1 // ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。 // // forEach(), filter(), every() 和some()都会跳过空位。 // map()会跳过空位,但会保留这个值 // join()和toString()会将空位视为undefined,而undefined和null会被处理成空字符串。 // // filter方法 // ['a',,'b'].filter(x => true) // ['a','b'] // // // every方法 // [,'a'].every(x => x==='a') // true // // // some方法 // [,'a'].some(x => x !== 'a') // false // // // map方法 // [,'a'].map(x => 1) // [,1] // // // join方法 // [,'a',undefined,null].join('#') // "#a##" // // // toString方法 // [,'a',undefined,null].toString() // ",a,," 对象 /** * Created by Administrator on 2017/11/13. */ const foo = 'bar' const baz = {foo} console.log(baz) //{ foo: 'bar' } const bazz = {foo:'baz'} console.log( bazz) //{ foo: 'baz' } function f(x, y) { return {x:x,y:y} } console.log( f(1,2) ) //{ x: 1, y: 2 } const o = { method(){ return "hello!" } } const oo = { method:function () { return "hello" } } // ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内 let lastword = 'last word'; const aa = { 'fisrst':'hello', [lastword]:'world', ['a'+'bc']:123 } console.log( aa['fisrst'] ) //hello 冒号不能少 console.log( aa[lastword] ) // world 冒号不能少 console.log( aa['last word'] ) //world console.log( aa['lastword'] ) //undefined console.log( aa['abc'] ) //123 // 方法的 name 属性 函数的name属性,返回函数名。对象方法也是函数,因此也有name属性。 const person = { sayName(){ console.log('hello') } } console.log(person.sayName.name) //sayName // ES5 比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。 // ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。 // 不同之处只有两个:一是+0不等于-0,二是NaN等于自身。 // +0 === -0 //true // NaN === NaN // false // // Object.is(+0, -0) // false // Object.is(NaN, NaN) // true // Object.assign() Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。 // 注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。 const target = {a:1} const source1 = { b:2 } const source2 = { c:3 } Object.assign(target,source1,source2) console.log(target) //{ a: 1, b: 2, c: 3 } // Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。 const obj1 = {a:{b:2}} const obj2 = Object.assign({},obj1) obj1.a.b = 3 console.log( obj2.a.b ) //3 // 同名属性的替换/ const target22 = {a:{b:'c',d:'e'}} const source22 = {a:{b:'hello'}} console.log( Object.assign(target22,source22) ) //{ a: { b: 'hello' } } // 常见用途 // Object.assign方法有很多用处。 // (1)为对象添加属性 class Point{ constructor(x,y){ Object.assign(this,{x,y}) //Point.x 确实可以取得到 } } // (2)为对象添加方法 // Object.assign(SomeClass.prototype,{ //很简明的写法 // someMethod(arg1,arg2){}, // anotherMethod(){} // }) // (3)克隆对象 function clone(origin) { return Object.assign({},origin) } // 上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。 // 不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。 function clone2(origin) { let originProto = Object.getPrototypeOf(origin); return Object.assign(Object.create(originProto), origin); } // (4)合并多个对象 const merge = (target,...source222)=>Object.assign(target,...source222) // 6 属性的可枚举性和遍历 // 对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。 let objjj = {foo:123} console.log( Object.getOwnPropertyDescriptor(objjj,'foo') ) // { value: 123, // writable: true, // enumerable: true, // configurable: true } // 描述对象的enumerable属性,称为”可枚举性“,如果该属性为false,就表示某些操作会忽略当前属性。 // 目前,有四个操作会忽略enumerable为false的属性。 // // for...in循环:只遍历对象自身的和继承的可枚举的属性。 // Object.keys():返回对象自身的所有可枚举的属性的键名。 // JSON.stringify():只串行化对象自身的可枚举的属性。 // Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。 // 属性的遍历 // ES6 一共有 5 种方法可以遍历对象的属性。 // // (1)for...in // // for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。 // // (2)Object.keys(obj) // // Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。 // // (3)Object.getOwnPropertyNames(obj) // // Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。 // // (4)Object.getOwnPropertySymbols(obj) // // Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。 // // (5)Reflect.ownKeys(obj) // // Reflect.ownKeys返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。 // // 以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。 // // 首先遍历所有数值键,按照数值升序排列。 // 其次遍历所有字符串键,按照加入时间升序排列。 // 最后遍历所有 Symbol 键,按照加入时间升序排列。 // Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 }) // // ['2', '10', 'b', 'a', Symbol()] // 上面代码中,Reflect.ownKeys方法返回一个数组,包含了参数对象的所有属性。这个数组的属性次序是这样的,首先是数值属性2和10,其次是字符串属性b和a,最后是 Symbol 属性。 // super 关键字 // 我们知道,this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。 const proto = { foo:'hello' } const objjjjj = { find(){ return super.foo } } Object.setPrototypeOf(objjjjj,proto) console.log( objjjjj.find() ) //hello // Object.keys(),Object.values(),Object.entries() /** * Created by Administrator on 2017/11/13. */ // ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。 // Set 本身是一个构造函数,用来生成 Set 数据结构。 const sett = new Set([1,2,3,4,4]) console.log( ...sett ) //1 2 3 4 // Set 结构的实例有以下属性。 // // Set.prototype.constructor:构造函数,默认就是Set函数。 // Set.prototype.size:返回Set实例的成员总数。 // Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。 // // add(value):添加某个值,返回 Set 结构本身。 // delete(value):删除某个值,返回一个布尔值,表示删除是否成功。 // has(value):返回一个布尔值,表示该值是否为Set的成员。 // clear():清除所有成员,没有返回值。 // 上面这些属性和方法的实例如下。 sett.add(1).add(2).add(2) console.log(sett) //Set { 1, 2, 3, 4 } // // 遍历操作 // Set 结构的实例有四个遍历方法,可以用于遍历成员。 // // keys():返回键名的遍历器 // values():返回键值的遍历器 // entries():返回键值对的遍历器 // forEach():使用回调函数遍历每个成员 // 需要特别指出的是,Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。 // // 因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference) let a = new Set([1,2,3]) let b = new Set([2,3,4]) //bing集 let union = new Set([...a,...b]) //jiaoji let intersect = new Set([...a].filter(x => b.has(x))) //差集 let difference = new Set([...a].filter(x => b.has(x))) // 如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用Array.from方法。 // // 方法一 // 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 // Map // JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。 // 为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。 const map = new Map([ ['name','张三'], ['title','Author'] ]) console.log(map) //Map { 'name' => '张三', 'title' => 'Author' } console.log(map.size) //2 map.has('name') // true map.get('name') // "张三" map.has('title') // true map.get('title') // "Author" // 由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。 const mappp = new Map(); const k1 = ['a']; const k2 = ['a']; mappp .set(k1, 111) .set(k2, 222); console.log(mappp.get(k1) )// 111 console.log(mappp.get(k2)) // 222 // 如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0和-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefined和null也是两个不同的键。虽然NaN不严格相等于自身,但 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 // 遍历方法 // Map 结构原生提供三个遍历器生成函数和一个遍历方法。 // // keys():返回键名的遍历器。 // values():返回键值的遍历器。 // entries():返回所有成员的遍历器。 // forEach():遍历 Map 的所有成员。 与其他数据结构的互相转换 (1)Map 转为数组 前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(...)。 const myMap = new Map() .set(true, 7) .set({foo: 3}, ['abc']); [...myMap] // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ] (2)数组 转为 Map 将数组传入 Map 构造函数,就可以转为 Map。 new Map([ [true, 7], [{foo: 3}, ['abc']] ]) // Map { // true => 7, // Object {foo: 3} => ['abc'] // } (3)Map 转为对象 如果所有 Map 的键都是字符串,它可以转为对象。 function strMapToObj(strMap) { let obj = Object.create(null); for (let [k,v] of strMap) { obj[k] = v; } return obj; } const myMap = new Map() .set('yes', true) .set('no', false); strMapToObj(myMap) // { yes: true, no: false } (4)对象转为 Map function objToStrMap(obj) { let strMap = new Map(); for (let k of Object.keys(obj)) { strMap.set(k, obj[k]); } return strMap; } objToStrMap({yes: true, no: false}) // Map {"yes" => true, "no" => false} (5)Map 转为 JSON Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。 function strMapToJson(strMap) { return JSON.stringify(strMapToObj(strMap)); } let myMap = new Map().set('yes', true).set('no', false); strMapToJson(myMap) // '{"yes":true,"no":false}' 另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。 function mapToArrayJson(map) { return JSON.stringify([...map]); } let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']); mapToArrayJson(myMap) // '[[true,7],[{"foo":3},["abc"]]]' (6)JSON 转为 Map JSON 转为 Map,正常情况下,所有键名都是字符串。 function jsonToStrMap(jsonStr) { return objToStrMap(JSON.parse(jsonStr)); } jsonToStrMap('{"yes": true, "no": false}') // Map {'yes' => true, 'no' => false} 但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是数组转为 JSON 的逆操作。 function jsonToMap(jsonStr) { return new Map(JSON.parse(jsonStr)); } jsonToMap('[[true,7],[{"foo":3},["abc"]]]') // Map {true => 7, Object {foo: 3} => ['abc']} WeakMap 含义 WeakMap结构与Map结构类似,也是用于生成键值对的集合。 // WeakMap 可以使用 set 方法添加成员 const wm1 = new WeakMap(); const key = {foo: 1}; wm1.set(key, 2); wm1.get(key) // 2 // WeakMap 也可以接受一个数组, // 作为构造函数的参数 const k1 = [1, 2, 3]; const k2 = [4, 5, 6]; const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]); wm2.get(k2) // "bar" WeakMap与Map的区别有两点。 首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。 const map = new WeakMap(); map.set(1, 2) // TypeError: 1 is not an object! map.set(Symbol(), 2) // TypeError: Invalid value used as weak map key map.set(null, 2) // TypeError: Invalid value used as weak map key 上面代码中,如果将数值1和Symbol值作为 WeakMap 的键名,都会报错。 其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。 WeakMap的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。请看下面的例子。 const e1 = document.getElementById('foo'); const e2 = document.getElementById('bar'); const arr = [ [e1, 'foo 元素'], [e2, 'bar 元素'], ]; 上面代码中,e1和e2是两个对象,我们通过arr数组对这两个对象添加一些文字说明。这就形成了arr对e1和e2的引用。 一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放e1和e2占用的内存。 // 不需要 e1 和 e2 的时候 // 必须手动删除引用 arr [0] = null; arr [1] = null; 上面这样的写法显然很不方便。一旦忘了写,就会造成内存泄露。 WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。 基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。一个典型应用场景是,在网页的 DOM 元素上添加数据,就可以使用WeakMap结构。当该 DOM 元素被清除,其所对应的WeakMap记录就会自动被移除。 const wm = new WeakMap(); const element = document.getElementById('example'); wm.set(element, 'some information'); wm.get(element) // "some information" 上面代码中,先新建一个 Weakmap 实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时,WeakMap 里面对element的引用就是弱引用,不会被计入垃圾回收机制。 也就是说,上面的 DOM 节点对象的引用计数是1,而不是2。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对,也会自动消失。 总之,WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。 注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。 const wm = new WeakMap(); let key = {}; let obj = {foo: 1}; wm.set(key, obj); obj = null; wm.get(key) // Object {foo: 1} 上面代码中,键值obj是正常引用。所以,即使在 WeakMap 外部消除了obj的引用,WeakMap 内部的引用依然存在。 WeakMap 的语法 WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有key()、values()和entries()方法),也没有size属性。因为没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。这一刻可以取到键名,下一刻垃圾回收机制突然运行了,这个键名就没了,为了防止出现不确定性,就统一规定不能取到键名。二是无法清空,即不支持clear方法。因此,WeakMap只有四个方法可用:get()、set()、has()、delete()。 const wm = new WeakMap(); // size、forEach、clear 方法都不存在 wm.size // undefined wm.forEach // undefined wm.clear // undefined