從引用聊到深淺拷貝,從深拷貝過渡到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、find及findIndex方法,includes方法更簡單快捷返回Boolean值進行判斷,其二對於陣列中NaN值,includes可以識別到NaN。
const arr = [1, 2, 3, NaN] console.log(arr.includes(NaN)) // true console.log(arr.includes(2, 2)) // false 複製程式碼
-
Array.every()、Array.some()
Array.every() —— 返回值為Boolean型別,類似於if判斷中的 && 條件符,當陣列中每個值都滿足條件時返回true。其第一個引數為回撥函式,第二個引數為回撥函式的this指向。回撥函式的引數為對比結果為true的value,index及arr,到碰到false停止。
Array.some() —— 返回值為Boolean型別,類似於if判斷中的 || 條件符,當陣列中存在任意一個值滿足條件時返回true。其引數與every方法相同,但回撥函式的引數,some方法為對比結果為false的value,index及arr,到碰到true停止。
例子如下:
// every const arr = [1, 2, 3, 4, 5] const obj = { num: 3 } const everyResult = arr.every(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] return value < this.num }, obj) console.log(everyResult) // false // some const someResult = arr.some(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(someResult) // true 複製程式碼
相關文章
- js 深拷貝 vs 淺拷貝
- 淺拷貝與深拷貝
- JavaScript專題之深淺拷貝
- Vue效能提升之Object.freeze()
- 一張圖看懂JavaScript中陣列的迭代方法:forEach、map、filter、reduce、every、some
- [譯] 五個小技巧讓你寫出更好的 JavaScript 條件語句
- [譯文] 如何在 JavaScript 中更好地使用陣列
- ES6 - 阮一峰大大
感謝以上作者大大 ღ( ´・ᴗ・` )比心
第二篇文章,完結撒花 (✧◡✧)