JavaScript創建對象的幾種方式


一、工廠模式(字面量)

函數create()能根據接收的參數來構建一個包含所有必要信息的F對象。可無數次調用。
有個問題,怎么知道調用這個函數創建的對象是F對象呢?這個方法無法識別創建出來的對象的類型

function createF(name,age) {
	var tmp = new Object(); //創建一個新對象
	tmp.name = name; 
	tmp.age = age;
	tmp.sayName = function() {
		console.log(this.name);
	}
	return tmp; //返回
}
var t1 = createF('try',19);
t1.sayName();

二、構造函數模式

與上一種方式有什么不同?

  • 沒有顯示地創建對象;
  • 直接將屬性和方法賦給了this對象;
  • 沒有return語句。

要創建一個Person新實例,必須使用new操作符。事實上,任何函數只要通過new操作符來調用,那它就可以作為構造函數,構造函數如果不通過new來調用也就是普通函數。
會經歷以下4個步驟:

  1. 創建一個新對象;
  2. 將構造函數的作用域賦給新對象(因此this就指向了這個新對象);
  3. 執行構造函數中的代碼(為新對象添加屬性);
  4. 返回新對象。

創建自定義的構造函數意味着將來可以將它的實例表示為一種特定的類型,如p1是Person類型的。而這正是構造函數模式勝過工廠模式的地方。

function Person(name,age) {
	this.name = name;
	this.age = age;
	this.sayName = function() {
		console.log(this.name);
	}
}
var p1 = new Person('try',19); //用new來創建對象
p1.sayName(); //try
var p2 = new Person('try',19);
console.log(p1.constructor == Person); //true
console.log(p2.constructor == Person); //true

//檢測對象類型常用的還是instanceof,更可靠
console.log(p1 instanceof Person); //true
console.log(p2 instanceof Person); //true
console.log(p1 instanceof Object); //true
console.log(p2 instanceof Object); //true

 當然,也可以通過call() 或 apply()在某個特殊對象的作用域中調用構造函數。如Person.apply(p1,'try');,這是在對象p1的作用域中調用的,因此調用后
 就擁有量Person的所有屬性和sayName方法(並不是所有方法,僅限於在構造函數中出現的,后面會講原因)。
 構造函數有問題嗎?按道理所有Person類型的實例的sayName方法都是一樣的呀,但是構造函數里就不一樣。

console.log(p1.sayName == p2.sayName); //false

創建兩個完成相同任務的Function實例的確沒有必要。那就可以向下面這樣在構造函數外面定義一個新函數,再讓構造函數里的函數等於它。這樣一來,由於構造函數里的sayName包含的是一個指向函數的指針,因此p1 p2對象就共享了全局作用於中定義同一個這個函數。那么又有問題了,這樣,還有封裝性可言?

function Person() {
  this.sayName = sayName;
}
function sayName() {
  console.log(this.sayName);
}

三、原型對象

 首先來解決,什么是原型?
 每創建一個函數,都會同時有一個prototype(原型)屬性,這個屬性是一個指針,指向prototype(原型)對象。與此同時,所有原型對象都會自動獲得一個constructor(構造函數)屬性,這也是一個指針,指向那個函數,如下圖:

那么也就是,通過這個構造函數,我們可以繼續為原型添加其他屬性與方法。
​ 當把一個函數作為構造函數 (理論上任何函數都可以作為構造函數) 使用new創建對象的時候,那么這個對象就會存在一個默認的不可見的屬性,來指向了構造函數的原型對象。 這個不可見的屬性我們一般用 [[prototype]] 來表示,只是這個屬性沒有辦法直接訪問到。

一下幾點要注意:

  • 從上面的圖示中可以看到,創建p1對象雖然使用的是Person構造函數,但是對象創建出來之后,這個p1對象其實已經與Person構造函數沒有任何關系了,p1對象的[[ prototype ]]屬性指向的是Person構造函數的原型對象。
  • 如果使用new Person()創建多個對象,則多個對象都會同時指向Person構造函數的原型對象。
  • 我們可以手動給這個原型對象添加屬性和方法,那么p1,p2,p3…這些對象就會共享這些在原型中添加的屬性和方法。
  • 如果我們訪問p1中的一個屬性name,如果在p1對象中找到,則直接返回。如果p1對象中沒有找到,則直接去p1對象的[[prototype]]屬性指向的原型對象中查找,如果查找到則返回。(如果原型中也沒有找到,則繼續向上找原型的原型—原型鏈。 后面再講)。
  • 如果通過p1對象添加了一個屬性name,則p1對象來說就屏蔽了原型中的屬性name。 換句話說:在p1中就沒有辦法訪問到原型的屬性name了。但可用delete操作符徹底刪除實例屬性,從而能夠重新訪問原型中的屬性。
  • 通過p1對象只能讀取原型中的屬性name的值,而不能修改原型中的屬性name的值。 p1.name = “李四”; 並不是修改了原型中的值,而是在p1對象中給添加了一個屬性name。
function Person(name,age) {
	this.name = name;
	this.age = age;
	this.colors = ['red'];
}
Person.prototype.sayName = function() {
	console.log(this.name);
}
var p1 = new Person('try',19);
p1.sayName(); //try

p1.name = 'aa';
var p2 = new Person('tt',19); 
p2.sayName(); //tt

Person.prototype.sayAge = function() {
	console.log(this.age);
}
p2.sayAge(); //19

原型對象幾種屬性與方法

constructor屬性:指向構造函數(也就是之前的Person());

但要注意,當原型對象指向另一個新的對象(包括字面量式的重寫,因為這也是創建了一個新對象)時,constructor不再指向Person了;如下

console.log(Person.prototype.constructor); //Person
Person.prototype = {
	name: 'ttt',
	age: 30
}
console.log(Person.prototype.constructor); //Object

當然,如果必要(最好寫上),可以特意在新對象中包含一個constructor屬性並將其值設為Person,確保通過該屬性能夠訪問到適當的值。

console.log(Person.prototype.constructor); //Person
Person.prototype = {
	constructor: Person,
	name: 'ttt',
	age: 30
}
console.log(Person.prototype.constructor); //Person

hasOwnProperty方法:檢測一個屬性是存在於實例中還是原型中,這個方法(是從Object繼承來的)只在給定屬性存在於對象實例中才返回true。

與此相類似的

in方法:只要該屬性存在於對象中就返回true,無論是實例還是原型都一樣;

那么,如何判斷一個屬性是否存在於原型中:
如果一個屬性存在,但是沒有在對象本身中,則一定存在於原型中。

原型對象與構造函數方法比較

原型對象 構造函數
屬性 共有,一經修改全部實例都會改變 每個實例獨有一份
方法 所有實例公用方法 每個實例都有獨立的,即便方法相同也要重新復制
缺陷 大家都是訪問的同一個對象,如果一個對象對原型的屬性進行了修改,則會反映到所有的對象上面 每個對象都有自己獨有的一份,大家不會共享,造成內存的浪費和性能的低下。
function Person(name,age) {
	this.name = name;
	this.age = age;
	this.sayAge = function() {
		console.log(this.age);
	}
}
var p1 = new Person('try','12');
Person.prototype.colors = ['red'];
Person.prototype.sayName = function() {
	console.log(this.name);
}

// 原型對象的問題:
p1.colors.unshift('yellow');
var p2 = new Person('ttt','12');
//p1實例中修改的值,在p2中也顯現了,共有
console.log(p2.colors); //yellow,red

//構造函數的問題:
console.log(p1.sayAge == p2.sayAge); //false
//p1 p2各有一份

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

這也是最最常用的方法。構造函數模式用於定義實例屬性,原型模式用於定義方法和共享的屬性

function Person(name,age) {
	this.name = name;
	this.age = age;
	this.colors = [];
}
Person.prototype = {
	constructor: Person, //重寫原型對象切斷了聯系 要指回來 不然這個屬性沒法用了
	sayName: function() {
		console.log(this.name);
	},
	sayAge: function() {
		console.log(this.age);
	}
}
var p1 = new Person('a',19);
var p2 = new Person('b',20);
console.log(p2.colors); //[]
p1.colors.push('yellow');
console.log(p2.colors); //[]

console.log(p1.sayName == p2.sayName); //true

五、動態原型模式

將所有信息都封裝在了構造函數中,通過在構造函數中初始化原型(僅在必要的情況下),又保持了同時使用構造函數和原型的有點。換句話說,可以通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。但注意使用動態原型時,不能使用對象字面量重寫原型,因為如果在已經創建了實例的情況下重寫原型,那么就會切斷現有實例和新原型之間的聯系

function Person() {
	this.name = 't';
	this.age = 19;
	if(typeof this.sayName != 'function') {
		//if不必一一檢查所有屬性和方法,只需檢查初始化后應該存在的任何屬性和方法即可
		Person.prototype.sayName = function() {
			console.log(this.name);
		}
		Person.prototype.sayAge = function() {
			console.log(this.age);
		}
	}
}
var p1 = new Person(); //初次調用構造函數
var p2 = new Person(); //不是初次了,if內的語句不會執行,因為這時候原型已完成初始化

參考《JavaScript高級程序設計第3版》。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM