CodingMeUp / AboutFE

知识归纳、内容都在issue里
74 stars 14 forks source link

16、JS原生的方法实现各种工具方法及call/apply/bind #17

Open CodingMeUp opened 7 years ago

CodingMeUp commented 7 years ago

paserInt

function _parseInt(str, radix) {
 let str_type = typeof str;
 let res = 0;
 if (str_type !== 'string' && str_type !== 'number') {
  // 如果类型不是 string 或 number 类型返回NaN
  return NaN
 }

 // 字符串处理
 str = String(str).trim().split('.')[0]
 let length = str.length;
 if (!length) {
  // 如果为空则返回 NaN
  return NaN
 }

 if (!radix) {
  // 如果 radix 为0 null undefined
  // 则转化为 10
  radix = 10;
 }
 if (typeof radix !== 'number' || radix < 2 || radix > 36) {
  return NaN
 }

 for (let i = 0; i < length; i++) {
  // 字符串倒叙
  let arr = str.split('').reverse().join('');
  res += Math.floor(arr[i]) * Math.pow(radix, i)
 }

 return res;
}
CodingMeUp commented 7 years ago

JS + ES6 快速排序

原理:

快速排序也是分治法思想的一种实现,他的思路是使数组中的每个元素与基准值(Pivot,通常是数组的首个值,A[0])比较,数组中比基准值小的放在基准值的左边,形成左部;大的放在右边,形成右部;接下来将左部和右部分别递归地执行上面的过程:选基准值,小的放在左边,大的放在右边。。。直到排序结束。

步骤:

image image

let qsort = (list) => { 
    if( list.length === 0 ){
      return []
    } 

    let [x, ...xs] = list
    return [...qsort(xs.filter(u=>u<=x)), x, ...qsort(xs.filter(u=>u>x))]
}

let list = [345345,3422,123,66,-2,444]
list = qsort(list)
list.forEach( i=> { console.log(i)})  // 从小到大  若 从那大小 就上面u x比较替换下顺序
//babel 转换后
"use strict";

function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }

function _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); }

var qsort = function qsort(list) {
  if (list.length === 0) {
    return [];
  }

  var _list = _toArray(list),
      x = _list[0],
      xs = _list.slice(1);

  return [].concat(_toConsumableArray(qsort(xs.filter(function (u) {
    return u <= x;
  }))), [x], _toConsumableArray(qsort(xs.filter(function (u) {
    return u > x;
  }))));
};

var list = [345345, 3422, 123, 66, -2, 444];
list = qsort(list);
list.forEach(function (i) {
  console.log(i);
});
CodingMeUp commented 7 years ago

DOM相关

UL插3个LI

这种问题 要做到以下加分 0-变量命名:节点类的变量,加上 nd 前缀,会更加容易辨识,当然,也有同学习惯借用 jquery 中的 $,关于变量命名的更多内容可以去阅读《可读代码的艺术》; 1-选择符命名:给 CSS 用和 JS 用的选择符分开,给 JS 用的选择符建议加上 js- 或 J- 前缀,提高可读性,还有没有其他好处,请思考; 2-容错能力:应该对节点的存在性做检查,这样代码才能更健壮,实际工作中,很可能你的这段代码会把其他功能搞砸,因为单个地方 JS 报错是可能导致后续代码不执行的,为啥要这样做?不理解的同学可以去看看防御性编程; 3-最小作用域原则:应该把代码段包在声明即执行的函数表达式(IIFE)里,不产生全局变量,也避免变量名冲突的风险,这是维护遗留代码必须谨记的。

(() => {
    var ndContainer = document.getElementById('js-list');
    if (!ndContainer) {
        return;
    }

    for (var i = 0; i < 3; i++) {     
        var ndItem = document.createElement('li');
        ndItem.innerText = i + 1; 
        // alert 出来的内容其实都是 3,而不是每个 <li> 的文本内容。上面两段代码都不能满足需求,因为 i 和 ndItem 的作用域范围是相同的。使用 ES6 的块级作用域能把问题解决(  let  i = 0 .... ):
       //  onclick addeventListen区别可以 转另外的事件类型
        //   ndItem.addEventListener('click', function () { 
              alert(i);
          });
        ndContainer.appendChild(ndItem);
    }
})();

如果LI有10000个 不可能使用i < 10000 事件监听增加了10000倍 就要用事件委托

    ndContainer.addEventListener('click', function (e) {
        const target = e.target;
        if (target.tagName === 'LI') {
            alert(target.innerHTML);
        }
    });

另外页面也不太流畅 也会有卡顿感、好在现代浏览器提供了 requestAnimationFrame API 来解决非常耗时的代码段对渲染的阻塞问题,该技术在 React 和 Angular 里面都有使用,如果你理解了 requestAnimationFrame 的原理,就很容易理解最新的 React Fiber 算法。 综合上面的分析,可以从减少 DOM 操作次数、缩短循环时间两个方面减少主线程阻塞的时间。减少 DOM 操作次数的良方是 DocumentFragment;而缩短循环时间则需要考虑使用分治的思想把 10000 个 LI标签 分批次插入到页面中,每次插入的时机是在页面重新渲染之前。由于 requestAnimationFrame 并不是所有的浏览器都支持,Paul Irish 给出了对应的 polyfill,这个 Gist 也非常值得你学习。

(() => {
    const ndContainer = document.getElementById('js-list');
    if (!ndContainer) {
        return;
    }

    const total = 30000;
    const batchSize = 4; // 每批插入的节点次数,越大越卡
    const batchCount = total / batchSize; // 需要批量处理多少次
    let batchDone = 0;  // 已经完成的批处理个数

    function appendItems() {
        const fragment = document.createDocumentFragment();
        for (let i = 0; i < batchSize; i++) {
            const ndItem = document.createElement('li');
            ndItem.innerText = (batchDone * batchSize) + i + 1;
            fragment.appendChild(ndItem);
        }

        // 每次批处理只修改 1 次 DOM
        ndContainer.appendChild(fragment);

        batchDone += 1;
        doBatchAppend();
    }

    function doBatchAppend() {
        if (batchDone < batchCount) {
            window.requestAnimationFrame(appendItems);
        }
    }

    // kickoff
    doBatchAppend();

    ndContainer.addEventListener('click', function (e) {
        const target = e.target;
        if (target.tagName === 'LI') {
            alert(target.innerHTML);
        }
    });
})();
CodingMeUp commented 7 years ago

DOM 遍历 DFS BFS

childNodes和children这两个时不太一样的,childNodes会将文本节点 空格 换行符也打印出来,这时候你就会看到很多undefined .undefined这样的东西,所以尽量使用children,然后用Array.from将其从HTMLCollection变为一个真正的数组。

array.from()...forEach 伪数组转数组

let printInfo = (node) => {
    console.log(node.tagName, '.' + node.className)

深度优先遍历以深度为主,即遍历完某一个节点之后,才会继续往下遍历兄弟节点,这个只需要循环遍历就行了。

function dfs (node) {
    printInfo(node)
    if(node.children.length){
        Array.from(node.children).forEach(function (el) {
            dfs(el)
        })
    }
}

dfs(root)

广度优先遍历,即父层遍历结束,才开始遍历子层,然后一直往下遍历,如果是下面这样一颗DOM树 广度优先则需要使用队列这种数据结构来管理待遍历的节点

const traverse = (ndRoot) => {
    const queue = [ndRoot];
    while (queue.length) {        
        const node = queue.shift();
        printInfo(node);

        if (!node.children.length) {
            continue;
        }

        Array.from(node.children).forEach(x => queue.push(x));    }
};

// kickoff
traverse(document.querySelector('.root'));
CodingMeUp commented 7 years ago

JS 数组对象去重

最优的数组去重算法是采用Map数据结构实现的算法 https://juejin.im/post/5b0284ac51882542ad774c45#heading-4

          Array.prototype.unique = function () {
            var res = [];
            var json = {};
            for (var i = 0; i < this.length; i++) {
               //      if (!json[this[i]])  
              if ( !json[this[i].obj ? this[i].obj._id : ''] ) {
                res.push(this[i]);
                json[this[i].obj._id] = 'diff';
              }
            }
            return res;
          }
Array.prototype.unique = function () {
  const tmp = new Map();
  return this.filter(item => {
    return !tmp.has(item) && tmp.set(item, 1);
  })
}
CodingMeUp commented 6 years ago

生产UUID

        let S4 = () => {
          return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
        }
        if(res.items && res.items.length > 0){
          res.items.map(
            (item)=>{
              item.uuid = S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()
            }
          )
        }

UUID生成器

使用crypto API生成符合RFC4122版本4的UUID。

const uuid = _ =>
  ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
    (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
  );
// uuid() -> '7982fcfe-5721-4632-bede-6000885be57d'
CodingMeUp commented 6 years ago

编码下载导出文件 excel

'use strict'

var Browser = require('nd-browser')

module.exports = {

  download: function (options) {
    var data = options.data
    var type = options.type
    var name = options.name

    if (typeof data === 'string') {
      console.log(1)
      data = [data]
    }

    var blob = new Blob(data, {
      type: type
    })

    if (Browser.browser === 'IE') {
      window.navigator.msSaveBlob(blob, name)
    } else {
      var anc = document.createElement('a')
      anc.href = window.URL.createObjectURL(blob)
      anc.setAttribute('download', name)
      anc.style.display = 'none'
      anc.click()
      anc.remove()
    }
  }

}

使用

blob.download({
     data:  [res],
     name: '导出日志' + new Date().getTime() + '.xlsx', // 本来是要xlsx的配arraybuffer 需求变更为csv
     //  name: '导出日志' + new Date().getTime() + '.csv',
     type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
 })

 if (typeof keyData === 'object') {
   let content = JSON.stringify(keyData, null, 2)
   blob.download({data: content, name: 'tenant.json', type: 'text/json'})
} 

nd-browser

/**
 * @module Browser
 * @author lzhengms <lzhengms@gmail.com>
 */

'use strict';

module.exports = (function(navigator) {

  var userAgent = navigator.userAgent,
    temp,
    match = userAgent.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [],
    match2 = userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|IEMobile/);

  if (/trident/i.test(match[1])) {
    temp = /\brv[ :]+(\d+)/g.exec(userAgent) || [];
    return {
      browser: 'IE',
      version: temp[1] || ''
    };
  }

  if (match[1] === 'Chrome') {
    temp = userAgent.match(/\bOPR\/(\d+)/);

    if (temp !== null) {
      return {
        browser: 'Opera',
        version: temp[1] || ''
      };
    }
  }

  // 针对 Edge
  if (match[1] === 'Chrome') {
    temp = userAgent.match(/\bedge\/(\d+)/i);

    if (temp !== null) {
      return {
        browser: 'IE',
        version: temp[1] || ''
      };
    }
  }

  match = match[2] ? [match[1], match[2]] : [navigator.appName, navigator.appVersion, '-?'];

  if ((temp = userAgent.match(/version\/(\d+)/i)) !== null) {
    match.splice(1, 1, temp[1]);
  }

  return {
    browser: match[0] === 'MSIE' ? 'IE' : match[0],
    version: match[1],
    mobile: match2 && match2[0]
  };

})(navigator);
CodingMeUp commented 6 years ago

将对象的变量名由连字符式转为驼峰式,支持对象的深度遍历转换

前端时间,在共享平台开发者门户做前端开发时,有个需求:服务端要基于新框架进行接口改造,原来旧接口返回的JSON数据使用的是驼峰式命名法,格式如下:

 buildType : 2
 chineseName : "部署测试"
 createTime : 1472489455000
 creator : 339691
 demandAnalysisUrl : "应用描述33"
 desc : "应用描述22"
 designAnalysisUrl : "应用描述44"
 members : [
   {
    yourName : 'li',
    yourAge : 23
   }
 ]

现在新改版的v2.0接口,由于基于新的框架,JSON数据使用的是连字符命名法,格式如下:

 build_type : 2
 chinese_name : "部署测试"
 create_time : 1472489455000
 creator : 339691
 demand_analysisUrl : "应用描述33"
 desc : "应用描述22"
 design_analysisUrl : "应用描述44"
 members : [
   {
    your_name : 'li',
    your_nge : 23
   }
 ]

因此,Web端也要进行相应的修改。为了使改动量最小,需要实现一个适配函数,新接口返回结果经过该函数处理后,得到驼峰式命名的JSON数据,对该函数的要求如下:

也许以后的开发中,有同学也会遇到类似问题,需要实现类似功能,故在这里分享下该函数的实现源码:

函数实现

/**
 * 将对象的变量名由连字符式转为驼峰式,支持对象的深度遍历转换
 * @param obj JSON对象
 * @return JSON 驼峰式的JSON对象
 */
function obj2CamelCased(obj){
    if (!(obj instanceof Object)) {
        return obj;
    }

    var newObj = {};
    for (var prop in obj) {
        if (obj.hasOwnProperty(prop)) { //推荐在 for in 时,总是使用 hasOwnProperty 进行判断,没人可以保证运行的代码环境是否被污染过。 
            var camelProp = prop.replace(/_([a-z])/g, function (g) {
                return g[1].toUpperCase();
            });
            if (obj[prop] instanceof Array) { //值为数组的处理
                newObj[camelProp] = [];
                var oldArray = obj[prop];
                for (var k = 0, kLen = oldArray.length; k < kLen; k++) {
                    newObj[camelProp].push(arguments.callee(oldArray[k]));
                }
            } else if (obj[prop] instanceof Object) { //值为对象的处理
                newObj[camelProp] = arguments.callee(obj[prop]);
            } else { //值为字符串,或数字等的处理
                newObj[camelProp] = obj[prop];
            }
        }
    }
    return newObj;
}
CodingMeUp commented 6 years ago

开篇闲扯

前一段时间的一个案子是开发一个有声课件,大致就是通过导入文档、图片等资源后,页面变为类似 PPT 的布局,然后选中一张图片,可以插入音频,有单页编辑和全局编辑两种模式。其中音频的导入方式有两种,一种是从资源库中导入,还有一种就是要提到的录音。
说实话,一开始都没接触过 HTML5 的 Audio API,而且要基于在我们接手前的代码中进行优化。当然其中也踩了不少坑,这次也会围绕这几个坑来说说感受(会省略一些基本对象的初始化和获取,因为这些内容不是这次的重点,有兴趣的同学可以自行查找 MDN 上的文档):

开始录音前,要先获取当前设备是否支持 Audio API。早期的方法 navigator.getUserMedia 已经被 navigator.mediaDevices.getUserMedia 所代替。正常来说现在大部分的现代浏览器都已经支持navigator.mediaDevices.getUserMedia 的用法了,当然MDN上也给出了兼容性的写法

const promisifiedOldGUM = function (constraints) {
  // First get ahold of getUserMedia, if present
  const getUserMedia = (navigator.getUserMedia ||
      navigator.webkitGetUserMedia ||
      navigator.mozGetUserMedia)

  // Some browsers just don't implement it - return a rejected promise with an error
  // to keep a consistent interface
  if (!getUserMedia) {
    return Promise.reject(new Error('getUserMedia is not implemented in this browser'))
  }

  // Otherwise, wrap the call to the old navigator.getUserMedia with a Promise
  return new Promise(function (resolve, reject) {
    getUserMedia.call(navigator, constraints, resolve, reject)
  })
}

// Older browsers might not implement mediaDevices at all, so we set an empty object first
if (navigator.mediaDevices === undefined) {
  navigator.mediaDevices = {}
}

// Some browsers partially implement mediaDevices. We can't just assign an object
// with getUserMedia as it would overwrite existing properties.
// Here, we will just add the getUserMedia property if it's missing.
if (navigator.mediaDevices.getUserMedia === undefined) {
  navigator.mediaDevices.getUserMedia = promisifiedOldGUM
}

因为这个方法是异步的,所以我们可以对无法兼容的设备进行友好的提示

navigator
.mediaDevices
.getUserMedia(constraints)
.then(function (mediaStream) {
    // 成功
    }, function (error) {
    // 失败
    const { name } = error
    let errorMessage
    switch (name) {
      // 用户拒绝
      case 'NotAllowedError':
      case 'PermissionDeniedError':
        errorMessage = '用户已禁止网页调用录音设备'
        break
      // 没接入录音设备
      case 'NotFoundError':
      case 'DevicesNotFoundError':
        errorMessage = '录音设备未找到'
        break
      // 其它错误
      case 'NotSupportedError':
        errorMessage = '不支持录音功能'
        break
      default:
        errorMessage = '录音调用错误'
        window.console.log(error)
    }
    return errorMessage
})

一切顺利的话,我们就可以进入下一步了。
(这里有对获取上下文的方法进行了省略,因为这不是这次的重点)

开始录音、暂停录音

这里有个比较特别的点,就是需要添加一个中间变量来标识是否当前是否在录音。因为在火狐浏览器上,我们发现一个问题,录音的流程都是正常的,但是点击暂停时却发现怎么也暂停不了,我们当时是使用 disconnect 方法。这种方式是不行的,这种方法是需要断开所有的连接才可以。后来发现,应该增加一个中间变量 this.isRecording 来判断当前是否正在录音,当点击开始时,将其设置为true,暂停时将其设置为false
当我们开始录音时,会有一个录音监听的事件 onaudioprocess ,如果返回 true 则会将流写入,如果返回 false 则不会将其写入。因此判断this.isRecording,如果为 false 则直接 return

// 一些初始化
const audioContext = new AudioContext()
const sourceNode = audioContext.createMediaStreamSource(mediaStream)
const scriptNode = audioContext.createScriptProcessor(BUFFER_SIZE, INPUT_CHANNELS_NUM, OUPUT_CHANNELS_NUM)
sourceNode.connect(this.scriptNode)
scriptNode.connect(this.audioContext.destination)
// 监听录音的过程
scriptNode.onaudioprocess = event => {
    if (!this.isRecording) return // 判断是否正则录音
    this.buffers.push(event.inputBuffer.getChannelData(0)) // 获取当前频道的数据,并写入数组
}

当然这里也会有个坑,就是无法再使用,自带获取当前录音时长的方法了,因为实际上并不是真正的暂停,而是没有将流写入罢了。于是我们还需要获取一下当前录音的时长,需要通过一个公式进行获取

const getDuration = () => {
    return (4096 * this.buffers.length) / this.audioContext.sampleRate // 4096为一个流的长度,sampleRate 为采样率
}

这样就能够获取正确的录音时长了。

结束录音

结束录音的方式,我采用的是先暂停,之后需要试听或者其它的操作先执行,然后再将存储流的数组长度置为0。

获取频率

getVoiceSize = analyser => {
    const dataArray = new Uint8Array(analyser.frequencyBinCount)
    analyser.getByteFrequencyData(dataArray)
    const data = dataArray.slice(100, 1000)
    const sum = data.reduce((a, b) => a + b)
    return sum
}

具体可以参考https://developer.mozilla.org/zh-CN/docs/Web/API/AnalyserNode/frequencyBinCount

其它

结语

这次遇到的大部分问题都是兼容性的问题,因此在上面踩了不少坑,尤其是移动端的问题,一开始还有出现因为获取录音时长写法错误的问题,导致直接卡死的情况。这次的经历也弥补了 HTML5 API上的一些空白,当然最重要的还是要提醒一下大家,这种原生的 API 文档还是直接查看 MDN 来的简单粗暴!

CodingMeUp commented 6 years ago

字谜

使用递归。对于给定字符串中的每个字母,为字母创建字谜。使用map()将字母与每部分字谜组合,然后使用reduce()将所有字谜组合到一个数组中,最基本情况是字符串长度等于2或1。

const anagrams = str => {
  if (str.length <= 2) return str.length === 2 ? [str, str[1] + str[0]] : [str];
  return str.split('').reduce((acc, letter, i) =>
    acc.concat(anagrams(str.slice(0, i) + str.slice(i + 1)).map(val => letter + val)), []);
};
// anagrams('abc') -> ['abc','acb','bac','bca','cab','cba']

求平均数

使用reduce()将每个值添加到累加器,初始值为0,总和除以数组长度。

const average = arr => arr.reduce((acc, val) => acc + val, 0) / arr.length;
// average([1,2,3]) -> 2

大写每个单词的首字母

使用replace()匹配每个单词的第一个字符,并使用toUpperCase()来将其大写。

const capitalizeEveryWord = str => str.replace(/\b[a-z]/g, char => char.toUpperCase());
// capitalizeEveryWord('hello world!') -> 'Hello World!'

首字母大写

使用slice(0,1)和toUpperCase()大写第一个字母,slice(1)获取字符串的其余部分。 省略lowerRest参数以保持字符串的其余部分不变,或将其设置为true以转换为小写。(注意:这和上一个示例不是同一件事情)

const capitalize = (str, lowerRest = false) =>
str.slice(0, 1).toUpperCase() + (lowerRest ? str.slice(1).toLowerCase() : str.slice(1));
// capitalize('myName', true) -> 'Myname'

# 检查回文
将字符串转换为toLowerCase(),并使用replace()从中删除非字母的字符。然后,将其转换为tolowerCase(),将(’‘)拆分为单独字符,reverse(),join(’‘),与原始的非反转字符串进行比较,然后将其转换为tolowerCase()。

```js
const palindrome = str => {
  const s = str.toLowerCase().replace(/[\W_]/g,'');
  return s === s.split('').reverse().join('');
}
// palindrome('taco cat') -> true

计数数组中值的出现次数

每次遇到数组中的特定值时,使用reduce()来递增计数器。

const countOccurrences = (arr, value) => arr.reduce((a, v) => v === value ? a + 1 : a + 0, 0);
// countOccurrences([1,1,2,1,2,3], 1) -> 3

Curry科里化

使用递归。如果提供的参数(args)数量足够,则调用传递函数f,否则返回一个curried函数f。

const curry = (fn, arity = fn.length, ...args) => 
                        arity <= args.length ? fn(...args) : curry.bind(null, fn, arity, ...args);
// curry(Math.pow)(2)(10) -> 1024
// curry(Math.min, 3)(10)(50)(2) -> 2

Deep flatten array

使用递归,使用reduce()来获取所有不是数组的元素,flatten每个元素都是数组。

const deepFlatten = arr =>
  arr.reduce((a, v) => a.concat(Array.isArray(v) ? deepFlatten(v) : v), []);
// deepFlatten([1,[2],[[3],4],5]) -> [1,2,3,4,5]

数组之间的区别

从b创建一个Set,然后在a上使用Array.filter(),只保留b中不包含的值。

const difference = (a, b) => { const s = new Set(b); return a.filter(x => !s.has(x)); };
// difference([1,2,3], [1,2]) -> [3]

两点之间的距离

使用Math.hypot()计算两点之间的欧几里德距离。

const distance = (x0, y0, x1, y1) => Math.hypot(x1 - x0, y1 - y0);
// distance(1,1, 2,3) -> 2.23606797749979

转义正则表达式

使用replace()来转义特殊字符。

const escapeRegExp = str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// escapeRegExp('(test)') -> \\(test\\)

阶乘

使用递归。如果n小于或等于1,则返回1。否则返回n和n – 1的阶乘的乘积。

const factorial = n => n <= 1 ? 1 : n * factorial(n - 1);
// factorial(6) -> 720 

斐波那契数组生成器

创建一个特定长度的空数组,初始化前两个值(0和1)。使用Array.reduce()向数组中添加值,后面的一个数等于前面两个数相加之和(前两个除外)。

const fibonacci = n =>
  Array(n).fill(0).reduce((acc, val, i) => acc.concat(i > 1 ? acc[i - 1] + acc[i - 2] : i), []);
// fibonacci(5) -> [0,1,1,2,3]

过滤数组中的非唯一值

将Array.filter()用于仅包含唯一值的数组。

const filterNonUnique = arr => arr.filter(i => arr.indexOf(i) === arr.lastIndexOf(i));
// filterNonUnique([1,2,2,3,4,4,5]) -> [1,3,5]

Flatten数组

使用reduce()来获取数组中的所有元素,并使用concat()来使它们flatten。

const flatten = arr => arr.reduce((a, v) => a.concat(v), []);
// flatten([1,[2],3,4]) -> [1,2,3,4]

从数组中获取最大值

使用Math.max()与spread运算符(…)结合得到数组中的最大值。

const arrayMax = arr => Math.max(...arr);
// arrayMax([10, 1, 5]) -> 10

获取滚动位置

如果已定义,请使用pageXOffset和pageYOffset,否则使用scrollLeft和scrollTop,可以省略el来使用window的默认值。

const getScrollPos = (el = window) =>
  ({x: (el.pageXOffset !== undefined) ? el.pageXOffset : el.scrollLeft,
    y: (el.pageYOffset !== undefined) ? el.pageYOffset : el.scrollTop});
// getScrollPos() -> {x: 0, y: 200}

最大公约数(GCD)

使用递归。基本情况是当y等于0时。在这种情况下,返回x。否则,返回y的GCD和x / y的其余部分。

const gcd = (x, y) => !y ? x : gcd(y, x % y);
// gcd (8, 36) -> 4

用range初始化数组

使用Array(end-start)创建所需长度的数组,使用map()来填充范围中的所需值,可以省略start使用默认值0。

const initializeArrayRange = (end, start = 0) =>
  Array.apply(null, Array(end - start)).map((v, i) => i + start);
// initializeArrayRange(5) -> [0,1,2,3,4]

Powerset

使用reduce()与map()结合来遍历元素,并将其组合成包含所有组合的数组。

const powerset = arr =>
  arr.reduce((a, v) => a.concat(a.map(r => [v].concat(r))), [[]]);
// powerset([1,2]) -> [[], [1], [2], [2,1]]

用值初始化数组

使用Array(n)创建所需长度的数组,fill(v)以填充所需的值,可以忽略value使用默认值0。

const initializeArray = (n, value = 0) => Array(n).fill(value);
// initializeArray(5, 2) -> [2,2,2,2,2]

测试功能所花费的时间

使用performance.now()获取函数的开始和结束时间,console.log()所花费的时间。第一个参数是函数名,随后的参数传递给函数。

const timeTaken = callback => {
  console.time('timeTaken');
  const r = callback();
  console.timeEnd('timeTaken');
  return r;
};

// timeTaken(() => Math.pow(2, 10)) -> 1024
// (logged): timeTaken: 0.02099609375ms

来自键值对的对象

使用Array.reduce()来创建和组合键值对。

const objectFromPairs = arr => arr.reduce((a, v) => (a[v[0]] = v[1], a), {});
// objectFromPairs([['a',1],['b',2]]) -> {a: 1, b: 2}

管道

使用Array.reduce()通过函数传递值。

const pipe = (...funcs) => arg => funcs.reduce((acc, func) => func(acc), arg);
// pipe(btoa, x => x.toUpperCase())("Test") -> "VGVZDA=="

范围内的随机整数

使用Math.random()生成一个随机数并将其映射到所需的范围,使用Math.floor()使其成为一个整数。

const randomIntegerInRange = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
// randomIntegerInRange(0, 5) -> 2

# 范围内的随机数
使用Math.random()生成一个随机值,使用乘法将其映射到所需的范围。
```js
const randomInRange = (min, max) => Math.random() * (max - min) + min;
// randomInRange(2,10) -> 6.0211363285087005

随机化数组的顺序

使用sort()重新排序元素,利用Math.random()来随机排序。

const shuffle = arr => arr.sort(() => Math.random() - 0.5);
// shuffle([1,2,3]) -> [2,3,1]

重定向到URL

使用window.location.href或window.location.replace()重定向到url。 传递第二个参数来模拟链接点击(true – default)或HTTP重定向(false)。

const redirect = (url, asLink = true) =>
  asLink ? window.location.href = url : window.location.replace(url);
// redirect('https://google.com')

反转一个字符串

使用数组解构和Array.reverse()来颠倒字符串中的字符顺序。合并字符以使用join(‘‘)获取字符串。

const reverseString = str => [...str].reverse().join('');
// reverseString('foobar') -> 'raboof'

RGB到十六进制

使用按位左移运算符(«)和toString(16),然后padStart(6,“0”)将给定的RGB参数转换为十六进制字符串以获得6位十六进制值。

const rgbToHex = (r, g, b) => ((r << 16) + (g << 8) + b).toString(16).padStart(6, '0');
// rgbToHex(255, 165, 1) -> 'ffa501'

滚动到顶部

使用document.documentElement.scrollTop或document.body.scrollTop获取到顶部的距离。 从顶部滚动一小部分距离。使用window.requestAnimationFrame()来滚动。

const scrollToTop = _ => {
  const c = document.documentElement.scrollTop || document.body.scrollTop;
  if (c > 0) {
    window.requestAnimationFrame(scrollToTop);
    window.scrollTo(0, c - c / 8);
  }
};
// scrollToTop()

随机数组值

使用Array.map()和Math.random()创建一个随机值的数组。使用Array.sort()根据随机值对原始数组的元素进行排序。

count shuffle = arr => {
  let r = arr.map(Math.random);
  return arr.sort((a, b) => r[a] - r[b]);
}
// shuffle([1, 2, 3] -> [2, 1, 3])

数组之间的相似性

使用filter()移除不是values的一部分值,使用includes()确定。

const similarity = (arr, values) => arr.filter(v => values.includes(v));
// similarity([1,2,3], [1,2,4]) -> [1,2]

按字符串排序(按字母顺序排列)

使用split(’‘)分割字符串,sort()使用localeCompare(),使用join(’‘)重新组合。

const sortCharactersInString = str =>
  str.split('').sort((a, b) => a.localeCompare(b)).join('');
// sortCharactersInString('cabbage') -> 'aabbceg'

数组总和

使用reduce()将每个值添加到累加器,初始化值为0。

const sum = arr => arr.reduce((acc, val) => acc + val, 0);
// sum([1,2,3,4]) -> 10

交换两个变量的值

使用数组解构来交换两个变量之间的值。

[varA, varB] = [varB, varA];
// [x, y] = [y, x]

列表的tail

返回arr.slice(1)

const tail = arr => arr.length > 1 ? arr.slice(1) : arr;
// tail([1,2,3]) -> [2,3]
// tail([1]) -> [1]

数组唯一值

使用ES6 Set和… rest操作符去掉所有重复值。

const unique = arr => [...new Set(arr)];
// unique([1,2,2,3,4,4,5]) -> [1,2,3,4,5]

URL参数

使用match() 与适当的正则表达式来获得所有键值对,适当的map() 。使用Object.assign()和spread运算符(…)将所有键值对组合到一个对象中,将location.search作为参数传递给当前url。

const getUrlParameters = url =>
  url.match(/([^?=&]+)(=([^&]*))/g).reduce(
    (a, v) => (a[v.slice(0, v.indexOf('='))] = v.slice(v.indexOf('=') + 1), a), {}
  );
// getUrlParameters('http://url.com/page?name=Adam&surname=Smith') -> {name: 'Adam', surname: 'Smith'}

验证数字

使用!isNaN和parseFloat()来检查参数是否是一个数字,使用isFinite()来检查数字是否是有限的。

const validateNumber = n => !isNaN(parseFloat(n)) && isFinite(n) && Number(n) == n;
// validateNumber('10') -> true
CodingMeUp commented 6 years ago

JS中的数组过滤 多条件多数据筛选

//根据名字和年龄多元素筛选
export function multiFilter(array, filters) {
  const filterKeys = Object.keys(filters)
  // filters all elements passing the criteria
  return array.filter((item) => {
    // dynamically validate all filter criteria
    return filterKeys.every(key => {
        //ignore when the filter is empty Anne
      if(!filters[key].length) return true
      return !!~filters[key].indexOf(item[key])
    })
  })
}
/*
 * 这段代码并非我原创,感兴趣的可以去原作者那里点个赞
 * 作者是:@author https://gist.github.com/jherax
 * 这段代码里我只加了一行,解决部分筛选条件清空时候整体筛选失效的问题
 */

var filters = {
    name:['Leila', 'Jay'],
    age:[]
}
/* 结果:
 * [{name: "Leila", age: 16, gender: "female"},
 *  {name: "Jay", age: 19, gender: "male"}]
 */
CodingMeUp commented 5 years ago
/**
 * 转换为二维数组
 * @param nodes
 * @param parentId
 * @returns {Array}
 */
function toArray(nodes, parentId = '') {
    if (!nodes) return []
    var childKey = 'children'
    var r = []
    if (nodes instanceof Array) {
        for (var item of nodes) {
            var node = {}
            for (var key in item) {
                if (key != childKey) {
                    node[key] = item[key]
                }
            }

            node['parent'] = parentId
            r.push(node)
            if (item[childKey]) {
                r = r.concat(toArray(item[childKey], item.id))
            }
        }
    } else {
        r.push(nodes)
        if (nodes[childKey]) {
            r = r.concat(toArray(nodes[childKey]))
        }
    }
    return r
}
/**
 * 转换为树状结构
 * @param data
 * @param parent_id
 * @returns {Array}
 */
// 线性数据转化为树。
function toTree(data, parent_id) {
    var tree = []
    var temp
    for (var i = 0; i < data.length; i++) {
        if (data[i].parent == parent_id || typeof data[i].parent == 'undefined') {
            var obj = data[i]
            temp = toTree(data, data[i].id)
            if (temp.length > 0) {
                obj.children = temp
            }
            tree.push(obj)
        }
    }
    return tree
}
var treeData = [{
        id: 'start',
        shape: 'start',
        children: [{
            id: 'add',
            shape: 'addNodeBtn',
            children: [{
                id: '2sdff',
                shape: 'addNodeBtn'
            }]
        }]
    }]

var n2a = toArray(treeData)
var a2n = toTree(n2a, '')
CodingMeUp commented 4 years ago

原生实现 JS New 运算符

function Student(){
}
var student = new Student();

typeof Student === 'function' // true
student.constructor === Student;
Student.prototype.constructor === Student;

做了哪些事情呢

模拟实现

/**
 * 模拟实现 new 操作符
 * @param  {Function} ctor [构造函数]
 * @return {Object|Function|Regex|Date|Error}      [返回结果]
 *  eg:  var student =  new Student()  替换为 newOperator(Student);
 */
function newOperator(ctor){
    if(typeof ctor !== 'function'){
      throw 'newOperator function the first param must be a function';
    }
    // ES6 new.target 是指向构造函数
    newOperator.target = ctor;
    // 1.创建一个全新的对象,
    // 2.并且执行[[Prototype]]链接
    // 4.通过`new`创建的每个对象将最终被`[[Prototype]]`链接到这个函数的`prototype`对象上。
    var newObj = Object.create(ctor.prototype);
    // ES5 arguments转成数组 当然也可以用ES6 [...arguments], Aarry.from(arguments);
    // 除去ctor构造函数的其余参数
    var argsArr = [].slice.call(arguments, 1);
    // 3.生成的新对象会绑定到函数调用的`this`。
    // 获取到ctor函数返回结果
    var ctorReturnResult = ctor.apply(newObj, argsArr);
    // 小结4 中这些类型中合并起来只有Object和Function两种类型 typeof null 也是'object'所以要不等于null,排除null
    var isObject = typeof ctorReturnResult === 'object' && ctorReturnResult !== null;
    var isFunction = typeof ctorReturnResult === 'function';
    if(isObject || isFunction){
        return ctorReturnResult;
    }
    // 5.如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),那么`new`表达式中的函数调用会自动返回这个新的对象。
    return newObj;
}
CodingMeUp commented 4 years ago

原生实现 call apply bind

polyfill address https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

通过定义简单说一下call和apply方法,他们就是参数不同,作用基本相同。

  1. 每个函数都包含两个非继承而来的方法:apply()和call()。
  2. 他们的用途相同,都是在特定的作用域中调用函数。
  3. 接收参数方面不同,apply()接收两个参数,一个是函数运行的作用域(this),另一个是参数数组。
  4. call()方法第一个参数与apply()方法相同,但传递给函数的参数必须列举出来。

直接看个简单的demo

var jawil = {
    name: "jawil",
    sayHello: function (age) {
         console.log("hello, i am ", this.name + " " + age + " years old");
     }
};

var  lulin = {
    name: "lulin",
};

jawil.sayHello(24);

// hello, i am jawil 24 years old

然后看看使用apply和call之后的输出:

jawil.sayHello.call(lulin, 24);// hello, i am lulin 24 years old
jawil.sayHello.apply(lulin, [24]);// hello, i am lulin 24 years old

结果都相同。从写法上我们就能看出二者之间的异同。相同之处在于,第一个参数都是要绑定的上下文,后面的参数是要传递给调用该方法的函数的。不同之处在于,call方法传递给调用函数的参数是逐个列出的,而apply则是要写在数组中。

总结一句话介绍call和apply

分析call和apply的原理

上面代码,我们注意到了两点:

  1. call和apply改变了this的指向,指向到lulin
  2. sayHello函数执行

这里默认大家都对this有一个基本的了解,知道什么时候this该指向谁,我们结合这两句话来分析这个通用函数:f.apply or call (o),我们直接看一本书对其中原理的解读,具体什么书,我也不知道,参数我们先不管,先了解其中的大致原理。

f.call(o)其原理就是先通过 o.m = f 将 f作为o的某个临时属性m存储,然后执行m,执行完毕后将m属性删除。

// 第一步
lulin.fn = jawil.sayHello
// 第二步
lulin.fn()
// 第三步
delete lulin.fn

模拟实现 Apply

Function.prototype.applyFour = function(context) {
    var context = context || window
    var args = arguments[1] //获取传入的数组参数
    var fn = Symbol()
    context[fn] = this //假想context对象预先不存在名为fn的属性
    if (args == void 0) { //没有传入参数直接执行
        return context[fn]()
    }
    var fnStr = 'context[fn]('
    for (var i = 0; i < args.length; i++) {
        //得到"context.fn(arg1,arg2,arg3...)"这个字符串在,最后用eval执行
        fnStr += i == args.length - 1 ? args[i] : args[i] + ','
    }
    fnStr += ')'
    var returnValue = eval(fnStr) //还是eval强大
    delete context[fn] //执行完毕之后删除这个属性
    return returnValue
}

模拟实现 Call

上面 参数改一下

 return this.applyFour(([].shift.applyFour(arguments), arguments) 

bind 及 实现

jawil.sayHello.bind(lulin)(24); //hello, i am lulin 24 years old
jawil.sayHello.bind(lulin)([24]); //hello, i am lulin 24 years old

bind() 最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的 this 值。JavaScript新手经常犯的一个错误是将一个方法从对象中拿出来,然后再调用,希望方法中的 this 是原来的对象。(比如在回调中传入这个方法。)如果不做特殊处理的话,一般会丢失原来的对象。从原来的函数和原来的对象创建一个绑定函数,则能很漂亮地解决这个问题

用一句话总结bind的用法:该方法创建一个新函数,称为绑定函数,绑定函数会以创建它时传入bind方法的第一个参数作为this,传入bind方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

//简单模拟bind函数
Function.prototype.bind = Function.prototype.bind || function (context) {
    if(typeof this !== 'function'){
        throw new TypeError(this + ' must be a function');
    }
    var me = this;
    var args = Array.prototype.slice.callOne(arguments, 1);
// bind返回的函数如果作为构造函数,搭配new关键字出现的话,我们的绑定this就需要“被忽略”。
    var F = function () {};
    F.prototype = this.prototype;
    var bound = function () {
        var innerArgs = Array.prototype.slice.callOne(arguments);
        var finalArgs = args.concat(innerArgs);
        return me.applyFour(this instanceof F ? this : context || this, finalArgs);
    }
    bound.prototype = new F();
    return bound;
}
CodingMeUp commented 4 years ago

原生实现Promise

ISSUE 39

CodingMeUp commented 4 years ago

一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧 及 JS 实现

https://juejin.im/post/5cef46226fb9a07eaf2b7516#heading-29