2020 前端高频面试题

一:实现一个Vue自定义指令懒加载:

Intersection Observer API提供了一种异步观察目标元素与祖先元素或顶级文档viewport的交集中的变化的方法。

创建一个 IntersectionObserver对象,并传入相应参数和回调用函数,该回调函数将会在目标(target)元素和根(root)元素的交集大小超过阈值(threshold)规定的大小时候被执行。

var observer = new IntersectionObserver(callback, options);

IntersectionObserver是浏览器原生提供的构造函数,接受两个参数:callback是可见性变化时的回调函数(即目标元素出现在root选项指定的元素中可见时,回调函数将会被执行),option是配置对象(该参数可选)。

返回的 observer是一个观察器实例。实例的 observe 方法可以指定观察哪个DOM节点。

Vue自定义指令

下面的api来自官网自定义指令:

钩子函数

  • bind: 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

  • inserted: 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

  • update: 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新

    扫描二维码关注公众号,回复: 12994111 查看本文章
  • componentUpdated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用。

  • unbind: 只调用一次,指令与元素解绑时调用。

钩子函数参数

指令钩子函数会被传入以下参数:

  • el:指令所绑定的元素,可以用来直接操作 DOM。

  • binding:一个对象,包含以下 property:

    • name:指令名,不包括 v- 前缀。

    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。

    • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。

    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。

    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。

    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。

  • vnode:Vue 编译生成的虚拟节点。

  • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

实现 v-lazyload 指令

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
        <style>
            img {
                width: 100%;
                height: 300px;
            }
        </style>
    </head>
    <body>
        <div id="app">
            <p v-for="item in imgs" :key="item">
                <img v-lazyload="item">
            </p>
        </div>
    </body>
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    <script>
        Vue.directive("lazyload", {
            // 指令的定义
            bind: function(el, binding) {
                let lazyImageObserver = new IntersectionObserver((entries, observer) => {
                    entries.forEach((entry, index) => {
                        let lazyImage = entry.target;
                        // 相交率,默认是相对于浏览器视窗
                        if(entry.intersectionRatio > 0) {
                            lazyImage.src = binding.value;
                            // 当前图片加载完之后需要去掉监听
                            lazyImageObserver.unobserve(lazyImage);
                        }

                    })
                })
                lazyImageObserver.observe(el);
            },
        });
        var app = new Vue({
            el: "#app",
            data: {
                imgs: [
                    'https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657907683.jpeg',
                    'https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657913523.jpeg',
                    'https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657925550.jpeg',
                    'https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657930289.jpeg',
                    'https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657934750.jpeg',
                    'https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657918315.jpeg',
                ]
            },
        });
    </script>
</html>

二:统计HTML标签中出现次数最多的标签

const tags = document.getElementsByTagName('*');
let map = new Map();
let maxStr = '';
let max = 0;
// 只是使用下标来获取,没有使用数组的方法,所以不需要将类数组转为数组
for(let i = 0; i < tags.length; i++) {
    let value = map.get(tags[i].tagName)
    if(value) {
        map.set(tags[i].tagName, ++value)
    } else {
        map.set(tags[i].tagName, 1);
    }
    if(value > max) {
        maxStr = tags[i].tagName;
        max = value;
    }
}
console.log(`当前最多的标签为 ${maxStr},个数为 ${max}` );

三:简单实现一个Promise

class Promise {
    constructor(fn) {
        /**
         *  三种状态 
         *  pending:进行中
         *  fulfilled:已成功
         *  rejected: 已失败
         */
        this.status = 'pending';
        this.resoveList = []; // 成功后回调函数
        this.rejectList = []; // 失败后的回调函数

        fn(this.resolve.bind(this), this.reject.bind(this));
    }
    then(scb, fcb) {
        if (scb) {
            this.resoveList.push(scb);
        }
        if(fcb) {
            this.rejectList.push(fcb);
        }
        return this;
    }
    catch(cb) {
        if (cb) {
            this.rejectList.push(cb);
        }
        return this;
    }
    resolve(data) {
        if (this.status !== 'pending') return;
        this.status = 'fulfilled';
        setTimeout(() => {
            this.resoveList.forEach(s => {
                data = s(data);
            })
        })
    }
    reject(err) {
        if (this.status !== 'pending') return;
        this.status = 'rejected';
        setTimeout(() => {
            this.rejectList.forEach(f => {
                err = f(err);
            })
        })
    }
    /**
     * 实现Promise.resolve
     * 1.参数是一个 Promise 实例, 那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
     * 2.如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。
    */ 
    static resolve(data) {
        if (data instanceof Promise) {
            return data;
        } else {
            return new Promise((resolve, reject) => {
                resolve(data);
            })
        }
    }
    // 实现Promise.reject
    static reject(err) {
        if (err instanceof Promise) {
            return err;
        } else {
            return new Promise((resolve, reject) => {
                reject(err);
            })
        }
    }
    /**
     * 实现Promise.all
     * 1. Promise.all方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。
     * 2. 返回值组成一个数组
    */
    static all(promises) {
        return new Promise((resolve, reject) => {
            let promiseCount = 0;
            let promisesLength = promises.length;
            let result = [];
            for(let i = 0; i < promises.length; i++) {
                // promises[i]可能不是Promise类型,可能不存在then方法,中间如果出错,直接返回错误
                Promise.resolve(promises[i])
                    .then(res => {
                        promiseCount++;
                        // 注意这是赋值应该用下标去赋值而不是用push,因为毕竟是异步的,哪个promise先完成还不一定
                        result[i] = res;
                        if(promiseCount === promisesLength) {
                        return resolve(result);
                        }
                    },(err) => {
                        return reject(err);
                    }
                )
            }
        })
    }
    /**
     * 实现Promise.race
     * 1. Promise.race方法的参数与Promise.all方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。
     * 2. 返回那个率先改变的 Promise 实例的返回值
    */
    static race(promises) {
        return new Promise((resolve, reject) => {
            for(let i = 0; i < promises.length; i++) {
                Promise.resolve(promises[i])
                    .then(res => {
                        return resolve(res);
                    },(err) =>{
                        return reject(err);
                    }
                )
            }
        })
    }
}

测试用例

1. Promise.then

const p = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('resolve');
        resolve(222);
    }, 1000)
})

p.then(data => {
    setTimeout(() => {
        console.log('data', data);
    })
    return 3333;
}).then(data2 => {
    console.log('data2', data2);
}).catch(err => {
    console.error('err', err);
});

2. Promise.reject

const p1 = Promise.reject('出错了');
p1.then(null, function (s) {
    console.log(s); // 出错了
});

3. Promise.all && Promise.race

const q1 = new Promise((resolve, reject) => {
    resolve('hello')
});

const q2 = new Promise((resolve, reject) => {
    resolve('world')
});
Promise.all([q1, q2]).then(res => {
    console.log(res); // [ 'hello', 'world' ]
});
Promise.race([q1, q2]).then(res => {
    console.log(res); // hello
});

四:函数防抖和节流

函数防抖(debounce)

防抖:不管事件触发频率多高,一定在事件触发 n 秒后才执行,如果在一个事件执行的 n秒内又触发了这个事件,就以新的事件的时间为准,n秒后才执行,总之,触发完事件 n 秒内不再触发事件,n秒后再执行。

思路:

  1. 返回一个函数;

  2. 每次触发事件时都取消之前的定时器

需要注意问题:

  1. this指向

  2. 参数的传递

  3. 是否要立即调用一次

function debounce(fn, wait, immediate) {
    let timer = null;
    //  返回一个函数
    return function(...args) {
        // 每次触发事件时都取消之前的定时器
        clearTimeout(timer);
        // 判断是否要立即执行一次
        if(immediate && !timer) {
            fn.apply(this, args);
        }
        // setTimeout中使用箭头函数,就是让 this指向 返回的该闭包函数,而不是 debounce函数的调用者
        timer = setTimeout(() => {
            fn.apply(this, args)
        }, wait)
    }
}

通过闭包保存一个标记(timer)来保存setTimeout返回的值, 每当要触发函数的时候, 需要先把上一个setTimeout清除掉, 然后再创建一个新的setTimeout, 这样就能保证执行函数后的 wait 间隔内如果还要触发函数, 就不会执行fn

使用场景

  1. 监听resize或scroll,执行一些业务处理逻辑

window.addEventListener('resize', debounce(handleResize, 200));
window.addEventListener('scroll', debounce(handleScroll, 200));

使用到一些高频触发的函数,需要考虑防抖

  • window 的 resize、scroll

  • mousedown、mousemove

  • keyup、keydown ...

  1. 搜索输入框,在输入后200毫秒搜索

debounce(fetchSearchData, 200);

可以这样去理解记忆:函数防抖是 在事件触发 n 秒后才执行,在监听  scroll事件和 resize 事件时,只要 n 秒后才执行一次就可以了,不需要每次只要一触发 scroll或 resize的时候就执行,n秒内的执行是没有意义的(用户可能都感受不到,而且很容易造成卡顿)。

函数节流(throttle)

函数节流:不管事件触发频率有多高,只在单位时间内执行一次。

有两种思路实现:使用时间戳和定时器

使用时间戳

function throttle(fn, wait)  {
    // 记录上一次执行的时间戳
    let previous = 0;
    return function(...args) {
        // 当前的时间戳,然后减去之前的时间戳,大于设置的时间间隔,就执行函数,否则不执行
        if(Date.now() - previous > wait) {
            // 更新上一次的时间戳为当前时间戳
            previous = Date.now();
            fn.apply(this, args);
        }
    }
}

第一次事件肯定触发,最后一次不会触发(比如说监听 onmousemove,则鼠标停止移动时,立即停止触发事件)

使用定时器

function throttle(fn, wait)  {
    // 设置一个定时器
    let timer = null;
    return function(...args) {
        // 判断如果定时器不存在就执行,存在则不执行
        if(!timer) {
            // 设置下一个定时器
            timer = setTimeout(() => {
                // 然后执行函数,清空定时器
                timer = null;
                fn.apply(this, args)
            }, wait)
        }
    }
}

第一次事件不会触发(fn是放在 setTimeout中执行的,所以第一次触发事件至少等待 wait 毫秒之后才执行),最后一次一定触发

定时器和时间戳结合

两者结合可以实现,第一次事件会触发,最后一次事件也会触发

function throttle(fn, wait)  {
    // 记录上一次执行的时间戳
    let previous = 0;
    // 设置一个定时器
    let timer = null;
    return function(...args) {
        // 当前的时间戳,然后减去之前的时间戳,大于设置的时间间隔
        if(Date.now() - previous > wait) {
            clearTimeout(timer);
            timer = null
            // 更新上一次的时间戳为当前时间戳
            previous = Date.now();
            fn.apply(this, args);
        } else if(!timer) {
            // 设置下一个定时器
            timer = setTimeout(() => {
                timer = null;
                fn.apply(this, args)
            }, wait)
        }
    }
}

五:实现一个vue的双向绑定

// vue 2.x双向绑定
        var obj = {};
        var demo = document.getElementById('aa');
        var inp = document.getElementById('input');
        Object.defineProperty(obj,'txt',{
            get:function(val){
                return val;
            },
            set:function(newVal){
                inp.value = newVal;
                demo.innerHTML = newVal;
            }
        })
        document.addEventListener('input', function(e){
            obj.txt = e.target.value;
        })
// vue 3.x双向绑定
        var obj = {};
        var obj1 = new Proxy(obj, {
            // target就是第一个参数obj, receive就是返回的obj(返回的proxy对象)
            get: function (target, key, receive) {
                // 返回该属性值
                return target[key];
            },
            set: function (target, key, newVal, receive) {
                // 执行赋值操作
                target[key] = newVal;
                document.getElementById('text').innerHTML = target[key];
            }
        })
        document.addEventListener('keyup', function (e) {
            obj1[0] = e.target.value;
        });

猜你喜欢

转载自blog.csdn.net/AN0692/article/details/108866655