什么是声明式编程?
在开始讲解我们的主要内容之前,先来介绍一下什么是声明式编程?
用最简单的话说,就是以声明的方式编写代码:
比如, 我们使用for循环对数组进行遍历,而在声明式编程中我们仅仅调用数组的方法 forEach
进行遍历,
写在for循环内部的处理逻辑,我们也通过一个 callback
函数变量传达给 forEach
方法,
forEach
每一次访问数组元素, 都会对其调用回调函数:
for (let i = 0; i < arr.length; i++) {
console.log(arr[i])
}
arr.forEach((i)=>console.log(i))
使用forEach
方法我们可以用更少的代码,更直接的意图,达到同样的目的,这就是声明式编程的魅力。
不同于平常的命令式编程, 告诉机器怎么循环, 怎么处理, 我们仅需要告诉机器 "需要做什么"。
如何优雅的处理数组?
数组声明式方法
Array.prototype.forEach
首先从我们刚刚使用到的forEach
开始:
arr.forEach(callback(currentValue [, index [, array]])[, thisArg])
forEach
接受一个 callback
函数, 这两者正代表着 做什么(遍历), 怎么做(callback)
其中 callback
函数还接受三个参数, 分别
- 当前遍历到的值
- 当前所在的数组索引
- 当前的数组引用
这里涵盖了我们大部分的使用场景, 当然它还接受一个 thisArg
作为调用 callback
时的 this
指向。
深入解析forEach
这是 MDN 给出的 forEach
垫片(Polyfill)函数, 我们不难看出本质上就是利用 while
循环对数组进行操作。
if (!Array.prototype.forEach) {
Array.prototype.forEach = function(callback, thisArg) {
var T, k;
if (this == null) {
throw new TypeError(' this is null or not defined');
}
var O = Object(this);
var len = O.length >>> 0;
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function');
}
if (arguments.length > 1) {
T = thisArg;
}
k = 0;
while (k < len) {
var kValue;
if (k in O) {
kValue = O[k];
callback.call(T, kValue, k, O);
}
k++;
}
};
}
这也意味着, 我们的 callback
并不支持异步操作, 我们可以通过一些小技巧来进行异步操作, 我们将在后续进行统一讲解。
Array.prototype.map
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])
map
与 forEach
最大的不同是, map
将callback
返回的数据拼接, 返回一个全新的结果数组。
深入理解 map
知道原理才能更好的运用, 老样子, 让我们看看 MDN 上 map
的垫片函数
if (!Array.prototype.map) {
Array.prototype.map = function(callback/*, thisArg*/) {
var T, A, k;
if (this == null) {
throw new TypeError('this is null or not defined');
}
var O = Object(this);
var len = O.length >>> 0;
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
if (arguments.length > 1) {
T = arguments[1];
}
A = new Array(len);
k = 0;
while (k < len) {
var kValue, mappedValue;
if (k in O) {
kValue = O[k];
mappedValue = callback.call(T, kValue, k, O);
A[k] = mappedValue;
}
k++;
}
return A;
};
}
本质上, map
就是声明一个新数组并存储每一个callback
执行后的值, 即便 callback
不返回, 返回 undefined
或者返回null
也将一并加入结果数组。
如果我们仅需要数组的部分内容, 请使用 filter
Array.prototype.filter
var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])
filter
方法将创建一个新数组, 其中包含所有 callback
函数返回为 true
的元素。
// 只要大于2的元素
arr.filter( a => ( a > 2 )}
它将会跳过所有结果为 filter
的数组元素。
深入理解 filter
filter
的内部实现便是通过一个 if-else
进行判断, 如果 callback
为真即添加入结果数组。
if (!Array.prototype.filter){
Array.prototype.filter = function(func, thisArg) {
'use strict';
if ( ! ((typeof func === 'Function' || typeof func === 'function') && this) )
throw new TypeError();
var len = this.length >>> 0,
res = new Array(len),
t = this, c = 0, i = -1;
if (thisArg === undefined){
while (++i !== len){
if (i in this){
if (func(t[i], i, t)){
res[c++] = t[i];
}
}
}
}
else{
while (++i !== len){
if (i in this){
if (func.call(thisArg, t[i], i, t)){
res[c++] = t[i];
}
}
}
}
res.length = c;
return res;
};
}
some, every
这两个方法有点类似, 我就放在一起介绍了
-
some
方法接受一个callback
函数, 返回一个boolean
值仅在遇到 第一个
callback
结果为true
的数组元素时停止遍历。 当所有元素为false
时返回false
空数组返回
false
-
every
方法接受一个callback
函数, 返回一个boolean
值仅在遇到 第一个
callback
结果为false
的数组元素时停止遍历。 当所有元素为true
时返回true
空数组返回
true
深入理解 some 和 every
本质上利用了 return
中断函数执行的特点, 与 break
类似。
两者会跳过空值(undefined
), 也就是没有被赋值的引用
话不多说, 来看实现
-
some
if (!Array.prototype.some) { Array.prototype.some = function(fun/*, thisArg*/) { 'use strict'; if (this == null) { throw new TypeError('Array.prototype.some called on null or undefined'); } if (typeof fun !== 'function') { throw new TypeError(); } var t = Object(this); var len = t.length >>> 0; var thisArg = arguments.length >= 2 ? arguments[1] : void 0; for (var i = 0; i < len; i++) { if (i in t && fun.call(thisArg, t[i], i, t)) { return true; } } return false; }; }
-
every
if (!Array.prototype.every) { Array.prototype.every = function(callbackfn, thisArg) { 'use strict'; var T, k; if (this == null) { throw new TypeError('this is null or not defined'); } var O = Object(this); var len = O.length >>> 0; if (typeof callbackfn !== 'function') { throw new TypeError(); } if (arguments.length > 1) { T = thisArg; } k = 0; while (k < len) { var kValue; if (k in O) { kValue = O[k]; var testResult = callbackfn.call(T, kValue, k, O); if (!testResult) { return false; } } k++; } return true; }; }
Array.prototype.reduce
arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
这或许是所有数组方法中最难以掌握的一个, 它接受一个 callback
函数和一个初始值, 之后它的每一次执行都依赖上一次执行的返回值。
如果没有提供initialValue,reduce 会从索引1的地方开始执行 callback 方法,以第一个索引的值作为初始值, 跳过第一个索引。如果提供initialValue,从索引0开始。
回调函数 callback
接受四个参数:
- accumulator 总计值, 也就是之前所有值的总和, 第一次执行为 参数初始值
- currentValue 当前值
- index 当前索引
- array 数组引用
accumulator 等于上一次 callback
函数的返回值。
适合处理 处理数组的每一次访问都依赖于上一次访问, 当然 reduce
是从头向尾访问, 而 reduceRight
则是从尾向头访问。
深入理解 Reduce
如果数组只有一个元素, 则返回唯一值, 如果数组为空并且没有初始值, 则会报错。
if (!Array.prototype.reduce) {
Object.defineProperty(Array.prototype, 'reduce', {
value: function(callback /*, initialValue*/) {
if (this === null) {
throw new TypeError( 'Array.prototype.reduce ' +
'called on null or undefined' );
}
if (typeof callback !== 'function') {
throw new TypeError( callback +
' is not a function');
}
var o = Object(this);
var len = o.length >>> 0;
var k = 0;
var value;
// 如果数组小等于 1, 则返回唯一值或报错
if (arguments.length >= 2) {
value = arguments[1];
} else {
while (k < len && !(k in o)) {
k++;
}
if (k >= len) {
throw new TypeError( 'Reduce of empty array ' +
'with no initial value' );
}
value = o[k++];
}
while (k < len) {
if (k in o) {
value = callback(value, o[k], k, o);
}
k++;
}
return value;
}
});
}
那些你不得不知道技巧
合理运用
首先我给大家介绍了每个数组方法及其差异, 我们需要让他们在不同的场合发挥最大的威力, 减少性能浪费。
1. 判断情况
首先我们要确定我们需要的返回结果
数组方法 | 返回结果 | forEach | undefined | some | boolean | every | boolean | filter | array | map | array | reducer | any |
---|
every
和 some
还有 forEach
都可用于遍历, 但是不返回新数组。
every
和 some
适用于判断数组情况, 返回 boolean
,
map
和 filter
适用于生成结果, 返回 结果数组,
一般来说仅遍历的话, 请使用 forEach
。
2. 替代 for 实现 break 或 continue
如果我们仅需要遍历, 但是并不需要完全遍历数组。
这时候可以使用 forEach
和 some
或 every
来节省开销。
some
和 every
利用 callback
return 可以在我们需要的时候中断循环。
forEach
中 利用 callback
return 可以跳过此次访问。
这样就可以达到 break
和 continue
的目的了
3. 链式调用
map
和 filter
都可以返回一个数组作为结果, 不同的是 filter
可以过滤元素, 而map不行。
所以我们可以通过串联, 来元素进行处理和过滤, 得到我们最终需要的元素。
// 得到所有大于3的数字, 乘以2之后相加
[1,2,3,4,5].filter(a=>a>3).map(a=>a*2).reduce((a,c)=>a+c, 0)
异步函数 async/await callback
这里是一个重点, 刚刚所有的方法垫片中 我们可以看到, 各类声明式方法仅仅是在其中执行我们的 callback
函数,
并相应的返回我们需要的结果, 而不会去等待一个 Promise 是否完成
这并不支持我们在声明式方法中使用 Async/await 函数, 所以接下来给大家介绍几个兼容的方法技巧
forEach 实现并行和串行
附上一段大佬的代码, 下列代码本意上是1s后依次输出 1, 4, 9
但是最后的结果是并行输出 1, 4, 9
var getNumbers = () => {
return Promise.resolve([1, 2, 3])
}
var multi = num => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (num) {
resolve(num * num)
} else {
reject(new Error('num not specified'))
}
}, 1000)
})
}
async function test () {
var nums = await getNumbers()
nums.forEach(async x => {
var res = await multi(x)
console.log(res)
})
}
test()
简单地说, 原因在于,Async函数会通过await 等待内部 Promise 执行, 此时Async函数返回一个pending
的Promise。
而 forEach
略过 callback
Async 函数, 并没有等待 callback
函数的 Promise
状态变为 fulfilled
, 直接结束循环。
最后 每次访问的 callback
函数 await 结束, 执行方法, 并行触发。
为了实现串行触发, 处理这个结果我们可以这样处理
- 重写 forEach 函数
async function asyncForEach(array, callback) { for (let index = 0; index < array.length; index++) { await callback(array[index], index, array) } } async function test () { var nums = await getNumbers() asyncForEach(nums, async x => { var res = await multi(x) console.log(res) }) }
- 使用 迭代器, 也就是 for of 循环
async function test () { var nums = await getNumbers() for(let x of nums) { var res = await multi(x) console.log(res) } }
异步 map 处理方法
大致原因跟 forEach
相似, 不同的是 map
返回一个新的数组, 所以我们需要用 Promise.all
来接收
由此保证所有 map
结果 Promise
状态都为 fulfilled
。
var arr = [1, 2, 3, 4, 5];
var results: number[] = await Promise.all(arr.map(async (item): Promise<number> => {
await callAsynchronousOperation(item);
return item + 1;
}));
参考文章
- MDN developer.mozilla.org/zh-CN/docs/…
- 当 async/await 遇上 forEach objcer.com/2017/10/12/…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!