摘要:類是由一個構造函數和一個關聯的原型組成的一種設計模式。該模式的一個缺點是,為了讓構造函數中的變量在使用它們的方法的作用域內,這些方法必須放置于實例對象中,這會導致方法副本的擴散。
參考書籍:《Effective JavaScript》
對象和原型 理解prototype、getPrototypeOf和__proto__之間的不同原型包括三個獨立但相關的訪問器。
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__是檢索對象原型的非標準函數。
類是由一個構造函數和一個關聯的原型組成的一種設計模式。
使用Object.getPrototypeOf函數而不要使用__proto__屬性__proto__屬性提供了Object.getPrototypeOf方法所不具備的額外能力,即修改對象原型鏈接的能力。這種能力會造成嚴重的影響,應當避免使用,原因如下:
可移植性:并不是所有的平臺都支持改變對象原型的特性,所以無法編寫可移植的代碼。
性能問題:現代的JavaScript引擎痘深度優化了獲取和設置對象屬性的行為,如更改了對象的內部結構(如添加或刪除該對象或其原型鏈中的對象的屬性)會使一些優化失效。
可預測性:修改對象的原型鏈會影響對象的整個繼承層次結構,在某些情況下這樣的操作可能有用,但是保持繼承層次結構的相對穩定是一個基本的準則。
可以使用ES5中的Object.create函數來創建一個具有自定義原型鏈的新對象。
提示::
始終不要修改對象的__proto__屬性。
使用Object.create函數給新對象設置自定義的原型。
使構造函數與new操作符無關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。
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語句所覆蓋。
提示:
通過使用new操作符或Object.create方法在構造函數定義中調用自身使得該構造函數與調用語法無關。
當一個函數期望使用new操作符調用時,清晰地文檔化該函數。
在原型中存儲方法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); } };
一般情況下,任何不可變的數據可以被存儲在原型中從而被安全地共享。有狀態的數據原則上也可以存儲在原型中,只要你真正想共享它。然而迄今為止,在原型對象中最常見的數據是方法,而每個實例的狀態都存儲在實例對象中。
提示:
共享可變數據可能會出問題,因為原型是被其所有的實例共享的。
將可變的實例狀態存儲在實例對象中。
認識到this變量的隱式綁定問題編寫一個簡單的、可定制的讀取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(/ /); return lines.map(function (line) { return line.split(this.regexp); }); }; var reader = new CSVReader(); reader.read("a, b, c d, e, f "); // [["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(/ /); return lines.map(function (line) { return line.split(this.regexp); }, this); }; var reader = new CSVReader(); reader.read("a, b, c d, e, f "); // [["a", "b", "c"], ["d", "e", "f"]]
但是,不是所有基于回調函數的API都考慮周全。另一種解決方案是使用詞法作用域的變量來存儲這個額外的外部this綁定的引用。
CSVReader.prototype.read = function (str) { var lines = str.trim().split(/ /); 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 d, e, f "); // [["a", "b", "c"], ["d", "e", "f"]]
在ES5的環境中,另一種有效的方法是使用回調函數的bind方法。
CSVReader.prototype.read = function (str) { var lines = str.trim().split(/ /); 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 d, e, f "); // [["a", "b", "c"], ["d", "e", "f"]]
提示:
this變量的作用域總是由其最近的封閉函數所確定。
使用一個局部變量(通常命名為self、me或that)使得this綁定對于內部函數是可用的。
在子類的構造函數中調用父類的構造函數場景圖(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); };
提示:
在子類構造函數中顯示地傳入this作為顯式地接收者調用父類構造函數。
使用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); };
提示:
繼承標準類往往由于一些特殊的內部屬性(如[[Class]])而被破壞。
使用屬性委托優于繼承標準類。
將原型視為實現細節原型是一種對象行為的實現細節。
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; }; }
提示:
避免使用輕率的猴子補丁。
記錄程序庫所執行的所有猴子補丁。
考慮通過將修改置于一個到處函數中,使猴子補丁稱為可選的。
使用猴子補丁為缺失的標準API提供polyfills。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107678.html
摘要:但實例化該構造函數仍然得到的是的實例。或者,為了避免在所有查找屬性的地方都插入這段樣本代碼,我們可以將該模式抽象到的構造函數中。該構造函數封裝了所有在單一數據類型定義中編寫健壯字典的技術細節。 參考書籍:《Effective JavaScript》 數組和字典 對象是JavaScript中最萬能的數據結構。取決于不同的環境,對象可以表示一個靈活的鍵值關聯記錄,一個繼承了方法的面向對象數...
摘要:五不要增加內置的原型增加內置構造函數如,和等的原型屬性是一個增強功能性的強大的方法,但這可能會嚴重影響可維護性,因為這種做法將使代碼變得更加不可預測。推薦使用后者,這樣根據名字就能明顯地區分出變量函數和基本常量等。 一、盡量少用全局變量 減少全局名字空間污染,最理想的情況是一個應用程序僅有一個全局變量。 二、單一var模式 var a = 1, b = 2, sum =...
摘要:訪問全局對象在瀏覽器中,全局對象可以通過屬性在代碼的任何位置訪問除非你做了些比較出格的事情,像是聲明了一個名為的局部變量。 前言 才華橫溢的Stoyan Stefanov,在他寫的由O’Reilly初版的新書《JavaScript Patterns》(JavaScript模式)中,我想要是為我們的讀者貢獻其摘要,那會是件很美妙的事情。具體一點就是編寫高質量JavaScript的一些要素...
摘要:前言月份開始出沒社區,現在差不多月了,按照工作的說法,就是差不多過了三個月的試用期,準備轉正了一般來說,差不多到了轉正的時候,會進行總結或者分享會議那么今天我就把看過的一些學習資源主要是博客,博文推薦分享給大家。 1.前言 6月份開始出沒社區,現在差不多9月了,按照工作的說法,就是差不多過了三個月的試用期,準備轉正了!一般來說,差不多到了轉正的時候,會進行總結或者分享會議!那么今天我就...
閱讀 3511·2023-04-25 14:57
閱讀 2560·2021-11-22 14:56
閱讀 2079·2021-09-29 09:45
閱讀 1761·2021-09-22 15:53
閱讀 3313·2021-08-25 09:41
閱讀 896·2019-08-29 15:22
閱讀 3289·2019-08-29 13:22
閱讀 3122·2019-08-29 13:08