文章目录
- 将 border-left-width 转换成 borderLeftWidth
- 过滤范围
- 原位(in place)过滤范围
- 降序排列
- 复制和排序数组
- 创建一个可扩展的 calculator
- 映射到 names
- 映射到对象
- 按年龄对用户排序
- 随机排列数组
- 获取平均年龄
- 数组去重
- 从数组创建键(值)对象
- Iterable object(可迭代对象)
- Symbol.iterator
- 字符串是可迭代的
- 显式调用迭代器
- 可迭代(iterable)和类数组(array-like)
- Array.from
- 总结
✅任务
将 border-left-width 转换成 borderLeftWidth
重要程度5️⃣
编写函数 camelize(str)
将诸如 “my-short-string” 之类的由短划线分隔的单词变成骆驼式的 “myShortString”。
即:删除所有短横线,并将短横线后的每一个单词的首字母变为大写。
示例:
camelize("background-color") == 'backgroundColor';
camelize("list-style-image") == 'listStyleImage';
camelize("-webkit-transition") == 'WebkitTransition';
提示:使用 split
将字符串拆分成数组,对其进行转换之后再 join
回来。
解决方案
function camelize(str) { return str .split('-') // splits 'my-long-word' into array ['my', 'long', 'word'] .map( // capitalizes first letters of all array items except the first one // converts ['my', 'long', 'word'] into ['my', 'Long', 'Word'] (word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1) ) .join(''); // joins ['my', 'Long', 'Word'] into 'myLongWord' }
过滤范围
重要程度4️⃣
写一个函数 filterRange(arr, a, b)
,该函数获取一个数组 arr
,在其中查找数值大于或等于 a
,且小于或等于 b
的元素,并将结果以数组的形式返回。
该函数不应该修改原数组。它应该返回新的数组。
例如:
let arr = [5, 3, 8, 1];
let filtered = filterRange(arr, 1, 4);
alert( filtered ); // 3,1(匹配值)
alert( arr ); // 5,3,8,1(未修改)
解决方案
function filterRange(arr, a, b) { // 在表达式周围添加了括号,以提高可读性 return arr.filter(item => (a <= item && item <= b)); } let arr = [5, 3, 8, 1]; let filtered = filterRange(arr, 1, 4); alert( filtered ); // 3,1(匹配的值) alert( arr ); // 5,3,8,1(未经改动的数组中的值)
原位(in place)过滤范围
重要程度4️⃣
写一个函数 filterRangeInPlace(arr, a, b)
,该函数获取一个数组 arr
,并删除其中介于 a
和 b
区间以外的所有值。检查:a ≤ arr[i] ≤ b
。
该函数应该只修改数组。它不应该返回任何东西。
例如:
let arr = [5, 3, 8, 1];
filterRangeInPlace(arr, 1, 4); // 删除了范围在 1 到 4 之外的所有值
alert( arr ); // [3, 1]
解决方案
function filterRangeInPlace(arr, a, b) { for (let i = 0; i < arr.length; i++) { let val = arr[i]; // 如果超出范围,则删除 if (val < a || val > b) { arr.splice(i, 1); i--; } } } let arr = [5, 3, 8, 1]; filterRangeInPlace(arr, 1, 4); // 删除 1 到 4 范围之外的值 alert( arr ); // [3, 1]
降序排列
重要程度4️⃣
let arr = [5, 2, 1, -10, 8];
// ……你的代码以降序对其进行排序
alert( arr ); // 8, 5, 2, 1, -10
解决方案
let arr = [5, 2, 1, -10, 8]; arr.sort((a, b) => b - a); alert( arr );
复制和排序数组
重要程度5️⃣
我们有一个字符串数组 arr
。我们希望有一个排序过的副本,但保持 arr
不变。
创建一个函数 copySorted(arr)
返回这样一个副本。
let arr = ["HTML", "JavaScript", "CSS"];
let sorted = copySorted(arr);
alert( sorted ); // CSS, HTML, JavaScript
alert( arr ); // HTML, JavaScript, CSS (no changes)
解决方案
我们可以使用
slice()
来创建一个副本并对其进行排序:function copySorted(arr) { return arr.slice().sort(); } let arr = ["HTML", "JavaScript", "CSS"]; let sorted = copySorted(arr); alert( sorted ); alert( arr );
创建一个可扩展的 calculator
重要程度5️⃣
创建一个构造函数 Calculator
,以创建“可扩展”的 calculator 对象。
该任务由两部分组成。
-
首先,实现
calculate(str)
方法,该方法接受像"1 + 2"
这样格式为“数字 运算符 数字”(以空格分隔)的字符串,并返回结果。该方法需要能够理解加号+
和减号-
。用法示例:
let calc = new Calculator; alert( calc.calculate("3 + 7") ); // 10
-
然后添加方法
addMethod(name, func)
,该方法教 calculator 进行新操作。它需要运算符name
和实现它的双参数函数func(a,b)
。例如,我们添加乘法
*
,除法/
和求幂**
:let powerCalc = new Calculator; powerCalc.addMethod("*", (a, b) => a * b); powerCalc.addMethod("/", (a, b) => a / b); powerCalc.addMethod("**", (a, b) => a ** b); let result = powerCalc.calculate("2 ** 3"); alert( result ); // 8
- 此任务中没有括号或复杂的表达式。
- 数字和运算符之间只有一个空格。
- 你可以自行选择是否添加错误处理功能。
解决方案
- 请注意方法的存储方式。它们只是被添加到
this.methods
属性中。- 所有检测和数字转换都通过
calculate
方法完成。将来可能会扩展它以支持更复杂的表达式。function Calculator() { this.methods = { "-": (a, b) => a - b, "+": (a, b) => a + b }; this.calculate = function(str) { let split = str.split(' '), a = +split[0], op = split[1], b = +split[2]; if (!this.methods[op] || isNaN(a) || isNaN(b)) { return NaN; } return this.methods[op](a, b); }; this.addMethod = function(name, func) { this.methods[name] = func; }; }
映射到 names
重要程度5️⃣
你有一个 user
对象数组,每个对象都有 user.name
。编写将其转换为 names 数组的代码。
例如:
let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };
let users = [ john, pete, mary ];
let names = /* ... your code */
alert( names ); // John, Pete, Mary
解决方案
let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; let users = [ john, pete, mary ]; let names = users.map(item => item.name); alert( names ); // John, Pete, Mary
映射到对象
重要程度5️⃣
你有一个 user
对象数组,每个对象都有 name
,surname
和 id
。
编写代码以该数组为基础,创建另一个具有 id
和 fullName
的对象数组,其中 fullName
由 name
和 surname
生成。
例如:
let john = { name: "John", surname: "Smith", id: 1 };
let pete = { name: "Pete", surname: "Hunt", id: 2 };
let mary = { name: "Mary", surname: "Key", id: 3 };
let users = [ john, pete, mary ];
let usersMapped = /* ... your code ... */
/*
usersMapped = [
{ fullName: "John Smith", id: 1 },
{ fullName: "Pete Hunt", id: 2 },
{ fullName: "Mary Key", id: 3 }
]
*/
alert( usersMapped[0].id ) // 1
alert( usersMapped[0].fullName ) // John Smith
所以,实际上你需要将一个对象数组映射到另一个对象数组。在这儿尝试使用箭头函数 =>
来编写。
解决方案
let john = { name: "John", surname: "Smith", id: 1 }; let pete = { name: "Pete", surname: "Hunt", id: 2 }; let mary = { name: "Mary", surname: "Key", id: 3 }; let users = [ john, pete, mary ]; let usersMapped = users.map(user => ({ fullName: `${user.name} ${user.surname}`, id: user.id })); /* usersMapped = [ { fullName: "John Smith", id: 1 }, { fullName: "Pete Hunt", id: 2 }, { fullName: "Mary Key", id: 3 } ] */ alert( usersMapped[0].id ); // 1 alert( usersMapped[0].fullName ); // John Smith
请注意,在箭头函数中,我们需要使用额外的括号。
我们不能这样写:
let usersMapped = users.map(user => { fullName: `${user.name} ${user.surname}`, id: user.id });
我们记得,有两种箭头函数的写法:直接返回值
value => expr
和带主体的value => {...}
。JavaScript 在这里会把
{
视为函数体的开始,而不是对象的开始。解决方法是将它们包装在普通括号()
中:let usersMapped = users.map(user => ({ fullName: `${user.name} ${user.surname}`, id: user.id }));
这样就可以了。
按年龄对用户排序
重要程度5️⃣
编写函数 sortByAge(users)
获得对象数组的 age
属性,并根据 age
对这些对象数组进行排序。
例如:
let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };
let arr = [ pete, john, mary ];
sortByAge(arr);
// now: [john, mary, pete]
alert(arr[0].name); // John
alert(arr[1].name); // Mary
alert(arr[2].name); // Pete
解决方案
function sortByAge(arr) { arr.sort((a, b) => a.age - b.age); } let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; let arr = [ pete, john, mary ]; sortByAge(arr); // 排序后的数组为:[john, mary, pete] alert(arr[0].name); // John alert(arr[1].name); // Mary alert(arr[2].name); // Pete
译注:解决方案的代码还可以更短一些
function sortByAge(arr) { arr.sort((a, b) => a.age - b.age); }
因为
sort()
方法的语法为arr.sort([compareFunction])
,如果没有指明compareFunction
,那么元素会被按照转换为的字符串的诸个字符的 Unicode 编码进行排序,如果指明了compareFunction
,那么数组会按照调用该函数的返回值排序。即a
和b
是两个将要被比较的元素:
- 如果
compareFunction(a, b)
小于0
,那么a
会被排列到b
之前;- 如果
compareFunction(a, b)
等于0
,那么a
和b
的相对位置不变。备注:ECMAScript 标准并不保证这一行为,而且也不是所有浏览器都会遵守(例如 Mozilla 在 2003 年之前的版本);- 如果
compareFunction(a, b)
大于0
,那么b
会被排列到a
之前。因此,升序排列的函数可以简写为:
(a, b) => a.age - b.age
。
随机排列数组
重要程度3️⃣
编写函数 shuffle(array)
来随机排列数组的元素。
多次运行 shuffle
可能导致元素顺序的不同。例如:
let arr = [1, 2, 3];
shuffle(arr);
// arr = [3, 2, 1]
shuffle(arr);
// arr = [2, 1, 3]
shuffle(arr);
// arr = [3, 1, 2]
// ...
所有元素顺序应该具有相等的概率。例如,可以将 [1,2,3]
重新排序为 [1,2,3]
或 [1,3,2]
或 [3,1,2]
等,每种情况的概率相等。
解决方案
简单的解决方案可以是:
function shuffle(array) { array.sort(() => Math.random() - 0.5); } let arr = [1, 2, 3]; shuffle(arr); alert(arr);
这样是可以的,因为
Math.random() - 0.5
是一个可能是正数或负数的随机数,因此排序函数会随机地对数组中的元素进行重新排序。但是,由于排序函数并非旨在以这种方式使用,因此并非所有的排列都具有相同的概率。
例如,请考虑下面的代码。它运行 100 万次
shuffle
并计算所有可能结果的出现次数:function shuffle(array) { array.sort(() => Math.random() - 0.5); } // 所有可能排列的出现次数 let count = { '123': 0, '132': 0, '213': 0, '231': 0, '321': 0, '312': 0 }; for (let i = 0; i < 1000000; i++) { let array = [1, 2, 3]; shuffle(array); count[array.join('')]++; } // 显示所有可能排列的出现次数 for (let key in count) { alert(`${key}: ${count[key]}`); }
示例结果(取决于 Javascript 引擎):
123: 250706 132: 124425 213: 249618 231: 124880 312: 125148 321: 125223
我们可以清楚地看到这种倾斜:
123
和213
的出现频率比其他情况高得多。使用不同的 JavaScript 引擎运行这个示例代码得到的结果可能会有所不同,但是我们已经可以看到这种方法是不可靠的。
为什么它不起作用?一般来说,
sort
是一个“黑匣子”:我们将一个数组和一个比较函数放入其中,并期望其对数组进行排序。但是由于比较的完全随机性,这个黑匣子疯了,它发疯地确切程度取决于引擎中的具体实现方法。还有其他很好的方法可以完成这项任务。例如,有一个很棒的算法叫作 Fisher-Yates shuffle。其思路是:逆向遍历数组,并将每个元素与其前面的随机的一个元素互换位置:
function shuffle(array) { for (let i = array.length - 1; i > 0; i--) { let j = Math.floor(Math.random() * (i + 1)); // 从 0 到 i 的随机索引 // 交换元素 array[i] 和 array[j] // 我们使用“解构分配(destructuring assignment)”语法来实现它 // 你将在后面的章节中找到有关该语法的更多详细信息 // 可以写成: // let t = array[i]; array[i] = array[j]; array[j] = t [array[i], array[j]] = [array[j], array[i]]; } }
让我们以相同的方式测试一下:
function shuffle(array) { for (let i = array.length - 1; i > 0; i--) { let j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } } // 所有可能排列的出现次数 let count = { '123': 0, '132': 0, '213': 0, '231': 0, '321': 0, '312': 0 }; for (let i = 0; i < 1000000; i++) { let array = [1, 2, 3]; shuffle(array); count[array.join('')]++; } // 显示所有可能排列的出现次数 for (let key in count) { alert(`${key}: ${count[key]}`); }
示例输出:
123: 166693 132: 166647 213: 166628 231: 167517 312: 166199 321: 166316
现在看起来不错:所有排列都以相同的概率出现。
另外,在性能方面,Fisher — Yates 算法要好得多,没有“排序”开销。
获取平均年龄
重要程度4️⃣
编写 getAverageAge(users)
函数,该函数获取一个具有 age
属性的对象数组,并返回平均年龄。
平均值的计算公式是 (age1 + age2 + ... + ageN) / N
。
例如:
let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 29 };
let arr = [ john, pete, mary ];
alert( getAverageAge(arr) ); // (25 + 30 + 29) / 3 = 28
解决方案
function getAverageAge(users) { return users.reduce((prev, user) => prev + user.age, 0) / users.length; } let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 29 }; let arr = [ john, pete, mary ]; alert( getAverageAge(arr) ); // 28
数组去重
重要程度4️⃣
arr
是一个数组。
创建一个函数 unique(arr)
,返回去除重复元素后的数组 arr
。
例如:
function unique(arr) {
/* your code */
}
let strings = ["Hare", "Krishna", "Hare", "Krishna",
"Krishna", "Krishna", "Hare", "Hare", ":-O"
];
alert( unique(strings) ); // Hare, Krishna, :-O
解决方案
让我们先遍历数字:
- 对于每个元素,我们将检查结果数组是否已经有该元素。
- 如果有,则忽略,否则将其添加到结果中。
function unique(arr) { let result = []; for (let str of arr) { if (!result.includes(str)) { result.push(str); } } return result; } let strings = ["Hare", "Krishna", "Hare", "Krishna", "Krishna", "Krishna", "Hare", "Hare", ":-O" ]; alert( unique(strings) ); // Hare, Krishna, :-O
代码有效,但其中存在潜在的性能问题。
方法
result.includes(str)
在内部遍历数组result
,并将每个元素与str
进行比较以找到匹配项。所以如果
result
中有100
个元素,并且没有任何一项与str
匹配,那么它将遍历整个result
并进行100
次比较。如果result
很大,比如10000
,那么就会有10000
次的比较。这本身并不是问题,因为 JavaScript 引擎速度非常快,所以遍历一个有
10000
个元素的数组只需要几微秒。但是我们在
for
循环中对arr
的每个元素都进行了一次检测。因此,如果
arr.length
是10000
,我们会有10000 * 10000
= 1 亿次的比较。那真的太多了。所以该解决方案仅适用于小型数组。
进一步,在后面的 【Map and Set(映射和集合)】一章中,我们将看到如何对该方法进行优化。
从数组创建键(值)对象
重要程度4️⃣
假设我们收到了一个用户数组,形式为:{id:..., name:..., age:... }
。
创建一个函数 groupById(arr)
从该数组创建对象,以 id
为键(key),数组项为值。
例如:
let users = [
{id: 'john', name: "John Smith", age: 20},
{id: 'ann', name: "Ann Smith", age: 24},
{id: 'pete', name: "Pete Peterson", age: 31},
];
let usersById = groupById(users);
/*
// 调用函数后,我们应该得到:
usersById = {
john: {id: 'john', name: "John Smith", age: 20},
ann: {id: 'ann', name: "Ann Smith", age: 24},
pete: {id: 'pete', name: "Pete Peterson", age: 31},
}
*/
处理服务端数据时,这个函数很有用。
在这个任务里我们假设 id
是唯一的。没有两个具有相同 id
的数组项。
请在解决方案中使用数组的 .reduce
方法。
解决方案
function groupById(array) { return array.reduce((obj, value) => { obj[value.id] = value; return obj; }, {}) }
Iterable object(可迭代对象)
可迭代(Iterable) 对象是数组的泛化。这个概念是说任何对象都可以被定制为可在 for..of
循环中使用的对象。
数组是可迭代的。但不仅仅是数组。很多其他内建对象也都是可迭代的。例如字符串也是可迭代的。
如果从技术上讲,对象不是数组,而是表示某物的集合(列表,集合),for..of
是一个能够遍历它的很好的语法,因此,让我们来看看如何使其发挥作用。
Symbol.iterator
通过自己创建一个对象,我们就可以轻松地掌握可迭代的概念。
例如,我们有一个对象,它并不是数组,但是看上去很适合使用 for..of
循环。
比如一个 range
对象,它代表了一个数字区间:
let range = {
from: 1,
to: 5
};
// 我们希望 for..of 这样运行:
// for(let num of range) ... num=1,2,3,4,5
为了让 range
对象可迭代(也就让 for..of
可以运行)我们需要为对象添加一个名为 Symbol.iterator
的方法(一个专门用于使对象可迭代的内建 symbol)。
- 当
for..of
循环启动时,它会调用这个方法(如果没找到,就会报错)。这个方法必须返回一个 迭代器(iterator) —— 一个有next
方法的对象。 - 从此开始,
for..of
仅适用于这个被返回的对象。 - 当
for..of
循环希望取得下一个数值,它就调用这个对象的next()
方法。 next()
方法返回的结果的格式必须是{done: Boolean, value: any}
,当done=true
时,表示循环结束,否则value
是下一个值。
这是带有注释的 range
的完整实现:
let range = {
from: 1,
to: 5
};
// 1. for..of 调用首先会调用这个:
range[Symbol.iterator] = function() {
// ……它返回迭代器对象(iterator object):
// 2. 接下来,for..of 仅与下面的迭代器对象一起工作,要求它提供下一个值
return {
current: this.from,
last: this.to,
// 3. next() 在 for..of 的每一轮循环迭代中被调用
next() {
// 4. 它将会返回 {done:.., value :...} 格式的对象
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
// 现在它可以运行了!
for (let num of range) {
alert(num); // 1, 然后是 2, 3, 4, 5
}
请注意可迭代对象的核心功能:关注点分离。
range
自身没有next()
方法。- 相反,是通过调用
range[Symbol.iterator]()
创建了另一个对象,即所谓的“迭代器”对象,并且它的next
会为迭代生成值。
因此,迭代器对象和与其进行迭代的对象是分开的。
从技术上说,我们可以将它们合并,并使用 range
自身作为迭代器来简化代码。
就像这样:
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
this.current = this.from;
return this;
},
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
for (let num of range) {
alert(num); // 1, 然后是 2, 3, 4, 5
}
现在 range[Symbol.iterator]()
返回的是 range
对象自身:它包括了必需的 next()
方法,并通过 this.current
记忆了当前的迭代进程。这样更短,对吗?是的。有时这样也可以。
但缺点是,现在不可能同时在对象上运行两个 for..of
循环了:它们将共享迭代状态,因为只有一个迭代器,即对象本身。但是两个并行的 for..of
是很罕见的,即使在异步情况下。
ℹ️无穷迭代器(iterator)
无穷迭代器也是可能的。例如,将
range
设置为range.to = Infinity
,这时range
则成为了无穷迭代器。或者我们可以创建一个可迭代对象,它生成一个无穷伪随机数序列。也是可能的。
next
没有什么限制,它可以返回越来越多的值,这是正常的。当然,迭代这种对象的
for..of
循环将不会停止。但是我们可以通过使用break
来停止它。
字符串是可迭代的
数组和字符串是使用最广泛的内建可迭代对象。
对于一个字符串,for..of
遍历它的每个字符:
for (let char of "test") {
// 触发 4 次,每个字符一次
alert( char ); // t, then e, then s, then t
}
对于代理对(surrogate pairs),它也能正常工作!(译注:这里的代理对也就指的是 UTF-16 的扩展字符)
let str = '
标签:arr,迭代,编程语言,JavaScript,数据类型,alert,let,数组,name
From: https://blog.csdn.net/weixin_51568389/article/details/126448387