天天看點

JavaScript OOP 建立對象的7種方式

我寫JS代碼,可以說一直都是面向過程的寫法,除了一些用來封裝資料的對象或者jQuery插件,可以說對原生對象了解的是少之又少。是以我拿着《JavaScript進階程式設計 第3版》惡補了一下,這裡坐下總結筆記,屬于菜鳥級别,大神請直接無視。

1、工廠模式

1 /**
 2  * 工廠模式
 3  */
 4 function createPerson(name,age,job){
 5     var o = new Object();
 6     o.name = name;
 7     o.age  = age;
 8     o.job = job;
 9     o.sayName = function(){
10         console.log(this.name);
11     };
12     // 等價于  o.sayName = new Function("console.log(this.name);");
13     return o;
14 }
15 var p1 = createPerson('lvyahui1',12,'devloper');
16 var p2 = createPerson('lvyahui2',23,'desigen');
17 p1.sayName();
18 p2.sayName();
19 console.log(p1.sayName === p2.sayName); // false      

這種方法存在很多問題,比如多個對象的方法是獨立的,沒用共享。不能判斷對象的類型

2、構造函數模式

1 /**
 2  * 構造函數模式
 3  */
 4 function Person (name,age,job){
 5     this.name = name;
 6     this.age = age;
 7     this.job = job;
 8     this.sayName = function(){
 9         console.log(this.name);
10     };
11 }
12 var pp1 = new Person('lvyahui1',12,'dev');
13 var pp2 = new Person('lvyahui2',12,'desien');
14 pp1.sayName();
15 pp2.sayName();
16 console.log(pp1.constructor === Person);    //  true
17 console.log(pp2.constructor === Person);    //  true
18 console.log(pp1 instanceof Object);         //  true
19 console.log(pp2 instanceof Person);         //  true      

這中方式解決了對象類型判斷的問題,但卻沒有解決方法共享的問題,方法依然在每個對象裡建立了一遍。要解決這樣的問題,可以像下面這樣

1 function Home (name,age,job){
2     this.name = name;
3     this.age = age;
4     this.job = job;
5     this.sayAge = sayAge;
6 }
7 function sayAge(){
8     console.log(this.age);
9 }      

但這樣有帶來了新的問題,在全局作用域定義的function隻能在對象環境運作,違背了全局的概念;而且,當要定義的方法非常多的石猴。就要定義n多的全局函數,毫無封裝性可言。

另外,需要注意的是,構造函數也是函數,它與普通函數的唯一差別,就在于調用方式的不同。如下面這樣,我門可以拿它當普通函數用,這樣屬性和方法将被綁定到浏覽器全局的執行環境window上,或者綁定到别的對象上運作,将屬性方法綁定到别的對象

1 // 構造函數也是函數
 2 //  1
 3 var person = new Person('hahha',23,'ddd');
 4 //  2 做普通函數
 5 Person('window',11,'2323dd');
 6 //window.sayName();
 7 
 8 //  3 在另一個對象的作用域中使用
 9 var o = new Object();
10 Person.call(o,'lvyahui',23,'2323'),
11 o.sayName();      

3、原型模式

js中每一個函數都有一個特殊的屬性-prototype(原型),這個屬性是一個指針,指向一個對象。這個對象包含所有以這個函數為構造函數建立的對象共享的屬性和方法,首先看下面的代碼

1 /**
 2  * 原型模式
 3  */
 4 function Dog(){}
 5 Dog.prototype.name = 'a mi';
 6 Dog.prototype.age = 1;
 7 Dog.prototype.sayName = function(){
 8     console.log(this.name);
 9 }
10 
11 var d1 = new Dog(),
12     d2 = new Dog();
13 d1.sayName();
14 d2.sayName();
15 console.log(d1.sayName === d2.sayName);         //  true
16 console.log(Dog.prototype.isPrototypeOf(d1));   //  true
17 console.log(Object.getPrototypeOf(d1)); //傳回 [[prototype]]的值
18 console.log(Object.getPrototypeOf(d1) === Dog.prototype);      

這裡将屬性和方法都添加到了函數的原型對象中。在第4行的代碼中我定義了Dog構造方法,這讓Dog的原型對象的constructor屬性指向了Dog。5-7行代碼又為Dog的原型對象添加了2個屬性和一個方法。

第11行、12行代碼建立了兩個對象執行個體,這兩個執行個體中都隻包含了一個特殊的屬性[[Prototype]],這個屬性指向了構造函數的prototype,構造函數的prototype屬性指向了原型對象,原型對象的constructor屬性有指會了Dog構造方法,這裡比較繞,我直接取書上面的圖給大家看,稍作了修改,圖裡面的是Person構造函數,這裡實在抱歉,暫時沒在ubuntu系統上找到好的作圖工具,隻能将就了,有知道的可以介紹給我用用。

JavaScript OOP 建立對象的7種方式

另外這個圖其實還省略了以個繼承關系,就是Person對象其實是繼承Obejct的,這是預設規則,别的語言也有。這也是js對象一建立就有object的方法和屬性的原因。

再一個,上面的代碼中的

  • isPrototypeOf方法傳回的是參數對象的[[Prototype]]屬性是否指向調用isPrototypeOf的對象。
  • getPrototype方法傳回[[Prototype]]屬性的值。

原型很重要的一個概念是屬性搜尋(方法我認為也是屬性),從執行個體對象開始一直往原型鍊上遊搜尋,如果找到了就停止搜尋,是以如過我們在執行個體對象中添加原型中已有的屬性或方法,會屏蔽掉原型鍊中的屬性或方法。看下面的代碼

1 d1.name = 'sai mao';    // 屏蔽原型中的屬性
2 console.log(d1.name);
3 // 要恢複原型中的屬性,必須顯示删除執行個體中的屬性
4 delete d1.name;
5 console.log(d1.name);      

 可以看到,可以使用delete恢複原型中的屬性,而下面的方法可以用來檢測一個屬性是在執行個體對象中,還是在原型鍊的原型對象中。

1 // 檢測一個屬性是在執行個體中,還是在原型中
 2 d1.name = 'hhhh';
 3 console.log(d1.hasOwnProperty('name')); // 隻有在給定的屬性存在于對象執行個體中才傳回true
 4 delete d1.name;
 5 console.log(d1.hasOwnProperty('name'));
 6 
 7 //單獨使用in操作符,隻要能在對象中找到屬性則傳回true
 8 d1.name = 'dsfdsfsd';
 9 console.log('name' in d1);  // true
10 console.log('name' in d2);  // ture
11 
12 //同時使用hasOwnProperty 和 in操作符,就能确定這個屬性到底是在原型中還是在執行個體中
13 function hasPrototypeProperty(object,name){
14     return !object.hasOwnProperty(name) && name in object;
15 }
16 console.log('d1 hasPrototypeProperty :'+hasPrototypeProperty(d1, 'name'));
17 console.log('d2 hasPrototypeProperty :'+hasPrototypeProperty(d2, 'name'));      

例外有一種更簡單的原型寫法

1 // 更簡單的原型文法
 2 function Cat(){}
 3 Cat.prototype = {
 4     name    :   'mimi',
 5     age     :   12,
 6     job     :   'doubi',
 7     sayName :   function(){
 8         console.log(this.name);
 9     }
10 };
11 
12 var cat = new Cat();
13 console.log(cat instanceof Object);
14 console.log(cat instanceof Cat);
15 console.log(cat.constructor === Cat);//false
16 console.log(cat.constructor === Object);//true      

這種方式其實是以字面量的方法重新建立了一個對象,然後指派給了原型指針。它丢棄了原來的原型對象,是以很顯然的原型對象的constructor屬性不再指向Cat,而是指向了Obejct,有多種方法可以修複構造函數,比如在定義字面量對象的時候,就顯示制定constructor屬性為Cat,也可以使用下面的方法。

1 // 重設Cat的constructor屬性
 2 Cat.prototype.constructor = Cat;
 3 //  但這樣constructor變成可枚舉的了
 4 var cat_keys = Object.keys(Cat.prototype);
 5 console.log(cat_keys);//[ 'name', 'age', 'job', 'sayName', 'constructor' ]
 6 console.log(Object.keys(cat));//[]
 7 
 8 // 重設constructor的屬性
 9 Object.defineProperty(Cat.prototype,'constructor',{
10     enumerable:false,
11     value:Cat
12 });
13 cat_keys = Object.keys(Cat.prototype);
14 console.log(cat_keys);//[ 'name', 'age', 'job', 'sayName' ]      

原型模式也不是沒有問題,比如不好向構造函數傳遞參數。它最大的問題是對引用類型的原型屬性的共享問題,看下面的代碼

1 // 原型模式最大的問題在于對引用類型的屬性共享問題
 2 
 3 function House(){}
 4 
 5 House.prototype = {
 6     constructor:House,
 7     friends:['lvyahui','d']
 8 };
 9 
10 var h1 = new House(),
11     h2 = new House();
12 h1.friends.push('li');
13 console.log(h1.friends);//[ 'lvyahui', 'd', 'li' ]
14 console.log(h2.friends);//[ 'lvyahui', 'd', 'li' ]      

4、構造函數與原型對象方式組合的模式

組合模式可以說吸取了構造函數模式與原型模式的優點,既保證了每個對象執行個體都有自己獨立的屬性和方法,同時有可以實作共享的屬性和方法。

1 /**
 2  * 常用方式,組合使用構造函數模式和原型模式
 3  */
 4 
 5 function Movie(name,length){
 6     this.name= name;
 7     this.length = length;
 8     this.links = ['h1','h2'];
 9 }
10 Movie.prototype = {
11     constructor:Movie,
12     sayName : function (){
13         console.log(this.name);
14     }
15 };
16 var m1 = new Movie('diany1',14),
17     m2 = new Movie('diany2',23);
18 m1.links.push('h3');
19 
20 console.log(m1.links);
21 console.log(m2.links);
22 console.log(m1.links === m2.links);
23 console.log(m1.sayName === m2.sayName);      

這種方式集各家之長,我想這種方式應該是用的比較多的了吧(本人還未畢業,對企業裡實際情況不太了解,有知道的可以悄悄告訴我)

當然,還有一種更好的寫法,就是所謂的動态原型模式

5、動态原型模式

1 function Miss(name,age){
 2     this.name = name;
 3     this.age = age;
 4 
 5     if(typeof this.sayName != 'function'){
 6         Miss.prototype.sayName = function(){
 7             console.log(this.name);
 8         }
 9     }
10 }
11 
12 var miss = new Miss('lvyahui',12);
13 miss.sayName();      

這種方式的在保持了組合模式的優點的前提下,讓代碼看起了封裝性更好,也更安全。

6、寄生構造模式

 這中方式與工廠模式,就隻有一點差別,通過new 構造函數的形式建立對象,像下面這樣,注意它隻帶建立對象的時候與工廠模式有差別(16行 new)

1 /**
 2  * 寄生構造模式
 3  */
 4 
 5 function createPerson2(name,age,job){
 6     var o = new Object();
 7     o.name = name;
 8     o.age  = age;
 9     o.job = job;
10     o.sayName = function(){
11         console.log(this.name);
12     };
13     // 等價于  o.sayName = new Function("console.log(this.name);");
14     return o;
15 }
16 var p1 = new createPerson2('lvyahui1',12,'devloper');      

7、穩妥構造函數模式

這種模式基于穩妥對象的概念,這種對象是沒有公共屬性,它的方法中也不使用this的對象。大家都知道js中的this一直都是讓人頭疼的問題。

穩妥模式與寄生模式類似,差別在于

  • 不通過new操作符調用構造函數
  • 不在行建立對象的執行個體方法中使用this
1 /**
 2  * 穩妥構造模式
 3  */
 4 function Girl(name,age){
 5     var o = new Object();
 6     o.sayName = function(){
 7         console.log(name);
 8     };
 9     o.sayAge = function(){
10         console.log(age);
11     };
12     return o;
13 }
14 var gril = Girl('d',21);
15 console.log(gril.sayName());
16 console.log(gril.sayAge());
17 //    輸出
18 //    d
19 //    undefined
20 //    21
21 //    undefined
22 //    為什麼呢?      

大家知道這個輸出為什麼是這麼嗎?知道的留言告訴我吧。

好了,就寫這麼多了,總結一下,原生的建立js對象的最普适的方法應該是組合模式了吧。

繼續閱讀