參考書籍:《Effective JavaScript》css
原型包括三個獨立但相關的訪問器。html
C.prototype
用於創建由new C()
建立的對象的原型。Object.getPrototypeOf(obj)
是ES5中用來獲取obj對象的原型對象的標準方法。obj.__proto__
是獲取obj對象的原型對象的非標準方法。function User(name, passwordHash) { this.name = name; this.passwordHash = passwordHash; } User.prototype.toString = function () { return '[User ' + this.name + ']'; }; User.prototype.checkPassword = function (password) { return hash(password) === this.passwordHash; } var u = new User('sfalken', '0ef33ae791068ec64b502d6cb0191387');
User函數帶有一個默認的prototype屬性,其包含一個開始幾乎爲空的對象。當咱們使用new操做符建立User的實例時,產生的對象u獲得了自動分配的原型對象,該原型對象被存儲在User.prototype
中。程序員
Object.getPrototypeOf(u) === User.prototype; // true
u.__proto__ === User.prototype; // true
提示:編程
C.prototype
屬性是new C()
建立的對象的原型。Object.getPrototypeOf(obj)
是ES5中檢索對象原型的標準函數。Obj.__proto__
是檢索對象原型的非標準函數。__proto__屬性提供了Object.getPrototypeOf
方法所不具有的額外能力,即修改對象原型連接的能力。這種能力會形成嚴重的影響,應當避免使用,緣由以下:設計模式
可使用ES5中的Object.create
函數來建立一個具備自定義原型鏈的新對象。數組
提示::安全
Object.create
函數給新對象設置自定義的原型。function User(name, passwordHash) { this.name = name; this.passwordHash = passwordHash; } var u = User('baravelli', 'd8b74df393528d51cd19980ae0aa028e'); u; // undefined this.name; // baravelli this.passwordHash; // d8b74df393528d51cd19980ae0aa028e
若是調用者忘記使用new關鍵字,該函數不但會返回無心義的undefined,並且會建立(若是這些全局變量已經存在則會修改)全局變量name和passwordHash。閉包
若是將User函數定義爲ES5的嚴格代碼,那麼它的接收者默認爲undefined。app
function User(name, passwordHash) { "use strict"; this.name = name; this.passwordHash = passwordHash; } var u = User('baravelli', 'd8b74df393528d51cd19980ae0aa028e'); // Uncaught TypeError: Cannot set property 'name' of undefined
一個更爲健壯的方式是提供一個無論怎麼調用都工做如構造函數的函數。函數
function User(name, passwordHash) { if (!this instanceof User) { return new User(name, passwordHash); } this.name = name; this.passwordHash = passwordHash; } var x = User('baravelli', 'd8b74df393528d51cd19980ae0aa028e'); var y = new User('baravelli', 'd8b74df393528d51cd19980ae0aa028e'); x instanceof User; // true y instanceof User; // true
上述模式的一個缺點是它須要額外的函數調用,且難適用於可變參數函數,由於沒有一種模擬apply方法將可變參數函數做爲構造函數調用的方式。
一種更爲奇異的方式是利用ES5的Object.create函數。
function User(name, passwordHash) { var self = this instanceof User ? this : Object.create(User.prototype); self.name = name; self.passwordHash = passwordHash; return self; }
Object.create
須要一個原型對象做爲參數,並返回一個繼承自原型對象的新對象。
多虧了構造函數覆蓋模式,使用new操做符調用上述User函數的行爲與以函數調用它的行爲是同樣的,這能工做徹底得益於JavaScript容許new表達式的結果能夠被構造函數的顯示return語句所覆蓋。
提示:
Object.create
方法在構造函數定義中調用自身使得該構造函數與調用語法無關。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(/* ... */);
上述代碼中的每一個實例都包含toString和checkPassword方法的副本,而不是經過原型共享這些方法。
將方法存儲在原型,使其能夠被全部的實例使用,而不須要存儲方法實現的多個副本,也不須要給每一個實例對象增長額外的屬性。
同時,現代的JavaScript引擎深度優化了原型查找,因此將方法複製到實例對象並不必定保證查找的速度有明顯的提高,並且實例方法比起原型方法確定會佔用更多的內存。
提示:
任意一段程序均可以簡單地經過訪問JavaScript對象的屬性名來獲取相應地對象屬性,例如for in
循環、ES5的Object.keys
函數和Object.getOwnPropertyNames
函數。
一些程序員使用命名規範給私有屬性前置或後置一個下劃線字符_。
然而實際上,一些程序須要更高程度的信息隱藏。
對於這種情形,JavaScript爲信息隱藏提供了閉包。閉包將數據存儲到封閉的變量中而不提供對這些變量的直接訪問,獲取閉包內部結構的惟一方式是該函數顯式地提供獲取它的途徑。
利用這一特性在對象中存儲真正的私有數據。不是將數據做爲對象的屬性來存儲,而是在構造函數中以變量的方式存儲它。
function User(name, passwordHash) { this.toString = function () { return '[User ' + name + ']'; }; this.checkPassword = function (password) { return hash(password) === passwordHash; } }
上述代碼的toString和checkPassword方法是以變量的方式來引用name和passwordHash變量的,而不是以this屬性的方式來引用,User的實例不包含任何實例屬性,所以外部的代碼不能直接訪問User實例的name和passwordHash變量。
該模式的一個缺點是,爲了讓構造函數中的變量在使用它們的方法的做用域內,這些方法必須放置於實例對象中,這會致使方法副本的擴散。
提示:
一種錯誤的作法是不當心將每一個實例的數據存儲到了其原型中。
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]
每次調用addChild方法,都會將值添加到Tree.prototype.children數組中。
實現Tree類的正確方式是爲每一個實例對象建立一個單獨的children數組。
function Tree(x) { this.value = x; this.children = []; // instance state } Tree.prototype = { addChild: function(x) { this.children.push(x); } };
通常狀況下,任何不可變的數據能夠被存儲在原型中從而被安全地共享。有狀態的數據原則上也能夠存儲在原型中,只要你真正想共享它。然而迄今爲止,在原型對象中最多見的數據是方法,而每一個實例的狀態都存儲在實例對象中。
提示:
編寫一個簡單的、可定製的讀取CSV(逗號分隔型取值)數據的類。
function CSVReader(separators) { this.separators = separators || [',']; this.regexp = new RegExp(this.separators.map(function (sep) { return '\\' + sep[0]; }).join('|')); }
實現一個簡單的read方法能夠分爲兩步來處理。第一步,將輸入的字符串分爲按行劃分的數組。第二步,將數組的每一行再分爲按單元格劃分的數組。結果得到一個二維的字符串數組。
CSVReader.prototype.read = function (str) { var lines = str.trim().split(/\n/); return lines.map(function (line) { return line.split(this.regexp); }); }; var reader = new CSVReader(); reader.read('a, b, c\nd, e, f\n'); // [['a, b, c'], ['d, e, f']]
上述代碼的bug是,傳遞給line.map
的回調函數引用的this指向的是window,所以,this.regexp
產生undefined值。
備註:'a, b, c'.split(undefined)
返回['a, b, c']
。
幸運的是,數組的map方法能夠傳入一個可選的參數做爲其回調函數的this綁定。
CSVReader.prototype.read = function (str) { var lines = str.trim().split(/\n/); return lines.map(function (line) { return line.split(this.regexp); }, this); }; var reader = new CSVReader(); reader.read('a, b, c\nd, e, f\n'); // [['a', 'b', 'c'], ['d', 'e', 'f']]
可是,不是全部基於回調函數的API都考慮周全。另外一種解決方案是使用詞法做用域的變量來存儲這個額外的外部this綁定的引用。
CSVReader.prototype.read = function (str) { var lines = str.trim().split(/\n/); var self = this; // save a reference to outer this-binding return lines.map(function (line) { return line.split(this.regexp); }); }; var reader = new CSVReader(); reader.read('a, b, c\nd, e, f\n'); // [['a', 'b', 'c'], ['d', 'e', 'f']]
在ES5的環境中,另外一種有效的方法是使用回調函數的bind方法。
CSVReader.prototype.read = function (str) { var lines = str.trim().split(/\n/); return lines.map(function (line) { return line.split(this.regexp); }.bind(this)); // bind to outer this-binding }; var reader = new CSVReader(); reader.read('a, b, c\nd, e, f\n'); // [['a', 'b', 'c'], ['d', 'e', 'f']]
提示:
場景圖(scene graph)是在可視化的程序中(如遊戲或圖形仿真場景)描述一個場景的對象集合。一個簡單的場景包含了在該場景中的全部對象(稱爲角色),以及全部角色的預加載圖像數據集,還包含一個底層圖形顯示的引用(一般被稱爲context)。
function Scene(context, width, height, images) { this.context = context; this.width = width; this.height = height; this.images = images; this.actors = []; } Scene.prototype.register = function (actor) { this.actors.push(actor); }; Scene.prototype.unregister = function (actor) { var i = this.actors.indexOf(actor); if (i >= 0) { this.actors.splice(i, 1); } }; Scene.prototype.draw = function () { this.context.clearRect(0, 0, this.width, this.height); for (var a = this.actors, i = 0, n = a.length; i < n; i++) { a[i].draw(); } };
場景中的全部角色都繼承自基類Actor。
function Actor(scene, x, y) { this.scene = scene; this.x = x; this.y = y; scene.register(this); } Actor.prototype.moveTo = function (x, y) { this.x = x; this.y = y; this.scene.draw(); }; Actor.prototype.exit = function() { this.scene.unregister(this); this.scene.draw(); }; Actor.prototype.draw = function () { var image = this.scene.images[this.type]; this.scene.context.drawImage(image, this.x, this.y); }; Actor.prototype.width = function () { return this.scene.images[this.type].width; }; Actor.prototype.height = function () { return this.scene.images[this.type].height; };
咱們將角色的特定類型實現爲Actor的子類。例如,在街機遊戲中太空飛船就會有一個拓展自Actor的SpaceShip類。
爲了確保SpaceShip的實例能做爲角色被正確地初始化,其構造函數必須顯式地調用Actor的構造函數。經過將接收者綁定到該新對象來調用Actor能夠達到此目的。
function SpaceShip(scene, x, y) { Actor.call(this, scene, x, y); this.points = 0; }
調用Actor的構造函數能確保Actor建立的全部實例屬性都被添加到了新對象(SpaceShip實例對象)中。爲了使SpaceShip成爲Actor的一個正確地子類,其原型必須繼承自Actor.prototype
。作這種拓展的最好的方式是使用ES5提供的Object.create
方法。
SpaceShip.prototype = Object.create(Actor.prototype);
一旦建立了SpaceShip的原型對象,咱們就能夠向其添加全部的可被實例共享的屬性。
SpaceShip.prototype.type = 'spaceShip'; SpaceShip.prototype.scorePoint = function () { this.points++; }; SpaceShip.prototype.left = function () { this.moveTo(Math.max(this.x - 10, 0), this.y); }; SpaceShip.prototype.right = function () { var maxWidth = this.scene.width - this.width(); this.moveTo(Math.min(this.x + 10, maxWidth), this.y); };
提示:
Object.create
函數來構造子類的原型對象以免調用父類的構造函數。function Actor(scene, x, y) { this.scene = scene; this.x = x; this.y = y; this.id = ++Actor.nextID; scene.register(this); } Actor.nextID = 0;
function Alien(scene, x, y, direction, speed, strength) { Actor.call(this, scene, x, y); this.direction = direction; this.speed = speed; this.strength = strength; this.damage = 0; this.id = ++Alien.nextID; // conflicts with actor id! } Alien.nextID = 0;
Alien類與其父類Actor類都視圖給實例屬性id寫數據。若是在繼承體系中的兩個類指向相同的屬性名,那麼它們指向的是同一個屬性。
該例子顯而易見的解決方法是對Actor標識數和Alien標識數使用不一樣的屬性名。
function Actor(scene, x, y) { this.scene = scene; this.x = x; this.y = y; this.actorID = ++Actor.nextID; // distinct from alienID scene.register(this); } Actor.nextID = 0; function Alien(scene, x, y, direction, speed, strength) { Actor.call(this, scene, x, y); this.direction = direction; this.speed = speed; this.strength = strength; this.damage = 0; this.alienID = ++Alien.nextID; // distinct from actorID } Alien.nextID = 0;
提示:
一個操做文件系統的庫可能但願建立一個抽象的目錄,該目錄繼承了數組的全部行爲。
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
遺憾的是,這種方式破壞了數組的length屬性的預期行爲。
var dir = new Dir('/tmp/mysite', ['index.html', 'script.js', 'style.css']); dir.length; // 0
失敗的緣由是length屬性只對在內部標記爲「真正的」數組的特殊對象起做用。ECMAScript標準規定它是一個不可見的內部屬性,稱爲[[Class]]。
數組對象(經過Array構造函數或[]語法建立)被加上了值爲「Array」的[[Class]]屬性,函數被加上了值爲「Function」的[[Class]]屬性。
事實證實,length的行爲只被定義在內部屬性[[Class]]的值爲「Array」的特殊對象中。對於這些對象,JavaScript保持length屬性與該對象的索引屬性的數量同步。
但當咱們拓展Array類時,子類的實例並非經過new Array()
或字面量[]語法建立的。因此,Dir的實例[[Class]]屬性值爲「Object」。
更好的實現是定義一個entries數組的實例屬性。
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); };
提示:
原型是一種對象行爲的實現細節。
JavaScript提供了便利的內省機制(introspection mechanisms)來檢查對象的細節。Object.prototype.hasOwnProperty
方法肯定一個屬性是否爲對象「本身的」屬性(即一個實例屬性),而徹底忽略原型繼承機構。Object.getPrototypeOf
和__proto__
特性容許程序員遍歷對象的原型鏈並單獨查詢其原型對象。
檢查實現細節(即便沒有修改它們)也會在程序的組件之間建立依賴。若是對象的生產者修改了實現細節,那麼依賴於這些對象的使用者就會被破壞。
提示:
因爲對象共享原型,所以每個對象均可以增長、刪除或修改原型的屬性,這個有爭議的實踐一般被稱爲猴子補丁(monkey-patching)。
猴子補丁的吸引力在於它的強大,數組缺乏一個有用的方法,你本身就能夠增長它。
Array.prototype.split = function (i) { // alternative #1 return [this.slice(0, 1), this.slice(i)]; };
可是當多個庫以不兼容的方式給同一個原型打猴子補丁時,問題就出現了。
Array.prototype.split = function (i) { // alternative #2 var i = Math.floor(this.length / 2); return [this.slice(0, 1), this.slice(i)]; };
如今,任一對數組split方法的使用都大約有50%的機會被破壞。
一個方法能夠將這些修改置於一個函數中,用戶能夠選擇調用或忽略。
function addArrayMethods() { Array.prototype.split = function (i) { return [this.slice(0, 1), this.slice(i)]; } }
儘管猴子補丁很危險,可是有一種特別可靠並且有價值的使用場景:polyfill。
if (typeof Array.prototype.map !== 'function') { Array.prototype.map = function (f, thisArg) { var result = []; for (var i = 0, n = this.length; i < n; i++) { result[i] = f.call(thisArg, this[i], i); } return result; }; }
提示: