js創建對象的幾種方法及繼承

創建對象

通過Object構造函數或對象字面量創建單個對象

 這些方式有明顯的缺點:使用同一個接口創建很多對象,會產生大量的重複代碼。爲了解決這個問題,出現了工廠模式。  

工廠模式

   考慮在ES中無法創建類(ES6前),開發人員發明了一種函數,用函數來封裝以特定接口創建對象的細節。(實現起來是在一個函數內創建好對象,然後把對象返回)。    

function createPerson(name,age,job){
    var o=new Object();
    o.name=name;
    o.age=age;
    o.job=job;
    o.sayName=function(){
        alert(this.name);
    };
    return 0;
}

var person1=createPerson("Nicholas",29,"Software Engineer");
var person2=createPerson("Greg",27,"Doctor");
複製代碼

構造函數模式

像Object和Array這樣的原生構造函數,在運行時會自動出現在執行環境。此外,也可以創建自定義的構造函數,從而定義自定義對象類型的屬性和方法。

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.sayName=function(){
        alert(this.name);
    };
}

var person1=new Person(...);
var person2=new Person(...);
複製代碼

 與工廠模式相比,具有以下特點:  

  •  沒有顯式創建對象;

  • 直接將屬性和方法賦給了this對象;

  • 沒有return語句;

  • 要創建新實例,必須使用new操作符;(否則屬性和方法將會被添加到window對象)

  • 可以使用instanceof操作符檢測對象類型(instanceof運算符用於測試構造函數的prototype屬性是否出現在對象的原型鏈中的任何位置)

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
var auto = new Car('Honda', 'Accord', 1998);

console.log(auto instanceof Car);
// expected output: true

console.log(auto instanceof Object);
// expected output: true
複製代碼
  •  

構造函數的問題:

構造函數內部的方法會被重複創建,不同實例內的同名函數是不相等的。可通過將方法移到構造函數外部解決這一問題,但面臨新問題:封裝性不好。

原型模式

我們創建的每個函數都有一個prototype屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。(prototype就是通過調用構造函數而創建的那個對象實例的原型對象)。

 使用原型對象的好處是可以讓所有對象實例共享它所包含的屬性和方法。換句話說,不必在構造函數中定義對象實例的信息,而是可以將這些信息直接添加到原型對象中。    

function Person(){
}

Person.prototype.name="Nicholas";
Person.prototype.age=29;
Person.prototype.job="...";
Person.prototype.sayName=function(){
    ...
};

var person1=new Person();
person1.sayName();//"Nicholas"
複製代碼

更常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象,並重設constructor屬性。

function Person(){
}

Person.prototype={
    name:"...",
    age:29,
    job:"...",
    sayName:function(){
        ...
    }
};

Object.defineProperty(Person.prototype,"constructor",{
    enumerable:false,
    value:Person,
});
複製代碼

 原型對象的問題:  

  •  他省略了爲構造函數傳遞初始化參數這一環節,結果所有實例在默認情況下都將取得相同的屬性值,雖然這會在一定程度帶來一定的不便,但不是最大的問題,最大的問題是由其共享的本性所決定的。
  • 對於包含基本值的屬性可以通過在實例上添加一個同名屬性隱藏原型中的屬性。然後,對於包含引用數據類型的值來說,會導致問題。

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

這是創建自定義類型的最常見的方式。

 構造函數模式用於定義實例屬性,而原型模式用於定義方法和共享的屬性。所以每個實例都會有自己的一份實例屬性的副本,但同時共享着對方法的引用,最大限度的節省了內存。同時支持向構造函數傳遞參數。    

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.friends=["S","C"];
}

Person.prototype={
    constructor:Person,
    sayName:function(){
        alert(this.name);
    }
};

var person1=new Person(...);
複製代碼

動態原型模式

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;

    if(typeof this.sayName!="function"){
        Person.prototype.sayName=function(){
            alert(this.name);
        };
    }
}
複製代碼

 這裏只有sayName()不存在的情況下,纔會將它添加到原型中,這段代碼只會在初次調用構造函數時才執行。這裏對原型所做的修改,能夠立刻在所有實例中得到反映。  

Object.create()

ES5定義了一個名爲Object.create()的方法,它創建一個新對象,其中第一個參數是這個對象的原型,第二個參數對對象的屬性進行進一步描述。

Object.create()介紹

Object.create(null) 創建的對象是一個空對象,在該對象上沒有繼承 Object.prototype 原型鏈上的屬性或者方法,例如:toString(), hasOwnProperty()等方法

Object.create()方法接受兩個參數:Object.create(obj,propertiesObject) ;

obj:一個對象,應該是新創建的對象的原型。

propertiesObject:可選。該參數對象是一組屬性與值,該對象的屬性名稱將是新創建的對象的屬性名稱,值是屬性描述符(這些屬性描述符的結構與Object.defineProperties()的第二個參數一樣)。注意:該參數對象不能是 undefined,另外只有該對象中自身擁有的可枚舉的屬性纔有效,也就是說該對象的原型鏈上屬性是無效的。

var o = Object.create(Object.prototype, {
  // foo會成爲所創建對象的數據屬性
  foo: { 
    writable:true,
    configurable:true,
    value: "hello" 
  },
  // bar會成爲所創建對象的訪問器屬性
  bar: {
    configurable: false,
    get: function() { return 10 },
    set: function(value) {
      console.log("Setting `o.bar` to", value);
    }
  }
});
console.log(o);//{foo:'hello'}
var test1 = Object.create(null) ;
console.log(test1);// {} No Properties 
因爲在bar中設置了configurable 使用set,get方法默認都是不起作用,所以bar值無法賦值或者獲取
這裏的o對象繼承了 Object.prototype  Object上的原型方法
我們可以 對象的 __proto__屬性,來獲取對象原型鏈上的方法 如:
console.log(o.__proto__);//{__defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, __lookupSetter__: ƒ, …}
console.log(test1.__proto__);//undefined
複製代碼

通過打印發現, 將{}點開,顯示的是 No Properties ,也就是在對象本身不存在屬性跟方法,原型鏈上也不存在屬性和方法,

new object()

var test1 = {x:1};

var test2 = new Object(test1);

var test3 = Object.create(test1);
console.log(test3);//{} 
//test3等價於test5
var test4 = function(){
  
}
test4.prototype = test1;
var test5 = new test4();
console.log(test5);
console.log(test5.__proto__ === test3.__proto__);//true
console.log(test2);//{x:1}
複製代碼
var test1 = {};
var test2 = new Object();
var test3 = Object.create(Object.prototype);
var test4 = Object.create(null);//console.log(test4.__proto__)=>undefined 沒有繼承原型屬性和方法
console.log(test1.__proto__ === test2.__proto__);//true
console.log(test1.__proto__ === test3.__proto__);//true
console.log(test2.__proto__ === test3.__proto__);//true
console.log(test1.__proto__ === test4.__proto__);//false
console.log(test2.__proto__ === test4.__proto__);//false
console.log(test3.__proto__ === test4.__proto__);//false
複製代碼

總結:使用Object.create()是將對象繼承到__proto__屬性上

var test = Object.create({x:123,y:345});
console.log(test);//{}
console.log(test.x);//123
console.log(test.__proto__.x);//3
console.log(test.__proto__.x === test.x);//true

var test1 = new Object({x:123,y:345});
console.log(test1);//{x:123,y:345}
console.log(test1.x);//123
console.log(test1.__proto__.x);//undefined
console.log(test1.__proto__.x === test1.x);//false

var test2 = {x:123,y:345};
console.log(test2);//{x:123,y:345};
console.log(test2.x);//123
console.log(test2.__proto__.x);//undefined
console.log(test2.__proto__.x === test2.x);//false
複製代碼

繼承

我這裏就介紹一種吧,剩下的可以去權威指南里看去

原型鏈

ECMAScript 中描述了原型鏈的概念,並將原型鏈作爲實現繼承的主要方法。其基本思想是利用原 型讓一個引用類型繼承另一個引用類型的屬性和方法。簡單回顧一下構造函數、原型和實例的關係:每 個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型 對象的內部指針。那麼,假如我們讓原型對象等於另一個類型的實例,結果會怎麼樣呢?顯然,此時的 原型對象將包含一個指向另一個原型的指針,相應地,另一個原型中也包含着一個指向另一個構造函數 的指針。假如另一個原型又是另一個類型的實例,那麼上述關係依然成立,如此層層遞進,就構成了實 例與原型的鏈條。這就是所謂原型鏈的基本概念。

實現原型鏈有一種基本模式,其代碼大致如下。

function SuperType(){
        this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.subproperty = false;
}
//繼承了 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
    return this.subproperty;
    };
    var instance = new SubType();
alert(instance.getSuperValue());
//true
複製代碼

以上代碼定義了兩個類型:SuperType 和 SubType。每個類型分別有一個屬性和一個方法。它們 的主要區別是 SubType 繼承了 SuperType,而繼承是通過創建 SuperType 的實例,並將該實例賦給 SubType.prototype 實現的。實現的本質是重寫原型對象,代之以一個新類型的實例。換句話說,原 來存在於 SuperType 的實例中的所有屬性和方法,現在也存在於 SubType.prototype 中了。在確立了 繼承關係之後,我們給 SubType.prototype 添加了一個方法,這樣就在繼承了 SuperType 的屬性和方 法的基礎上又添加了一個新方法。這個例子中的實例以及構造函數和原型之間的關係如圖 6-4 所示。

原型鏈的問題

 

原型鏈雖然很強大,可以用它來實現繼承,但它也存在一些問題。其中,最主要的問題來自包含引 用類型值的原型。想必大家還記得,我們前面介紹過包含引用類型值的原型屬性會被所有實例共享;而 這也正是爲什麼要在構造函數中,而不是在原型對象中定義屬性的原因。在通過原型來實現繼承時,原 型實際上會變成另一個類型的實例。於是,原先的實例屬性也就順理成章地變成了現在的原型屬性了。 下列代碼可以用來說明這個問題

 function SuperType(){
        this.colors = ["red", "blue", "green"];
        }
function SubType(){
}
//繼承了 SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType(); 
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType(); 
alert(instance2.colors); //"red,blue,green,black"
複製代碼

這個例子中的 SuperType 構造函數定義了一個 colors 屬性,該屬性包含一個數組(引用類型值)。 SuperType 的每個實例都會有各自包含自己數組的 colors 屬性。當 SubType 通過原型鏈繼承了 SuperType 之後,SubType.prototype 就變成了 SuperType 的一個實例,因此它也擁有了一個它自 己的 colors 屬性——就跟專門創建了一個 SubType.prototype.colors 屬性一樣。但結果是什麼 呢?結果是 SubType 的所有實例都會共享這一個 colors 屬性。而我們對 instance1.colors 的修改 能夠通過 instance2.colors 反映出來,就已經充分證實了這一點。

原型鏈的第二個問題是:在創建子類型的實例時,不能向超類型的構造函數中傳遞參數。實際上, 應該說是沒有辦法在不影響所有對象實例的情況下,給超類型的構造函數傳遞參數。有鑑於此,再加上 前面剛剛討論過的由於原型中包含引用類型值所帶來的問題,實踐中很少會單獨使用原型鏈。

借用構造函數

在解決原型中包含引用類型值所帶來問題的過程中,開發人員開始使用一種叫做借用構造函數 (constructor stealing)的技術(有時候也叫做僞造對象或經典繼承)。這種技術的基本思想相當簡單,即 在子類型構造函數的內部調用超類型構造函數。別忘了,函數只不過是在特定環境中執行代碼的對象, 因此通過使用 apply()和 call()方法也可以在(將來)新創建的對象上執行構造函數,如下所示:

function SuperType(){
    this.colors = ["red", "blue", "green"];
    }
 function SubType(){
//繼承了 SuperType
    SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);    //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors);    //"red,blue,green"
複製代碼

代碼中加粗的那一行代碼“借調”了超類型的構造函數。通過使用 call()方法(或 apply()方法 也可以),我們實際上是在(未來將要)新創建的 SubType 實例的環境下調用了 SuperType 構造函數。 這樣一來,就會在新 SubType 對象上執行 SuperType()函數中定義的所有對象初始化代碼。結果, SubType 的每個實例就都會具有自己的 colors 屬性的副本了。

1. 傳遞參數

相對於原型鏈而言,借用構造函數有一個很大的優勢,即可以在子類型構造函數中向超類型構造函 數傳遞參數。看下面這個例子。

function SuperType(name){
    this.name = name;
}
function SubType(){
//繼承了 SuperType,同時還傳遞了參數 SuperType.call(this, "Nicholas");
//實例屬性
    this.age = 29;
}
var instance = new SubType();
alert(instance.name);    //"Nicholas";
alert(instance.age);     //29
複製代碼

以上代碼中的 SuperType 只接受一個參數 name,該參數會直接賦給一個屬性。在 SubType 構造 函數內部調用 SuperType 構造函數時,實際上是爲 SubType 的實例設置了 name 屬性。爲了確保 SuperType 構造函數不會重寫子類型的屬性,可以在調用超類型構造函數後,再添加應該在子類型中 定義的屬性。

2. 借用構造函數的問題

如果僅僅是借用構造函數,那麼也將無法避免構造函數模式存在的問題——方法都在構造函數中定 義,因此函數複用就無從談起了。而且,在超類型的原型中定義的方法,對子類型而言也是不可見的,結 果所有類型都只能使用構造函數模式。考慮到這些問題,借用構造函數的技術也是很少單獨使用的。

組合繼承

組合繼承(combination inheritance),有時候也叫做僞經典繼承,指的是將原型鏈和借用構造函數的 技術組合到一塊,從而發揮二者之長的一種繼承模式。其背後的思路是使用原型鏈實現對原型屬性和方 法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣,既通過在原型上定義方法實現了函數 複用,又能夠保證每個實例都有它自己的屬性。下面來看一個例子。

 function SuperType(name){
        this.name = name;
        this.colors = ["red", "blue", "green"];
}
    SuperType.prototype.sayName = function(){
        alert(this.name);
    }
    function SubType(name, age){
//繼承屬性 
SuperType.call(this, name);
    this.age = age;
}
//繼承方法
SubType.prototype = new SuperType(); 
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){
    alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors);
instance1.sayName();
instance1.sayAge();
//"red,blue,green,black"
//"Nicholas";
//29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors);
instance2.sayName();
instance2.sayAge();
//"red,blue,green"
//"Greg";
//27
複製代碼

在這個例子中,SuperType 構造函數定義了兩個屬性:name 和 colors。SuperType 的原型定義 了一個方法 sayName()。SubType 構造函數在調用 SuperType 構造函數時傳入了 name 參數,緊接着 又定義了它自己的屬性 age。然後,將 SuperType 的實例賦值給 SubType 的原型,然後又在該新原型 上定義了方法 sayAge()。這樣一來,就可以讓兩個不同的 SubType 實例既分別擁有自己屬性——包 括 colors 屬性,又可以使用相同的方法了

組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,成爲 JavaScript 中最常用的繼 承模式。而且,instanceof 和 isPrototypeOf()也能夠用於識別基於組合繼承創建的對象。 9

原型式繼承

可以在不必預先定義構造函數的情況下實現繼承,其本質是執行對給定對象的淺 複製。而複製得到的副本還可以得到進一步改造。

寄生式繼承

與原型式繼承非常相似,也是基於某個對象或某些信息創建一個對象,然後增強 對象,最後返回對象。爲了解決組合繼承模式由於多次調用超類型構造函數而導致的低效率問 題,可以將這個模式與組合繼承一起使用。

寄生組合式繼承

集寄生式繼承和組合繼承的優點與一身,是實現基於類型繼承的最有效方式。

發佈了110 篇原創文章 · 獲贊 21 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章