天天看點

javascript 原型、原型鍊、對象複制等原理和示例分析(下)

原型

原型是 javascript 面向對象特性中重要的概念,也是大家太熟悉的概念。因為在絕大多

數的面向對象語言中,對象是基于類的(例如 java 和 c++ ) ,對象是類執行個體化的結果。而在

javascript 語言中,沒有類的概念

① ,對象由對象執行個體化。打個比方來說,基于類的語言中類

就像一個模具,對象由這個模具澆注産生,而基于原型的語言中,原型就好像是一件藝術品

的原件,我們通過一台 100% 精确的機器把這個原件複制出很多份。

前面小節的例子中都沒有涉及原型,僅僅通過構造函數和 new 語句生成類,讓我們看

看如何使用原型和構造函數共同生成對象。

function person() {}

person.prototype.name = ‘byvoid’;

person.prototype.showname = function() {

console.log(this.name);

};

var person = new person();

person.showname();

上面這段代碼使用了原型而不是構造函數初始化對象。 這樣做與直接在構造函數内定義

屬性有什麼不同呢?

**構造函數内定義的屬性繼承方式與原型不同, 子對象需要顯式調用父對象才能繼承構

造函數内定義的屬性。

構造函數内定義的任何屬性, 包括函數在内都會被重複建立, 同一個構造函數産生的

兩個對象不共享執行個體。**

構造函數内定義的函數有運作時閉包的開銷, 因為構造函數内的局部變量對其中定義

的函數來說也是可見的。

下面這段代碼可以驗證以上問題:

function foo() {

var innervar = ‘hello’;

this.prop1 = ‘byvoid’;

this.func1 = function() {

innervar = ”;

}

foo.prototype.prop2 = ‘carbo’;

foo.prototype.func2 = function() {

console.log(this.prop2);

var foo1 = new foo();

var foo2 = new foo();

console.log(foo1.func1 == foo2.func1); // 輸出 false

console.log(foo1.func2 == foo2.func2); // 輸出 true

盡管如此,并不是說在構造函數内建立屬性不好,而是兩者各有适合的範圍。那麼我們

什麼時候使用原型,什麼時候使用構造函數内定義來建立屬性呢?

**除非必須用構造函數閉包,否則盡量用原型定義成員函數,因為這樣可以減少開銷。

盡量在構造函數内定義一般成員, 尤其是對象或數組, 因為用原型定義的成員是多個

執行個體共享的。**

接下來,我們介紹一下javascript中的原型鍊機制。

原型鍊

javascript 中有兩個特殊的對象: object 與 function ,它們都是構造函數,用于生

成對象。 object.prototype 是所有對象的祖先, function.prototype 是所有函數的原

型,包括構造函數。我把 javascript 中的對象分為三類,

一類是使用者建立的對象,

一類是構造函數對象,

一類是原型對象。

使用者建立的對象,即一般意義上用 new 語句顯式構造的對象。

構造函數對象指的是普通的構造函數,即通過 new 調用生成普通對象的函數。

原型對象特指構造函數 prototype 屬性指向的對象。

這三類對象中每一類都有一個 proto 屬性,它指向該對象的原型,從任何對象沿着它開始周遊都可以追溯到 object.prototype 。

構造函數對象有 prototype 屬性,指向一個原型對象,通過該構造函數建立對象時,被建立對象的 proto 屬性将會指向構造函數的 prototype 屬性。

原型對象有 constructor屬性,指向它對應的構造函數。讓我們通過下面這個例子來了解原型:

function foo() {}

object.prototype.name = ‘my object’;

foo.prototype.name = ‘bar’;

var obj = new object();

var foo = new foo();

console.log(obj.name); // 輸出 my object

console.log(foo.name); // 輸出 bar

console.log(foo.proto.name); // 輸出 bar

console.log(foo.proto.proto.name); // 輸出 my object

console.log(foo.proto.constructor.prototype.name); // 輸出 bar

我們定義了一個叫做 foo () 的構造函數,生成了對象 foo 。同時我們還分别給 object和 foo 生成原型對象。

下圖解析了它們之間錯綜複雜的關系。

javascript 原型、原型鍊、對象複制等原理和示例分析(下)

對象的複制

javascript 和 java 一樣都沒有像c語言中一樣的指針,所有對象類型的變量都是指向對

象的引用,兩個變量之間指派傳遞一個對象并不會對這個對象進行複制,而隻是傳遞引用。

有些時候我們需要完整地複制一個對象,這該如何做呢? java 語言中有 clone 方法可以實

現對象複制,但 javascript 中沒有這樣的函數。是以我們需要手動實作這樣一個函數,一個

簡單的做法是複制對象的所有屬性:

object.prototype.clone = function() {

var newobj = {};

for (var i in this) {

newobj[i] = this[i];

return newobj;

var obj = {

name: ‘byvoid’,

likes: [‘node’]

var newobj = obj.clone();

obj.likes.push(‘python’);

console.log(obj.likes); // 輸出 [ ‘node’, ‘python’ ]

console.log(newobj.likes); // 輸出 [ ‘node’, ‘python’ ]

上面的代碼是一個對象淺拷貝(shallow copy)的實作,即隻複制基本類型的屬性,而

共享對象類型的屬性。 淺拷貝的問題是兩個對象共享對象類型的屬性, 例如上例中 likes 屬

性指向的是同一個數組。

實作一個完全的複制,或深拷貝(deep copy)并不是一件容易的事,因為除了基本資料

類型,還有多種不同的對象,對象内部還有複雜的結構,是以需要用遞歸的方式來實作:

if (typeof(this[i]) == ‘object’ || typeof(this[i]) == ‘function’) {

newobj[i] = this[i].clone();

} else {

array.prototype.clone = function() {

var newarray = [];

for (var i = 0; i < this.length; i++) {

newarray[i] = this[i].clone();

newarray[i] = this[i];

return newarray;

function.prototype.clone = function() {

var that = this;

var newfunc = function() {

return that.apply(this, arguments);

newfunc[i] = this[i];

return newfunc;

likes: [‘node’],

display: function() {

},

newobj.likes.push(‘python’);

console.log(obj.likes); // 輸出 [ ‘node’ ]

console.log(newobj.display == obj.display); // 輸出 false

上面這個實作看起來很完美,它不僅遞歸地複制了對象複雜的結構,還實作了函數的深

拷貝。這個方法在大多數情況下都很好用,但有一種情況它卻無能為力,例如下面的代碼:

var obj1 = {

ref: null

var obj2 = {

ref: obj1

obj1.ref = obj2;

這段代碼的邏輯非常簡單,就是兩個互相引用的對象。當我們試圖使用深拷貝來複制

obj1 和 obj2 中的任何一個時,問題就出現了。因為深拷貝的做法是遇到對象就進行遞歸

複制,那麼結果隻能無限循環下去。對于這種情況,簡單的遞歸已經無法解決,必須設計一

套**圖論算法, 分析對象之間的依賴關系, 建立一個拓撲結構圖, 然後分别依次複制每個頂點,

并重新建構它們之間的依賴關系**。