手写源码系列

670 阅读15分钟

1. JS API实现

1.1 模拟实现new

分析:

    1. 默认创建一个实例对象(而且是属于当前这个类的一个实例)
    1. 声明其THIS指向,让其指向这个新创建的实例
    1. 代码执行
    1. 若客户自己返回引用值,则以自己返回的为主,否则返回创建的实例
function _new(Func, ...args) {
    //默认创建一个实例对象(而且是属于当前这个类的一个实例)
    // let obj = {};
    // obj.__proto__ = Func.prototype; //=>IE大部门浏览器中不允许我们直接操作__proto__
    let obj = Object.create(Func.prototype);
    
    //也会把类当做普通函数执行
    //执行的时候要保证函数中的this指向创建的实例
    let result = Func.call(obj, ...args);
    
    //若客户自己返回引用值,则以自己返回的为主,否则返回创建的实例
    if ((result !== null && typeof result === "object") || (typeof result === "function")) {
        return result;
    }
    return obj;
}

// 测试:
function Foo(name) {
    this.name = name
}
const newObj = _new(Foo, 'zhangsan')
console.log(newObj)                 // Foo {name: "zhangsan"}
console.log(newObj instanceof Foo)  // true

1.2 bind

分析:

  1. 判断是否是函数
  2. 保留原函数
  3. 定义一个新函数,如果新函数被new了那么this指向当前函数实例,否则指向传入的this
  4. 合并传入参数
  5. 继承原函数原型上的属性和方法
if (!Function.prototype.myBind) { 
    Function.prototype.myBind = function (oThis) { 
        if (typeof this !== "function") { 
                // 与 ECMAScript 5 最接近的
                // 内部 IsCallable 函数
                // Function.prototype.bind - 试图被绑定的对象是不可调用的
            throw new TypeError( 
                "Function.prototype.bind - what is trying to be bound is not callable"
            ); 
        } 
        let aArgs = Array.prototype.slice.call(arguments, 1), 
            _this = this, // 这里的this即原函数
            fBound = function(){ 
                // new会改变this指向:如果bind绑定后的函数被new了,那么this指向会发生改变,指向当前函数的实例
                return _this.apply(this instanceof _this ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)))
            };
        // 继承原函数原型上的属性和方法
        fBound.prototype = Object.create(_this.prototype);
        return fBound; 
    }; 
}


//测试
const obj = { name: 'myName' }
function foo(...args) {
    console.log('name', this.name)
    console.log('arguments', args)
}

foo.myBind(obj, 'a', 'b', 'c')()    //myName ['a', 'b', 'c']

参考文章:字节跳动面试官—麻烦你搞个方法出来🌈使得以下程序最后能输出 success

1.3 call

if (!Function.prototype.myCall) {
    Function.prototype.myCall = function(context = window, ...args) {
        context === null ? context = window : null;
        let type = typeof context;
        //=>基本类型值
        if (type !== 'object' || typeof !== 'function') { 
            switch (type) { 
                case 'number':
                    context = new Number(context);
                    break;
                case 'string':
                    context = new String(context);
                    break;
                case 'boolean':
                    context = new Boolean(context);
                case 'symbol':
                    context = Object(context);
                    break;
            }
        }
        const $fn = Symbol('$fn') // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
        context[$fn] = this; // this指向调用call的对象,即我们要改变this指向的函数
        const result = context[$fn](...args); // 执行当前函数
        delete context[$fn]; // 删除我们声明的fn属性
        return result; // 返回函数执行结果
    }
} 


foo.myCall(obj)

1.4 apply

if (!Function.prototype.myApply) {
    Function.prototype.myApply = function (context = window, args) {
        context === null ? context = window : null;
        let type = typeof context;
        switch (type) {
            case 'number':
                context = new Number(context);
                break;
            case 'string':
                context = new String(context);
                break;
            case 'boolean':
                context = new Boolean(context);
                break;
            case 'symbol':
                context = Object(context);
                break;
        }
        const $fn = Symbol('$fn') // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
        context[$fn] = this; // this指向调用call的对象,即我们要改变this指向的函数
        const result = context[$fn](...args); // 执行当前函数
        delete context[$fn]; // 删除我们声明的fn属性
        return result; // 返回函数执行结果
    }
}

foo.myApply(obj, []) 

1.5 防抖

防抖,即短时间内大量触发同一事件,只会执行一次函数,实现原理为设置一个定时器,约定在xx毫秒后再触发事件处理,每次触发事件都会重新设置计时器,直到xx毫秒内无第二次操作,防抖常用于搜索框/滚动条的监听事件处理,如果不做防抖,每输入一个字/滚动屏幕,都会触发事件处理,造成性能浪费。

function debounce(func, wait) {
    let timeout = null
    return function() {
        let context = this
        let args = arguments
        if (timeout) clearTimeout(timeout)
        timeout = setTimeout(() => {
            func.apply(context, args)
        }, wait)
    }
}

超大杯版:

1.6 节流

防抖是延迟执行,而节流是间隔执行,函数节流即每隔一段时间就执行一次,实现原理为设置一个定时器,约定xx毫秒后执行事件,如果时间到了,那么执行函数并重置定时器,和防抖的区别在于,防抖每次触发事件都重置定时器,而节流在定时器到时间后再清空定时器,如上拉加载页面。

// 定时器版
function throttle(func, wait) {
    let timeout = null
    return function() {
        let context = this
        let args = arguments
        if (!timeout) {
            timeout = setTimeout(() => {
                timeout = null
                func.apply(context, args)
            }, wait)
        }

    }
}

// 时间戳版
const throttle = (fn, wait = 300) => {
    let prev = 0
    let result
    return function(...args) {
        let now = +new Date()
        if(now - prev > wait) {
            prev = now
            return result = fn.apply(this, ...args)
        }
    }
}

// 综合版
 const throttle = (fn, wait = 300,  {
    // 参数解构赋值
    leading = true,
    trailing = true,
} = {}) => {
    let prev = 0
    let timerId
    const later = function(args) {
        timerId && clearTimeout(timerId)
        timerId = setTimeout(() => {
            timerId = null
            fn.apply(this, args)
      }, wait)
    }
    return function (...args) {
        let now = +new Date()
        if(!leading) return later(args)
        if(now - prev > wait) {
            fn.apply(this, args)
            prev = now
        } else if(trailing) {
            later(args)
        }
    }
}

// leading:false 表示禁用第一次执行

// trailing: false 表示禁用停止触发的回调

// 注意:leading:false 和 trailing: false 不能同时设置

超大杯版:

方法使用时间戳使用定时器
开始触发时立刻执行n秒后执行
停止触发后不再执行事件继续执行一次事件

详细文章:前端战五渣学JavaScript——防抖、节流和rAF

1.7 instanceof

const instanceOf = (A, B) => {
    let p = A;
    while(p) {
        if(p === B.prototype) {
            return true;
        }
        p = p.__proto__
    }
    return false;
}

console.log(instanceOf([], Object))

1.8 深拷贝,浅拷贝

let obj = {
    a: 100,
    b: [100, 200, 300],
    c: {
       x: 10
    },
    d: /^\d+$/,
    e: function () {
        console.log(1);
    }
}

1. 浅拷贝

// 方法1: 展开运算符
let obj2 = {...obj};

// 方法2:
let obj2 = {};
for(let key in obj) {
   if(!obj.hasOwnProperty(key)) break;
   obj2[key] = obj[key];
}

// 方法3
Object.assign()
// 方法4
Array.prototype.concat()
// 方法5
Array.prototype.slice()

Object.assign()

参考文章:Object.assign

2. 深拷贝

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。

有种特殊情况需注意就是对象存在循环引用的情况,即对象的属性直接的引用了自身的情况,解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题

function deepClone(obj, hash = new WeakMap()) {
    // 过滤一些特殊情况
    if(obj === null) return null;
    if(typeof obj !== "object") return obj;
    if(obj instanceof RegExp) return new RegExp(obj);
    if(obj instanceof Date) return new Date(obj);
    // 是对象的话就要进行深拷贝
    if (hash.get(obj)) return hash.get(obj)
    // if (typeof window !== 'undefined' && window.JSON) return JSON.parse(JSON.stringify(obj));
    // let newObj = {}
    // let newObj = new Object()
    let newObj = new obj.constructor; // 不直接创建空对象的目的:克隆的结果和之前保持所属类  =》 即能克隆普通对象,又能克隆某个实例对象
    // // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
    hash.set(obj, newObj)
    for(let key in obj) {
        if(obj.hasOwnProperty(key)) {
             newObj[key] = deepClone(obj[key], hash);
        }
    }
    // let newObj = obj.constructor === Array ? [] : {};
    //for(let key in obj) {
    //    newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : //obj[key];
    //}
    return newObj;
}

使用 JSON.parse(JSON.stringify())的缺陷 在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在数组中则会返回 null

1.9 迭代器

var it = makeIterator(["a", "b"]);
console.log(it.next())
console.log(it.next())
console.log(it.next())

function makeIterator(array) {
    let nextindex=0
    return{
        next:function () {
            if(nextindex<array.length){
                return {value:array[nextindex++],done:false}
            }else{
                return {value: undefined,done: true}
            }
        }
    }
}

1.10 Object.create

//实现Object.create方法
function create(proto) {
    function Fn() {};
    Fn.prototype = proto;
    return new Fn();
}
let demo = {
    c : '123'
}
let cc = Object.create(demo)

1.11 实现let

try{
    throw 1;
 }catch(a){
    console.log(a);
}

(function (){
   var a = 1;
})();

{
  let a = 1;
}
console.log(a);

为什么不直接使用 IIFE 来创建作用域? IIFE 和 try/catch 并不是完全等价的,因为如果将一段代码中的任意一部分拿出来 用函数进行包裹,会改变这段代码的含义,其中的 this、return、break 和 contine 都会发生变化。IIFE 并不是一个普适的解决方案,它只适合在某些情况下进行手动操作。

1.12 现代模块机制

var MyModules = (function Manager() {
    var modules = {};

    function define(name, deps, impl) {
        for (var i = 0; i < deps.length; i++) {
            deps[i] = modules[deps[i]];
        }
        modules[name] = impl.apply(impl, deps);
    }

    function get(name) {
        return modules[name];
    }
    return {
        define: define,
        get: get
    };
})();

使用:

MyModules.define("bar", [], function() {
    function hello(who) {
        return "Let me introduce: " + who;
    }
    return {
        hello: hello
    };
});
MyModules.define("foo", ["bar"], function(bar) {
    var hungry = "hippo";

    function awesome() {
        console.log(bar.hello(hungry).toUpperCase());
    }
    return {
        awesome: awesome
    };
});
var bar = MyModules.get("bar");
var foo = MyModules.get("foo");
console.log(bar.hello("hippo")); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO

1.13 显示混入

// 非常简单的 mixin(..) 例子 : 
function mixin( sourceObj, targetObj ) { 
    for (var key in sourceObj) { 
        // 只会在不存在的情况下复制
        if (!(key in targetObj)) { 
            targetObj[key] = sourceObj[key]; 
        } 
    } 
    return targetObj; 
}

1.14 实现class

ES6 的 class 内部是基于寄生组合式继承,它是目前最理想的继承方式,通过 Object.create 方法创造一个空对象,并将这个空对象继承 Object.create 方法的参数,再让子类(subType)的原型对象等于这个空对象,就可以实现子类实例的原型等于这个空对象,而这个空对象的原型又等于父类原型对象(superType.prototype)的继承关系

而 Object.create 支持第二个参数,即给生成的空对象定义属性和属性描述符/访问器描述符,我们可以给这个空对象定义一个 constructor 属性更加符合默认的继承行为,同时它是不可枚举的内部属性(enumerable:false)

而 ES6 的 class 允许子类继承父类的静态方法和静态属性,而普通的寄生组合式继承只能做到实例与实例之间的继承,对于类与类之间的继承需要额外定义方法,这里使用 Object.setPrototypeOf 将 superType 设置为 subType 的原型,从而能够从父类中继承静态方法和静态属性

1.15 私有变量实现

使用 Proxy 代理所有含有 _ 开头的变量,使其不可被外部访问

1.16 手写promise, async/await

1.17 面向切面编程

2. 工具函数

2.1 解析URL参数

function parseParam(url) {
    // 将浏览器地址中 ‘?’ 后面的字符串取出来
    const paramsStr = /.+\?(.+)$/.exec(url)[1];
    // 将截取的字符串以 ‘&’ 分割后存到数组中
    const paramsArr = paramsStr.split('&');
    // 定义存放解析后的对象
    let paramsObj = {};
    // 遍历
    paramsArr.forEach(param => {
      // 判断是否含有key和value
      if (/=/.test(param)) {
        // 结构获取对象的key和value
        let [key, val] = param.split('=');
        // 解码
        val = decodeURIComponent(val);
        // 判断是否转为数字
        val = /^\d+$/.test(val) ? parseFloat(val) : val;
        // 判断存放对象中是否存在key属性
        if (paramsObj.hasOwnProperty(key)) {
          // 存在的话就存放一个数组
          paramsObj[key] = [].concat(paramsObj[key], val);
        } else {
          // 不存在就存放一个对象
          paramsObj[key] = val;
        }
      } else {
        // 没有value的情况
        paramsObj[param] = true;
      }
    })
    return paramsObj;
}

2.2 JsonP原理

function jsonp({url, params, cb}) { 
   return new Promise((resolve, reject) => {
     window[cb] = function (data) {  // 声明全局变量
        resolve(data)
        document.body.removeChild(script)
      }
      params = {...params, cb}
      let arrs = []
      for(let key in params) {
         arrs.push(`${key}=${params[key]}`)
      }
      let script = document.createElement('script')
      script.src = `${url}?${arrs.join('&')}`
      document.body.appendChild(script)
   })
}

2.3 统计网页中出现的标签

实现步骤:

  1. 获取所有的DOM节点
  2. NodeList集合转化为数组
  3. 获取数组每个元素的标签名
  4. 去重
new Set([...document.querySelectorAll('*')].map(ele=>ele.tagName)).size

2.4 异步加载图片

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image();
    image.onload = function() {
      resolve(image);
    };
    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };
    image.src = url;
  });
}

2.5 JS精度丢失问题

function add(num1, num2) {
    const num1Digits = (num1.toString().split('.')[1] || '').length;
    const num2Digits = (num2.toString().split('.')[1] || '').length;
    const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
    return (num1 * baseNum + num2 * baseNum) / baseNum;
}
console.log(add(0.1,0.2))

2.6 sleep函数

function sleep(time) {
    return new Promise((resolve,reject)=>{
        setTimeout(resolve,time)
    })
}

sleep(1000).then(value=>{
    console.log('11111')
})

2.7 原生AJAX

function ajax(options) {
  let method = options.method || 'GET', // 不传则默认为GET请求
      params = options.params, // GET请求携带的参数
      data   = options.data, // POST请求传递的参数
      url    = options.url + (params ? '?' + Object.keys(params).map(key => key + '=' + params[key]).join('&') : ''),
      async  = options.async === false ? false : true,
      success = options.success,
      headers = options.headers;

  let xhr;
  // 创建xhr对象
  if(window.XMLHttpRequest) {
    xhr = new XMLHttpRequest();
  } else {
    xhr = new ActiveXObject('Microsoft.XMLHTTP');
  }

  xhr.onreadystatechange = function() {
    if(xhr.readyState === 4 && xhr.status === 200) {
      success && success(xhr.responseText);
    }
  }

  xhr.open(method, url, async);
  
  if(headers) {
    Object.keys(Headers).forEach(key => xhr.setRequestHeader(key, headers[key]))
  }

  method === 'GET' ? xhr.send() : xhr.send(data)
}

2.8 Symbol用法以及常见应用

// Symbol 基本数据类型 string number boolean null undefined
// Symbol对象数据类型 object

// 特点:独一无二 永远不相等
let s1 = Symbol('tmc'); // symbol中的标识 一般只放number或string 不然结果返回Symbol([object Object])
let s2 = Symbol();
console.log(s1 === s2)

let obj = {
    [s1]: 1,
    a: 2
}
// 声明的Symbol属性是不可枚举的 for - in 可以遍历自身属性和原型上的属性ß
for(let key in obj) {
    console.log(obj[key])
}
// 获取对象上的属性
console.log(Object.getOwnPropertySymbols(obj));

let s3 = Symbol.for('tmc');
let s4 = Symbol.for('tmc');
console.log(s3 === s4);
// 通过Symbol来获取key值
console.log(Symbol.keyFor(s3))

// Symbol 内置对象
// Symbol.iterator 实现对象的遍历
// 元编程 可以去对原生js的操作进行修改
let instance = {
    [Symbol.hasInstance](value) {
        return 'a' in value;
    }
};
console.log({a: 3} instanceof instance)

let arr = [1, 2, 3];
arr[Symbol.isConcatSpreadable] = false; // 拼接数组时不展开
console.log([].concat(arr, [1, 2, 3]));

// match split search方法
let obj1 = {
    [Symbol.match](value) {
        return value.length === 3;
    }
}
console.log('abc'.match(obj1));

//species 衍生对象
class MyArray extends Array {
    constructor(...args) {
        super(...args)
    }
    // 强制修改一下
    static get [Symbol.species]() {
        return Array
    }
}
let v = new MyArray(1, 2, 3);
let c = v.map(item => item*=2); // c是v的衍生对象
console.log(c instanceof MyArray)

// Symbol.toPrimitive
// 数据类型转化
let obj3 = {
    [Symbol.toPrimitive](type) {
        console.log(type)
        return 123
    }
}
console.log(obj++)

// Symbol.toStringTag
let obj5 = {
    [Symbol.toStringTag]: 'xxxx'
}

console.log(Object.prototype.toString.call(obj5));

2.9 实现一个事件委托

错误版:

ul.addEventListener('click', function (e) {
  console.log(e,e.target)
  if (e.target.tagName.toLowerCase() === 'li') {
      console.log('打印')  // 模拟fn
  }
})

有个小bug,如果用户点击的是 li 里面的 span,就没法触发 fn,这显然不对

<ul id="xxx">下面的内容是子元素1
  <li>li内容>>> <span> 这是span内容123</span></li>
  下面的内容是子元素2
  <li>li内容>>> <span> 这是span内容123</span></li>
  下面的内容是子元素3
  <li>li内容>>> <span> 这是span内容123</span></li>
</ul>

高级版

function delegate(element, eventType, selector, fn) {
  element.addEventListener(eventType, e => {
      let el = e.target
      while (!el.matches(selector)) {
          if (element === el) {
              el = null
              break
          }
          el = el.parentNode
      }
      el && fn.call(el, e, el)
  },true)
  return element
}

2.10 实现一个可以拖拽的div

var dragging = false
var position = null

xxx.addEventListener('mousedown',function(e){
  dragging = true
  position = [e.clientX, e.clientY]
})

document.addEventListener('mousemove', function(e){
  if(dragging === false) return null
  const x = e.clientX
  const y = e.clientY
  const deltaX = x - position[0]
  const deltaY = y - position[1]
  const left = parseInt(xxx.style.left || 0)
  const top = parseInt(xxx.style.top || 0)
  xxx.style.left = left + deltaX + 'px'
  xxx.style.top = top + deltaY + 'px'
  position = [x, y]
})
document.addEventListener('mouseup', function(e){
  dragging = false
})

2.11 实现一个同时允许任务数量最大为n的函数

function limitRunTask(tasks, n) {
  return new Promise((resolve, reject) => {
    let index = 0, finish = 0, start = 0, res = [];
    function run() {
      if (finish == tasks.length) {
        resolve(res);
        return;
      }
      while (start < n && index < tasks.length) {
        // 每一阶段的任务数量++
        start++;
        let cur = index;
        tasks[index++]().then(v => {
          start--;
          finish++;
          res[cur] = v;
          run();
        });
      }
    }
    run();
  })
  // 大概解释一下:首先如何限制最大数量n
  // while 循环start < n,然后就是then的回调
}

2.12 Promise限制并发数

// promise限制并发数量,比如小程序请求接口最多10个一起,那么超出的第11个必须等第10个结束后才可以进行请求,我们就通过promise来实现
class limitPromise{    
  constructor(max){        
    // 当前最大的请求次数        
    this._max = max        
    // 当前的请求次数        
    this._count = 0        
    // 当前超出的请求任务        
    this._tasklist= []    
  }   
  // fn 当前请求的函数  args 当前请求的参数    
  call(fn,...args){        
    return new Promise((resolve,reject)=>{            
      let task = this._createTask(fn,args,resolve,reject)            
      if(this._count>=this._max){                
        this._tasklist.push(task)            
      }else{                
        task()            
      }        
    })    
  }    
  // 创建任务   
  _createTask(fn,args,resolve,reject){        
    return () => {            
      fn(...args)                
        .then(resolve)                
        .catch(reject)                
        .finally( _ => {                    
          this._count--                    
          if(this._tasklist.length){                        
            let task = this._tasklist.shift()                       
            task()                    
          }                
        })            
      this._count++        
    }    
  }
}
let limit = new limitPromise(2)
// 我是一个请求方式
let fn = function(){    
	xxxx
}

let ajax = (data)=>{    
	limit._call(fn,data)
}

2.13 图片懒加载

getBoundClientRect 的实现方式,监听 scroll 事件(建议给监听事件添加节流),图片加载完会从 img 标签组成的 DOM 列表中删除,最后所有的图片加载完毕后需要解绑监听事件 intersectionObserver 的实现方式,实例化一个 IntersectionObserver ,并使其观察所有 img 标签

当 img 标签进入可视区域时会执行实例化时的回调,同时给回调传入一个 entries 参数,保存着实例观察的所有元素的一些状态,比如每个元素的边界信息,当前元素对应的 DOM 节点,当前元素进入可视区域的比率,每当一个元素进入可视区域,将真正的图片赋值给当前 img 标签,同时解除对其的观察

2.14 promsiify

使用方式: promisify 函数是将回调函数变为 promise 的辅助函数,适合 error-first 风格(nodejs)的回调函数,原理是给 error-first 风格的回调无论成功或者失败,在执行完毕后都会执行最后一个回调函数,我们需要做的就是让这个回调函数控制 promise 的状态即可 这里还用了 Proxy 代理了整个 fs 模块,拦截 get 方法,使得不需要手动给 fs 模块所有的方法都包裹一层 promisify 函数,更加的灵活

2.15 封装async/await

使用:

3. 数组操作

3.1 数组去重

const arr = [1,2,2,4,1,6,5, undefined, 'undefined', '6','null', null, NaN, NaN, {a: 1}, {}, {a: 1}]

1. 使用对象

设置tmp为对象,对象的键存储数组元素的值,最终返回对象的所有键

function array_unique (arr) {
  if (arr.length === 0) {
    return arr
  }
  let tmp = {}
  let len = arr.length
  for (let i = 0; i < len; i++) {
    if (tmp[arr[i]] === undefined) {
      tmp[arr[i]] = i
    }
  }
  return Object.keys(tmp)
}

// 调用数组去重
let newArr = array_unique(arr)
console.log(newArr) // ["1", "2", "4", "5", "6", "undefined", "NaN", "[object Object]", "null"]

缺点:

  1. 不能区分数字和字符串吗
  2. 不能区分undefined/'undefined', null/'null'
  3. 返回的数据类型和原有的数据类型不一致
  4. 不能区分NaN

2. 使用Map

function array_unique (arr) {
    if (arr.length === 0) {
        return arr
    }
    let tmp = new Map()
    let len = arr.length
    for (let i = 0; i < len; i++) {
        if (!tmp.has(arr[i])) {
            tmp.set(arr[i], i)
        }
    }
    return [...tmp.keys(tmp)]
}
let newArr = array_unique(arr)
console.log(newArr) // [1, 2, 4, 6, 5, undefined, "undefined", "6", "null", null, NaN, {a: 1}, {}, {a: 1}]

缺点:

不能区分对象 如{a: 1}、{a: 1}

3. new Set()

let newArr = [...new Set(arr)] // Array.from(new Set(arr))
console.log(newArr) // [1, 2, 4, 6, 5, undefined, "undefined", "6", "null", null, NaN, {a: 1}, {}, {a: 1}]

缺点:

不能区分对象 如{a: 1}、{a: 1}

4. 使用数组

设置tmp为数组,数组中存储唯一的元素,最终返回tmp

function array_unique (arr) {
  let len = arr.length
  if (!len) {
    return []
  }
  let tmp = []
  for (let i = 0; i < len; i++) {
    // 判断数组arr的元素是否在数组tmp中
    if (tmp.indexOf(arr[i]) === -1) {
      tmp.push(arr[i])
    }
  }
  return tmp
}
let newArr = array_unique(arr)
console.log(newArr) // [1, 2, 4, 6, 5, undefined, "undefined", "6", "null", null, NaN, NaN, {a: 1}, {},  {a: 1}]

缺点:

  1. 不能筛选NaN
  2. 不能区分对象 如{a: 1}、{a: 1}

5. 使用 includes 代替2中的indexOf

function array_unique (arr) {
  let len = arr.length
  if (!len) {
    return []
  }
  let tmp = []
  for (let i = 0; i < len; i++) {
    // 判断数组arr的元素是否在数组tmp中
    if (!tmp.includes(arr[i])) {
      tmp.push(arr[i])
    }
  }
  return tmp
}
let newArr = array_unique(arr)
console.log(newArr) // [1, 2, 4, 6, 5, undefined, "undefined", "6", "null", null, NaN, {a: 1}, {}, {a: 1}]

缺点:

不能区分对象 如{a: 1}、{a: 1}

6. 使用findIndex替代includes

findIndex查询数组是否包含某元素,如果存在返回元素的索引,否则返回-1。它比indexOf更加先进的地方在于能传入callback

function array_unique (arr) {
  let len = arr.length
  if (!len) {
    return []
  }
  let tmp = []
  for (let i = 0; i < len; i++) {
    // 判断数组arr的元素是否在数组tmp中
    if (!(~tmp.findIndex(v => JSON.stringify(v) === JSON.stringify(arr[i])))) {
      tmp.push(arr[i])
    }
  }
  return tmp
}
let newArr = array_unique(arr)
console.log(newArr) // [1, 2, 4, 6, 5, undefined, "undefined", "6", "null", null, {a: 1}, {}]

参考文章:面试官在“逗”你系列:数组去重你会几种呀

3.2 数组交集

const a = [0, 1, 2, 3, 4, 5]
const b = [3, 4, 5, 6, 7, 8]
const duplicatedValues = [...new Set(a)].filter(item => b.includes(item))
duplicatedValues // [3, 4, 5]

3.3 数组差集

const a = [0, 1, 2, 3, 4, 5]
const b = [3, 4, 5, 6, 7, 8]
const diffValues = [...new Set([...a, ...b])].filter(item => !b.includes(item) || !a.includes(item)) // [0, 1, 2, 6, 7, 8]

3.4 摊平数组

// 递归
const lists = [1, [2, 3, [4, 5]]];
function reduceArr(list, depth) {
    if(depth === 0) return list;
    let result = [];
    for(let i = 0;i < list.length;i++) {
        if(Array.isArray(list[i])) {
            result = result.concat(list[i]);
        } else {
            result.push(list[i]);
        }
    }
    return reduceArr(result, --depth);
}
console.log(reduceArr(lists, 2));
// 循环
const lists = [1, [2, 3, [4, 5]]];
function reduceArr(list, depth) {
    let result = [];
    for(let i = 1;i <= depth;i++) {
        result.length && (list = result);
        result = [];
        for(let j = 0;j < list.length;j++) {
            if(Array.isArray(list[j])) {
                result = result.concat(list[j]);
            } else {
                result.push(list[j]);
            }
        }
    }
    if(depth) {
        return result;
    } else {
        return list;
    }
}
console.log(reduceArr(lists, 2));
// toString
let array = [1, [2], [3, [4, [5]]]]
function flat(arr) {
  return arr.toString().split(',').map(val => +val)
}
console.log(flat(array))
// reduce
let array = [1, [2], [3, [4, [5]]]]
function flat(arr) {
  return arr.reduce((pre, value) => {
    return Array.isArray(value) ? [...pre, ...flat(value)] : [...pre, value]
  }, [])
}
console.log(flat(array))

传入 Inifity 会将传入的数组变成一个一维数组

原理是每递归一次将 depth 参数减 1,如果 depth 参数为 0 时,直接返回原数组

参考文章:使用 reduce 实现数组的 flat 方法

3.5 数组循环api实现

3.5.1 实现map

  1. 循环实现数组map

map 的第二个参数为第一个参数回调中的 this 指向,如果第一个参数为箭头函数,那设置第二个 this 会因为箭头函数的词法绑定而失效

另外就是对稀疏数组的处理,通过 hasOwnProperty 来判断当前下标的元素是否存在与数组中

参考:循环实现数组 map 方法

  1. reduce实现数组map

3.5.2 实现数组 filter 方法

  1. 使用循环

  2. 使用reduce

3.5.3 实现数组的 some 方法

3.5.4 循环实现数组的 reduce 方法

因为可能存在稀疏数组的关系,所以 reduce 需要保证跳过稀疏元素,遍历正确的元素和下标

4. 文件读取

4.1 Promise 处理文件读取

const fs = require('fs')
const path = require('path');

const readfile = function (filename) {
    return new Promise((resolve, reject) => {
        fs.readFile(path.join(__dirname, filename), 'utf-8', function (error, data) {
            if (error) return reject(error)
            resolve(data)
        })
    })
}

readfile('./01.txt')
    .then(value => {
        console.log(value)
        return readfile('./02.txt')
    })
    .then(value => {
        console.log(value)
        return readfile('./03.txt')
    })
    .then(value => {
        console.log(value)
    }).catch(reason => {
    console.log(reason)
})

4.2 Generator 函数文件读取

const fs = require('fs')
const path = require('path');

const readfile = function (filename) {
    return new Promise((resolve, reject) => {
        fs.readFile(path.join(__dirname, filename), 'utf8', function (error, data) {
            if (error) return reject(error)
            resolve(data)
        })
    })
}
function* gen() {
    yield readfile('./01.txt')
    yield readfile('./02.txt')
    yield readfile('./03.txt')
}
const result = gen()

result.next().value.then(value=>{
    console.log(value)
    return result.next().value
}).then(value => {
    console.log(value)
    return result.next().value
}).then(value => {
    console.log(value)
}).catch(reason => {
    console.log(reason)
})

4.3 async 函数文件读取

const fs = require('fs')
const path = require('path');

const readfile = function (filename) {
    return new Promise((resolve, reject) => {
        fs.readFile(path.join(__dirname, filename), 'utf8', function (error, data) {
            if (error) return reject(error)
            resolve(data)
        })
    })
}
async function gen() {
    try{
        const f1=await readfile('./01.txt')
        const f2=await readfile('./02.txt')
        const f3 = await readfile('./03.txt')
        console.log(f1)
        console.log(f2)
        console.log(f3)
    }catch (e) {
        console.log(e)
    }
}
gen()

5. 函数式编程部分

5.1 柯里化

function curry(fn) {
  // 只允许传入函数,传入其它类型会报错
  if(typeof fn !== 'function') {
      throw Error('No function provided')
  }
  return function curriedFn(...args) {
      if (args.length < fn.length) {
          return function() {
              return curriedFn(...args.concat(Array.from(arguments)))
          }
      }
      return fn(...args)
  }
}

支持占位符:

使用方法:

通过占位符能让柯里化更加灵活,实现思路是,每一轮传入的参数先去填充上一轮的占位符,如果当前轮参数含有占位符,则放到内部保存的数组末尾,当前轮的元素不会去填充当前轮参数的占位符,只会填充之前传入的占位符

5.2 偏函数

const partial = function(fn, ...args) {
    return function(...fullArguments) {
        let position = 0
        for(let i = 0; i < args.length && position < fullArguments.length; i++) {
            if(args[i] === undefined) {
                args[i] = fullArguments[position++]
            }
        }
        return fn.apply(null, args)
    }
}

let subtract = (a, b) =>  b - a
subFrom20 = partial(subtract, undefined, 20);
console.log(subFrom20(5))

使用方式:

偏函数和柯里化概念类似,个人认为它们区别在于偏函数会固定你传入的几个参数,再一次性接受剩下的参数,而函数柯里化会根据你传入参数不停的返回函数,直到参数个数满足被柯里化前函数的参数个数 Function.prototype.bind 函数就是一个偏函数的典型代表,它接受的第二个参数开始,为预先添加到绑定函数的参数列表中的参数,与 bind 不同的是,上面的这个函数同样支持占位符

5.3 函数组合

function compose(...args) {
    return function(value) {
        return args.reverse().reduce(function(acc, fn) {
            return fn(acc)
        }, value)
    }
}

// 从右往左
const compose = (...fns) => value => reduce(fns.reverse(), (acc, fn) => fn(acc), value)

// pipe 从左往右
const pipe = (...fns) => value => reduce(fns, (acc, fn) => fn(acc), value)

5.4 reduce

reduce(array, fn, initialValue) {
    let accumlator;
    if(initialValue != undefined) {
        accumlator = initialValue
    } else {
        accumlator = array[0]
    }

    if(initialValue === undefined) {
        for(let i = 1; i < array.length; i++) {
            accumlator = fn(accumlator, array[i])
        }
    } else {
        for(let arr of array) {
            accumlator = fn(accumlator, arr)
        }
    }
    return [accumlator]
}

5.5 记忆函数

const memoize = (fn) => {
    let cache = {}
    return function() {
        let key = JSON.stringify(arguments)
        cache[key] = cache[key] || fn.apply(fn, arguments)
        return cache[key]
    }
}

5.6 惰性函数

1. 惰性调用

使用flowRight函数来模拟lodash的惰性调用

const fp = require('lodash/fp')

class MyWrapper {
  constructor (value) {
    this._wrapped = value
    this._actions = []
  }

  chain (value) {
    this._wrapped = value
    return this
  }

  filter (fn) {
    this._actions.push(fp.filter(fn))
    return this
  }

  map (fn) {
    this._actions.push(fp.map(fn))
    return this
  }

  sum () {
    this._actions.push(fp.sum)
    return this
  }

  value () {
    // fp.flowRight()
    // fp.compose()
    let fn = fp.compose(...this._actions.reverse())
    return fn(this._wrapped)
  }
}

let _ = {
  chain: function chain (value) {
    return new MyWrapper(value)
  }
}

let employees = [
  { name: 'Jack', age: 25, sex: 'male', salary: 20000 },
  { name: 'Tom', age: 30, sex: 'male', salary: 30000 },
  { name: 'Jim', age: 26, sex: 'male', salary: 25000 },
  { name: 'Carl', age: 25, sex: 'male', salary: 22000 },
  { name: 'Abel', age: 32, sex: 'male', salary: 32000 },
  { name: 'Gary', age: 31, sex: 'male', salary: 28000 },
  { name: 'Kevin', age: 23, sex: 'male', salary: 27000 }
]

let salary = _.chain(employees)
  .filter(e => e.age >= 30)
  .map(e => e.salary)
  .sum()
  .value()
console.log(salary)

2. 惰性载入

function createXHR(){
    if (typeof XMLHttpRequest != "undefined"){
        return new XMLHttpRequest();
    } else if (typeof ActiveXObject != "undefined"){
        if (typeof arguments.callee.activeXString != "string"){
            var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"], i, len;
            for (i=0,len=versions.length; i < len; i++){
                try {
                    new ActiveXObject(versions[i]);
                    arguments.callee.activeXString = versions[i];
                    break;
                } catch (ex){
                    //跳过 
                }
            }
        }
        return new ActiveXObject(arguments.callee.activeXString);
    } else {
        throw new Error("No XHR object available.");
    }
}

惰性载入表示函数执行的分支仅会发生一次。有两种实现惰性载入的方式。 第一种就是在函数被调用时再处理函数。在第一次调用的过程中,该函数会被覆盖为另外一个按合适方式执行的函数,这样任何对原函数的调用都不用再经过执行的分支了。例如,可以用下面的方式使用惰性载入重写 createXHR()。

function createXHR(){
    if (typeof XMLHttpRequest != "undefined"){
        createXHR = function(){return new XMLHttpRequest();
    } else if (typeof ActiveXObject != "undefined"){
        createXHR = function(){
            if (typeof arguments.callee.activeXString != "string"){
                var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"],  i, len;
                for (i=0,len=versions.length; i < len; i++){
                    try {
                        new ActiveXObject(versions[i]);
                        arguments.callee.activeXString = versions[i];
                            break;
                        } catch (ex){
                            //skip
                        }
                    }
                }
                return new ActiveXObject(arguments.callee.activeXString);
            };
        } else {
            createXHR = function(){
                throw new Error("No XHR object available.");
            };
        }
    return createXHR();
}

5.7 尾递归调用

斐波那契

利用函数记忆,将之前运算过的结果保存下来,对于频繁依赖之前结果的计算能够节省大量的时间,例如斐波那契数列,缺点就是闭包中的 obj 对象会额外占用内存

另外使用动态规划比前者的空间复杂度更低,也是更推荐的解法

5.8 函子

参考文章

6. node api

6.1 co模块

使用方式: run 函数接受一个生成器函数,每当 run 函数包裹的生成器函数遇到 yield 关键字就会停止,当 yield 后面的 promise 被解析成功后会自动调用 next 方法执行到下个 yield 关键字处,最终就会形成每当一个 promise 被解析成功就会解析下个 promise,当全部解析成功后打印所有解析的结果,衍变为现在用的最多的 async/await 语法

6. vue

MVVM
vue-router