手写js

80 阅读6分钟

// 来源:https://juejin.cn/post/6855129007852093453

/* 1.实现一个可以拖拽的DIV
<div id="xxx"></div>
*/
var dragging = false;
var position = null;

xx.addEventLisenter('mousedown', e => {
    dragging = true;
    position = [e.clientX, e.clientY]
})

document.addEventLisenter('mousemove', e => {
    if(!dragging) return;
    const currX = e.clientX;
    const currY = e.clientY;
    const diffX = currX - position[0]
    const diffY = currY - position[1]
    const left = parseInt(xx.style.left || 0)
    const top = parseInt(xx.style.top || 0)
    xx.style.left = left + diffX + 'px'
    xx.style.top = top + diffY + 'px'
    position = [currX, currY]
})

document.addEventListener('mouseup', () => {
    dragging = false;
})

/**
 * 2.手写防抖和节流函数
 * 
「节流throttle」,规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。场景👇

scroll滚动事件,每隔特定描述执行回调函数
input输入框,每个特定时间发送请求或是展开下拉列表,(防抖也可以)

节流重在加锁「flag = false」
 * 
 */
function throttle(fn, delay) {
    let flag = true,
        timer = null;
    return function (...args) {
        const that = this;
        if (!flag) return;

        flag = false;
        clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(that, args);
            flag = true;
        }, delay);
    }
}

/**
「防抖debounce」,在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。场景👇

浏览器窗口大小resize避免次数过于频繁
登录,发短信等按钮避免发送多次请求
文本编辑器实时保存

防抖重在清零「clearTimeout(timer)」
 * 
 */

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

/**
 * 3.实现数组去重
 * 
 */

 var array = [1, 1, '1', '1', null, null, 
 undefined, undefined, 
 new String('1'), new String('1'), 
 /a/, /a/,
 NaN, NaN
];

var unique1 = arr => [...new Set(arr)]
var unique2 = arr => arr.filter((item, index, arr) => arr.indexOf(item) === index)
var unique3 = arr => arr.reduce((pre, curr) => pre.includes(curr) ? pre : [...pre, curr], [])

/**
 * 4.实现柯里化函数 & compose
 * 
柯里化就是把接受「多个参数」的函数变换成接受一个「单一参数」的函数,并且返回接受「余下参数」返回结果的一种应用。

思路:
    判断传递的参数是否达到执行函数的fn个数
    没有达到的话,继续返回新的函数,并且返回curry函数传递剩余参数

 */

let currying = (fn, ...args) =>
    fn.length > args.length
    ? (...arguments) => currying(fn, ...args, ...arguments)
    : fn(...args);

// 用例
let addSum = (a, b, c) => a+b+c
let add = curry(addSum)
console.log(add(1)(2)(3))
console.log(add(1, 2)(3))
console.log(add(1,2,3))

// compose
// 函数组合
function compose(...fn) {
    if(!fn.length) return v => v;
    if(fn.length === 1) return fn[0];
    return fn.reduce((pre, curr) => (...args) => pre(curr(...args)))
}

// 用例
// 用法如下:
function fn1(x) {
    return x + 1;
}
function fn2(x) {
    return x + 2;
}
function fn3(x) {
    return x + 3;
}
function fn4(x) {
    return x + 4;
}
const a1 = compose(fn1, fn2, fn3, fn4);
console.log(a1(1)); // 1+4+3+2+1=11


/**
 * 5.实现数组flat
 「将多维度的数组降为一维数组」
 */

const flat = arr => {
    return arr.reduce((pre, curr) => {
        if(Array.isArray(curr)) {
            return [...pre, ...flat(curr)]
        } else {
            return [...pre, curr]
        }
    }, [])
} 

const flat1 = arr => arr.reduce((pre, curr) => pre.concat(Array.isArray(curr) ? flat(curr): curr), [])

const flat2 = (arr, deep = 1) =>
    deep > 0
        ? arr.reduce((pre, curr) => pre.concat(Array.isArray(curr) ? flat(curr) : curr), [])
        : arr.slice();

var arr1 = [1,2,3,[1,2,3,4, [2,3,4]]];
flat(arr1, Infinity);

/**
 * 6.深拷贝
 * 
 深拷贝解决的就是「共用内存地址所导致的数据错乱问题」
 详细解析:https://juejin.cn/post/6844903929705136141

思路:
    递归
    判断类型
    检查环(也叫循环引用)
    需要忽略原型
 */

function deepClone(obj, map = new WeakMap()) {
    if (obj == null || typeof obj !== 'object') return obj;
    if(map.has(obj)) {
        return map.get(obj);
    }
    const constructor = obj.constructor;
    if (/^(Date|RegExp)$/i.test(constructor.name)) {
        return new constructor(obj);
    }
    const constructorObj = new constructor();
    map.set(obj, constructorObj);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            constructorObj[key] = deepClone1(obj[key], map);
        }
    }
    return constructorObj;
}

//测试用例
let obj = {
    a: 1,
    b: {
        c: 2,
        d: 3
    },
    d: new RegExp(/^\s+|\s$/g)
}

let clone_obj = deepClone1(obj)
obj.d = /^\s|[0-9]+$/g
console.log(clone_obj)
console.log(obj)

/**
 * 7.实现一个对象类型的函数
 * 
 * 核心:Object.prototype.toString
 * isType函数👆,也属于「偏函数」的范畴,偏函数实际上是返回了一个包含「预处理参数」的新函数。
 */

const isType = type => obj => Object.prototype.toString.call(obj) === `[object ${type}]`;

let isArray = isType('Array')
let isFunction = isType('Function')
console.log(isArray([1,2,3]),isFunction(Map))

/**
 * 8.手写call、apply、bind
 * 
 * call、apply 改变this指向,唯一区别就是传递参数不同👇
 * 
如果不传入参数,默认指向为 window
将函数设为对象的属性
指定this到函数并传入给定参数执行函数
执行&删除这个函数
 * 
 */

Function.prototype.myCall = function(context, ...args) {
    context = Object(context) || window;
    const fn = Symbol()
    context[fn] = this;
    const res = context[fn](...args)
    delete context[fn]
    return res;
}

Function.prototype.myApply = function(context, args) {
    context = Object(context) || window;
    const fn = Symbol()
    context[fn] = this;
    const res = context[fn](...args);
    delete context[fn]
    return res;
}

//测试用例
let cc = {
    a: 1
}

function demo(x1, x2) {
    console.log(typeof this, this.a, this)
    console.log(x1, x2)
}
demo.apply(cc, [2, 3])
demo.myApply(cc, [2, 3])
demo.call(cc,33,44)
demo.myCall(cc,33,44)

// bind它并不是立马执行函数,而是有一个延迟执行的操作,就是生成了一个新的函数,需要你去执行它👇

Function.prototype.myBind = function(context, ...args) {
    return (...newArgs) => this.call(context, ...args, ...newArgs);
}

// 测试用例
let cc1 = {
    name : 'TianTian'
}
function say(something,other){
    console.log(`I want to tell ${this.name} ${something}`);
    console.log('This is some'+other)
}
let tmp = say.mybind(cc1,'happy','you are kute')
let tmp1 = say.bind(cc1,'happy','you are kute')
tmp()
tmp1()

/**
 * 9.实现new操作
 * 
 核心要点👇

创建一个新对象,这个对象的__proto__要指向构造函数的原型对象
执行构造函数
返回值为object类型则作为new方法的返回值返回,否则返回上述全新对象

 */

function myNew(constructor, ...args) {
    const obj = {};
    obj.__proto__ = constructor.prototype;
    const res = constructor.apply(obj, args)
    if (res && typeof res === 'object' || typeof res === 'function') {
        return res;
    }
    return obj;
}

/**
 * 10.实现instanceof
 * 
 「instanceof」 「运算符」用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

 语法👇
object instanceof constructor  
    object 某个实例对象
    construtor 某个构造函数

原型链的向上找,找到原型的最顶端,也就是Object.prototype,代码👇

 */

function myInstanceof(left, right) {
    if (left === null ||typeof left !== 'object') return false;
    let proto = left.__proto__;
    let prototype = right.prototype;
    while(true) {
        if (proto === null) return false;
        if (proto === prototype) return true;
        proto = proto.__proto__;
    }
}

/**
 * 11.实现sleep
 * 某个时间后就去执行某个函数,使用Promise封装👇
 */

function sleep (fn, delay) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(fn)
        }, delay);
    })
}
// 用例
let saySomething = (name) => console.log(`hello,${name}`)
async function autoPlay() {
    let demo = await sleep(saySomething('TianTian'),1000)
    let demo2 = await sleep(saySomething('李磊'),1000)
    let demo3 = await sleep(saySomething('掘金的好友们'),1000)
}
autoPlay()

/**
 * 12.实现数组reduce
 * 
 */

Array.prototype.myReduce = function(fn, initVal) {
    let res = initVal,
        i = 0;
    if (typeof initVal === 'undefined') {
        res = this[i];
        i++;
    }
    while(i < this.length) {
        res = fn(res, this[i]);
        i++;
    }
    return res;
}

// 用例
const arr = [3, 5, 1, 4, 2];
const a = arr.myReduce((t, v) => t + v);

/**
 * 13.实现promise
 * 上文promise 文件
 * 参考:https://juejin.cn/post/6844904096525189128
 * https://juejin.cn/post/6946022649768181774
 */
function promiseResolve(val) {
    if (val instanceof Promise) {
        return val;
    }
    return new Promise(resolve => resolve(val))
}

function promiseReject(val) {
    return new Promise((resolve, reject) => reject(val));
}

function promiseAll(arr) {
    let index = 0, res = [];
    return new Promise((resolve, reject) => {
        arr.forEach((fn, i) => {
            Promise.resolve(fn).then(data => {
                index++;
                res[i] = data;
                if(index === arr.length) {
                    resolve(res);
                }
            }, err => {
                reject(err);
            })
        });
    })
}

function promiseRace(arr) {
    return new Promise((resolve, reject) => {
        arr.forEach(fn => {
            Promise.resolve(fn).then(val => {
                resolve(val)
            }, err => reject(err))
        })
    })
}
// 待补充
function promiseAny() {}
function promiseAllSettLed() {}

/**
 * 14.手写继承
 * 原理 https://juejin.cn/post/6844903475021627400
 */

// 原型继承

// 构造函数继承

// 组合继承

// 寄生式继承

// class 继承

/**
 * 15.手写一下AJAX & jsonp
 * 参考:https://juejin.cn/post/7033275515880341512
 */

function ajax({
    url = '',
    method = 'GET',
    dataType = 'JSON',
    async = true
}) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open(url, method, async);
        xhr.responseType = dataType;
        xhr.onreadystatechange = function() {
            if(!/^[23]\d{2}$/.test(xhr.status)) return;
            if (xhr.readyState === 4) {
                resolve(xhr.responseText);
            }
        }
        xhr.onerror = function (err) {
            reject(err)
        };
        xhr.send();
    })
}
// jsonp
function jsonp({url, params, callbackName}) {
    const generateUrl = () => {
        let dataSrc = '';
        for(let key in params) {
            if(params.hasOwnProperty(key)) {
                dataSrc += `${key}=${params[key]}&`
            }
        }
        dataSrc += `callback=${callbackName}`;
        return `${url}?${dataSrc}`;
    }
    return new Promise((resolve, reject) => {
        const el = document.createElement('script');
        el.src = generateUrl();
        document.body.appendChild(el);
        window[callbackName] = function(data) {
            resolve(data);
            document.body.removeChild(el);
        }
    })
}


/**
 * 16.用正则实现 trim()
 * 
 * 去掉首位多余的空格👇
 * 
 */

function trim(str) {
    return str.replace(/^\s+|\s+$/g, '');
}

/**
 * 17.实现Object.create方法
 * 
 * 去掉首位多余的空格👇
 * 
 * Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
 * 
 * 参考:https://juejin.cn/post/7018337760687685669
 */

function create(obj, props) {
    function Fn() {}
    Fn.prototype = obj;
    Fn.prototype.constructor = Fn;
    // 支持第二个参数
    if(props) {
        Object.defineProperties(obj, props);
    }
    return new Fn();
}

/**
 * 18.10进制转换
 * 
 * 给定10进制数,转换成[2~16]进制区间数,就是简单模拟一下。
 * 原理:https://juejin.cn/post/7088176672284868621
 * 
 */
function convert(number, base = 2) {
    let rem, res, digits = '0123456789ABCDEF', stack = [];
    while(number) {
        rem = number % base;
        stack.push(rem);
        number = Math.floor(number / base);
    }
    while(stack.length) {
        res += digits[stack.pop()].toString();
    }
    return res;
}

/**
 * 19.数字转字符串千分位
 * 参考:https://juejin.cn/post/7201838656894746683
 */
function thoundth(num) {
    let [int, faction] = String(num).split('.');
    const intRes = int.replace(/(?=\B(\d{3})+$)/g, ',');
    return `${intRes}.${faction}`;
}

// 参考:https://juejin.cn/post/7201838656894746683

/**
 * 20.写版本号排序的方法
 * 
 * 题目描述:有一组版本号如下['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']。
 * 排序的结果为 ['4.3.5','4.3.4.5','2.3.3','0.302.1','0.1.1']
 */

function versionSort(arr) {
    return arr.sort((a, b) => {
        let i = 0;
        // 从高到低
        const arr1 = a.split('.');
        const arr2 = b.split('.');

        while(true) {
            const s1 = arr1[i];
            const s2 = arr2[i];
            i++;
            if (s1 === undefined || s2 === undefined) {
                return arr1.length - arr2.length;
            }
            if (s1 === s2) {
                continue;
            }
            return s1 - s2;
        }
    })
}

/**
 * 21.请实现 DOM2JSON 一个函数,可以把一个 DOM 节点输出 JSON 的格式
 * 
题目描述:
<div>
  <span>
    <a></a>
  </span>
  <span>
    <a></a>
    <a></a>
  </span>
</div>

把上诉dom结构转成下面的JSON格式

{
  tag: 'DIV',
  children: [
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] }
      ]
    },
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] },
        { tag: 'A', children: [] }
      ]
    }
  ]
}
 */

function dom2Json(dom) {
    let obj = {};
    obj.tag = dom.tagName;
    obj.children = [];
    dom.childNodes.forEach(child => obj.children.push(dom2Json(child)));
    return obj;
}

/**
 * 22.将虚拟 Dom 转化为真实 Dom
 * 题目描述:JSON 格式的虚拟 Dom 怎么转换成真实 Dom
 * 
 {
  tag: 'DIV',
  attrs:{
  id:'app'
  },
  children: [
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] }
      ]
    },
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] },
        { tag: 'A', children: [] }
      ]
    }
  ]
}
把上诉虚拟Dom转化成下方真实Dom
<div id="app">
  <span>
    <a></a>
  </span>
  <span>
    <a></a>
    <a></a>
  </span>
</div>
 */

function _render (vnode) {
    if (typeof vnode === 'number') {
        vnode = String(vnode)
    }
    if (typeof vnode === 'string') {
        return document.createTextNode(vnode);
    }
    const dom = document.createElement(vnode.tag);
    if (vnode.attrs) {
        Object.keys(vnode.attrs).forEach(key => {
            dom.setAttribute(key, vnode.attrs[key]);
        })
    }
    if (vnode.children) {
        vnode.children.forEach(child => {
            dom.appendChild(_render(child));
        })
    }
    return dom;
}

/**
 * 23.实现模板字符串解析功能
 * 
 题目描述:
    let template = '我是{{name}},年龄{{age}},性别{{sex}}';
    let data = {
        name: '姓名',
        age: 18
    }
    render(template, data); // 我是姓名,年龄18,性别undefined
 */

function render(tem, data) {
    return tem.replace(/\{\{(\w+)\}\}/g, (match, key) => {
        return data[key];
    })
}

/**
 * 24.列表转成树形结构
 * 
 题目描述:
[
    {
        id: 1,
        text: '节点1',
        parentId: 0 //这里用0表示为顶级节点
    },
    {
        id: 2,
        text: '节点1_1',
        parentId: 1 //通过这个字段来确定子父级
    }
    ...
]

转成
[
    {
        id: 1,
        text: '节点1',
        parentId: 0,
        children: [
            {
                id:2,
                text: '节点1_1',
                parentId:1
            }
        ]
    }
]
*/

function list2Tree (list) {
    const temp = {};
    const res = [];
    // 打平id 方便索引
    list.forEach(item => {
        temp[item.id] = item;
    });

    for(let i in temp) {
        const curr = temp[i];
        if (curr.parentId !== 0) {
            if(!temp[curr.parentId].children) {
                temp[curr.parentId].children = [];
            }
            temp[curr.parentId].children.push(curr);
        // 根节点
        } else {
            res.push(temp[i]);
        }
    }
    return res;
}

/**
 * 25.树形结构转成列表
 * 题目描述:
[
    {
        id: 1,
        text: '节点1',
        parentId: 0,
        children: [
            {
                id:2,
                text: '节点1_1',
                parentId:1
            }
        ]
    }
]
转成
[
    {
        id: 1,
        text: '节点1',
        parentId: 0 //这里用0表示为顶级节点
    },
    {
        id: 2,
        text: '节点1_1',
        parentId: 1 //通过这个字段来确定子父级
    }
    ...
]
 */
function tree2List(tree) {
    const res = [];
    function dfs (data) {
        data.forEach(item => {
            if(item.children) {
                dfs(item.children)
                delete item.children;
            }
            res.push(item);
        })
    }
    dfs(tree);
    return res;
}

/**
 * 26.类数组转化为数组的方法
 * 
 */
const arrayLike = document.querySelectorAll('div')

// 1.扩展运算符
const arrayLike1 = [...arrayLike]
// 2.Array.from
Array.from(arrayLike)
// 3.Array.prototype.slice
Array.prototype.slice.call(arrayLike)
// 4.Array.apply
Array.apply(null, arrayLike)
// 5.Array.prototype.concat
Array.prototype.concat.apply([], arrayLike)

/**
 * 27.发布订阅模式
 * 
题目描述:实现一个发布订阅模式拥有 on emit once off 方法
 * 
 */

class EventEmitter {
    constructor() {
        this.events = {};
    }
    on(type, callback) {
        if (!this.events[type]) {
            this.events[type] = [];
        }
        this.event[type].push(callback);
    }
    emit(type, ...args) {
        this.events[type]
        && this.events[type].forEach(fn => {
            fn.call(this, ...args);
        })
    }
    off(type, callback) {
        if (this.events[type]) {
            this.events[type] = this.events[type].filter(item => item !== callback)
        }
    }
    once(type, callback) {
        function fn(...args) {
            callback(...args);
            this.off(type, fn)
        }
        this.on(type, fn);
    }
}

// 测试用例
const event = new EventEmitter();

const handle = (...rest) => {
  console.log(rest);
};

event.on("click", handle);

event.emit("click", 1, 2, 3, 4);

event.off("click", handle);

event.emit("click", 1, 2);

event.once("dbClick", () => {
  console.log(123456);
});
event.emit("dbClick");
event.emit("dbClick");

/**
 * 28.字符串转化为驼峰
 * 
foo Bar => fooBar
foo-bar---- => fooBar
foo_bar__ => fooBar
*/

function camelCase(str) {
    return str.replace(/[-_\s]+(.)?/g, function(match, char) {
        return char ? char.toUpperCase() : '';
    })
}

/**
 * 29.解析 url 参数
 * 
就是提出 url 里的参数并转成对象
参考: https://juejin.cn/post/6996289669851774984
*/

function getUrlParams(url) {
    let obj = {};
    url.replace(/([^?&])=([^?&])/g, function(match, $1, $2) {
        obj[$1] = $2;
    });
    return obj;
}

/**
 * 29.JavaScript 实现异步任务调度器
 * 
题目要求
最近遇到了一个 JavaScript 手写代码题,要求实现一个具有并发数量限制的异步任务调度器,可以规定最大同时运行的任务。

参考: https://juejin.cn/post/7049231428294279199
https://juejin.cn/post/7018337760687685669
*/
// 延迟函数
const sleep = time => new Promise(resolve => setTimeout(resolve, time));

// 同时进行的任务最多2个
const scheduler = new Scheduler(2);

// 添加异步任务
// time: 任务执行的时间
// val: 参数
const addTask = (time, val) => {
    scheduler.add(() => {
        return sleep(time).then(() => console.log(val));
    });
};

addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
// 2
// 3
// 1
// 4

class Scheduler {
    constructor(max) {
        this.max = max;
        this.count = 0;
        this.queue = [];
    }

    add(fn) {
        this.queue.push(fn);
        this.runQueue();
    }

    runQueue() {
        if (this.queue.length && this.count < this.max) {
            const fn = this.queue.shift();
            this.count += 1;
            fn().then(() => {
                this.count -= 1;
                this.runQueue();
            })
        }
    }
}

/**
 * 30.手写LRU算法
 * 
就是提出 url 里的参数并转成对象
参考: https://juejin.cn/post/6968713283884974088
https://juejin.cn/post/7018337760687685669
*/

class LRUCache {
    constructor(capacity) {
        this.capacity = capacity;
        // 维护顺序
        this.map = new Map();
    }

    get(key) {
        if(this.map.has(key)) {
            const temp = this.map.get(key);
            this.map.delete(key);
            this.map.set(key, temp);
            return temp;
        } else {
            return -1;
        }
    }

    put(key, value) {
        if(this.map.has(key)) {
            this.map.delete(key);
        } else if (this.map.size >= this.capacity) {
           this.map.delete(this.map.keys().next().value)
        }
        this.map.set(key, value);
    }
}

// 用例
let cache = new LRUCache(2);
cache.put(1, 1);
cache.put(2, 2);
console.log("cache.get(1)", cache.get(1))// 返回  1
cache.put(3, 3);// 该操作会使得密钥 2 作废
console.log("cache.get(2)", cache.get(2))// 返回 -1 (未找到)
cache.put(4, 4);// 该操作会使得密钥 1 作废
console.log("cache.get(1)", cache.get(1))// 返回 -1 (未找到)
console.log("cache.get(3)", cache.get(3))// 返回  3
console.log("cache.get(4)", cache.get(4))// 返回  4

/**
 * 30.染百万条结构简单的大数据时 怎么使用分片思想优化渲染
 * 
*/

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)
// test
setTimeout(() => {
    const totle = 100000;
    const once = 20;
    const loopCount = Math.ceil(totle / once);
    let renderCount = 0;
    const ul = document.querySelector('ul');
    function add() {
        const fragment = document.createDocumentFragment();
        for(let i = o; i< once; i++) {
            const li = document.createElement('li');
            li.innerhTMl = Math.floor(Math.random() * total);
            fragment.appendChild(li);
        }
        ul.appendChild(fragment);
        renderCount += 1;
        loop();
    }
    function loop() {
        if(renderCount < loopCount) {
            window.requestAnimationFrame(add)
        }
    }
    loop();
}, 0);