最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 如何优雅的处理Javascript数组?深入解析这些你务必掌握的声明式编程!

    正文概述 掘金(7Mouse)   2021-03-04   580

    什么是声明式编程?

    在开始讲解我们的主要内容之前,先来介绍一下什么是声明式编程?

    用最简单的话说,就是以声明的方式编写代码:

    比如, 我们使用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 函数还接受三个参数, 分别

    1. 当前遍历到的值
    2. 当前所在的数组索引
    3. 当前的数组引用

    这里涵盖了我们大部分的使用场景, 当然它还接受一个 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])
    

    mapforEach 最大的不同是, mapcallback返回的数据拼接, 返回一个全新的结果数组。

    如何优雅的处理Javascript数组?深入解析这些你务必掌握的声明式编程!如何优雅的处理Javascript数组?深入解析这些你务必掌握的声明式编程!

    深入理解 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

    这两个方法有点类似, 我就放在一起介绍了

    1. some 方法接受一个 callback 函数, 返回一个 boolean

      仅在遇到 第一个 callback 结果为 true 的数组元素时停止遍历。 当所有元素为 false 时返回 false

      空数组返回 false

    2. every 方法接受一个 callback 函数, 返回一个 boolean

      仅在遇到 第一个 callback 结果为 false 的数组元素时停止遍历。 当所有元素为 true 时返回 true

      空数组返回 true

    深入理解 some 和 every

    本质上利用了 return 中断函数执行的特点, 与 break 类似。

    两者会跳过空值(undefined), 也就是没有被赋值的引用

    话不多说, 来看实现

    1. 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;
        };
      }
      
    2. 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 接受四个参数:

    1. accumulator 总计值, 也就是之前所有值的总和, 第一次执行为 参数初始值
    2. currentValue 当前值
    3. index 当前索引
    4. array 数组引用

    accumulator 等于上一次 callback 函数的返回值。

    适合处理 处理数组的每一次访问都依赖于上一次访问, 当然 reduce 是从头向尾访问, 而 reduceRight 则是从尾向头访问。

    如何优雅的处理Javascript数组?深入解析这些你务必掌握的声明式编程!

    深入理解 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. 判断情况

    首先我们要确定我们需要的返回结果

    数组方法返回结果
    forEachundefinedsomebooleaneverybooleanfilterarraymaparrayreducerany

    everysome 还有 forEach 都可用于遍历, 但是不返回新数组。

    everysome 适用于判断数组情况, 返回 boolean,

    mapfilter 适用于生成结果, 返回 结果数组,

    一般来说仅遍历的话, 请使用 forEach

    2. 替代 for 实现 break 或 continue

    如果我们仅需要遍历, 但是并不需要完全遍历数组。

    这时候可以使用 forEachsomeevery 来节省开销。

    someevery 利用 callback return 可以在我们需要的时候中断循环。

    forEach 中 利用 callback return 可以跳过此次访问。

    这样就可以达到 breakcontinue 的目的了

    3. 链式调用

    mapfilter 都可以返回一个数组作为结果, 不同的是 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 结束, 执行方法, 并行触发。

    为了实现串行触发, 处理这个结果我们可以这样处理

    1. 重写 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)
        })
      }
      
    2. 使用 迭代器, 也就是 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/…

    起源地下载网 » 如何优雅的处理Javascript数组?深入解析这些你务必掌握的声明式编程!

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元