参考书籍:《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
提示: