淺談JS深拷貝與new命令

About

隨著學(xué)習(xí)的深入,有時(shí)候不滿足僅僅看書看教程,喜歡在一些細(xì)節(jié)上鉆研,今天花時(shí)間研究了下JavaScriptnew命令與對象深拷貝,研究所得記錄如下,如果有不正確的地方還請大佬指正,謝謝。

一、new命令

之前團(tuán)隊(duì)leader問我new命令的工作原理,當(dāng)時(shí)我就懵了,下來花時(shí)間看了阮一峰大神的ES5教程,終于弄懂了這個(gè)問題,現(xiàn)將感悟記錄如下

1. new命令工作原理

當(dāng)調(diào)用構(gòu)造函數(shù)時(shí)使用new命令,new命令會(huì)做如下操作:

  • 創(chuàng)建一個(gè)空對象,作為將要返回的對象實(shí)例。
  • 將這個(gè)空對象的原型__proto__,指向構(gòu)造函數(shù)的prototype屬性。
  • 將這個(gè)空對象賦值給函數(shù)內(nèi)部的this關(guān)鍵字。
  • 開始執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼。

2. 使用new命令應(yīng)該注意的地方

  1. 本來new命令會(huì)產(chǎn)生一個(gè)新的對象,并讓構(gòu)造函數(shù)操作這個(gè)新的對象,并返回這個(gè)新的對象,所以構(gòu)造函數(shù)中不需要使用return語句,但是如果構(gòu)造函數(shù)中有return語句,并且return的是個(gè)對象,那么執(zhí)行了構(gòu)造函數(shù)就會(huì)返回return后面的對象而非新產(chǎn)生的對象,如果return后面是一個(gè)語句,那么new會(huì)忽略它而返回新的對象。
  2. 如果調(diào)用構(gòu)造函數(shù)時(shí)忘記使用new命令,那么構(gòu)造函數(shù)就會(huì)對執(zhí)行構(gòu)造函數(shù)時(shí)的執(zhí)行上下文對象進(jìn)行操作。所以使用new是必要的。

3. 如何避免忘記使用new命令

我們可以通過使用嚴(yán)格模式來避免犯錯(cuò),但是一般情況下,我們也可以通過在聲明構(gòu)造函數(shù)時(shí)加入一些容錯(cuò)機(jī)制來避免忘記使用new造成的不良后果。

function Constructor(param) {
    if ( new.target === Constructor){  // 判斷出使用了new命令,那么執(zhí)行構(gòu)造函數(shù)
        this.name = dog
        this.age = 24
    } else {  // 如果沒有使用new命令,那么重新調(diào)用構(gòu)造函數(shù)并且使用new命令
        return new Constructor(param)
    }
}

當(dāng)然也可以判斷if (this instanceOf Constructor) 。

二、深拷貝

眾所周知JavaScript中對復(fù)雜數(shù)據(jù)類型是采用引用的方式,即變量內(nèi)存儲(chǔ)的是數(shù)據(jù)的內(nèi)存地址而非真正的數(shù)據(jù),所以一旦修改了數(shù)據(jù),所有引用該數(shù)據(jù)的變量都會(huì)受影響。
淺拷貝的意思就是只拷貝第一層數(shù)據(jù),如果該數(shù)據(jù)還有引用型數(shù)據(jù),那么它仍舊不是完全獨(dú)立的,當(dāng)其引用的數(shù)據(jù)被修改,它仍舊會(huì)受到影響,比如:

a = [1,2,3,[4,5,6]]
b = [...a]  // 對a進(jìn)行淺拷貝 b =  [1,2,3,[4,5,6]]

b[0] = 100
console.log(a[0])  // 1 說明b數(shù)組的第一層已經(jīng)脫離了a數(shù)組

b[3][0] = 100
console.log(a[3][0]) // 100 說明b數(shù)組的第二層仍未脫離a數(shù)組,表示這并非真正的拷貝

1. 深拷貝需要注意的問題

從博客上看到其他作者寫的一些深拷貝的方法,一般都會(huì)有一些不完善的地方,比如:

1. 1 無法拷貝對象的構(gòu)造函數(shù)的prototype上的屬性

一般情況下,我們需要深拷貝對象時(shí)都是希望能夠完全復(fù)制一個(gè)對象,所以無論是原型屬性還是實(shí)例屬性我們需要完全復(fù)制,例如:

function Person(){}
Person.prototype.name = 'bing'

man = new Person() // {}
console.log(man.name) // 'bing'

上面的代碼中我們把一個(gè)屬性定義到構(gòu)造函數(shù)的原型上,我們通過實(shí)例能夠訪問到該屬性,但是如果我們不拷貝該屬性,那么這兩個(gè)對象就不是完全一樣的了。例如:

function deepCopy(obj) {
    var result = Array.isArray(obj) ? [] : {}
      for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
          if (typeof obj[key] === 'object' && obj[key]!==null) {
            result[key] = deepCopy(obj[key]);   //遞歸復(fù)制
          } else {
            result[key] = obj[key];
          }
        }
      }
      return result;
    }
b = deepCopy(man)
console.log(b.name) // undefined

1.2 如果對象的某一屬性是對象本身

如果對象的某一屬性是對象本身,那么會(huì)造成無限循環(huán),例如:

b = {
  'name': 'bing'
}
b.self = b

c = deepCopy(b) // Uncaught RangeError: Maximum call stack size exceeded

1.3 注意某些特殊對象

我們還應(yīng)該注意一些特殊的對象,比如:RegExpDate

a = new Regexp(/a/i)
b = deepCopy(a)

console.log(a) // /a/i
console.log(b) // /(?:)/

1.4 注意某些不可枚舉的屬性

我們知道使用for...in是會(huì)跳過不可枚舉的屬性的,這樣會(huì)導(dǎo)致如果某些不可枚舉的屬性不能被拷貝致使目標(biāo)對象和源對象不一致

const a = new Object()
Object.defineProperty(a, 'name', {value: 'bing', enumerable: false})
a.age = 23
for (let i in a) { console.log(`${i}: ${a[i]}`)}  // age: 23

通過上述代碼可以發(fā)現(xiàn)屬性name沒有被遍歷到,所以我們需要使用Object.getOwnPropertyNames()方法來獲取全部的key

1.5 注意symbol屬性

同樣,使用for...in也無法遍歷symbol屬性,所以我們需要使用Object.getOwnPropertySymbols()來獲取全部的symbol屬性

const a = new Object()
a.age = 23
sym = Symbol('foo')
a[sym] = 123

for (let i in a) { console.log(`${i}: ${a[i]}`)}  // age: 23

1.6 注意拷貝屬性描述對象

假設(shè)源對象中某一屬性是禁止配置的,或者是不可枚舉的,如果我們只拷貝了屬性的value而沒有拷貝其屬性描述對象,那么目標(biāo)對象中該屬性的屬性描述對象會(huì)變成一個(gè)默認(rèn)的對象,從而導(dǎo)致目標(biāo)對象與源對象不同。

const a = new Object()
Object.defineProperty(a, 'name', {value: 'bing', enumerable: false})
const b = new Object()
b.name = a.name

Object.getOwnPropertyDescriptor(a,'name')
// {value: "bing", writable: false, enumerable: false, configurable: false}
Object.getOwnPropertyDescriptor(b,'name')
// {value: "bing", writable: true, enumerable: true, configurable: true}

通過上面的代碼我們可以發(fā)現(xiàn)通過賦值的辦法,name的屬性描述對象并沒有被拷貝,所以這里應(yīng)該采用Object.defineProperty()方法。

1.7 注意傳入的參數(shù)不是對象類型

如果傳入的不是對象這種復(fù)雜數(shù)據(jù)類型,我們應(yīng)該直接返回,因?yàn)楹唵螖?shù)據(jù)類型并非引用型。

三、較為完善的深拷貝算法

function deepCopy(obj) {
  if (obj instanceof Date) { return new Date(obj) }  // 解決特殊對象的拷貝
  if (obj instanceof RegExp) { return new RegExp(obj)}  
  let result = new obj.constructor()  // 解決無法拷貝原型
  for (let key of Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj))) {
    if (obj[key] !== obj) {  // 解決無限循環(huán)
      if (typeof obj[key] === 'object' && obj[key] !== null) {
        result[key] = deepCopy(obj[key]);   //遞歸復(fù)制
      } else {
        Object.defineProperty(result, key, Object.getOwnPropertyDescriptor(obj, key))
      }
    }
  }
  return result;
}

另外,如果我們非常了解被拷貝的對象,并且知道該對象中無特殊Unicode碼點(diǎn),函數(shù),以及正則表達(dá)式,我們也可以通過該方法:

function deepCopy (obj) {
  return JSON.parse(JSON.stringify(obj))
}

結(jié)束語

本文可能會(huì)有一些不嚴(yán)謹(jǐn)?shù)牡胤?,因?yàn)樽髡弑救瞬攀鑼W(xué)淺,還請各位大佬批評指正,謝謝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容