手写JavaScript

106 阅读7分钟

本文内容转载自32个手写JS,巩固你的JS基础(面试高频)

1. 数组扁平化

数组扁平化是指将一个多维数组变为一个一维数组。

const arr = [1, [2, [3, [4, 5]]], 6];
// => [1, 2, 3, 4, 5, 6]

方法一:利用flat方法

const res = arr.flat(Infinite);

方法二:利用正则

const res2 = JSON.parse('[' + JSON.stringify(arr).replace(/\[|\]/g, '') + ']');

方法三:使用reduce

const flatten = arr => {
    return arr.reduce((pre, cur) => {
        return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
    }, [])
}
console.log(flatten(arr));

方法四:使用递归

const flatten = arr => {
    const res = [];
    const fn = arr => {
        for (const num of arr) {
            if (Array.isArray(num)) {
                fn(num);
            } else {
                res.push(num);
            }
        }
    }
    return res;
}

2. 数组去重

const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];
// => [1, '1', 17, true, false, 'true', 'a', {}, {}]

方法一:利用Set

const res = Array.from(new Set(arr));

方法二:两层for循环 + splice

const unique = arr => {
    let len = arr.length;
    for (let i = 0; i < len; i++) {
        for (let j = i + 1; j < len; j++) {
            if (arr[i] === arr[j]) {
                arr.splice(j, 1);
                j--;
                len--;
            }
        }
    }
    return arr;
}

方法三:利用indexOf

const unique = arr => {
    const res = [];
    for (let i = 0; i < arr.length; i++) {
        if (res.indexOf(arr[i]) === -1) {
            res.push(arr[i])
        }
    }
    return res;
}

方法四:利用includes

const unique = arr => {
    const res = [];
    for (const num of arr) {
        if (!res.includes(num)) {
           res.push(num); 
        }
    }
    return res;
}

方法五:利用filter

const unique = arr => {
    return arr.filter((item, index) => {
        return arr.indexOf(item) === index;
    })
}

方法六:利用Map

const unique = arr => {
    const map = new Map();
    const res = [];
    for (const num of arr) {
        if (!map.has(num)) {
            map.set(num, true);
            res.push(num);
        }
    }
    return res;
}

3. 类数组转数组

类数组是具有length属性,但不具有数组原型上的方法。常见的类数组有argumentsDOM操作方法返回的结果

方法一:Array.from

Array.from(document.querySelectorAll('div'))

方法二:扩展运算符

[...document.querySelectorAll('div')]

方法三:Array.prototype.slice.call

Array.prototype.slice.call(document.querySelectorAll('div'))

方法四:利用concat

Array.prototype.concat.apply([], document.querySelectorAll('div'));

4. debounce(防抖)

触发高频时间后n秒内函数只会执行一次,如果n秒内高频时间再次触发,则重新计算时间。

const debounce = (fn, time) => {
    let timeout = null;
    return function() {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            fn.apply(this, arguments);
        }, time)
    }
}

防抖常应用于用户进行搜索输入节约请求资源,window触发resize事件时进行防抖只触发一次。

5. throttle(节流)

高频时间触发,但n秒内只会执行一次,所以节流会稀释函数的执行频率。

const throttle = (fn, time) => {
    let timeout = null;
    return function() {
        if (!timeout) {
            timeout = setTimeout(() => {
                fn.apply(this, [...arguments]);
                clearTimeout(timeout);
                timeout = null;
            }, time);
        }
    }
}

6. 函数柯里化

指的是将一个接受多个参数的函数 变为 接受一个参数返回一个函数的固定形式,这样便于再次调用。

function add() {
  // 第一次执行时,定义一个数组专门用来存储所有的参数
  const _args = [...arguments];
  function fn() {
    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    _args.push(...arguments);
    return fn;
  }
  // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
  fn.toString = function() {
    return _args.reduce((sum, cur) => sum + cur);
  }
  return fn;
}
var a = add(1)(2)(3)(4);
var b = add(1)(2,3,4)(5);
console.log(a + a);  // 10
console.log(b + b);  // 20

valueOf偏向于运算,toString偏向于显示。上边的fn.toString可以换成fn.valueOf,对a进行运算时会优先执行valueOf,对a进行展示时会执行toString。

7. new操作符

function newOperator(ctor) {
    if (typeof ctor !== 'function') {
        throw new TypeError(ctor + 'is not a function.');
    }
    newOperator.target = ctor;
    var newObj = Object.create(ctor.prototype);
    var argsArr = [].slice.call(arguments, 1);
    var ctorReturnRes = ctor.apply(newObj, argsArr);
    var isObject = typeof ctorReturnRes === 'object' && ctorReturnRes !== null;
    var isFunction = typeof ctorReturnRes === 'function';
    if (isObject || isFunction) return ctorReturnRes;
    return newObj;
}

8. instanceof操作符

function instanceofFn(A, B) {
    if (typeof A !== 'object' || !A) return false;
    let p = A.__proto;
    while (p) {
        if (p === B.prototype) return true;
        p = p.__proto;
    }
    return false;
}

9. Object.is

Object.is 和 === 的区别体现在

+0 === -0;  // true
NaN === NaN; // false
Object.is(+0, -0); // false
Object.is(NaN, NaN); // true
const is = (x, y) => {
    if (x === y) {
        return x !== 0 || y !== 0 || 1/x === 1/y;
    } else {
        return x !== x || y !== y;
    }
}

10. 深拷贝

考虑SymbolSetMap数据结构的深拷贝。

const deepClone = (target, hash = new WeakMap()) {
    if (typeof target !== 'object' || target === null) {
        return target;
    }
    if (hash.has(target)) {
        return hash.get(target);
    }
    const cloneTarget = Array.isArray(target) ? [] : {};
    hash.set(target, cloneTarget);
    if (Object.prototype.toString.call(target) === '[object Set]') {
        target.forEach((value) => {
            cloneTarget.add(deepClone(value, hash)); 
        });
        return cloneTarget;
    }
    if (Object.prototype.toString.call(target) === '[object Map]') {
        target.forEach((item, key) => {
            cloneTarget.set(key, deepClone(item, hash));
        })
        return cloneTarget;
    }
    const symKeys = Object.getOwnPropertySymbols(target);
    if (symKeys.length) {
        symKeys.forEach((key) => {
            if (typeof target[key] === 'object' && target[key] !== null) {
                cloneTarget[key] = deepClone(target[key]);
            } else {
                cloneTarget[key] = target[key];
            }
            
        });
    }
    for (const key in target) {
        if (Object.prototype.hasOwnPerperty.call(target, key)) {
            if (typeof target[key] === 'object' && target[key] !== null) {
                cloneTarget[key] = deepClone(target[key], hash);
            } else {
                cloneTarget[key] = target[key];
            }
        }
    }
    return cloneTarget;
}

11. Promise

const PENDING = 'PENDING';  // 进行中
const FULFILLED = 'FULFILLED'; // 已成功
const REJECTED = 'REJECTED'; // 已失败

class Promise {
    constructor(exector) {
        // 初始化状态
        this.status = PENDING;
        this.value = undefined;  // 成功结果
        this.reason = undefined;  // 失败结果
        this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];
        const resolve = value => {
            // 只有进行中状态才能改变
            if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = value;
                this.onFulfilledCallbacks.forEach(fn => fn(this.value));
            }
        }
        const reject = reason => {
            // 只有进行中状态才能改变
            if (this.status === PENDING) {
                this.status = REJECTED;
                this.reason = reason;
                this.onRejectedCallbacks.forEach(fn => fn(this.reason));
            }
        }
        try {
            exector(resolve, reject);
        } catch(e) {
            reject(e);
        }
    }
    then(onFulfilled, onRejected) {
        onFulfilled = typeof onFulfilled === 'function' 
            ? onFulfilled : value => value;
        onRejected = typeof onRejected === 'function' 
            ? onRejected : reason => { 
                throw new Error(reason instanceof Error ? reason.message : reason) 
            }
        const self = this;
        return new Promise((resolve, reject) => {
            if (self.status === PENDING) {
                self.onFulfilledCallbacks.push(() => {
                    try {
                        // 模拟微任务
                        setTimeout(() => {
                            const result = onFulfilled(self.value);
                            result instanceof Promise
                                ? result.then(resolve, reject) : resolve(result);
                        })
                    } catch(e) {
                        reject(e);
                    }
                })
            }
        });
    }
    catch(onRejected) {
        return this.then(null, onRejected)
    }
    static resolve(value) {
        if (value instanceof Promise) {
            // 如果是Promise实例,直接返回
            return value;
        } else {
            // 如果不是Promise实例,返回一个新的Promise对象,状态为FULFILLED
            return new Promise((resolve, reject) => resolve(value)); 
        }
    }
    static reject(reason) {
        return new Promise((resolve, reject) => {
            reject(rason);
        });
    }
    static all(PromiseArr) {
        const len = PromiseArr.length;
        const values = new Array(len);
        // 记录已经成功执行的promise个数
        let count = 0;
        return new Promise((resolve, reject) => {
            for (let i = 0; i < len; i++) {
                Promise.resolve(promiseArr[i]).then(
                    val => {
                        values[i] = val;
                        count++;
                        // 如果全部执行完,返回promise的状态就可以改变了
                        if (count === len) resolve(values);
                    },
                    err => reject(err)
                )
            }
        });
    }
    static race(promiseArr) {
        return new Promise((resolve, reject) => {
            promiseArr.forEach(p => {
                Promise.resolve(p).then(
                    val => resolve(val),
                    err => reject(err)
                )
            })
        });
    }
}

12. Promise并行限制调度器

class Scheduler {
  constructor() {
    this.queue = [];
    this.maxCount = 2;
    this.runCount = 0;
  }
  add(promiseCreator) {
    this.queue.push(promiseCreator);
  }
  taskStart() {
    for (let i = 0; i < this.maxCount; i++) {
      this.request();
    }
  }
  request() {
    if (!this.queue || !this.queue.length || this.runCount >= this.maxCount) {
      return;
    }
    this.runCount++;
    this.queue.shift()().then(() => {
      this.runCount--;
      this.request();
    })
  }
  addTask(time, order) {
    this.add(() => {
      return new Promise(resolve => {
        setTimeout(resolve, time);
      }).then(() => {
        console.log(order);
      })
    })
  }
}

const scheduler = new Scheduler();
scheduler.addTask(1000, '1');
scheduler.addTask(500, '2');
scheduler.addTask(300, '3');
scheduler.addTask(400, '4');
scheduler.taskStart();

13. Ajax

const getJSON = function (url) {
    return new Promise((resolve, reject) => {
        const xhr = XMLHttpRequest 
            ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
        xhr.open('GET', url, false);
        xhr.setRequestHeader('Accept', 'application/json');
        xhr.onreadystatechange = function() {
            if (xhr.readyState !== 4) return;
            if (xhr.status === 200 || xhr.status === 304) {
                resolve(xhr.responseText);
            } else {
                reject(new Error(xhr.responseText));
            }
        };
        xhr.send();
    });
}

14. 实现event

function EventEmitter() {
  this.events = new Map();
}

const wrapCallback = (fn, once = false) => ({ callback: fn, once });

EventEmitter.prototype.addListener = function(type, fn, once = false) {
  const handler = this.events.get(type);
  if (!handler) {
    this.events.set(type, wrapCallback(fn, once));
  } else if (handler && handler.callback === 'function') {
    this.events.set(type, [handler, wrapCallback(fn, once)]);
  } else {
    handler.push(wrapCallback(fn, once));
  }
}

EventEmitter.prototype.removeListener = function(type, listener) {
  const handler = this.events.get(type);
  if (!handler) return;
  if (!Array.isArray(this.events)) {
    if (handler.callback === listener.callback) this.events.delete(type);
    else return;
  }
  for (let i = 0; i < handler.length; i++) {
    const item = handler[i];
    if (item.callback === listener.callback) {
      handler.splice(i, 1);
      i--;
      if (handler.length === 1) {
        this.events.set(type, handler[0]);
      }
    }
  }
}


EventEmitter.prototype.once = function (type, listener) {
  this.addListener(type, listener, true);
}

EventEmitter.prototype.emit = function (type, ...args) {
  const handler = this.events.get(type);
  if (!handler) return;
  if (Array.isArray(handler)) {
    handler.forEach(item => {
      item.callback.apply(this, args);
      if (item.once) {
        this.removeListener(type, item);
      }
    });
  } else {
    handler.callback.apply(this, args);
    if (handler.once) {
      this.events.delete(type);
    }
  }
  return true;
}

EventEmitter.prototype.removeAllListeners = function (type) {
  const handler = this.events.get(type);
  if (!handler) return;
  this.events.delete(type);
}

15. 图片懒加载

可以给图片img标签统一自定义属性data-src='default.png',当检测到图片出现在窗口之后再补充src属性,此时才会进行图片资源加载。

function lazyload() {
    const imgs = document.getElementsByTagName('img');
    const len = imgs.length;
    // 视口的高度
    const viewHeight = document.documentElement.clientHeight;
    // 滚动条的高度
    const scrollHeight = 
        document.documentElement.scrollTop || document.body.scrollTop;
    for (let i = 0; i < len; i++) {
        const offsetHeight = imgs[i].offsetTop;
        if (offsetHeight < viewHeight + scrollHeight) {
            const src = imgs[i].dataset.src;
            imgs[i].src = src;
        }
    }
}

document.addEventListener('scroll', lazyload);

16. 高性能渲染数据

合理使用createDocumentFragment和requestAnimationFrame,将操作切分为一小段一小段执行。

setTimeout(() => {
    // 插入十万条数据
    const total = 100000;
    // 一次插入的数据
    const once = 20;
    // 插入数据需要的次数
    const loopCount = Math.ceil(total / once);
    let countOfRender = 0;
    const ul = document.querySelector('ul');
    // 添加数据的方法
    function add() {
        const fragment = document.createDocumentFragment();
        for (let i = 0; i < once; i++) {
            const li = document.createElement('li');
            li.innerText = Math.floor(Math.random() * total);
            fragment.appendChild(li);
        }
        ul.appendChild(fragment);
        countOfRender += 1;
        loop();
    }
    function loop() {
        if (countOfRender < loopCount) {
            window.requestAnimationFrame(add);
        }
    }
    loop();
}, 0)
let ul = document.getElementById('container');
// 十万条数据
let total = 100000;
// 一次加载20条
let once = 20;
// 总共有多少页数据
let page = Math.ceil(total / once);
// 索引的起始值
let index = 0;

function loop(curTotal, curIndex) {
  if (curTotal <= 0) { 
    return false;
  }
  let pageCount = Math.min(curTotal, once);
  window.requestAnimationFrame(function() {
    let fragment = document.createDocumentFragment();
    for (let i = 0; i < pageCount; i++) {
      let li = document.createElement('li');
      li.innerText = curIndex + i + ':' + Math.random() * total;
      fragment.appendChild(li);
    }
    ul.appendChild(fragment);
    loop(total - pageCount, curIndex + pageCount);
  });
}

loop(total, index);

17. 浅比较和深比较

本段代码来自对 React Hooks 与 Immutable 数据流实战 课程的学习。

PureComponent 浅比较部分的核心源码:

function shallowEqual (objA: mixed, objB: mixed): boolean {
    // 下面的 is 相当于 === 的功能,只是对 + 0 和 - 0,以及 NaN 和 NaN 的情况进行了特殊处理
    
    // 第一关:基础数据类型直接比较出结果
    if (is(objA, objB)) {
        return true;
    }
    
    // 第二关:只要有一个不是对象数据类型就返回 false
    if (
        typeof objA !== 'object' || objA === null ||
        typoef objB !== 'object' || objB === null
    ) {
        return false;
    }
    
    // 第三关:在这里已经可以保证两个都是对象数据类型,比较两者的属性数量
    const keysA = Object.keys(objA);
    const keysB = Object.keys(objB);
    if (keys.length !== keysB.length) {
        return false;
    }
    
    // 第四关:比较两者的属性是否相等,值是否相等
    for (let i = 0; i < keys.length; i++) {
        if (
            !hasOwnProperty.call(objB, keysA[i]) ||
            !is(objA[keys[i]], objB[keys[i]])
        ) {
            return false;
        } else {
            // 如果是深比较,加入这个else条件下的代码,递归比较子节点下的对象属性
            if (!deepEqual(objA[keys[i]], objB[keys[i]])) {
                return false;
            }
        }
    }
    return true;
}

浅比较的情况下,属性值为引用类型就无法比较出正确结果了。深比较情况下,如果数据量很大,那么比较的性能会很差。因此,可以引入immutable来解决浅比较下引用类型比对失效的问题。immutable 数据一种利用结构共享形成的持久化数据结构,一旦有部分被修改,那么将会返回一个全新的对象,并且原来相同的节点会直接共享。

参考文章

32个手写JS,巩固你的JS基础(面试高频)

22 道高频 JavaScript 手写面试题及答案

详细的Promise源码实现,再被面试问到轻松解答

「前端进阶」高性能渲染十万条数据(时间分片)

React Hooks 与 Immutable 数据流实战