從引用聊到深淺拷貝,從深拷貝過渡到ES6新數據結構Map及Set,再到另一個map即Array.map()和與其類似的Array.flatMap(),中間會有其他相關話題,例如Object.freeze()與Object.assign()等等。前言一邊複習一邊學習,分清引用與深淺拷貝的區別,並實現淺 ...
從引用聊到深淺拷貝,從深拷貝過渡到ES6新數據結構Map及Set,再到另一個map即
Array.map()
和與其類似的Array.flatMap()
,中間會有其他相關話題,例如Object.freeze()
與Object.assign()
等等。
前言
一邊複習一邊學習,分清引用與深淺拷貝的區別,並實現淺拷貝與深拷貝,之後通過對深拷貝的瞭解,拓展到ES6新數據結構Map及Set的介紹,再引入對另一個數組的map方法的使用與類似數組遍歷方法的使用。通過一條隱式鏈將一長串知識點串聯介紹,可能會有點雜,但也會有對各知識點不同之處有明顯區分,達到更好的記憶與理解。
引用、淺拷貝及深拷貝
引用
通常在介紹深拷貝之前,作為引子我們會看見類似以下例子:
var testObj = { name: 'currName' } var secObj = testObj secObj.name = 'changedName' console.log(testObj) // { name: 'changedName' } 複製代碼
這其實就是一種引用,對於複雜數據結構,為了節省存儲資源,符號 “=” 其實並不是將值賦給新建的變數,而是做了一個地址引用,使其指向原來存儲在堆中的數據的地址,此時testObj與secObj都指向同一個地址,因此在修改secObj的數據內容時,即是對其指向的原有數據進行修改。
對於數組有相似的引用情況,代碼如下:
var testArr = [0, [1, 2]] var secArr = testArr secArr[0] = 'x' console.log(testArr) // [ 'x', [ 1, 2 ] ] 複製代碼
淺拷貝
對於淺拷貝,其與引用的區別,我們一邊實現淺拷貝,之後進行對比再解釋,實現如下:
function shallowCopy (obj) { var retObj = {} for (const key in obj) { if (obj.hasOwnProperty(key)) { retObj[key] = obj[key]; } } return retObj } var testObj = { 'name': 'currName', 'nums': [1, [2, 3]], 'objs': { 'innerobj': 'content' } } var secObj = shallowCopy(testObj) secObj.name = 'changedName' secObj.nums[0] = '一' secObj.nums[1] = ['二', '三'] console.log(testObj) // { name: 'currName', // nums: [ '一', [ '二', '三' ] ], // objs: { innerObj: 'changedContent' } } console.log(secObj) // { name: 'changedName', // nums: [ '一', [ '二', '三' ] ], // objs: { innerObj: 'changedContent' } } 複製代碼
從上例可以看出經過淺拷貝後得到的對象,對於第一層數據其修改後已經不能影響之前的數據,但對於內部還存在迭代器的數據屬性,還是有引用情況的存在,所以後者對這些屬性的修改,依舊會影響前者中這些屬性的內容。
引用與淺拷貝的區別就在於: 對第一層數據是否依舊修改後互相影響。
淺拷貝相關方法
Object.assign()
assign方法效果類似於在數組中的concat拼接方法,其可以將源對象中可枚舉屬性進行複製到目標對象上,並返回目標對象,該方法中第一個參數便就是目標對象,其他參數為源對象。因此該方法我們定義源對象為空對象時便可以在對拷貝的實現中使用,但需要註意的是Object.assign()其方法自身實行的便是淺拷貝,而不是深拷貝,因此通過該方法實現的拷貝只能是淺拷貝。
實現淺拷貝代碼如下:
var testObj = { 'name': 'currName', 'nums': [1, [2, 3]], 'objs': { 'innerObj': 'content' } } var secObj = Object.assign({}, testObj) secObj.name = 'changedName' secObj.nums[0] = '一' secObj.nums[1] = ['二', '三'] secObj.objs['innerObj'] = 'changedContent' console.log(testObj) // { name: 'currName', // nums: [ '一', [ '二', '三' ] ], // objs: { innerObj: 'changedContent' } } console.log(secObj) // { name: 'changedName', // nums: [ '一', [ '二', '三' ] ], // objs: { innerObj: 'changedContent' } } 複製代碼
Object.freeze()
freeze方法其效果在有一定程度與淺拷貝相同,但效果上還要比拷貝多上一層,即freeze凍結,但因為該方法自身 內部屬性,該方法的名稱又可以稱為“淺凍結”,對於第一層數據,如淺拷貝一般,不可被新對象改變,但被freeze方法凍結過的對象,其自身也無法添加、刪除或修改其第一層數據,但因為“淺凍結”這名稱中淺的這一明顯屬性,freeze方法對於內部如果存在更深層的數據,是可以被自身修改,且也會被“=”號所引用給新的變數。
簡單使用如下:
var testObj = { 'name': 'currName', 'nums': [1, [2, 3]], 'objs': { 'innerObj': 'content' } } var secObj = Object.freeze(testObj) secObj.name = 'changedName' secObj.nums[0] = '一' secObj.nums[1] = ['二', '三'] secObj.objs['innerObj'] = 'changedContent' secObj.age = 18 delete secObj.name console.log(testObj) // { name: 'currName', // nums: [ '一', [ '二', '三' ] ], // objs: { innerObj: 'changedContent' } } console.log(secObj) // { name: 'currName', // nums: [ '一', [ '二', '三' ] ], // objs: { innerObj: 'changedContent' } } 複製代碼
深拷貝
接上面對淺拷貝的介紹,很容易就可以想到深拷貝便是在淺拷貝的基礎上,讓內部存在更深層數據的對象,不止第一層不能改變原有數據,內部更深層次數據修改時也不能使原有數據改變,即消除了數據中所有存在引用的情況。通過對淺拷貝的實現,我們很容易就想到通過遞歸的方法對深拷貝進行實現。
以下就是通過遞歸實現深拷貝的過程:
Version 1: 對於深拷貝,因為存在數組與對象互相嵌套的問題,第一個版本先簡單統一處理對象的深拷貝,不深究數組對象的存在。
function deepCopy(content) { var retObj = {} for (const key in content) { if (content.hasOwnProperty(key)) { retObj[key] = typeof content[key] === 'object' ? deepCopy(content[key]) : content[key]; } } return retObj } var testObj = { 'name': 'currName', 'nums': [1, [2, 3]], 'objs': { 'innerObj': 'content' } } var secObj = deepCopy(testObj) secObj.name = 'changedName' secObj.nums[0] = '一' secObj.nums[1] = ['二', '三'] secObj.objs['innerObj'] = 'changedContent' secObj.age = 18 console.log(testObj) // { name: 'currName', // nums: [ 1, [ 2, 3 ] ], // objs: { innerObj: 'content' } } console.log(secObj) // { name: 'changedName', // nums: { '0': '一', '1': [ '二', '三' ] }, // objs: { innerObj: 'changedContent' }, // age: 18 } 複製代碼
Version 2: 完善數組與對象組合嵌套的情況
此時對於內部存在的數組來說,會被轉化為對象,鍵為數組的下標,值為數組的值,被存儲在新的對象中,因此有了我們完善的第二版。
function deepCopy (obj) { var tempTool = Array.isArray(obj) ? [] : {} for (const key in obj) { if (obj.hasOwnProperty(key)) { tempTool[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : Array.isArray(obj) ? Array.prototype.concat(obj[key]) : obj[key]; } } return tempTool } var testObj = { 'name': 'currName', 'nums': [1, [2, 3]], 'objs': { 'innerObj': 'content' } } var secObj = deepCopy(testObj) secObj.name = 'changedName' secObj.nums[0] = '一' secObj.nums[1] = ['二', '三'] secObj.objs['innerObj'] = 'changedContent' secObj.age = 18 console.log(testObj) // { name: 'currName', // nums: [ 1, [ 2, 3 ] ], // objs: { innerObj: 'content' } } console.log(secObj) // { name: 'changedName', // nums: [ '一', [ '二', '三' ] ], // objs: { innerObj: 'changedContent' }, // age: 18 } 複製代碼
ES6中 Map、Set
Map
對於Hash結構 即 鍵值對的集合,Object對象只能用字元串作為key值,在使用上有很大的限制,ES6提供的新的數據結構Map相對於Object對象,其“鍵”的範圍不限於字元串類型,實現了“值-值”的對應,使用上可以有更廣泛的運用。但Map在賦值時,只能接受如數組一般有lterator介面且每個成員都是雙元素的數組的數據結構作為參數,該數組成員是一個個表示鍵值對的數組,之外就只能通過Map自身set方法添加成員。
所以以下我們先介紹將對象轉為Map的方法,再對Map自身方法做一個簡單介紹,本節最後介紹一個Map的運用場景
Object轉為Map方法:
function objToMap (object) { let map = new Map() for (const key in object) { if (object.hasOwnProperty(key)) { map.set(key, object[key]) } } return map } var testObj = { 'name': 'currName', 'nums': [1, [2, 3]], 'objs': { 'innerObj': 'content' } } let map = objToMap(testObj) map.set('name', 'changedName') console.log(testObj) // { name: 'currName', // nums: [ 1, [ 2, 3 ] ], // objs: { innerObj: 'content' } } console.log(map) // Map { // 'name' => 'changedName', // 'nums' => [ 1, [ 2, 3 ] ], // 'objs' => { innerObj: 'content' } } 複製代碼
Map自身方法介紹
含增刪改查方法:set、get、has、delete;
遍歷方法:keys、values、entries、forEach;
其他方法:size、clear。
需要註意的是forEach方法還可以接受第二個參數,改變第一個參數即回調函數的內部this指向。
let map = new Map([ ['name', 'currName'], ['nums', [1, [2, 3]]], ['objs', {'innerObj': 'content'}] ]) // 增 刪 改 查 map.set('test', 'testContent') map.delete('objs') map.set('name', 'changedName') console.log(map.get('nums')) // [ 1, [ 2, 3 ] ] console.log(map.has('nums')) // true console.log(map) // Map { // 'name' => 'changedName', // 'nums' => [ 1, [ 2, 3 ] ], // 'test' => 'testContent' } // 遍歷方法 console.log(map.keys()) // [Map Iterator] { 'name', 'nums', 'test' } console.log(map.values()) // [Map Iterator] { 'changedName', [ 1, [ 2, 3 ] ], 'testContent' } console.log(map.entries()) // [Map Iterator] { // [ 'name', 'changedName' ], // [ 'nums', [ 1, [ 2, 3 ] ] ], // [ 'test', 'testContent' ] } const testObj = { objName: 'objName' } map.forEach(function (value, key) { console.log(key, value, this.objName) // name changedName objName // nums [ 1, [ 2, 3 ] ] objName // test testContent objName }, testObj) // 其他方法 console.log(map.size) // 3 console.log(map) // Map { // 'name' => 'changedName', // 'nums' => [ 1, [ 2, 3 ] ], // 'test' => 'testContent' } map.clear() console.log(map) // Map {} 複製代碼
Map應用場景
對於經典演算法問題中 上樓梯問題:共n層樓梯,一次僅能跨1或2步,總共有多少種走法?
這一類問題都有一個遞歸過程中記憶體溢出的bug存在,此時就可以運用Map減少遞歸過程中重覆運算的部分,解決記憶體溢出的問題。
let n = 100 let map = new Map() function upStairs (n) { if (n === 1) return 1 if (n === 2) return 2 if (map.has(n)) return map.get(n) let ret = upStairs(n - 1) + upStairs(n - 2) map.set(n, ret) return ret } console.log(upStairs(n)) // 573147844013817200000 複製代碼
WeakMap
本節介紹在ES6中,與Map相關且一同發佈的WeakMap數據結構。
WeakMap與Map區別
WeakMap與Map主要有下圖三個區別:
區別 Map WeakMap “鍵”類型: 任何類型 Object對象 自身方法: 基本方法:set、get、has、delete;
遍歷方法:keys、values、entries、forEach;
其他方法:size、clear。基本方法:set、get、has、delete。 鍵引用類型: 強引用 弱引用 此處我們對強弱引用進行簡單介紹:弱引用在回收機制上比強引用好,在“適當”的情況將會被回收,減少記憶體資源浪費,但由於不是強引用,WeakMap不能進行遍歷與size方法取得內部值數量。
WeakMap自身方法
含增刪改查方法:set、get、has、delete。
let wMap = new WeakMap() let key = {} let obj = {name: 'objName'} wMap.set(key, obj) console.log(wMap.get(key)) // { name: 'objName' } console.log(wMap.has(key)) // true wMap.delete(key) console.log(wMap.has(key)) // false 複製代碼
WeakMap應用場景
WeakMap因為鍵必須為對象,且在回收機制上的優越性,其可以用在以下兩個場景:
1. 對特定DOM節點添加狀態時。當DOM節點被刪除,將DOM節點作為“鍵”的WeakMap也會自動被回收。
2. 對類或構造函數中私有屬性綁定定義。當實例被刪除,被作為“鍵”的this消失,WeakMap自動回收。
示例代碼如下:
<!--示例一--> let element = document.getElementById('box') let wMap = new WeakMap() wMap.set(element, {clickCount: 0}) element.addEventListener('click', () => { let countObj = wMap.get(element) countObj.clickCount++ console.log(wMap.get(element).clickCount) // click -> n+=1 }) <!--示例二--> const _age = new WeakMap() const _fn = new WeakMap() class Girl { constructor (age, fn) { _age.set(this, age) _fn.set(this, fn) } changeAge () { let age = _age.get(this) age = age >= 18 ? 18 : null _age.set(this, age) _age.get(this) === 18 ? _fn.get(this)() : console.log('error') } } const girl = new Girl(25, () => console.log('forever 18 !')) girl.changeAge() // forever 18 ! 複製代碼
Set
介紹完ES6新增的Map與WeakMap數據結構,我們繼續介紹一同新增的Set數據結構。
Set之於Array,其實有點像Map之於Object,Set是在數組的數據結構基礎上做了一些改變,新出的一種類似於數組的數據結構,Set的成員的值唯一,不存在重覆的值。以下將對Set數據結構作一些簡單的介紹。
Set與Array之間的相互轉換
Set可以將具有Iterable介面的其他數據結構作為參數用於初始化,此處不止有數組,但僅以數組作為例子,單獨講述一下。
// Set -> Array let arr = [1, 2, 3, 3] let set = new Set(arr) console.log(set) // Set { 1, 2, 3 } // Array -> Set const arrFromSet1 = Array.from(set) const arrFromSet2 = [...set] console.log(arrFromSet1) // [ 1, 2, 3 ] console.log(arrFromSet2) // [ 1, 2, 3 ] 複製代碼
Set自身方法
Set內置的方法與Map類似
含增刪查方法:add、has、delete;
遍歷方法:keys、values、entries、forEach;
其他方法:size、clear。
let arr = [1, 2, 3, 3] let set = new Set(arr) // 增刪改查 set.add(4) console.log(set) // Set { 1, 2, 3, 4 } set.delete(3) console.log(set) // Set { 1, 2, 4 } console.log(set.has(4)) // true // 遍歷方法 因為在Set結構中沒有鍵名只有健值,所以keys方法和values方法完全一致 console.log(set.keys()) // [Set Iterator] { 1, 2, 4 } console.log(set.values()) // [Set Iterator] { 1, 2, 4 } for (const item of set.entries()) { console.log(item) //[ 1, 1 ] // [ 2, 2 ] // [ 4, 4 ] } const obj = { name: 'objName' } set.forEach(function (key, value) { console.log(key, value, this.name) // 1 1 'objName' // 2 2 'objName' // 4 4 'objName' }, obj) // 其他方法 console.log(set.size) // 3 set.clear() console.log(set) // Set {} 複製代碼
Set應用場景
因為擴展運算符...對Set作用,再通過Array遍歷方法,很容易求得並集、交集及差集,也可以通過間接使用Array方法,構造新的數據賦給Set結構變數。
let a = new Set([1, 2, 3]) let b = new Set([2, 3, 4]) // 並集 let union = new Set([...a, ...b]) console.log(union) // Set { 1, 2, 3, 4 } // 交集 let intersect = new Set([...a].filter(x => b.has(x))) console.log(intersect) // Set { 2, 3 } // 差集 let difference = new Set([...[...a].filter(x => !b.has(x)), ...[...b].filter(x => !a.has(x))]) console.log(difference) // Set { 1, 4 } // 賦新值 let aDouble = new Set([...a].map(x => x * 2)) console.log(aDouble) // Set { 2, 4, 6 } let bDouble = new Set(Array.from(b, x => x * 2)) console.log(bDouble) // Set { 4, 6, 8 } 複製代碼
WeakSet
WeakSet與Set對比
WeakSet之於Set,依舊相當於WeakMap之於Map。
WeakSet與Set之間不同之處,依然是:
1. WeakSet內的值只能為對象;
2. WeakSet依舊是弱引用。
WeakSet自身方法
因為弱引用的關係,WeakSet只有簡單的增刪查方法:add、delete、has
let obj1 = {'name': 1} let obj2 = {'name': 2} let wSet = new WeakSet() wSet.add(obj1).add(obj2) console.log(wSet.has(obj2)) // true wSet.delete(obj2) console.log(wSet.has(obj2)) // false 複製代碼
WeakSet應用場景
對於WeakSet的應用場景,其與WeakMap類似,因為弱引用的優良回收機制,WeakSet依舊可以存放DOM節點,避免刪除這些節點後引發的記憶體泄漏的情況;也可以在構造函數和類中存放實例this,同樣避免刪除實例的時候產生的記憶體泄漏的情況。
// 1 let wSet = new WeakSet() wSet.add(document.getElementById('box')) const _boy = new WeakSet() // 2 class Boy { constructor () { _boy.add(this) } method () { if (!_boy.has(this)) { throw new TypeError('Boy.prototype.method 只能在Boy的實例上調用!') } } } 複製代碼
數組中map方法及遍歷相關方法
講完大Map,此時我們繼續瞭解完小map,map即為Array.map(),是數組中一個遍歷方法。並將map作為一個引子,我們對比多介紹幾個Array中遍歷相關的方法。
Array.map()、Array.flatMap()
Array.map() —— 可以有三個參數,item、index、arr,此時當做forEach使用;常用方法是通過第一個參數遍歷修改後返回一個新數組。
Array.flatMap() —— 前置知識:Array方法中有一個ES6中新加入的數組展開嵌套的方法Array.flat(),其中可以有一個參數表示展開層數,預設只展開一層。而Array.flatMap() 為 Array.map()與Array.flat()方法的疊加。
例子如下:
// flat const testArr = [1, 2, [3, [4]]] const flatArr = testArr.flat() console.log(flatArr) // [1, 2, 3, Array(1)] -> 0: 1 // 1: 2 // 2: 3 // 3: [4] const arr = [1, 2, 3] // map const mapArr = arr.map(x => x * 2) console.log(mapArr) // [2, 4, 6] arr.map((item, index, arr) => { console.log(item, index, arr) // 1 0 [1, 2, 3] // 2 1 [1, 2, 3] // 3 2 [1, 2, 3] }) // flatMap // arr.flatMap(x => [x * 2]) === arr.map(x => x * 2) const flatMapArr = arr.flatMap(x => [x * 2]) console.log(flatMapArr) // [2, 4, 6] 複製代碼
Array.reduce()
Array.reduce() —— reduce方法與map最大的不同是不返回新的數組,其返回的是一個計算值,參數為回調函數與回調函數參數pre初始值,回調函數中參數為pre與next,當在預設情況時,pre為數組中第一個值,next為數組中第二個值,回調函數返回值可以滾雪球般更改pre值;而當index設置數值後,pre初始值為參數值,next從數組中第一個值一直取到數組最後一位。
例子如下:
const arr = [1, 2, 3, 4, 5] const result = arr.reduce((pre, next) => { console.log(pre, next) // 1 2 // 3 3 // 6 4 // 10 5 return pre + next }) console.log(result) // 15 arr.reduce((pre, next) => { console.log(pre, next) // 9 1 // 9bala 2 // 9balabala 3 // 9balabalabala 4 // 9balabalabalabala 5 return pre += 'bala' }, 9) 複製代碼
Array.filter()、Array.find()、Array.findIndex()
Array.filter() —— 返回值是一個數組,第一個參數為回調函數,第二個參數為回調函數中this指向。回調函數的參數有value,index及arr。滿足回調函數的中過濾條件的,會被push到返回值中新的數組中。
Array.find() —— 返回值是數組內的一個值,該方法返回數組內滿足條件的第一個值,第一個參數為回調函數,第二個參數為回調函數中this指向。回調函數的參數有查找到的符合條件前的value,index及arr。當查找的是數組中不可重覆的值時,建議使用find方法,會比filter更優越。
Array.findIndex() —— 返回值為Number,該方法返回數組內滿足條件的第一個值在數組中的index,第一個參數為回調函數,第二個參數為回調函數中this指向。回調函數中的參數與find方法類似。
例子如下:
const arr = [1, 2, 3, 4, 5] const obj = {num: 3} // filter const filterArr = arr.filter(function (value, index, arr) { console.log(index, arr) // 0 [1, 2, 3, 4, 5] // 1 [1, 2, 3, 4, 5] // 2 [1, 2, 3, 4, 5] // 3 [1, 2, 3, 4, 5] // 4 [1, 2, 3, 4, 5] return value > this.num }, obj) console.log(filterArr) // [4, 5] // find const findResult = arr.find(function (value, index, arr) { console.log(index, arr) // 0 [1, 2, 3, 4, 5] // 1 [1, 2, 3, 4, 5] // 2 [1, 2, 3, 4, 5] // 3 [1, 2, 3, 4, 5] return value > this.num }, obj) console.log(findResult) // 4 // findIndex const findIndexResult = arr.findIndex(function (value) { return value > this.num }, obj) console.log(findIndexResult) // 3 複製代碼
Array.includes()
Array.includes() —— 返回值為Boolean值,其可以簡單快捷的判斷數組中是否含有某個值。其第一個參數為需要查找的值,第二個參數為開始遍歷的位置,遍歷位置起始點預設為0。相比於indexOf、filter