手撕JS

113 阅读12分钟

实现深拷贝

function deepCopy(src){
  if (!src | !(src instanceof Object) | (typeof src === "function")) {
    return src || undefined
  }
  var constructor = src.constructor
  var dst = new constructor()
  for (var key in src) {
    if (src.hasOwnProperty(key)) {
      dst[key] = deepCopy(src[key])
    }
  }
  return dst
}

实现一个ajax

ajax实现局部刷新的原理是通过XmlHttpRequest对象来向服务器发送异步请求,通过js操作相应的DOM来更新页面

ajax实现过程:

  1. 创建XmlHttpRequest对象
  2. 初始化参数
  3. 发送信息
  4. 接收信息
function getData(url) {
  return new Promise(function(resolve, reject){
    const handler = function() {
      if (this.readyState !== 4) { // 4表示响应信息已经全部接收
        return;
      }
      if (this.status === 200) { // status 从服务器返回的状态码
        resolve(this.reposeText); // reposeText 从服务器返回数据的字符串格式
      } else {
        reject(new Error(this.statusText));// statusText 伴随状态码返回的信息,如status=200时,statusText='OK'
      }
    };
    const xhr = new XMLHttpRequest();// 创建XMLHttpRequest对象
    xhr.open("GET", url);// 初始化http的请求参数,但是不发送请求
		xhr.onreadystatechange = handler;// 状态改变触发的回调函数 接收请求
    xhr.responseType = "json";
    xhr.setRequestHeader("Accept", "application/json");// 给一个打开但是未发送的请求设置参数
    xhr.send();// 发送http请求
  });
};

数组去重

set去重:

缺点:对象无法去重

[...new Set(arr)]

indexOf去重:

缺点:NaN和对象不能去重

Array.prototype.unique = function() {
    var result = [];
    this.forEach((val) => {
        if(result.indexOf(val) < 0) {
            result.push(val)
        }
    })
    return result
}

// [1,{},{},NaN,NaN,null,null,undefined,undefined,'ss','ss'].unique();

includes去重:

缺点:对象不能去重

Array.prototype.unique = function () {
    let resultArr = [];
    this.forEach((item) => {
        if (!resultArr.includes(item)) {
            resultArr.push(item);
        }
    })
    return resultArr;
}

splice去重:

缺点:NaN和对象不能去重,null直接消失了

function unique(arr){            
    for(var i=0; i<arr.length; i++){
        for(var j=i+1; j<arr.length; j++){
            if(arr[i]==arr[j]){         //第一个等同于第二个,splice方法删除第二个
                arr.splice(j,1);
                j--;
            }
        }
    }
		return arr;
}

hasOwnProperty去重:

缺点:无,全部去重了

Array.prototype.unique1 = function () {
    let obj = {};
    return this.filter((item) => obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true));
}

filter去重:

function unique(arr) {
  return arr.filter(function(item, index, arr) {
    //当前元素,在原始数组中的第一个索引===当前索引值,否则返回当前元素
    return arr.indexOf(item) === index;
  });
}

map去重:

Array.prototype.unique = function () {
    let map = new Map();
    let resultArr = [];
    this.forEach((item) => {
        if (!map.has(item)) {
            resultArr.push(item);
            map.set(item,true)
        }
    })
    return resultArr;
}

reduce+includes去重:

Array.prototype.unique3 = function () {
    return this.reduce((prev, cur) => prev.includes(cur) ? prev : [...prev,cur], []);
}

扁平化数组

方法一:Array.prototype.flat()

  • Array.prototype.flat() 特性总结:
    • Array.prototype.flat() 用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。
    • 不传参数时,默认“拉平”一层,可以传入一个整数,表示想要“拉平”的层数。
    • 传入 <=0 的整数将返回原数组,不“拉平”
    • Infinity 关键字作为参数时,无论多少层嵌套,都会转为一维数组
    • 如果原数组有空位,Array.prototype.flat() 会跳过空位。
let arr = [[1, 2, 2],[3, 4, 5, 5],[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];

console.log(arr.flat(Infinity));
/* [1,  2,  2, 3,  4,  5,  5,  6,  7,  8, 9, 11, 12, 12, 13, 14, 10] */

方法二:转换为字符串,再把字符串对象用“,”转换成数组

  • 思路:可以先把多维数组先转换为字符串,再基于","分隔符将字符串对象分割成字符串数组

  • toString() 扁平化数组:

    let arr = [[1, 2, 2],[3, 4, 5, 5],[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];
    
    arr = arr.toString();
    // "1,2,2,3,4,5,5,6,7,8,9,11,12,12,13,14,10"
    
    arr = arr.toString().split(',');
    // ["1", "2", "2", "3", "4", "5", "5", "6", "7", "8", "9", "11", "12", "12", "13", "14", "10"]
    
    arr = arr.toString().split(',').map(item => parseFloat(item));
    // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]
    
  • JSON.stringify()扁平化数组:

    let arr = [[1, 2, 2],[3, 4, 5, 5],[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];
    
    arr = JSON.stringify(arr);
    // "[[1,2,2],[3,4,5,5],[6,7,8,9,[11,12,[12,13,[14]]]],10]"
    
    arr = JSON.stringify(arr).replace(/(\[|\])/g, '');
    // "1,2,2,3,4,5,5,6,7,8,9,11,12,12,13,14,10"
    
    arr = JSON.stringify(arr).replace(/(\[|\])/g, '').split(',').map(item=>parseFloat(item));
    // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]
    

方法三:循环验证是否为数组

  • 基于数组的some方法,只要数组里面有一项元素是数组就继续循环,扁平数组
  • 核心:[].concat(...arr)
let arr = [[1, 2, 2],[3, 4, 5, 5],[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];

while(arr.some(item => Array.isArray(item))) {
 arr = [].concat(...arr);
}

console.log(arr); // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]

方法四:forEach+isArray+push+recursivity

  • 相当于:自己实现一个 flat 扁平化
  • 实现思路:
    • 循环数组里的每一个元素
    • 判断该元素是否为数组
      • 是数组的话,继续循环遍历这个元素——数组
      • 不是数组的话,把元素添加到新的数组中
  • 实现流程:
    1. 创建一个空数组,用来保存遍历到的非数组元素
    2. 创建一个循环遍历数组的函数,cycleArray
    3. 取得数组中的每一项,验证Array.isArray()
      • 数组的话,继续循环
      • 非数组的话,添加到新数组中
    4. 返回新数组对象
let arr = [[1, 2, 2],[3, 4, 5, 5],[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];

// 方式一:
// forEach 遍历数组会自动跳过空元素
const eachFlat = (arr = [], depth = 1) => {
  const result = []; // 缓存递归结果
  // 开始递归
  (function flat(arr, depth) {
    // forEach 会自动去除数组空位
    arr.forEach((item) => {
      // 控制递归深度
      if (Array.isArray(item) && depth > 0) {
        // 递归数组
        flat(item, depth - 1)
      } else {
        // 缓存元素
        result.push(item)
      }
    })
  })(arr, depth)
  // 返回递归结果
  return result;
}
console.log(eachFlat(arr)); // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]

// 方式二:
// for of 循环不能去除数组空位,需要手动去除
const forFlat = (arr = [], depth = 1) => {
  const result = [];
  (function flat(arr, depth) {
    for (let item of arr) {
      if (Array.isArray(item) && depth > 0) {
        flat(item, depth - 1)
      } else {
        // 去除空元素,添加非undefined元素
        item !== void 0 && result.push(item);
      }
    }
  })(arr, depth)
  return result;
}
console.log(forFlat(arr)); // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]

方法五:reduce+ concat+ isArray+ recursivity

  • 相当于:自己实现一个 flat 扁平化
  • 思路与方法四相同
// 只能展开一层(可忽略不看):
let arr = [12, 23, [34, 56, [78, 90, 100, [110, 120, 130, 140]]]];
const myFlat = arr => {
  return arr.reduce((pre, cur) => {
    return pre.concat(cur);
  }, []);
};
console.log(myFlat(arr));// [ 12, 23, 34, 56, [ 78, 90, 100, [ 110, 120, 130, 140 ] ] ]

// 不可控制深度的直接全员扁平化(也就那样):
const myFlat = arr => {
  return arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? myFlat(cur) : cur);
  }, []);
};
console.log(myFlat(arr));// [12, 23, 34, 56, 78, 90, 100, 110, 120, 130, 140]

// 可控制展开深度的扁平化(最优):
// 使用 reduce、concat 和递归展开无限多层嵌套的数组
function flatDeep(arr, d = 1) {
   return d > 0 ? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? flatDeep(val, d - 1) : val), [])
                : arr.slice();
};
flatDeep(arr, Infinity);// [12, 23, 34, 56, 78, 90, 100, 110, 120, 130, 140]

方法六:使用栈的思想实现flat函数

  • 栈思想: 后进先出的数据结构
  • 实现思路:
    • 不断获取并删除栈中最后一个元素A,判断A是否为数组元素,直到栈内元素为空,全部添加到newArr
      • 是数组,则push到栈中,继续循环栈内元素,直到栈为空
      • 不是数组,则unshift添加到newArr
// 无递归数组扁平化,使用堆栈
// 注意:深度的控制比较低效,因为需要检查每一个值的深度
// 也可能在 shift / unshift 上进行 w/o 反转,但是末端的数组 OPs 更快
function flatten(input) {
  const stack = [...input]; // 将数组元素拷贝至栈,直接赋值会改变原数组
  const res = [];
	//如果栈不为空,则循环遍历
  while (stack.length) {
    const next = stack.pop(); // 删除数组最后一个元素,并获取它
    if (Array.isArray(next)) {
      stack.push(...next);  // 如果是数组再次入栈,并且展开了一层
    } else {
      res.unshift(next); // 如果不是数组就将其取出来放入结果数组中
    }
  }
	return res;
}
let arr = [12, 23, [34, 56, [78, 90, 100, [110, 120, 130, 140]]]];
console.log(flatten(arr));
// [12, 23, 34, 56, 78, 90, 100, 110, 120, 130, 140]

方法七:Use Generator function

function* flatten(array) {
    for (const item of array) {
        if (Array.isArray(item)) {
            yield* flatten(item);
        } else {
            yield item;
        }
    }
}

let arr = [1, 2, [3, 4, [5, 6]]];
const flattened = [...flatten(arr)]; // [1, 2, 3, 4, 5, 6]

休眠函数

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

// 用法
sleep(500).then(() => {
    // 这里写sleep之后需要去做的事情
})

// 用法
(async function() {
  console.log('Do some thing, ' + new Date());
  await sleep(500);
  console.log('Do other things, ' + new Date());
})();

斐波那契数列

普通递归:

会出现浏览器假死现象,毕竟递归需要堆栈,数字过大内存不够。

function fibonacci(n) {
    if (n == 1 || n == 2) {
        return 1
    };
    return fibonacci(n - 2) + fibonacci(n - 1);
}

优化递归:

把前两位数字做成参数避免重复计算

function fibonacci(n) {
    function fib(n, v1, v2) {
        if (n == 1)
            return v1;
        if (n == 2)
            return v2;
        else
            return fib(n - 1, v2, v1 + v2)
    }
    return fib(n, 1, 1)
}

优化递归:

利用闭包特性把运算结果存储在数组里,避免重复计算

var fibonacci = function () {
    let memo = [0, 1];
    return function fib (n) {
        if (memo[n] == undefined) {
            memo[n] = fib(n - 2) + fib(n - 1)
        }
        return memo[n]
    }
}()

for循环:

function fibonacci(n) {
    let n1 = 1,
        n2 = 1,
        sum = 1
    for(let i = 3; i <= n; i += 1) {
        sum = n1 + n2
        n1 = n2
        n2 = sum
    }
    return sum
}

for循环+解构赋值:

function fibonacci(n) {
    let n1 = 1; n2 = 1;
    for (let i = 2; i < n; i++) {
        [n1, n2] = [n2, n1 + n2]
    }
    return n2
}

发布订阅模式

快排

function quickSort(arr) {
    if(arr.length <= 1) {
        return arr
    }
    let middle = arr.shift() || 0
    let left = [], right = []
    for(let i=0; i < arr.length; i++) {
        if (arr[i] < middle) {
            left.push(arr[i])
        }
        if (arr[i] > middle) {
            right.push(arr[i])
        }
    }
    return quickSort(left).concat(middle, quickSort(right))
}

冒泡

function bubbleSort(arr: Array<number>) {
    for(let i=0, len=arr.length; i<len-1; i++) {
        for(let j=0; j < len-i-1; j++) {
            if(arr[j] > arr[j+1]) {
                [arr[j], arr[j+1]] = [arr[j+1], arr[j]]
            }
        }
    }
    return arr
}

手动控制并发请求

// ……

debounce防抖

防抖:

在事件被触发后不马上执行回调,n秒后再执行回调,如果在这n秒内又被触发,则重新计时

如果再次执行,就清空之前的定时器,重新加定时器

强调执行最后一次

function debounce(fn, delay) {
    let timer = null;
    return function () {
      let args = arguments;
      if (timer) {
        clearTimeout(timer);
      }
      timer = setTimeout(() => {
        fn(...args);
      }, delay);
    };
}

throttle节流

节流:强调一段固定的时间内只触发一次回调函数

定时器版本:

function throttle(fn,delay) {
    let timer;
    return function () {
        let args = arguments;
        if (timer) {
            return;
        }
        setTimeout(() => {
            fn(...args);
            timer = null; // 只有触发了函数,timer为null,下一次才能再次执行这个定时器
        },delay)
    }
}

时间戳版本:

function throttle(fn, delay) {
    // if (typeof fn !== 'function') {
    //     throw new TypeError('need a function!')
    // }
    let previous = 0;
    return function () {
        let now = Date.now();
        let args = arguments;
        if (now - previous > delay) {
            fn(...args);
            previous = now;
        }
    }
}

call

详细原理可以看this指向与call,apply,bind

xFn.call(xObj,x1,x2,……)

上面调用相当于给xObj添加一个xFn方法并把参数x1,x2……传进去执行,以下实现也是按照此逻辑

Function.prototype.myCall = function(context) {
		// context就是call的第一个参数 xObj
    // 如果没有传或传的值为空对象 context指向window
    context = context || window;
    context.fn = this //给context添加一个方法 指向this this就是我们调用call的那个函数xFn
    // 处理参数 将content后面的参数取出来
    let arg = [...arguments].slice(1) //[...xxx]把类数组变成数组,arguments为啥不是数组自行搜索 slice返回一个新数组
    let result = context.fn(...arg) //执行fn
    delete context.fn //删除方法
		return result;
}

apply

与call的区别是 参数是数组或类数组的对象

Function.prototype.myApply = function (context) {
  let context = context || window
  context.fn = this
  let result;
  // 需要判断是否存储第二个参数
  // 如果存在,就将第二个参数展开
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }
  delete context.fn
  return result
}

bind

与call和apply的区别是 bind返回的是新函数 不会立即执行

Function.prototype.myBind = function (context) {
    if (typeof this !== 'function') {
      throw new TypeError('Error')
    }
    let _this = this
    let args = [...arguments].slice(1)
    // 返回一个函数
    return function() {
      let context = context || window;
			//同样因为支持柯里化形式传参我们需要再次获取存储参数
      let newArgs = args.concat([...arguments]);
      context.fn = _this;
			let result;
      if (newArgs.length) {
        result = context.fn(...newArgs)
      } else {
        result = context.fn()
      }
      delete context.fn
      return result
    }
  }

instanceof

a instanceof Object

判断Object的prototype是否在a的原型链上。

原型链图解可参考:原型相关

function myInstanceof(target, origin) {
    const proto = target.__proto__;
    if (proto) {
      if (origin.prototype === proto) {
        return true;
      } else {
        return myInstanceof(proto, origin);//继续沿着原型链网上找
      }
    } else { // Object的_proto_指向null(原型链的尽头)
      return false;
    }
  }

实现柯里化函数

柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c)转换为可调用的 f(a)(b)(c)。柯里化不会调用函数。它只是对函数进行转换。

具体原理可见:函数柯里化

function myCurry(func) {
	// func 是要转换的函数
  return function curried(...args) {
		// func.length是原函数的形参数量
    if (args.length >= func.length) { // 实际传进来的参数数量>=原函数形参数量,才直接执行
      return func.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2)); // 传的参数不够时,就连着之前传的参数一起继续柯里化
      }
    }
  };

}

或者:

function myCurry(fn, ...args) {
    if (args.length >= fn.length) {
      return fn(...args);
    } else {
      return (...args2) => currying(fn, ...args, ...args2);
    }
  }

扩展题:

实现一个add方法,使计算结果能够满足如下预期:

add(1)(2)(3) = 6

add(1, 2, 3)(4) = 10

add(1)(2)(3)(4)(5) = 15

function add() {
  var _args = [...arguments]
  return function() {
    if (arguments.length === 0) {
      return _args.reduce(function (a, b) {
        return a + b
      })
    }
    [].push.apply(_args, [...arguments])
    return arguments.callee
  }
}

add(1, 2)(1)(2)(5)()

compose

compose函数可以将需要嵌套执行的函数平铺,嵌套执行就是一个函数的返回值将作为另一个函数的参数。右边的方法最开始执行,然后往左边返回(从右往左

详情可见:juejin.cn/post/684490…

compose(multiply, add)(10)等价于:multiply(add(10));

const myCompose = (...args) => x => args.reduceRight((res, cb) => cb(res), x);

pipe

compose函数的作用是一样的,也是将参数平铺,只不过他的顺序是从左往右

pipe(add, multiply)(10)等价于:multiply(add(10));

const myPipe = (...args) => x => args.reduce((res, cb) => cb(res), x)

实现new

new干了什么:

  1. 帮我们创建一个空对象;
  2. 将新对象的原型(prototype)指向构造函数的原型(prototype);
  3. 执行构造函数,把构造函数的属性添加到新对象;
  4. 返回创建的新对象;

详情:juejin.cn/post/684490…

function myNew(func, ...rest) {
    // 创建空对象,并将新对象的__proto__属性指向构造函数的prototype,func是构造函数
    const obj = Object.create(func.prototype)
    // 执行构造函数,改变构造函数的this指针,指向新创建的对象(新对象也就有了构造函数的所有属性)
    func.apply(obj, rest)
    return obj;
}

或者:

function myNew () {
  let obj = new Object()
  let constructor = [].shift.apply(arguments)
  obj.__proto__ = constructor.prototype
  let result = constructor.apply(obj, arguments)
  return typeof result === "object" ? result : obj
}

promise

const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

function MyPromise(fn) {
  const that = this
  that.state = PENDING //一开始 Promise 的状态应该是 pending
  that.value = null //保存 resolve 或者 reject 中传入的值
	//resolvedCallbacks 和 rejectedCallbacks 用于保存 then 中的回调,
	//因为当执行完 Promise 时状态可能还是等待中,
	//这时候应该把 then 中的回调保存起来用于状态改变时使用
  that.resolvedCallbacks = []
  that.rejectedCallbacks = []
  function resolve(value) {
    console.log('999')
    if (that.state === PENDING) { //判断当前状态是否为等待中,因为规范规定只有等待态才可以改变状态
      that.state = RESOLVED //将当前状态更改为对应状态
      that.value = value // 并且将传入的值赋值给 value
      that.resolvedCallbacks.map(cb => cb(that.value)) // 遍历回调数组并执行
    }
  }

  function reject(value) {
    if (that.state === PENDING) {
      that.state = REJECTED
      that.value = value
      that.rejectedCallbacks.map(cb => cb(that.value))
    }
  }
  try {
    console.log('000');
    fn(resolve, reject)
  } catch (e) {
    reject(e)
  }
}
MyPromise.prototype.then = function (onFulfilled, onRejected) { // 较为复杂的 then 函数
  const that = this
	// 判断两个参数是否为函数类型,因为这两个参数是可选参数
	// 当参数不是函数类型时,需要创建一个函数赋值给对应的参数
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : r => {
        throw r
      }
	// 状态是等待态的话,就往回调函数中 push 函数
  if (that.state === PENDING) {
    console.log('222')
    that.resolvedCallbacks.push(onFulfilled)
    that.rejectedCallbacks.push(onRejected)
  }
	// 当状态不是等待态时,就去执行相对应的函数
  if (that.state === RESOLVED) {
    console.log('333')
    onFulfilled(that.value)
  }
  if (that.state === REJECTED) {
    console.log('444')
    onRejected(that.value)
  }
}
// 调用 进入等待态的逻辑
new MyPromise((resolve, reject) => {
  setTimeout(() => {//pending状态
    resolve(1)
  }, 0)
  // resolve(1)
}).then(value => {
  console.log(value)
})