JavaScript 面向對象之創建對象的方式

工廠模式

由于 ES6 之前沒有 class 概念,所以使用函數來封裝的,工程模式采用最直接的傳入參數創建對象并賦值,然后返回對象的方式

function Great(name,age) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.getName = function() {
    return this.name;
  }
  return o;
}

var g1 = Great('link',21);
console.log(g1.getName(),g1.age);//link 21

但是工廠模式無法知道一個對象的類型

構造函數模式

使用 new 操作符,并且函數內部無需創建對象,也不需要 return 語句

使用 new 操作符會實行以下操作

  • 當創建一個新對象時,將構造函數內部的 this 指向對象
  • 執行代碼將值賦值給該對象
  • 返回這個對象

可以使用每個對象都擁有的屬性 constructor 來檢測其類型,也可以用 instanceof

function Great(name,age) {
  this.name = name;
  this.age = age;
  this.getName = function() {
    return this.name;
  }
}
var g1 = new Great('Bob',31);
var g2 = new Great('Tom',27);
console.log(g1.getName(),g1.age);//Bob 31
console.log(g2.getName(),g2.age);//Tom 27
console.log(g1.constructor == Great);//true
console.log(g2 instanceof Great);//true

當函數用

本身構造函數也是函數,對于任何函數,使用 new 操作符來調用,就可以當做構造函數;按照普通函數那樣調用也就更平常函數差不多

問題

主要出現在對象的內部函數上,我們知道對象一般以堆內存的方法存儲,也就是一堆變量和函數放在一堆,許多的對象就是許多這樣的堆

對于構造函數里面定義的函數,創建的每個實例都擁有它,并且名字相同,看似這些函數都是同一個,其實不然,用上面一條來解釋就知道,雖然他們擁有相同名字的函數,但是每個函數都在不同的堆里,互不影響。

問題就出在沒必要每個實例都有這樣的函數,畢竟調用函數時,都有每一個對象的 this 傳入,依靠這個 this 就能確保調用函數時依據的是當前對象所擁有的值,不會沖突,所以他們其實可以共用一個,反正靠 this 指向就行。然而構造函數的方法就導致每個實例都有獨自的函數,從而造成了大量內存浪費

此時可以使用一種方式解決

function Great(name,age) {
  this.name = name;
  this.age = age;
  this.getName = getName;
}
function getName() {
  return this.name;
}
var g1 = new Great('Bob',31);
var g2 = new Great('Tom',27);
console.log(g1.getName());//Bob
console.log(g2.getName());//Tom

通過定義全局函數的方式,讓他們共用一個

然而這樣又出現一個問題,當函數較多時,全部放在外面,不就與普通函數搞混了嗎,而且不好維護。所以,就此出現另一種模式,原型模式

原型模式

問題引申:上面的構造函數說道,想要通過共同享用的方式來減少不必要的開銷,但是太多的全局函數不利于和外部普通函數辨別,并且這也違背了封裝的思想,所以原型就是允許以特殊的方式定義所有實例都可以共享的屬性和方法,并且實例在共享的基礎上還可以有屬于自己的方法與屬性

每一個函數包括構造函數,都有一個 prototype 屬性,它是一個指針指向一個對象,該對象包含著特定類型的所有共享屬性和方法

無論是構造函數還是普通函數,反正只要是函數,都會默認創建一個 prototype 用來指向一個對象,也就是原型對象,而這個對象默認就有一個值,也就是 constructor,用來指向這個構造函數(函數),所以這也是為什么一旦重寫原型對象,就會導致 constructor 不在指向原來的構造函數,因為這個屬性已經沒有了

通常可以這么定義

function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
  Great.prototype.getName = function() {
    return '原型->' + this.name;
  }
}
var g1 = new Great();
var g2 = new Great();
console.log(g1.age,g1.getName())//21 "原型->Greatiga"
console.log(g1.getName === g2.getName)// true

可以看到,上面的 getName 方法是共有的

理解原型對象

  • 原型對象儲存著所有實例都可以共享的屬性和方法
  • 所有的函數在創建時,都會根據一組特定規則創建一個 prototype 來指向函數數的原型對象
  • 每一個原型對象又會擁有一個 constructor 用來指向定義該原型對象的函數
  • 除此之外每個對象還擁有從 Object 繼承而來的屬性和方法
  • 指向原型的指針叫 [[Prototype]],但是沒有標準的訪問方式,可以靠 __proto__

總結

  • 每一個原型對象都有一個 constructor 屬性指向構造它的構造函數
  • 每一個構造函數有一個 prototype 屬性指向它的原型對象,可以動態修改這個屬性
  • 每一個實例對象都有一個 [[Prototype]] 屬性指向它所擁有的原型,通常不能直接訪問修改,但可以通過 __proto__ 來查看

prototype.isPrototypeOf()

用來測試一個對象的原型是否是來自于某個類型

function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
  Great.prototype.getName = function() {
    return '原型->' + this.name;
  }
}
function GG() {
  GG.prototype.name = 'no';
}
var g1 = new Great();
var g2 = new Great();
console.log(Great.prototype.isPrototypeOf(g1))//true
console.log(GG.prototype.isPrototypeOf(g2))//false

ES5 新增 Object.getPrototypeOf()

用來獲取實例的原型對象,該方法直接返回原型對象

function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
  Great.prototype.getName = function() {
    return '原型->' + this.name;
  }
}
var g1 = new Great();
console.log(Object.getPrototypeOf(g1))//{name: "Greatiga", age: 21, getName: ?, constructor: ?}

屬性重寫和 hasOwnProperty()

除了添加新屬性以外,實例也可以重寫同名的屬性和方法
重寫不會影響原型中本就有的同名屬性和方法
解析器執行實例時,會從實例自身開始找是否有符合的屬性或方法,有就執行,否則就到原型中找
可以刪除重寫的屬性或方法,這樣就可以訪問原型中的屬性和方法

function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
  Great.prototype.getName = function() {
    return '原型->' + this.name;
  }
}
var g1 = new Great();
var g2 = new Great();
g1.getName = function() {
  return '實例->' + this.name;
}
g2.name = 'Tomk';
console.log(g1.getName())//實例->Greatiga
console.log(g2.getName())//原型->Tomk

delete g1.getName;
console.log(g1.getName())//原型->Greatiga

通過 hasOwnProperty() 方法測試一個屬性來自于哪里,在實例中就為 true 否則為 false,這個方法是從 Object 那里繼承而來的

function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
  Great.prototype.getName = function() {
    return '原型->' + this.name;
  }
}
var g1 = new Great();
g1.getName = function() {
  return '實例->' + this.name;
}
console.log(g1.hasOwnProperty('name'));//false
console.log(g1.hasOwnProperty('getName'));//true

in 操作符與原型

通常用在兩個地方

for-in 循環
屬性 in 對象

屬性 in 對象

可以與 hasOwnProperty 組合使用來判斷對象來自于實例還是原型

  • in可以先判斷一個屬性是否存在
  • 通過 hasOwnProperty 又可以判斷是否在實例中
function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
}
var g1 = new Great();
g1.name = 'Link';
console.log('name' in g1);//true
console.log('getName' in g1);//false

console.log((g1.hasOwnProperty('name')) && ('name' in g1));//true 
//在實例中
console.log((g1.hasOwnProperty('age')) && ('age' in g1))//false 
//在原型中

for-in

我們應用在對象上時遍歷出來的是屬性名字

  • 包括實例中所有屬性
  • 包括原型中所有屬性
  • 包括被標記為不可枚舉的屬性 Enumerable: false

上述條件的所有屬性都會被遍歷,IE8 之前的版本中,原型中被屏蔽不可枚舉的屬性無法被遍歷,該問題在后續版本已修復

function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
}
var g1 = new Great();
g1.area = 'China';
g1.toString = function() {
  return this.age;
}
for (post in g1) {
  console.log(post);
}
//area
//toString
//name
//age

Object.keys() 和 Object.getOwnPropertyNames()

可以獲得對象上所有可以枚舉的屬性,該方法返回一個數組

function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
}
var g1 = new Great();
g1.area = 'China';
g1.toString = function() {
  return this.age;
}

console.log(Object.keys(Great.prototype));//(2) ["name", "age"]
console.log(Object.keys(g1));//(2) ["area", "toString"]

可以看到,它會根據實例對象來確定遍歷的屬性,對象為原型就遍歷原型,對象為實例對象就只遍歷實例對象的屬性

倘若要獲取對象所有實例屬性,則可以用 Object.getOwnPropertyNames()

function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
}
var g1 = new Great();
g1.area = 'China';
g1.toString = function() {
  return this.age;
}

console.log(Object.getOwnPropertyNames(Great.prototype));//(3) ["constructor", "name", "age"]
console.log(g1.constructor == Great);//true

簡潔語法和動態性

原始定義原型的方式過于繁雜,可以簡潔一點,但是這樣的方式會導致 constructor 指向了 Object 而非構造函數,所以如果需要該值就得正確指定它的值

function Great() {}
Great.prototype = {
  name : 'Greatiga',
  age : 21,
  getName : function() {
    return this.age;
  }
};
var g1 = new Great();
console.log(g1.constructor == Great);//false
Great.prototype.constructor = Great;
console.log(g1.constructor == Great);//true

動態性就是指修改原型對象的屬性后能即使在實例中反映出來,無論該實例的創建是在修改前還是修改后,都有響應;原因很簡單,實例執行時,解析器都會從當前對象找起,一直到原型對象,當然也就能響應了

注意,修改原型對象時,不要用字面量的方式去修改,這會改變指針指向導致實例對象找不到原型。為什么呢?實例對象創建時會有一個屬性(指針)指向原型對象,如果你只是修改原型中的對象那不會有事,因為你只是換了這堆磚頭的一塊磚,依然還是這堆磚,實例們依然可以找到

然而你直接用字面量方式去修改,就相當于弄了一堆新磚頭,但是之前就已經有的實例對象還是指著原來那堆磚頭并非你用字面量創建的新的一堆磚頭,這不就導致該實例對象斷開與原型的連接了嗎,因為他現在存的那個指針值指向的不過是一堆沒人要的磚頭,說不定早就被當垃圾回收了呢

看下方例子

function Great() {}
Great.prototype = {
  name : 'Greatiga',
  age : 21,
};
var g = new Great();
Great.prototype.name = 'Great';
console.log(g.name);//Great
//此時正常,因為依然還是這個原型對象
Great.prototype = {
  name : 'Tom',
  age : 23,
};
console.log(g.name);//Great
//還是 Great?
g.__proto__ = Great.prototype;
console.log(g.name);//Tom

看上面倒數第三行那里,依然還是 Great 而不是 Tom,這并非程序錯誤,而是原型對象整個都變了,而實例 g 依然還是指著第一次改變名字后的那個原型對象,除非你改變實例 g 的指向,讓它指向重寫的那個原型對象,否則就永遠找不到

原生對象的原型和原型存在的問題

原生對象

比如 Array,String,Date,RegExp這些原生對象,他們也一樣擁有原型對象,我們不僅可以訪問這些原型對象,還可以給它增加新屬性

console.log(Array.prototype.map);//? map() { [native code] }
console.log(String.prototype.toLocaleUpperCase);//? toLocaleUpperCase() { [native code] }
console.log(Date.prototype.getYear);//? getYear() { [native code] }
String.prototype.printf = function() {
  console.log(this.constructor)
};
console.log(String.prototype.printf);
//? () {
//  console.log(this.constructor)
//}
console.log('Great'.printf());//? String() { [native code] }

存在的問題

第一個,因為所有屬性都由原型定義好了,所有一創建實例,實例對象就擁有所有屬性,有的時候沒必要這樣

第二,比較關鍵,就比如某個原型屬性被改變了,那么其他所有實例都會改變,如果我們想要每個實例都有自己的屬性,即使這些這些屬性同名,也要有不同的值,而僅靠原型對象時無法實現的,因為他們都共享同一個。

怎么解決?回想之前的構造函數模式不就可以嗎,只是當時我們想要共享屬性所以拋棄了,但是現在我們想要一部分共享,一部分特有,這樣一來就引出了組合模式

構造函數和原型組合模式

組合這兩種模式:構造函數模式定義實例可以獨有的屬性,原型模式定義所有實例共享的屬性。這樣一來,每個實例對象都有屬于自己的實例屬性,同時又可以共享部分共同的屬性方法,從而最大限度節省了內存。

function Great(name,age) {
  this.name = name;
  this.age = age;
}
Great.prototype = {
  time : '2020-01-01',
  getName: function() {
    return this.name;
  },
  setAge: function(s) {
    this.age = s;
  },
  getAge : function() {
    return this.age;
  }
};
var g1 = new Great('Great',21);
var g2 = new Great('Never',20);
console.log(g1.getName(), g1.getAge());//Great 21
console.log(g1.time);//2020-01-01
g2.setAge(23);
console.log(g2.getName(), g2.getAge());//Never 23
console.log(g2.time);//2020-01-01

如上例子,兩個對象擁有不同的名字和年齡,同時又共享一個時間

動態原型模式

引入此概念也是為了更好的封裝,我們知道字面量方式定義原型對象時往往是在構造函數外部。動態原型模式可以將原型的定義與構造模式的賦值一起放入構造函數內部,采用這種方式在內部定義時,不要使用字面量的方式

function Great(name,age) {
  this.name = name;
  this.age = age;
  Great.prototype.time = '2020-01-01';
  Great.prototype.getName = function() {
    return this.name;
  },
  Great.prototype.setAge = function(s) {
    this.age = s;
  },
  Great.prototype.getAge = function() {
      return this.age;
  }
}
var g1 = new Great('Great',21);
var g2 = new Great('Never',20);
console.log(g1.getName(), g1.getAge());//Great 21
console.log(g1.time);//2020-01-01
g2.setAge(23);
console.log(g2.getName(), g2.getAge());//Never 23
console.log(g2.time);//2020-01-01

寄生原型模式

這一種模式旨在前面幾種模式都無法使用的情況下使用,模式與普通的函數構造模式差不多,創建對象,賦值,返回對象,萬不得已才用,而且紅皮書上講的也不是太細致,所以暫時不深究了

穩妥模式

在某些情境下,this 與 new 可能會不太安全,此時才用原有的創建對象、賦值、返回對象的方式來創建構造函數

  • 不使用 this 創建實例屬性,不使用 new 調用構造函數

寄生原型模式與穩妥模式博主用的比較少,還在學習中...

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。