前端学习笔记-手写篇

274 阅读10分钟

1、数组相关

1-1、生成一个从1到n的顺序数组,并打乱顺序

function creNumArr(n) {
    let arr = [], i = 0;
    while (i < n) {
        arr.push(++i)
    }
    return arr.sort(() => Math.random() > 0.5 ? 1 : -1);
};

1-2、生成一个长度为n,值范围为1-max的数组,可以有重复值

function creNumArr2(n, max) {
    return Array.from(
        { length: n },
        v => parseInt((Math.random() * max).toFixed())
    )
}

或者

function creNumArr2(n, max) {
    return [...new Array(n)].map(v => parseInt((Math.random() * max).toFixed()))
}

注意,new Array(10)创建的是一个稀疏数组,对于稀疏数组 mapfilterforEach 等方法无效果。如

let a = new Array(10).map(v => 1);
console.log(a[0]);//undefined

1-3、统计数组中的每个元素出现的次数

let b = creNumArr2(5, 100); //长度为5,值为1-100随机的一个数组
let c = b.reduce((result, v) => {
    if (result.has(v)) {
        let _d = result.get(v);
        result.set(v, ++_d);
    } else {
        result.set(v, 1);
    }
    return result;
}, new Map());
console.log(c);

1-4、数组去重

es6的方法,里用Set值的唯一性

Array.from(new Set(arr));
[...new Set(arr)];//或者用扩展运算符

es5的方法

a.filter((v, i, arr) => arr.indexOf(v) === i);

1-5、求两个数组的并集

let a = new Set([1, 2, 3])
let b = new Set([4, 3, 2])
let union = new Set([...a, ...b])

1-6、类数组转为数组

es6方法

const arr = [...fakeArray];
const arr2 = Array.from(fakeArray);//或者

es5方法

const arr = Array.prototype.slice.call(fakeArray)

1-7、数组扁平化

概念:将一个[1,2,[3,[4,5]]]数组转为[1,2,3,4,5]的形式就是扁平化。

使用flat

const newArr = arr.flat(Infinity)

使用递归

const flatArr = function f(arr) {
    const newArr = [],
        len = arr.length;
    for(let i =0;i<len;i++){
        if(Array.isArray(arr[i])){
            newArr.push(...f(arr[i]));
        }else{
            newArr.push(arr[i]);
        }
    }
    return newArr;
}

使用while循环

const flatArr = arr => {
    while(arr.some(v => Array.isArray(v))){
        arr = [].concat(...arr);
        //关键在于contact里边,如果参数是数组,那么添加的是数组中的元素,而不是这个数组
    }
}

1-8、从数组里随机取出4个值拼接成字符串

const arr = [1,3,'t','i','o',9].sort(() => Math.random() > 0.5 ? 1 : -1);
const str = arr.join('').slice(0,4);
console.log(str);

2、一些简单的算法

2-1、冒泡排序

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

2-2、选择排序

function selectAort(arr) {
    let _len = arr.length;
    for (let i = 0; i < _len; i++) {
        let min = i;
        for (let j = i + 1; j < _len; j++) {
            if (arr[j] < arr[min]) {
                min = j;
            }
        }
        if (min !== i) {
            [arr[min], arr[i]] = [arr[i], arr[min]];
        }
    }
}

2-3、快速排序

const quikcSort = function f(arr) {
    const _len = arr.length;
    if (_len <= 1) {
        return arr;
    } else {
        let leftArr = [], rightArr = [];
        for (let i = 1; i < _len; i++) {
            if (arr[i] > arr[0]) {
                rightArr.push(arr[i])
            } else {
                leftArr.push(arr[i])
            }
        }
        return [...f(leftArr), arr[0], ...f(rightArr)]
    }
}

3、函数相关

3-1、函数防抖

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。主要应用:

  • search搜索联想,用户在不断输入值时,用防抖来节约请求资源;
  • window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
function debounce(fn, wait = 500) {
    let timer;
    return function() {
        timer && clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, arguments)
        }, wait)
    }
}

3-2、函数节流

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。主要应用:

  • 鼠标不断点击触发,mousedown(单位时间内只触发一次);
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
function throttle(fn, wait = 500) {
    let prev = Date.now();
    return function() {
        const now = Date.now();
        if (now - prev > wait) {
            fn.apply(this, arguments);
            prev = Date.now();
        }
    }
}

3-3、函数柯里化和反柯里化

f(1,2,3)改写成f(1)(2)(3)的形式称为柯里化。反过来,将f(1)(2)(3)改写成f(1,2,3)称为反柯里化

const curry = fn => {
    const judge = (...args) => {
        if (args.length === fn.length){
            return fn(...args);
        } 
        return (...arg) => judge(...args, ...arg);
    }
    return judge;
}

3-4、偏函数

偏函数就是将一个有多个参数的函数,固定其中几个参数,剩余参数作为参数传入的一个新函数。 偏函数和函数柯里化都是闭包的一种应用。

const partial = (fn, ...args) => {
    return (...arg) => {
        return fn(...args, ...arg);
    };
}

3-5、蹦床函数

蹦床函数是为了解决递归函数可能存在的栈溢出问题,原理是使用while循环把递归改成迭代循环的形式。可以理解用用空间换取时间,会比正常的递归慢,但慢也比溢出好。

栈溢出的错误提示为: //Uncaught RangeError: Maximum call stack size exceeded 调用栈溢出了,出现死循环调用,或者大量的递归调用。

解决栈溢出(函数调用栈)的方法:

  1. 采用尾调用优化的写法,但只在严格模式下生效。
  2. 采用蹦床函数,其本质是递归改成了迭代循环的形式。 如果要实现一个1到100的累加,普通的递归为:
const add = function f(v) {
    if (v < 1) {
        return v;
    } else {
        return v + f(v - 1);
    }
}

尾调用优化的形式为:

const add = function f(v, m = 0) {
    if(v === 1){
        return v+m;
    }else{
        return f(null, v-1, v + m);
    }
}

使用蹦床函数为:

//蹦床函数
const trampoline = f => (...args) => {
    let result = f(...args);
    while (typeof result === 'function') {
        result = result();
    }
    return result;
}

const add = function f(v, m = 0) {
    if(v === 1){
        return v+m;
    }else{
        return f.bind(null, v-1, v + m);
        //用bind的方法返回一个新的函数
    }
}

const add2 = trampoline(add);//用蹦床函数,得到一个迭代循环形式的函数

4、call、apply、bind

4-1、实现call

Function.prototype.myCall = function(asThis, ...arg) {
    if (asThis === undefined || asThis === null) {
        asThis = window;
    } else {
        asThis = new Object(asThis);
    }
    let FN = Symbol('FN');
    asThis[FN] = this;
    let result = asThis[FN](...arg);
    delete asThis[FN];
    return result;
}

4-1、实现bind

Function.prototype.myBind = function(asThis, ...arg) {
    let _this = this;
    if (typeof _this !== 'function') {
        throw new Error('not a function');
    }
    let newFn = function() {
        return _this.apply(
            newFn.prototype.isPrototypeOf(this) ? this : asThis,
            [...arg, ...arguments]
        )
    }
    newFn.prototype = _this.prototype;
    return newFn;
}

也经常看到这样的处理

var fNOP = function () {};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();

认为是为了防止原型链篡改。但这样模拟实现的bind和标准的bind是有差异的,具体参考这篇文章 连续bind返回值的个人理解

5、浅拷贝和深拷贝

5-1、概念区分

赋值:

  • 当我们复制引用类型的变量时,实际上复制的是栈中存储的地址。创建一个对象的地址的引用就是赋值。

浅拷贝

  • 创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
  • 扩展运算符对对象实例的拷贝属于一种浅拷贝

深拷贝

  • 将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

循环引用

  • 即对象的属性间接或直接的引用了自身的情况。
  • 解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。 这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构。JSON.parse(JSON.stringify(a))在处理重复引用是会报错

5-2、浅拷贝

function shallowCopy(target) {
    let cloneTarget = {};
    for (const key in target) {
        if (Object.prototype.hasOwnProperty.call(target, key)) {
            cloneTarget[key] = target[key];
        }
    }
    return cloneTarget;
};

5-3、深拷贝

5-3-1、乞丐版

JSON.parse(JSON.stringify(a));

缺点:

  1. 不支持undefined(支持null);
  2. 不支持循环引用,会报错
  3. 不支持Date,会变成 ISO8601 格式的字符串。不支持正则表达式
  4. 不支持函数

5-3-2、支持数组,普通对象,解决重复引用

function clone(target, map = new WeakMap()) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        if (map.has(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);
        for (const key in target) {
            //for in会遍历出原型链上所有可枚举的属性
            cloneTarget[key] = clone(target[key], map);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

5-3-3、支持Date、RegExp、Error类型

function deepClone(o, map = new WeakMap()) {
    if (o && typeof o === 'object') {
        let typeArr = ['[object Object]', '[object Array]', '[object Date]', 
        '[object RegExp]', '[object Error]', '[object Set]', '[object Map]'],
            t = Object.prototype.toString.call(o);
        if (~typeArr.indexOf(t)) {
            if (map.has(o)) {
                return map.get(o); //防止循环引用
            }
            let tempObj = new o.__proto__.constructor();
            map.set(o, tempObj);//tempObj的内容在后边会修改,这里是一个引用
            //如果是map.set(o, o);则会引用本身,会出错
            if (t === '[object Date]') {
                return new Date(o); //Date类型
            }
            if (t === '[object RegExp]') {
                return new RegExp(o); //RegExp类型
            }
            if (t === '[object Error]') {
                return new Error(o); //Error类型
            }
            if (t === '[object Set]') {
                //Set类型
                o.forEach(v => {
                        tempObj.add(deepClone(v, map));
                });
                return tempObj;
            }
            if (t === '[object Map]') {
                //Map类型
                o.forEach(v => {
                        tempObj.set(deepClone(v, map));
                });
                return tempObj;
            }
            //数组,普通对象和其他类型
            for (let key in o) {
                if (Object.prototype.hasOwnProperty.call(o, key)) {
                    tempObj[key] = deepClone(o[key], map);
                }
            }
            return tempObj;
        } else {
            return o;//如果是其他类型,直接返回
        }
    } else {
        return o;//函数、null和基本类型的值直接返回
    }
}

函数没必要复制,两个对象使用一个在内存中处于同一个地址的函数也是没有任何问题的。如果一定要拷贝一个函数,可以采用eval的方法。

5-3-4while循环展开

所有的递归写法都可以用while展开成迭代循环的形式。这样可以避免可能存在的递归爆栈问题。但执行速度上会慢一些。理解为用空间换时间

5-3-5用MessageChannel实现深拷贝

let obj = {
    a: 1,
    b: {
        c: 2,
        d: 3,
    },
    f: undefined
}
obj.c = obj.b;
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c

function deepCopy(obj) {
    return new Promise((resolve) => {
        const { port1, port2 } = new MessageChannel();
        port2.onmessage = ev => resolve(ev.data);
        port1.postMessage(obj);
    });
}

deepCopy(obj).then((copy) => { // 请记住`MessageChannel`是异步的这个前提!
    let copyObj = copy;
    console.log(copyObj, obj)
});
  • MessageChannel拷贝的原理和web worker的postMessage一致
  • 可以复制undefined,
  • 可以解决循环引用,
  • 是异步的

5-3-6特殊例子的深拷贝

如果是一个所有元素都为基本数据类型的数组,可以如下操作

let a = [1,7,8,2,0];
let b = a.slice();//浅拷贝,但这里有深拷贝的效果
let c = [...a];//浅拷贝

如果是一个所有值都为基本数据类型的对象,可以有如下操作

let a = {
    aa: 1,
    bb: 2
}
let b = Object.assign({}, a);//浅拷贝,但这里有深拷贝的效果
let c = {...a};//浅拷贝

6、new操作

6-1、new操作的过程

要创建 Person 的实例,应使用 new 操作符。以这种方式调用构造函数会执行如下操作。

  1. 在内存中创建一个新对象。
  2. 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。
  3. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数复杂数据类型(或者叫引用类型),则返回该对象;否则,返回刚创建的新对象。

6-2、手写实现

function myNew() {
    // 1、获得构造函数,同时删除 arguments 中第一个参数
    const Con = Array.prototype.shift.call(arguments);
    // 2、创建一个空的对象并链接到原型
    const obj = {};
    Object.setPrototypeOf(obj, Con.prototype);
    //或者pbj.__proto__ = Con.prototype;//但是这个效率低一些
    //或者const obj = Object.create(Con.prototype);
    // 3、绑定 this 实现继承,obj 可以访问到构造函数中的属性
    const res = Con.apply(obj, arguments)
    // 4、优先返回构造函数返回的对象
    return res instanceof Object ? res : obj;
}

__proto__ 属性在 ES6 时才被标准化,以确保 Web 浏览器的兼容性,但是不推荐使用,除了标准化的原因之外还有性能问题。为了更好的支持,推荐使用 Object.getPrototypeOf()

  • 获取对象的[[Prototype]]推荐使用Object.getPrototypeOf()Reflect.getPrototypeOf();
  • 设置对象的[[Prototype]]推荐使用Object.setPrototypeOf()Reflect.setPrototypeOf();

7、实现一个instanceof

比较对象的__proto__和目标的prototype是否相等,如果不相等,则沿着原型链往上层查找进行匹配。

const myInstanceof = (o1, o2) => {
    let proto = o1.__proto__;
    while(proto !== o2.prototype){
        if(proto){
            proto = proto.__proto__;
        }else{
            return false;
        }
    }
    return true;
}

8、实现简单的promise

class myPromise {
    constructor(executor) {
        this.status = "pending"; // 默认promise状态
        this.value; // resolve成功时的值
        this.resolveQueue = []; // 成功时回调队列
        const resolve = value => {
            if (this.status === "pending") {
                this.value = value;
                this.status = "fulfilled";
                this.resolveQueue.forEach(fn => fn())
            }
        }
        executor(resolve);
    }
    then(onFullfilled) {
        return new myPromise((resolve) => {
            setTimeout(() => {// 异步
                const res = onFullfilled(this.value);
                if(res instanceof myPromise){
                    res.then(resolve)
                }else{
                    resolve(res);
                }
            }, 0)
        });
    }
}

9、发布订阅者模式

暂留

10、用setTimeout实现setInterval

setInterval()的特性,在一个任务结束和下一个任务开始之间的时间间隔是无法保证的。有些循环定时任务可能会因此而被跳过。可以用setTimeout来代替

暂留