首頁 >web前端 >js教程 >跟我學習javascript的prototype使用注意事項_javascript技巧

跟我學習javascript的prototype使用注意事項_javascript技巧

WBOY
WBOY原創
2016-05-16 15:31:591201瀏覽

一、在prototype上保存方法

不使用prototype進行JavaScript的編碼是完全可行的,例如:

function User(name, passwordHash) { 
 this.name = name; 
 this.passwordHash = passwordHash; 
 this.toString = function() { 
  return "[User " + this.name + "]"; 
 }; 
 this.checkPassword = function(password) { 
  return hash(password) === this.passwordHash; 
 }; 
} 

var u1 = new User(/* ... */); 
var u2 = new User(/* ... */); 
var u3 = new User(/* ... */); 

當創建了多個User類型的實例時,就存在問題了:不僅是name和passwordHash屬性在每個實例上都存在,toString和checkPassword方法在每個實例上都有一份拷貝。就像下圖表示的:

但是,當toString和checkPassword被定義在prototype上時,上圖就變成下面這個樣子了:

toString和checkPassword方法現在定義在了User.prototype物件上,也意味著這兩個方法只存在一份拷貝,並且被所有的User實例共用。

也許你會認為將方法作為拷貝放在每個實例上,會節省方法查詢的時間。 (當方法定義在prototype上時,首先會在實例本身上尋找方法,如果沒有找到才會去prototype上繼續找)

但是在現代的JavaScript執行引擎中,對方法的查詢進行了大量優化,所以這個查詢時間幾乎是不需要考慮的,那麼將方法放在prototype物件上就節省了很多記憶體。

二、使用閉包來保存私有資料

JavaScript的物件系統從其語法上而言並不鼓勵使用資訊隱藏(Information Hiding)。因為當使用諸如this.name,this.passwordHash的時候,這些屬性預設的存取等級就是public的,在任何位置都能夠透過obj.name,obj.passwordHash來對這些屬性進行存取。

在ES5環境中,也提供了一些方法來更方便的存取一個物件上所有的屬性,例如Object.keys(),Object.getOwnPropertyNames()。所以,有些開發人員使用一些規約來定義JavaScript物件的私有屬性,例如最典型的是使用底線作為屬性的前綴來告訴其他開發人員和使用者這個屬性是不應該直接存取的。

但是這樣做,並不能從根本上解決問題。其他開發人員和使用者還是能夠對帶有下劃線的屬性進行直接存取。對於確實需要私有屬性的場合,可以使用閉包來實作。

從某種意義而言,在JavaScript中,閉包對於變數的存取策​​略和物件的存取策略是兩個極端。閉包中的任何變數預設都是私有的,只有在函數內部才能存取這些變數。例如,可以將User類型實作如下:

function User(name, passwordHash) { 
 this.toString = function() { 
  return "[User " + name + "]"; 
 }; 
 this.checkPassword = function(password) { 
  return hash(password) === passwordHash; 
 }; 
} 

此時,name和passwordHash都沒有被儲存為實例的屬性,而是透過局部變數來保存。然後根據閉包的訪問規則,實例上的方法可以對它們進行訪問,而在其它地方則不能。

使用這種模式的一個缺點是,利用了局部變數的方法都需要被定義在實例本身上,不能講這些方法定義在prototype物件上。正如在Item34中討論的那樣,這樣做的問題是會增加記憶體的消耗。但是在某些特別的場合下,即使將方法定義在實例上也是可行的。

三、實例狀態只保存在實例物件上

一個類型的prototype和該類型的實例之間是」一對多「的關係。那麼,需要確保實例相關的資料不會被錯誤地保存在prototype之上。例如,對於一個實現了樹結構的類型而言,將它的子節點保存在該類型的prototype上就是不正確的:

function Tree(x) { 
 this.value = x; 
} 
Tree.prototype = { 
 children: [], // should be instance state! 
 addChild: function(x) { 
  this.children.push(x); 
 } 
}; 

var left = new Tree(2); 
left.addChild(1); 
left.addChild(3); 

var right = new Tree(6); 
right.addChild(5); 
right.addChild(7); 

var top = new Tree(4); 
top.addChild(left); 
top.addChild(right); 

top.children; // [1, 3, 5, 7, left, right] 

當狀態被保存到了prototype上時,所有實例的狀態都會被集中地保存,在上面這種場景中顯然是不正確的:本來屬於每個實例的狀態被錯誤地共享了。如下圖:

正確的實作應該是這樣的:

function Tree(x) { 
 this.value = x; 
 this.children = []; // instance state 
} 
Tree.prototype = { 
 addChild: function(x) { 
  this.children.push(x); 
 } 
}; 

此时,实例状态的存储如下所示:

可见,当本属于实例的状态被共享到prototype上时,也许会产生问题。在需要在prototype上保存状态属性前,一定要确保该属性是能够被共享的。

总体而言,当一个属性是不可变(无状态)的属性时,就能将它保存在prototype对象上(比如方法能够被保存在prototype对象上就是因为这一点)。当然,有状态的属性也能够被放在prototype对象上,这要取决于具体的应用场景,典型的比如用来记录一个类型实例数量的变量。使用Java语言作为类比的话,这类能够存储在prototype对象上的变量就是Java中的类变量(使用static关键字修饰)。

四、避免继承标准类型

ECMAScript标准库不大,但是提供了一些重要的类型如Array,Function和Date。在一些场合下,你也许会考虑继承其中的某个类型来实现特定的功能,但是这种做法并不被鼓励。

比如为了操作一个目录,可以让目录类型继承Array类型如下:

function Dir(path, entries) { 
 this.path = path; 
 for (var i = 0, n = entries.length; i < n; i++) { 
  this[i] = entries[i]; 
 } 
} 
Dir.prototype = Object.create(Array.prototype); 
// extends Array 

var dir = new Dir("/tmp/mysite", ["index.html", "script.js", "style.css"]); 
dir.length; // 0 

但是可以发现,dir.length的值是0,而不是期待中的3。

发生这种现象的原因在于:只有当对象是真正的Array类型时,length属性才会起作用。

在ECMAScript标准中,定义了一个不可见的内部属性被称为 [[class]]。该属性的值只是一个字符串,所以不要被误导认为JavaScript也实现了自己的类型系统。所以,对于Array类型,这个属性的值就是“Array”;对于Function类型,这个属性的值就是“Function”。下表是ECMAScript定义的所有[[class]] 值:

那么当对象的类型确实是Array时,length属性的特别之处就在于:length的值会和该对象中被索引的属性个数保持一致。比如对于一个数组对象arr,arr[0]和arr[1]就表示该对象有两个被索引的属性,那么length的值就是2。当添加了arr[2]的时候,length的值会被自动同步成3。同样地,当设置length值为2时,arr[2]会被自动设置成undefined。

但是当继承Array类型并创建实例时,该实例的 [[class]] 属性并不是Array,而是Object。因此length属性不能正确的工作。

在JavaScript中,也提供了用于查询 [[class]] 属性的方法,即使用Object.prototype.toString方法:

var dir = new Dir("/", []); 
Object.prototype.toString.call(dir); // "[object Object]" 
Object.prototype.toString.call([]); // "[object Array]" 

因此,更好的实现方法是使用组合而不是继承:

function Dir(path, entries) { 
 this.path = path; 
 this.entries = entries; // array property 
} 
Dir.prototype.forEach = function(f, thisArg) { 
 if (typeof thisArg === "undefined") { 
  thisArg = this; 
 } 
 this.entries.forEach(f, thisArg); 
}; 

以上代码将不再使用继承,而是将一部分功能代理给内部的entries属性来实现,该属性的值是一个Array类型对象。

ECMAScript标准库中,大部分的构造函数都会依赖内部属性值如 [[class]] 来实现正确的行为。对于继承这些标准类型的子类型,无法保证它们的行为是正确的。因此,不要继承ECMAScript标准库中的类型如:
Array, Boolean, Date, Function, Number,RegExp,String

以上就是对使用prototype的几点注意事项进行总结,希望可以帮助大家正确的使用prototype。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn