參考書籍:《Effective JavaScript》數組
對象是JavaScript中最萬能的數據結構。取決於不一樣的環境,對象能夠表示一個靈活的鍵值關聯記錄,一個繼承了方法的面向對象數據抽象,一個密集或稀疏的數組,或一個散列表。安全
JavaScript對象的核心是一個字符串屬性名稱和屬性值的映射表。這使得使用對象實現字典易如反掌,由於字典就是可變長的字符串與值的映射集合。網絡
JavaScript提供了枚舉一個對象屬性名的利器,for ... in
循環,可是其除了枚舉出對象「自身」的屬性外,還會枚舉出繼承過來的屬性。數據結構
若是咱們建立一個自定義的字典並將其元素做爲該字典對象自身的屬性。併發
function NaiveDict() { } NaiveDict.prototype.count = function () { var i = 0; for (var name in this) { // counts every property i++; } return i; }; NaiveDict.prototype.toString = function () { return '[object NaiveDict]'; }; var dict = new NaiveDict(); dict.alice = 34; dict.bob = 24; dict.chris = 62; dict.count(); // 5
上述代碼的問題在於咱們使用同一個對象來存儲NaiveDict數據結構的固定屬性(count和toString)和特定字典的變化條目(alice、bob和chris)。所以,當調用count來枚舉字典的全部屬性時,它會枚舉出全部的屬性(count、toString、alice、bob和chris),而不是僅僅枚舉出咱們關心的條目。函數
一個類似的錯誤是使用數組類型來表示字典。oop
var dict = new Array(); dict.alice = 34; dict.bob = 24; dict.chris = 62; dict.bob; // 24
上述代碼面對原型污染時很脆弱。原型污染指當枚舉字典的條目時,原型對象中的屬性可能會致使出現一些不指望的屬性。例如,應用程序中的其餘庫可能決定增長一些便利的方法到Array.prototype
中。性能
Array.prototype.first = function () { return this[0]; }; Array.prototype.last = function () { return this[this.length - 1]; }; var names = []; for (var name in dict) { names.push(name); } names; // ['alice', 'bob', 'chris', 'first', 'last']
這告訴咱們將對象做爲輕量級字典的首要原則是:應該僅僅將Object的直接實例做爲字典,而不是其子類(例如,NaiveDict),固然也不是數組。測試
var dict = {}; dict.alice = 34; dict.bob = 24; dict.chris = 62; var names = []; for (var name in dict) { names.push(name); } names; // ['alice', 'bob', 'chris']
固然,這仍然不能保證對於原型污染時安全的,由於任何人仍然能增長屬性到Object.prototype
中,可是經過使用Object的直接實例,咱們能夠將風險僅僅侷限於Object.prototype
。ui
提示:
Object.prototype
的直接子類,以使for ... in
循環免收原型污染。在ES5未發佈以前,你可能會嘗試設置一個構造函數的原型屬性爲null或者undefined來建立一個空原型的新對象。
但實例化該構造函數仍然獲得的是Object的實例。
function C() {} C.prototype = null; var o = new C(); Object.getPrototypeOf(o) === null; // false Object.getPrototypeOf(o) === Object.prototype; // true
ES5首先提供了標準方法來建立一個沒有原型的對象。
var o = Object.create(null); Object.getPrototypeOf(o) === null; // true
一些不支持Object.create
函數的舊的JavaScript環境可能支持另外一種值得一提的方式。
var o = { __proto__: null }; o instanceof Object; // false (no-standard)
提示:
Object.create(null)
建立的自由原型的空對象是不太容易被污染的。{ __proto__: null }
。__proto__
既不標準,已不是徹底可移植的,而且可能在將來的JavaScript環境中去除。JavaScript的對象操做老是以繼承的方式工做,即便是一個空的對象字面量也繼承了Object.prototype
的大量屬性。
var dict = {}; 'alice' in dict; // false 'toString' in dict; // true
幸運的是,Object.prototype
提供了hasOwnProperty方法,當測試字典條目時它能夠避免原型污染。
dict.hasOwnProperty('alice'); // false dict.hasOwnProperty('toString'); // false
咱們還能夠經過在屬性查找時使用一個測試來防止其受污染的影響。
dict.hasOwnProperty('alice') ? dict.alice : undefined;
hasOwnProperty方法繼承自Object.prototype
對象,可是若是在字典中存儲一個同爲「hasOwnProperty」名稱的條目,那麼原型中的hasOwnProperty方法不能再被獲取到。
dict.hasOwnProperty = 10; dict.hasOwnProperty('alice'); // error: dict.hasOwnProperty is not a function
此時咱們能夠採用call方法,而不用將hasOwnProperty做爲字典的方法來調用。
var hasOwn = Object.prototype.hasOwnProperty; // 或者,var hasOwn = {}.hasOwnProperty; hasOwn.call(dict, 'alice');
爲了不在全部查找屬性的地方都插入這段樣本代碼,咱們能夠將該模式抽象到Dict的構造函數中。該構造函數封裝了全部在單一數據類型定義中編寫健壯字典的技術細節。
function Dict(elements) { // allow an optional initial table this.elements = elements || {}; // simple Object } Dict.prototype.has = function (key) { // own property only return {}.hasOwnProperty.call(this.elements, key); }; Dict.prototype.get = function (key) { // own property only return this.has(key) ? this.elements[key] : undefined; }; Dict.prototype.set = function (key, val) { this.elements[key] = val; }; Dict.prototype.remove = function (key) { delete this.elements[key]; }; var dict = new Dict({ alice: 34, bob: 24, chris: 62 }); dict.has('alice'); // true dict.get('bob'); // 24 dict.has('toString'); // false
上述代碼比使用JavaScript默認的對象語法更健壯,並且也一樣方便使用。
在一些JavaScript的環境中,特殊的屬性名__proto__可能致使其自身的污染問題。
在某些環境中,__proto__屬性只是簡單地繼承自Object.prototype
,所以空對象是真正的空對象。
var empty = Object.create(null); '__proto__' in empty; // false (in some environments) var hasOwn = {}.hasOwnProperty; hasOwn.call(empty, '__proto__'); // false (in some environments)
在其餘的環境中,只有in操做符輸入爲true。
var empty = Object.create(null); '__proto__' in empty; // true (in some environments) var hasOwn = {}.hasOwnProperty; hasOwn.call(empty, '__proto__'); // false (in some environments)
不幸的是,某些環境會由於存在一個實例屬性__proto__而永久地污染全部的對象。
var empty = Object.create(null); '__proto__' in empty; // true (in some environments) var hasOwn = {}.hasOwnProperty; hasOwn.call(empty, '__proto__'); // true (in some environments)
這意味着,在不一樣的環境中,下面的代碼可能有不一樣的結果。
var dict = new Dict(); dict.has('__proto__'); // ?
爲了達到最大的可移植性和安全性,咱們只能爲每一個Dict方法的「__proto__」關鍵字增長一種特例。
function Dict(elements) { // allow an optional initial table this.elements = elements || {}; // simple Object this.hasSpecialProto = false; // has '__proto__' key? this.specialProto = undefined; // '__proto__' element } Dict.prototype.has = function (key) { if (key === '__proto__') { return this.hasSpecialProto; } // own property only return {}.hasOwnProperty.call(this.elements, key); }; Dict.prototype.get = function (key) { if (key === '__proto__') { return this.specialProto; } // own property only return this.has(key) ? this.elements[key] : undefined; }; Dict.prototype.set = function (key, val) { if (key === '__proto__') { this.hasSpecialProto = true; this.specialProto = val; } else { this.elements[key] = val; }; } Dict.prototype.remove = function (key) { if (key === '__proto__') { this.hasSpecialProto = false; this.specialProto = undefined; } else { delete this.elements[key]; } }; var dict = new Dict(); dict.has('__proto__'); // false
無論環境是否處理__proto__屬性,該實現保證是可工做的。
提示:
直觀地說,一個JavaScript對象是一個無序的屬性集合。ECMAScript標準併爲規定屬性存儲的任何特定順序,甚至對於枚舉對象也沒涉及。
這致使的問題是,for ... in
循環會挑選必定的順序來枚舉對象的屬性。一個常見的錯誤是提供一個API,要求一個對象表示一個從字符串到值的有序映射,例如,建立一個有序的報表。
function report(highScores) { var result = ''; var i = 1; for (var name in highScores) { // unpredictable order result += i + '. ' + name + ': ' + highScores[name] + '\n'; i++; } return result; } report([{ name: 'Hank', points: 1110100 }, { name: 'Steve', points: 1064500 }, { name: 'Billy', points: 1050200 }]); // ?
因爲不一樣的環境能夠選擇以不一樣的順序來存儲和枚舉對象屬性,因此這個函數會致使產生不一樣的字符串,獲得順序混亂的「最高分」報表。
若是你須要依賴一個數據結構中的條目順序,請使用數組而不是字典。若是上述例子中的report函數的API使用一個對象數組而不是單個對象,那麼它徹底能夠工做在任何JavaScript環境中。
function report(highScores) { var result = ''; for (var i = 0, n = highScores.length; i < n; i++) { var score = highScores[i]; result += (i + 1) + '. ' + score.name + ': ' + score.points + '\n'; } return result; } report([{ name: 'Hank', points: 1110100 }, { name: 'Steve', points: 1064500 }, { name: 'Billy', points: 1050200 }]); // 1. Hank: 1110100\n2. Steve: 1064500\n3. Billy: 1050200\n
一個微妙的順序依賴的典型例子是浮點型運算。假設有一個映射標題和等級的電影字典。
var ratings = { 'Good Will Hunting': 0.8, 'Mystic River': 0.7, '21': 0.6, 'Doubt': 0.9 }; var total = 0, count = 0; for (var key in ratings) { // unpredictable order total += ratings[key]; count++; } total /= count; total; // ?
浮點型算術運算的四捨五入會致使計算順序的微妙依賴。當組合未定義順序的枚舉時,可能會致使循環不可預知。
事實證實,流行的JavaScript環境實際上使用不一樣的順序執行這個循環。
一些環境根據加入對象的順序來枚舉對象的key
(0.8 + 0.7 + 0.6 + 0.9) / 4 // 0.75
其餘環境老是先枚舉潛在的數組索引,而後纔是其餘key。例如,電影「21」的名字剛好是一個可行的數組索引。
(0.6 + 0.8 + 0.7 + 0.9) / 4 // 0.7499999999999999
這種狀況下,更好的表示方式是在字典中使用整數值。
(8 + 7 + 6 + 9) / 4 / 10 // 0.75 (6 + 8 + 7 + 9) / 4 / 10 // 0.75
提示:
for ... in
循環來枚舉對象屬性應當與順序無關。for ... in
循環很是便利,但它很容易受到原型污染的影響。例如,若是咱們增長一個產生對象屬性名數組的allKeys方法。
Object.prototype.allKeys = function () { var result = []; for (var key in this) { result.push(key); } return result; }; ({ a: 1, b: 2, c: 3 }).allKeys(); // ['a', 'b', 'c', 'allKeys']
遺憾的是,該方法也污染了其自身。
更爲友好的是將allKeys定義爲一個函數而不是方法。
function allKeys(obj) { var result = []; for (var key in obj) { result.push(key); } return result; }
若是你確實想在Object.prototype
增長屬性,ES5提供了一種更加友好的機制。
Object.defineProperty
方法能夠定義一個對象的屬性並指定該屬性的元數據。
Object.defineProperty(Object.prototype, 'allKeys', { value: function () { var result = []; for (var key in this) { result.push(key); } return result; }, wirtable: true, enumerable: false, configurable: true });
提示:
Object.prototype
中增長屬性。Object.prototype
方法。Object.prototype
中增長屬性,請使用ES5中的Object.defineProperty
方法將它們定義爲不可枚舉的屬性。一個社交網絡有一組成員,每一個成員有一個存儲其朋友信息的註冊列表。
function Member(name) { this.name = name; this.friends = []; } var a = new Member('Alice'), b = new Member('Bob'), c = new Member('Carol'), d = new Member('Dieter'), e = new Member('Eli'), f = new Member('Fatima'); a.friends.push(b); b.friends.push(c); c.friends.push(e); d.friends.push(b); e.friends.push(d, f);
搜索該網絡意味着須要遍歷該社交網絡圖。這一般經過工做集(work-set)來實現。工做集以單個根節點開始,而後添加發現的節點,移除訪問過的節點。
Member.prototype.inNetwork = function (other) { var visited = {}; var workset = {}; workset[this.name] = this; // 工做集以單個根節點開始 for (var name in workset) { var member = workset[name]; delete workset[name]; // modified while enumerating 移除訪問過的節點 if (name in visited) { // don't revisit members continue; } visited[name] = member; if (member === other) { // found? return true; } member.friends.forEach(function (friend) { // 添加發現的節點 workset[friend.name] = friend; }); } return false; };
不幸的是,在許多JavaScript環境中這段代碼根本不能工做。
a.inNetwork(f); // false
事實上,ECMAScript對併發修改在不一樣JavaScript環境下的行爲規定了:若是被枚舉的對象在枚舉期間添加了新的屬性,那麼在枚舉期間並不能保證新添加的屬性可以被訪問。也就是,若是咱們修改了被枚舉的對象,則不能保證for ... in
循環的行爲是可預見的。
讓咱們進行另外一種遍歷圖的嘗試。此次本身管理循環控制。當咱們使用循環時,應該使用本身的字典抽象以免原型污染。
function WorkSet() { this.entries = new Dict(); this.count = 0; } Workset.prototype.isEmpty = function () { return this.count === 0; }; WorkSet.prototype.add = function (key, val) { if (this.entries.has(key)) { return; } this.entries.set(key, val); this.count++; }; WorkSet.prototype.get = function (key) { return this.entries.get(key); }; WorkSet.prototype.remove = function (key) { if (!this.entries.has(key)) { return; } this.entries.remove(key); this.count--; }; WorkSet.prototype.pick = function () { return this.entries.pick(); }; Dict.prototype.pick = function () { for (var key in this.elements) { if (this.has(key)) { return key; } } throw new Error('empty dictionary'); };
如今咱們可使用簡單的while循環來實現inNetwork方法。
Member.prototype.inNetwork = function (other) { var visited = {}; var workset = new WorkSet(); workset.add(this.name, this); // 工做集以單個根節點開始 while (!workset.isEmpty()) { var name = workset.pick(); var member = workset.get(name); workset.remove(name); // 移除訪問過的節點 if (name in visited) { // don't revisit members continue; } visited[name] = member; if (member === other) { // found? return true; } member.friends.forEach(function (friend) { // 添加發現的節點 workset.add(friend.name, friend); }); } return false; };
pick方法是一個不肯定性的例子。不肯定性指的是一個操做並不能保證使用語言的語義產生一個單一的可預見的結果。這個不肯定性來源於這樣一個事實:for ... in
循環可能在不一樣的JavaScript環境中選擇不一樣的枚舉順序。
將工做條目存儲到數組中而不是集合中,則inNetwork方法將老是以徹底相同的順序遍歷圖。
Member.prototype.inNetwork = function (other) { var visited = {}; var worklist = [this]; // 工做集以單個根節點開始 while (worklist.length > 0) { var member = worklist.pop(); // 移除訪問過的節點 if (member.name in visited) { // don't revisit continue; } visited[member.name] = member; if (member === other) { // found? return true; } member.friends.forEach(function (friend) { // 添加發現的節點 worklist.push(friend); // add to work-list }); } return false; };
提示:
for ... in
循環枚舉一個對象的屬性時,確保不要修改該對象。for ... in
循環。var scores = [98, 74, 85, 77, 93, 100, 89]; var total = 0; for (var score in scores) { total += score; } var mean = total / scores.length; mean; // ?
for ... in
循環始終枚舉全部的key,即便是數組的索引屬性,對象屬性key始終是字符串。因此最終mean值爲17636.571428571428。
迭代數組內容的正確方法是使用傳統的for循環。
var scores = [98, 74, 85, 77, 93, 100, 89]; var total = 0; for (var i = 0, n = scores.length; i < n; i++) { total += scores[i]; } var mean = total / scores.length; mean; // 88
提示:
for ... in
循環。JavaScript的for循環至關簡潔。可是搞清楚終止條件是一個累贅。
for (var i = 0; i <= n; i++) { ... } // extra end iteration for (var i = 1; i < n; i++) { ... } // missing first iteration for (var i = n; i >= 0; i--) { ... } // extra start iteration for (var i = n - 1; i > 0; i--) { ... } // missing last iteration
ES5爲最經常使用的一些模式提供了便利的方法。
Array.prototype.forEach
是其中最簡單的一個。
for (var i = 0, n = players.length; i < n; i++) { players[i].score++; } // 可用如下代碼替代上面的循環 players.forEach(function (p) { p.score++; });
另外一種常見的模式是對數組的每一個元素進行一些操做後創建一個新的數組。
var trimmed = []; for (var i = 0, n = input.length; i < n; i++) { trimmed.push(input[i].trim()); } // 可用如下代碼替代上面的循環 var trimmed = []; input.forEach(function (s) { trimmed.push(s.trim()); });
經過現有的數組創建一個新的數組的模式是如此的廣泛,因此ES5引入了Array.prototype.map
方法使該模式更簡單、更優雅。
var trimmed = input.map(function (s) { return s.trim(); });
另外一個種常見的模式是計算一個新的數組,該數組只包含現有數組的一些元素。Array.prototype.filter
使其變得很簡便。
listings.filter(function (listing) { return listing.price >= min && listing.price <= max; });
咱們能夠定義本身的迭代抽象。例如,提取出知足謂詞的數組的前幾個元素。
function takeWhile(a, pred) { var result = []; for (var i = 0, n = a.length; i < n; i++) { if (!pred(a[i], i)) { break; } result[i] = a[i]; } return result; } var prefix = takeWhile([1, 2, 4, 8, 16, 32], function (n) { return n < 10; }); // [1, 2, 4, 8]
咱們也能夠將takeWhile函數添加到Array.prototype
中使其做爲一個方法(前參閱前面關於對相似Array.prototype
的標準原型添加猴子補丁的影響的討論)。
Array.prototype.takeWhile = function (pred) { var result = []; for (var i = 0, n = this.length; i < n; i++) { if (!pred(this[i], i)) { break; } result[i] = this[i]; } return result; }; var prefix = [1, 2, 4, 8, 16, 32].takeWhile(function (n) { return n < 10; }); // [1, 2, 4, 8]
循環只有一點優於迭代函數,那就是前者有控制流操做,如break和continue。舉例來講,使用forEach方法來實現takeWhile函數將是一個尷尬的嘗試。
function takeWhile(a, pred) { var result = []; a.forEach(function (x, i) { if (!pred(x)) { // ? } result[i] = x; }); return result; }
咱們可使用一個內部異常來提早終止該循環,可是這既尷尬有效率低下。
function takeWhile(a, pred) { var result = []; var earlyExit = {}; // unique value signaling loop break try { a.forEach(function (x, i) { if (!pred(x)) { throw earlyExit; } result[i] = x; }); } catch (e) { if (e !== earlyExit) { // only catch earlyExit throw e; } } return result; }
此外,ES5的數組方法some和every能夠用於提早終止循環。
some方法返回一個布爾值表示其回調對數組的任何一個元素是否返回了一個真值。
[1, 10, 100].some(function (x) { return x > 5; }); // true [1, 10, 100].some(function (x) { return x < 0; }); // false
every方法返回一個布爾值表示其回調是否對數組的全部元素返回了一個真值。
[1, 2, 3, 4, 5].every(function (x) { return x > 0; }); // true [1, 2, 3, 4, 5].some(function (x) { return x < 3; }); // false
這兩個方法都是短路循環(short-circuiting)。若是對some方法的回調一旦產生了一個真值,則some方法會直接返回,不會執行其他的元素。類似的,every方法的回調一旦產生了假值,則會當即返回。
可使用every實現takeWhile函數。
function takeWhile(a, pred) { var result = []; a.every(function(x, i) { if (!pred(x)) { return false; // break } result[i] = x; return true; // continue }); return result; } var arr = [1, 2, 4, 8, 16, 32]; // arr數組裏的元素須從小到大排序 var prefix = takeWhile(arr, function (n) { return n < 10; }); // [1, 2, 4, 8]
提示:
Array.prototype.forEach
和Array.prototype.map
)替代for循環使得代碼更可讀,而且避免了重複循環控制邏輯。Array.prototype
中的標準方法被設計成其餘對象可複用的方法,即便這些對象並無繼承Array。
例如,函數的arguments對象沒有繼承Array.prototype
,可是咱們能夠提取出forEach方法對象的引用並使用call方法來遍歷每個參數。
function highlight() { [].forEach.call(arguments, function (widget) { widget.setBackground('yellow'); }); }
在Web平臺,DOM(Document Object Model)的NodeList類是另外一個類數組對象的實例。
數組對象的基本契約總共有兩個簡單的規則:
這就是一個對象須要實現的與Array.prototype
中任一方法兼容的全部行爲。
一個簡單的對象字面量能夠用來建立一個類數組對象。
var arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3, }; var result = Array.prototype.map.call(arrayLike, function (s) { return s.toUpperCase(); }); // ['A', 'B, 'C']
字符串也表現爲不可變的數組,由於它們是可索引的,而且其長度也能夠經過length屬性獲取。
var result = Array.prototype.map.call('abc', function (s) { return s.toUpperCase(); }); // ['A', 'B, 'C']
模擬JavaScript數組的全部行爲很精妙,這要歸功於數組行爲的兩個方面。
幸運的是,對於使用Array.prototype
中的方法,這兩條規則都不是必須的,由於在增長或刪除索引屬性的時候它們都會強制地更新length屬性。
var arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3, }; Array.prototype.pop.call(arrayLike); arrayLike; // { 0: 'a', 1: 'b', length: 2 }
只有一個Array方法不是徹底通用的,即數組鏈接方法concat。
function namesColumn() { return ['Names'].concat(arguments); } namesColumn('Alice', 'Bob', 'Chris'); // ['Names', { 0: 'Alice', 1: 'Bob', 2: 'Chris' }]
爲了使concat方法將一個類數組對象視爲真正的數組,咱們不得不本身轉換該數組。
function namesColumn() { return ['names'].concat([].slice.call(arguments)); } namesColumn('Alice', 'Bob', 'Chris'); // ['Names', 'Alice', 'Bob', 'Chris']
提示:
字面量是一種表示數組的優雅的方法。
var a = [1, 2, 3, 4, 5]; // 也可使用數組構造函數來替代 var a = new Array(1, 2, 3, 4, 5);
事實證實,Array構造函數存在一些微妙的問題。
首先,你必須確保,沒有人從新包裝過Array類。
function f(Array) { return new Array(1, 2, 3, 4, 5); } f(String); // new String(1)
你還必須確保沒有人修改過全局的Array變量。
Array = String; new Array(1, 2, 3, 4, 5); // new String(1)
若是使用單個數字來調用Array構造函數,效果徹底不一樣。
var arr1 = [17]; // 建立一個元素只有17的數組,其長度屬性爲1 var arr2 = new Array(17); // 建立一個沒有元素的數組,但其長度屬性爲17
提示: