一些面经的小知识(二)

732 阅读5分钟

1、拷贝

当你把一个对象或数组作为值赋给一个变量时,实际上是将这个对象或数组的引用地址赋给了变量,当这个变量发生了改变对象或数组的操作时,原对象和数组也会受到影响跟着改变,所以我们想到通过拷贝的方式重新复制一份。

大家应该都知道JS拷贝有分浅拷贝和深拷贝~

浅拷贝:

(1)Object.assign

const obj1 = {
    a: 1,
    b: { c: 2 }
};
const obj2 = Object.assign({}, obj1);

(2)es6解构

const obj1 = {
    a: 1,
    b: { c: 2 }
};
const obj2 = { ...obj1 };

(3)遍历一次的key、value赋值

(4)数组的话还有slice

但是浅拷贝还是无法解决对象或数组嵌套的问题,这时我们需要考虑深拷贝。

深拷贝:

(1)JSON.stringify和JSON.parse

const obj1 = {
    a: 1,
    b: { c: 2 }
};
const obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.c = 3;
console.log(obj1);
console.log(obj2);

你会发现obj1和obj2已经独立互不影响了。

是不是用这种方法就可以完美解决问题呢?

其实这种方法,他是会忽略对象中值为undefined的属性的。

const obj1 = {
    a: 1,
    b: { c: undefined }
};
const obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1);
console.log(obj2);

(2)递归

function clone(source) {
    var target = {};
    for(var i in source) {
        if (source.hasOwnProperty(i)) {
            if (typeof source[i] === 'object') {
                target[i] = clone(source[i]);
            } else {
                target[i] = source[i];
            }
        }
    }

    return target;
}

👆的方法欠缺参数检验、是否是对象的判断不严谨以及没有兼容数组。

其实判断是否是对象常用的方法是Object.prototype.toString.call(obj) === '[object Object]'

如果想了解完美的解决办法,可以看看这里:

github.com/jsmini/clon…

其实这种深拷贝方法还有个问题,就是循环引用和层级过深会导致爆栈,这里有篇文章写的很好,大家可以看看:

juejin.im/post/684490…

2、数组去重

(1)利用对象

const arr = [1,2,3,4,2,5,4,6,7,6,8];
const obj = {};
const newArr = [];
arr.forEach((item) => {
    if (!obj[item]) {
        newArr.push(item);
        obj[item] = 1;
    }
});
console.log(newArr);

2、利用ES6的Set

const arr = [1,2,3,4,2,5,4,6,7,6,8];
const mySet = new Set(arr);
console.log(mySet);

这样虽然得出了去重后的结果,但还差一步,这还是一个Set,我们得把它转回数组,用什么方法呢?是的,Array.from。

const newArr = Array.from(mySet);
console.log(newArr);

当然数组去重还有其他方法,比如 原数组遍历,检查新数组是否存在即将插入的元素,不存在则插入,存在则不插入,但这会用到indexOf,其实本质也是把新数组遍历了,所以性能没有上面这两种好。

3、如果让你实现一个搜索框,你会考虑到什么?(这里的搜索框就像百度的搜索框,输入后在下面会展示搜索结果)

这道题应该大部分人会想到的是输入关键字触发请求,那用户每次输入都会触发请求的话,无疑对网络、性能、体验都是不好的,所以我们该用节流函数。

简单的节流函数:

function throttle(fn, duration) {
    let flag = true;
    return function() {
        const context = this;
        if (flag) {
            flag = false;
            setTimeout(() => {
                fn && fn.apply(context, arguments)
                flag = true;
            }, duration);
        }
    }
}

节流函数一般可以用在onresize,onscroll,onmousemove,onmouseover等场景。

诶说到节流函数,那顺便提提防抖函数吧,这两者有什么不一样呢?

我觉得差别在于时间的控制吧,防抖函数也是在设置的时间内只会执行一次,但是如果第二次触发则是会重新计算时间。

function debounce(fn, duration) {
    let timer = null;
    return function() {
        const context = this;
        if (timer) {
            clearTimeout(timer);
            timer = null;
        }
        timer = setTimeout(() => {
            fn && fn.apply(context, arguments);
            timer = null;
        }, duration);
    }
}

回到搜索框的问题上,除了设置节流以外,其实还有个问题。如果用户输入“abc”第一次触发,发出了请求,再继续输入“def”又触发一次,又发出一次请求,但是这两次请求返回顺序是不固定的,这样得到的搜索结果很可能并不是用户想要的,体验会很差。

所以这样需要用到XMLHttpRequest的中止请求abort,当你上一次请求的结果未返回时,先abort中止它,再进行下一次请求。

4、实现拖拽效果,用鼠标拖动一个东西,随意拖动,随意放下。

首先要理解这里不是让我们用html5的drag,然后自己画图想想这里要怎么利用各种坐标距离,再试着用代码实现一下看看。

<html>
<head>
    <meta ...>
    <title>test</title>
    <style>
        #box {
            width: 100px;
            height: 100px;
            position: absolute;
            top: 0px;
            left: 0px;
            background-color: #FF0000;
        }
    </style>
    <script>
        window.onload = function() {
            const box = document.getElementById('box');
            box.addEventListener('mousedown', mouseDown);
            box.addEventListener('mouseup', mouseUp);
            let boxLeft = 0; // 用来保存鼠标点击时 box绝对位置的left
            let boxTop = 0; // 用来保存鼠标点击时 box绝对位置的top
            let initX = 0; // 用来保存鼠标点击box时 的X坐标
            let initY = 0; // 用来保存鼠标点击box时 的Y坐标
            let distanceX = 0; // 用来保存box X轴上移动的距离
            let distanceY = 0; // 用来保存box Y轴上移动的距离
            
            function mouseDown(e) {
                // 鼠标点击box触发
                initX = e.pageX;
                initY = e.pageY;
                boxLeft = e.target.offsetLeft;
                boxTop = e.target.offsetTop;
                // 这里给body绑定鼠标移动事件,是因为body覆盖整个页面。
                // 如果给box绑定鼠标移动事件,当鼠标快速滑动的时候,滑出了box,那事件就会中断了
                document.body.addEventListener('mousemove', mouseMove);
            }

            function mouseUp() {
                // 鼠标松开时触发
                document.body.removeEventListener('mousemove' mouseMove);
            }

            function mouseMove(e) {
                // 鼠标滑动时触发
                distanceX = e.pageX - initX;
                distanceY = e.pageY - initY;
                box.style.left = boxLeft + distanceX;
                box.style.top = boxTop + distanceY;
            }
        }    
    </script>
</head>
<body>
    <div id="box"></div> 
</body>
</html>

5、大数相加

javascript支持的数字范围是(-2^53, 2^53),超过了就会丢失精度。

如果后端返回了两个超大数字符串,我们要怎样将两者相加呢?因为如果转成数字相加,超大数字符串转成数字精度会丢失,相加的结果必然不准确;如果直接字符串相加,那也只是字符串拼接,并不是我们想要的结果。

我们来看一下,其实数字相加原理是每一位相加,然后满十进一。

   12345

+ 56789

——————

   69134

那我们是不是可以将字符串每一位相加,模拟数字的相加呢?

function add(str1, str2) {
    const arr1 = str1.split('').reverse();
    const arr2 = str2.split('').reverse();
    const res = [];
    let flag = 0;
    while(arr1.length || arr2.length || flag) {
        const num1 = arr1.shift() || 0;
        const num2 = arr2.shift() || 0;
        const sum = Number(num1) + Number(num2) + flag;
        if (sum > 9) {
            flag = 1;
            res.push(sum % 10);
        } else {
            flag = 0;
            res.push(sum);
        }
    }
    return res.reverse().join('');
}

6、实现一个简单的模版引擎

现在有个模板字符串 '<p>hello,我是{{name}},年龄:{{info.age}}<p>,工作经历:{{info.experience.company}},工作时间:{{info.experience.time}}',

数据data = {name: 'abc', info: {age: 24, experience: {company: 'abc', time: 'two years'}}} ,如何实现模板的数据替换?

这里关键是replace方法的使用,replace方法的第二个参数可以是个回调函数。

function compile(tpl, data) {
    const regex = /\{\{([^}]*)\}\}/g;    const string = tpl.trim().replace(regex, function(match, $1) {
        if ($1) {
            const arr = $1.split('.');
            return getValue(data, arr);
        } else {
            return '';
        }
    });
    console.log(string);
}

function getValue(data, arr) {
    let attr;
    if (arr.length) {
        attr = arr.shift();
        return getValue(data[attr], arr);
    }
    return data;
}
const tpl = '<p>hello,我是{{name}},年龄:{{info.age}}<p>,工作经历:{{info.experience.company}},工作时间:{{info.experience.time}}';
const data = {name: 'abc', info: {age: 24, experience: {company: 'def', time: 'two years'}}};
compile(tpl, data);

7、闭包的作用

闭包作用一是外部可以读取函数内部的变量;二是函数里声明的变量始终保持在内存中,不会在函数调用后被自动清除。

运用场景比如解决遍历索引,函数柯里化,节流防抖,bind等等。

未完待续~~