Javascript 對象深度解析

對象-高程及月影js視頻學習筆記

對象的深拷貝和淺拷貝

ES5 淺拷貝

Object.assign({}, conf)

只能拷貝一級,深層的源改變,目標也會跟着改變。

遞歸 深拷貝

function deepCopy(des, src) {
  for (var key in src) {
    if(typeof src[key] !== 'object') {
      des[key] = src[key];
    } else {
      des[key] = des[key] || {};
      deepCopy(des[key], src[key]);
    }
  }
  return des;
}

創建對象

構造函數模式

new 和 Object.create

首先了解下Object.create的實現方式

Object.create =  function (o) {
    var F = function () {};
    F.prototype = o;
    return new F();
};

new操作符會做一下幾個事情:

  • 創建一個新對象
  • 將構造函數的作用域賦給新對象
  • 執行構造函數中的代碼(爲新創建的對象添加屬性)
  • 返回新對象

其中被new的函數就叫構造函數。

注意:

function C() {
  this.z = 3;
  this.func = function() {console.log(1)}
}
var c1 = new C();
var c2 = new C();
c1.func === c2.func // false

執行c1.func === c2.func會返回false,這是因爲每次實例化的時候都會創造一個新的函數對象,實際上沒有必要這麼做,首先能想到的是,把要創建的函數放到構造函數外面:

function Person(name, age, job){this.sayName = sayName}
function sayName() {console.log(1)}

var p1 = new Person(1,2,3);
var p2 = new Person(1,2,3);

p1.sayName === p2.sayName // true

但是放在全局中就是去了封裝的意義,所以引入了原型的模式。

原型模式

每個函數都有一個prototype(原型)的屬性,這個屬性指向一個對象,可以存儲有特定類型的所有實例共享的屬性和方法。

注:箭頭函數不包含這個屬性。。。

此時,下式依舊成立

function Person() {}
Person.prototype.name = 'aaa';
Person.prototype.sayName = ()=>{console.log(2)};

var p1 = new Person(1,2,3);
var p2 = new Person(1,2,3);

p1.sayName === p2.sayName // true

構造函數與原型對象

Person.prototype就是Person的原型對象,默認情況下,每個函數都存在prototype屬性,這個屬性存的對象裏都默認有一個constructor屬性指向該函數(當然還有一個_ proto_繼承自Object)。

Person.prototype.constructor 指向 Person。

// _ proto_:顯示在具體實例上的一個屬性

// prototype:構造函數上的一個屬性

isPrototypeOf

A.prototype.isPrototypeOf(B):A是不是B的原型對象

B instanceof A:B是不是A的實例

判斷某個對象的[[Prototype]](實例,擁有[[Prototype]]屬性的實例對象,通常瀏覽器的實現是_ proto_)是否指向調用isPrototypeOf()這個方法的對象(原型對象Person.prototype)。

讀取某個對象的屬性時,會按照實例對象、_ proto_ 對象、 _ proto__ proto_等等順序依次查找,比如上面person的例子,可以找到p1和p2的sayName和name屬性。

但是如果直接給p1.name賦值,則無法改變 _ proto_對象中的name值,會在對象實例本身添加這個屬性,以後讀取p1.name值時,因先從實例找起,所以不會再讀到原型上的值。

Person
name: "bbb"
__proto__:{name:"aaa", sayName: ......}

但是通過修改_ proto_ 的name屬性,就會同時改變p1和p2的原型的name值 以及 Person的原型對象的name值,因爲Person.prototype === p1. _ proto_ ,Person.prototype === p2. _ proto_

使用delete操作符刪掉p1.name,則又能訪問到原型上的name屬性,注意 delete p1.name只能刪掉實例上的屬性,可以通過delete p1.proto.name刪掉原型上的屬性,同樣的操作原型的話,上述三個都會改變。

hasOwnProperty

可以通過這個方法來判斷某屬性是來自實例還是來自原型。

p1.hasOwnProperty('name') // false
in

可以判斷是否對象實例或原型中有該屬性。可以訪問到不可枚舉的屬性,如constructor和_ proto_、prototype。也可以通過in和hasOwnProperty來判斷屬性是否在原型中。

for-in

只能訪問到可枚舉實例和原型屬性,constructor和_ proto_、prototype等不可枚舉的訪問不到。

Object.keys(obj)

對象上所有可枚舉實例屬性。返回的是字符串數組

Object.getOwnPropertyNames()

對象上所有實例屬性。返回的是字符串數組

原型的動態性

一般的,想之前提到的一樣,每次修改原型對象是,能立即在所有對象實例中反應出來,因爲他們之間的連接是一個指針,而給一個副本。

但是,重寫原型對象的話,把原型修改爲另一個對象,就切斷構造函數與最初原型之間的聯繫。

重寫之前的實例中的[[Prototype]]指針仍指向最初的原型。

在重寫原型對象之後創建的實例[[Prototype]]指針指向重寫的原型。z

// 重寫原型對象
function Person() {}

var friend = new Person();

Person.prototype = {
  constructor: Person, // 如果不加這個的話,重寫這裏會沒有constructor,prototype的prototype裏會有Object的constructor。但是這種寫法也有一個問題,constructor會變成可枚舉的,所以也可以向下面那種方式寫。
  name: 'aaa'
}
// Object.defineProperty(Person.prototype, 'constructor', {
  // enumerable: false, 
  // value: Person
// });
friend.sayName(); // error

原生對象的原型

原生對象比如Object,Array,String等,比如Array.prototype中有sort()方法。

根據動態性,可以給原生對象的原型上添加方法,比如Array,這樣當前環境的所有數組都可以調用到新添加的方法。

PS: 不推薦在產品化的程序中修改原生對象的原型,這樣可能會導致明明衝突,而且,也可能會意外的重寫原生方法。

如果往數組上添加新方法,因爲默認可枚舉,所以for-in操作數組時,會把往數組上添加的新特性一起for-in出來。

解決這個問題,可以使用Object.defineProperty來給原型對象添加方法。使用此方法時,默認不可枚舉。

原型模式創建對象的優缺點

優點

可以讓所有對象實例共享它所包含的屬性和方法(直接定義在構造函數中的函數,每個實例都會創造一個新的函數)

缺點
  • 只用原型模式,沒有構造函數傳遞初始化參數這一環節,會導致默認情況下都取得相同的屬性值。
  • 由於原型中的屬性時被所有實例共享的,方法(function)比較適合這種共享的模式,其餘的,改變一個實例的屬性時,如果是值類型的,會在實例上創建這個屬性,覆蓋掉原型中的屬性,不會影響到其他的實例;但是如果是引用類型的,比如下面這個例子,就會導致所有實例的該屬性都改變,因爲此時相當於直接在操作原型,並沒有在實例上添加這個屬性。
function Person() {}

Person.prototype = {
  constructor: Person,
  name: 'aaa',
  friends: [1, 2, 3]
}

var friend1 = new Person();
var friend2 = new Person();

friend1.friends.push(4);

console.log(friend1.friends) // [1, 2, 3, 4]
console.log(friend2.friends) // [1, 2, 3, 4]

組合使用構造函數模式和原型模式

在構造函數中定義各自特有的屬性,在prototype中寫共享的方法。

通常都是用這種方式創建自定義類型。

動態原型模式

在構造函數中初始化原型(僅在必要的情況下)

可以通過檢查某個應該存在的方法是否有效,來決定是否要初始化原型。

function Person(name) {
  this.name = name;
  // 方法
  if(typeof this.name !== "function") {
    Person.prototype.sayName = function(){console.log(1)}
  }
}

但是只要有一次創建實例時,typeof this.name !== “function”,那麼所有實例(之前創建和之後)都會有sayName這個方法。所以不能使用對象字面量來重寫原型,這樣就會切斷之前創建的實例與新原型之間的聯繫。

寄生構造函數模式

封裝創建對象的代碼,再返回新創建的對象。

構造函數在不返回值的情況下,默認會返回新對象的實例(new操作符做的事情),寄生模式相當於手動做new操作符的一些事情,可以重寫new調用構造函數時返回的值。

function Person(name) {
  var o = new Object();

  o.name = name;

  return o;
}

這種方式有個問題,因爲相當於改變了new的默認行爲,所以不存在默認的原型,原型就是Object,所以不能使用instanceOf來確定對象類型,所以一般情況下不要使用這種模式。

繼承(原型鏈)

繼承包括接口繼承和實現繼承。

實現繼承通過原型鏈實現,利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。

…prototype = new Object()的方式才能繼承,讓一個原型對象等於另一個類型的實例。

直接…prototype = …prototype只是一直在修改原型,而不是鏈式繼承。

這種方式有個弊端,在給對象原型賦值的時候,實例化了另一個類型(eg:A.prototype = new B(); ),即調用了類型B的構造函數,通常我們希望在實例化A的時候,再調用B的構造函數,如果B的構造函數有一些方法,或者需要傳參的方法,這種傳undefined參數的實例化可能會引發一些問題,可以用Object.create()來解決:

A.prototype = Object.create(B.prototype);

前文中提到過,Object.create是創建了一個構造函數爲空新對象,賦原型在實例化,所以可以避免可能的構造函數異常執行。

原型鏈的問題

  • 之前原型模式創建對象的缺點中提到過,包含引用類性值的原型造成的問題。(不單獨使用原型鏈,放到構造函數中)
  • 創建子類型實例時,不能向父類型(子類型繼承父類型)中傳參。(ES6中可以使用super([arguments]),比如react中的super(props),還有其他的解決辦法,見下面的借用構造函數)

借用構造函數

使用apply()、call()等方法在新創建的對象上執行繼承對象的構造函數。還可以通過此種方式綁定當前this並傳參,或者用super。

getter和setter

使用Object.defineProperty()定義屬性時,可以設置get、set、enumerable。

數據綁定視圖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章