1

参考书籍:《Effective JavaScript》

数组和字典

对象是JavaScript中最万能的数据结构。取决于不同的环境,对象可以表示一个灵活的键值关联记录,一个继承了方法的面向对象数据抽象,一个密集或稀疏的数组,或一个散列表。

使用Object的直接实例构造轻量级的字典

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),而不是仅仅枚举出我们关心的条目。

一个相似的错误是使用数组类型来表示字典。

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

提示:

  • 使用对象字面量构建轻量级字典。
  • 轻量级字典应该是Object.prototype的直接子类,以使for ... in循环免收原型污染。

使用null原型以防止原型污染

在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)

提示:

  • 在ES5环境中,使用Object.create(null)创建的自由原型的空对象是不太容易被污染的。
  • 在一些老的环境中,考虑使用{ __proto__: null }
  • 但是注意__proto__既不标准,已不是完全可移植的,并且可能在未来的JavaScript环境中去除。
  • 绝不要使用“__proto__”名作为字典的key,因为一些环境将其作为特殊的属性对待。

使用hasOwnProperty方法以避免原型污染

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__属性,该实现保证是可工作的。

提示:

  • 使用hasOwnProperty方法避免原型污染。
  • 使用词法作用域和call方法避免覆盖hasOwnProperty方法。
  • 考虑在封装hasOwnProperty测试样板代码的类中实现字典操作。
  • 使用字典类避免将“__proto__”作为key来使用。

使用数组而不要使用字典来存储有序集合

直观地说,一个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循环来枚举对象属性应当与顺序无关。
  • 如果聚集运算字典中的数据,确保聚集操作与数据无关。
  • 使用数组而不是字典来存储有序集合。

绝不要在Object.prototype中增加可枚举的属性

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循环枚举一个对象的属性时,确保不要修改该对象。
  • 当迭代一个对象时,如果该对象的内容可能会在循环期间被改变,应该使用while循环或经典的for循环来代替for ... in循环。
  • 为了在不断变化的数据结构中能够预测枚举,考虑使用一个有序的数据结构,例如数组,而不要使用字典对象。

数组迭代要优先使用for循环而不是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循环而不是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.forEachArray.prototype.map)替代for循环使得代码更可读,并且避免了重复循环控制逻辑。
  • 使用自定义的迭代函数来抽象未被标准库支持的常见循环模式。
  • 在需要提前终止循环的情况下,仍然推荐使用传统的循环。另外,some和every方法也可用于提前退出。

在类数组对象上复用通用的数组方法

Array.prototype中的标准方法被设计成其他对象可复用的方法,即使这些对象并没有继承Array。

例如,函数的arguments对象没有继承Array.prototype,但是我们可以提取出forEach方法对象的引用并使用call方法来遍历每一个参数。

function highlight() {
    [].forEach.call(arguments, function (widget) {
        widget.setBackground('yellow');
    });
}

在Web平台,DOM(Document Object Model)的NodeList类是另一个类数组对象的实例。

数组对象的基本契约总共有两个简单的规则:

  • 具有一个范围在0到22^32 - 1的整型length属性。
  • length属性大于该对象的最大索引。

这就是一个对象需要实现的与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数组的所有行为很精妙,这要归功于数组行为的两个方面。

  • 将length属性值设为小于n的值会自动地删除索引值大于或等于n的所有属性。
  • 增加一个索引值为n(大于或等于length属性值)的属性会自动地设置length属性为n + 1。

幸运的是,对于使用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']

提示:

  • 对于类数组对象,通过提取方法对象并使用其call方法来复用通用的Array方法。
  • 任意一个具有索引属性和恰当length属性的对象都可以使用通用的Array方法。

数组字面量优于数组构造函数

字面量是一种表示数组的优雅的方法。

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

提示:

  • 如果数组构造函数的第一个参数是数字则数组的构造函数行为是不同的。
  • 使用数组字面量替代数组构造函数。

3santiago3
113 声望2 粉丝