JS基础之一步步手写系列

134 阅读12分钟

写这篇文章的目的是为了更好的鞭策自己学习JS底层一些函数的实现方式,写的内容基于自己的学习理解可能有的地方会有问题,还请大家指正

手写JS常用方法

1.手写deepClone

首先理解什么是深克隆,我觉得这一点没必要说的太复杂,既然是克隆其实都一样,本质上大家都没有区别,都是将自己的数据做了一份拷贝给新的数据,只是因为js中引用类型拷贝时为了性能默认拷贝的是他的地址,不会再走一步做更深层次的数据拷贝罢了,所以我们的核心问题就是把这些数据在内存中开辟新的空间进行真正的克隆

手写前先想清楚自己要实现的效果,从简到难,把用例写出来:

//1-Q
let obj={
    a:1
}
let newClone=deepClone(obj);

首先要了解我们要处理的引用类型,首先粗略的用typeof将引用类型和普通数据类型做划分

// 1-A
function deepClone(obj){
        //这里是粗略的判断,事实上这种判断方式只能针对基本的Object和Array,我们慢慢完善
    if(typeof obj==='object'){
        
    }else{
        // 非引用类型直接返回
        return obj;
    }
}

我们需要做的就是遍历对象obj里的属性并一一赋值给新的对象即可

// 1.2-A
function deepClone(obj){
    if(typeof obj==='object'){
        let newClone={};
        for(let key in obj){
            newClone[key]=obj[key];
        }
        return newClone;
    }else{
        return obj;
    }
}

这样这个函数就能拷贝基本的单层obj了,然而他当然有很多问题,首先要解决的是如果属性本身也是一个obj该怎么办 于是我们完善用例:

// 2-Q
let obj={
    a:1,
    //增加一个对象属性
    b:{bb:2}
}
let newClone=deepClone(obj);

因为我们的函数的功能就是返回当前对象的深拷贝,所以这里的对象属性要想也是深拷贝的则递归调用即可

//2-A
function deepClone(obj){
    if(typeof obj==='object'){
        let newClone={};
        for(let key in obj){
            newClone[key]=deepClone(obj[key]);
        }
        return newClone;
    }else{
        return obj;
    }
}

现在我们的函数可以处理本身属性也是Object的类型了,不过我们要接着完善用例,考虑上数组这种引用类型:

如果深拷贝的对象里面有数组,甚至数组里有数组,对象里有数组,数组里有对象,或者深拷贝的本身就是数组怎么办

// 3-Q
let obj={
    a:1,
    b:{bb:2},
    //增加数组
    c:[3],
    d:[[4],5]
}
//或者参数本身就是数组
let arr=[
    {a:1},
    {b:[2]}
]

这些情况其实统一成一种情况即可,因为当我们加上单数组的深拷贝,那么由于这个函数的功能是深拷贝数组和对象,所以只需要和上面一样递归调用该函数即可将深层的值(无论是数组还是对象)

由于现在处理的值不再只是对象了,我们把参数的命名改为target,同时通过数组的方法判断一下; 在这种不是很严格的模式中,我们认为如果你的typeof是object的情况下,不是数组就是对象 同时这里要补充一句,由于for in的遍历模式既可以遍历对象也可以遍历数组,所以这里遍历那块暂时不用改

// 3-A
function deepClone(target){
    if(typeof target==='object'){.
        //判断一下类型
        let newClone=Array.isArray(target)?{}:[];
        for(let key in target){
            newClone[key]=deepClone(target[key]);
        }
        return newClone;
    }else{
        return target;
    }
}

我们继续完善,这时面试官会告诉你这种情况存在循环引用,我们完善一下用例:

// 4-Q
let obj={
    a:1,
    b:{bb:2},
    c:[3],
    d:[[4],5]
}
//自己的一个自己属性是自己
obj.obj=obj;

这种情况下我们需要引入一个map作为参数,他的作用是每次深拷贝一个属性就将当前属性作为key,新克隆出的newClone作为value存入map中,当要克隆一个属性时判断当前的属性在map中是否存在,存在则表明其已经克隆过,则直接返回其value即可

//4-A
function deepClone(target,map=new Map()){
    if(typeof target==='object'){
    // 存在则表明其已经克隆过,则直接返回其value
        if(map.has(target)){
            return map.get(target);
        }else{
            let newClone=Array.isArray(target)?{}:[];
 //否则将其放入map,这里可以现在就放的原因是newClone是引用类型,放的是内存地址,所以现在即使没有值也可以先放
            map.set(target,newClone)
            for(let key in target){
                newClone[key]=deepClone(target[key]);
            }
            return newClone;
        }
    }else{
        return target;
    }
}

当然,这里我们可以将Map优化换成WeakMap,原因是WeakMap的特性在这里十分适用,WeakMap中key必须为对象类型且对其key为弱引用不增加引用计数,这意味着一旦其他地方对其的引用计数全部结束变为0,则其会自动执行垃圾回收机制,而你用普通的map则不会,普通的map会一直引用这个对象增加引用计数直到你销毁他且其他地方均不引用;

//4.2-A
// 只改变参数类型即可
function deepClone(target,map=new WeakMap()){
    if(typeof target==='object'){
        if(map.has(target)){
            return map.get(target);
        }else{
            let newClone=Array.isArray(target)?{}:[];
            map.set(target,newClone)
            for(let key in target){
                newClone[key]=deepClone(target[key]);
            }
            return newClone;
        }
    }else{
        return target;
    }
}

我们继续优化,这里我们对引用类型的判断过于草率与不准确,我们都知道typeof存在的问题

typeof null === 'object'  //如果使用typeof则null被判断为引用类型
typeof function(){} ==='function' //如果使用typeof则本来为引用类型的函数被判断为非引用类型

所以我们需要补充一个工具函数来判断是否是引用类型

// 判断是否为引用类型
function isQuote(target){
    if(typeof target==='function'){
        return true
    }else if(typeof target==='object'&&target!=null){
        return true
    }else{
        return false;
    }
}

接着我们完善优化一下下面的代码,因为对各种引用全用for in循环会造成下面几个问题:

  • 性能差,遍历速度慢;
  • for in会不止遍历对象自身的属性,还会遍历原型上属性;
  • for in只遍历可枚举属性

再者我们要想判断某种数据的真正类型,最佳的方式是使用toString方法

Object.prototype.toString.call(target)

image.png

所以接下来的代码是对这些方面做优化:

我们其实只需要先关注引用类型中属性是否可以遍历

1.如果是不可遍历的那种属性我们直接返回一个新的对象即可 2.如果是可遍历的属性我们遍历以后循环调用即可(和Array一样)

时间有限,我这里就只对上述情况中的2中的Map和Set多做处理,如果想实现较为完备的deepClone可以参考我引用的文章或者直接参考lodash库

首先完善我们的工具类:

// 和上面的isObject是一样的,只是缩写了
function isQuote(target){
    return target!==null && (typeof target==='function'||typeof target === 'object')
}
//这里只判断了这四种引用类型,其实还有很多可以去继续处理
function getType(target){
    switch (Object.prototype.toString.call(target)){
        case '[object Object]':
            return 'object'
        case '[object Array]':
            return 'array'
        case '[object Map]':
            return 'map'
        case '[object Set]':
            return 'set'
        default:
            return 'others'
    }
}

然后是我们的deepClone函数:

function deepClone(target,map=new WeakMap()){
    if(isQuote(target)){
        if(map.has(target)){
            return map.get(target);
        }
        if('Array'===getType(target)){
        //如果是数组的话,我们可以使用数组的forEach方法遍历
            let newClone=new Array();
            map.set(target,newClone);
            target.forEach((value,index)=>{
                newClone[index]=deepClone(value,map);
            })
            return newClone;
        }else if('Object'===getType(target)){
        //如果是对象的话,我们可以使用对象的的Object.keys()结合数组的map方法遍历(遍历方式很多,这里只是想尽量不重复)
            let newClone=new Object();
            map.set(target,newClone);
            Object.keys().map((indexValue)=>{
                newClone[indexValue]=deepClone(target[indexValue],map);
            })
            return newClone;
        }else if('Set'===getType(target)){
        //如果是Set的话,我们可以使用for of方法遍历
            let newClone=new Set();
            map.set(target,newClone);
            for(let value of target){
                newClone.set(deepClone(value));
            }
            return newClone;
        }else if('Map'===getType(target)){
            let newClone=new Map();
            map.set(target,newClone);
            //如果是Map的话,我们可以使用for of结合Map的entries方法遍历
            for(let keyWithValue of target.entries()){
                map.set(keyWithValue[0],deepClone(keyWithValue[1]));
            }
            return newClone;
        }else {
            throw new Error("该类型暂未处理")//这里需要继续完善你所想实现的深克隆数据类型
        }
    }else{
    // 这里还可以处理一些例如Symbol等特殊类型
        return target;
    }
}

一些参考链接 github.com/lodash/loda…

juejin.cn/post/684490…

手写call,apply,bind

call

首先我们来手写call,还是一样以用例为主导,先看一下call的用法

// 1-Q
let obj={
    a:1
}
function fn(){
    console.log(this.a);
}
fn.call(obj);// 1

call的用法就是将一个函数的this绑定到你想要绑定的对象中,然后执行这个函数 我们先来实现这个功能

我们的思路是这样,既然你要我这个函数this绑定到对象中,那我直接把这个函数作为一个属性放到你对象中,这样this不就是这个对象了嘛,然后我们调用新加进对象的这个函数,调用完以后删除就好了

所以我们来写相关代码

// 1-A
Function.prototype.call2=function (obj){
    // 首先我们获取调用call的函数
    let fn=this;
    // 然后将这个函数作为属性f放入对象中
    obj.f=fn;
    // 然后调用这个函数
    obj.f();
    // 最后删除他
    delete obj.f;
}

其实到这里call的核心思想我们已经完成了,剩下的就是完善call的用法,我们补充用例

// 2-Q
let obj={
    a:1
}
function fn(val){
    console.log(val)
    console.log(this.a);
}
fn.call(obj,1);

即call是可以在后面传参数的,并且参数是以逗号连接的,最后会传到fn的参数中 所以这里我们需要适配参数这样一个功能

// 2-A
Function.prototype.call2=function (obj){
    // 首先我们获取调用call的函数
    let fn=this;
    // 然后将这个函数放入对象中
    obj.f=fn;
    // 获取参数中第二个及以后的参数作为函数的参数
    let args=[...arguments].slice(1);
    // 然后调用这个函数并传入参数
    obj.f(args);
    // 最后删除他
    delete obj.f;
}

我们接着完善用例

// 3-Q
let a=2;
function fn(val){
    console.log(val)
    console.log(this.a);
}
fn.call(null,1); // 1 2

js是允许call这边传参绑定对象为null,且在非严格模式下当绑定的是null时默认会变成全局的window对象 所以这里我们加一重判断

// 3-A
Function.prototype.call2=function (obj){
    // 首先我们获取调用call的函数
    let fn=this;
    //如果obj为null则绑定window
    obj=obj || window;
    // 然后将这个函数放入对象中
    obj.f=fn;
    // 获取参数中第二个及以后的参数作为函数的参数
    let args=[...arguments].slice(1);
    // 然后调用这个函数
    obj.f(args);
    // 最后删除他
    delete obj.f;
}

我们继续完善用例:

// 4-Q
let obj={
    a:1
}
let a=2;
function fn(val){
    console.log(val)
    console.log(this.a);
    return 'result'
}
let result=fn.call(obj,1); // 1 1
console.log(result);// result
obj.call(obj)// TypeError: obj.call is not a function

可以看到我们的call是会接收函数的返回值作为自己的返回值的,同时我们的call只能对函数使用,其他对象不行;我们接着来完善代码

// 4-A
Function.prototype.call2=function (obj){
    // 首先我们获取调用call的函数
    let fn=this;
    // 如果调用的对象不是function,则抛出异常
    if(typeof fn!=='function'){
        throw new Error('Type error');
    }
    //如果obj为null则绑定window
    obj=obj || window;
    // 然后将这个函数放入对象中
    obj.f=fn;
    // 获取参数中第二个及以后的参数作为函数的参数
    let args=[...arguments].slice(1);
    // 然后调用这个函数
    let result=obj.f(args);
    // 最后删除他
    delete obj.f;
    //返回函数的返回值
    return result
}

这样一个基本ok的call我们就实现完了

apply

apply和call原理类似,区别在于后面传的参数,举个例子:

fn.call(obj,1,2);
fn.apply(obj,[1,2]);

我们写出apply的用例

// 1-Q
let obj={
    a:1
}
function fn(key,value){
    console.log(this.a);
    console.log(key);
    console.log(value);
}
fn.apply(obj,['key','value'])// 1 key value

所以我们其实只需要将上面的call的代码关于参数那块做一下改动即可,当然我们可以做更多完善,例如这里我们临时声明的f使用Symbol对象,这样可以防止原obj里有同名对象被我们替换的问题

// 1-A
Function.prototype.apply2=function (obj){
    if(typeof this!=='function'){
        throw 'type error';
    }
    let fn=this;
    // 使用symbol
    let f=Symbol();
    obj=obj||window;
    obj[f]=fn;
    let result=arguments[1]?obj[f](arguments[1]):obj[f]();
    delete obj[f];
    return result;
}

手写防抖debounce,节流throttle

手写数组扁平化flatArray

手写promise,promise.all

手写reduce

手写JS常见算法题

手写快排quickSort

斐波那契数列

斐波那契数列

合并二维有序数组为一维

合并二维有序数组成一维有序数组

关键思想:链表加头节点-双指针

var mergeTwoLists = function(l1, l2) {
    let p1 = l1, p2 = l2, newHead = new ListNode(0), q = newHead;
    while(p1 && p2) {
        if(p1.val < p2.val) {
            q.next = p1;
            p1 = p1.next;
        } else {
            q.next = p2;
            p2 = p2.next;
        }
        q = q.next;
    }
    q.next = p1 ? p1 : p2;
    return newHead.next;
};

反转链表

链表:反转链表

关键思想:三指针

var reverseList = function(head) {
    if(head==null){
        return head;
    }
    if(head.next==null){
        return head;
    }
    let pre=head,cur=head.next;
    pre.next=null;
    while(cur){
        let next=cur.next;
        cur.next=pre;
        pre=cur;
        cur=next;
    }
    return pre;
};

有环链表

链表:链表有环 关键思想:快慢指针

var hasCycle = function(head) {
    if(head==null){
        return false;
    }
    let slow=head;
    let fast=head.next;
    while(fast!=null){
        if(slow===fast){
            return true;
        }else{
            slow=slow.next;
            if(fast.next!=null){
                fast=fast.next.next;
            }else{
                return false;
            }
        }
    }
    return false;
};

括号字符串

堆栈队列:判断括号字符串是否有效

关键思想:贪心

var checkValidString = function(s) {
    let minCount = 0, maxCount = 0;
    const n = s.length;
    for (let i = 0; i < n; i++) {
        const c = s[i];
        if (c === '(') {
            minCount++;
            maxCount++;
        } else if (c === ')') {
            minCount = Math.max(minCount - 1, 0);
            maxCount--;
            if (maxCount < 0) {
                return false;
            }
        } else {
            minCount = Math.max(minCount - 1, 0);
            maxCount++;
        }
    }
    return minCount === 0;
};

Topk

返回数组中第k个最大元素

合为n

找出数组中和为sum的n个数

贪心

贪心:具有给定数值的最小字符串

二叉树深度遍历

二叉树:最大深度

二叉树广度遍历

二叉树:层次遍历

翻转二叉树

翻转二叉树

剪枝

剪枝:判断数独是否有效

二分查找

二分查找:求解平方根

字典树

字典树:实现一个字典树

爬楼梯

爬楼梯问题

最短距离

最短距离

LRU

LRU缓存