美文网首页让前端飞Web前端之路JavaScript 进阶营
前端常见面试题(十五)@郝晨光

前端常见面试题(十五)@郝晨光

作者: 郝晨光 | 来源:发表于2019-07-28 03:10 被阅读179次

    什么是函数防抖和函数节流?有什么区别?

    概念

    函数防抖

    函数防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
    通俗一点:在一段固定的时间内,只能触发一次函数,在多次触发事件时,只执行最后一次。

    函数节流

    函数节流,就是限制一个函数在一定时间内只能执行一次。

    区别

    函数节流与函数防抖的区别:
    我们以一个案例来讲一下它们之间的区别:设定一个间隔时间为一秒,在一分钟内,不断的移动鼠标,让它触发一个函数,打印一些内容。
    函数防抖:会打印1次,在鼠标停止移动的一秒后打印。
    函数节流:会打印60次,因为在一分钟内有60秒,每秒会触发一次。
    总结:节流是为了限制函数的执行次数,而防抖是为了限制函数的执行时机。

    使用时机

    1. 搜索功能,在用户输入结束以后才开始发送搜索请求,可以使用函数防抖来实现;
    2. 改变浏览器窗口尺寸,可以使用函数节流,避免函数不断执行;
    3. 滚动条scroll事件,通过函数节流,避免函数不断执行。
    4. ···

    简单实现

    函数防抖
    /**
     * @function debounce 函数防抖
     * @param {Function} fn 需要防抖的函数
     * @param {Number} interval 间隔时间
     * @return {Function} 经过防抖处理的函数
     * */
    function debounce(fn, interval) {
        let timer = null; // 定时器
        return function() {
            // 清除上一次的定时器
            clearTimeout(timer);
            // 拿到当前的函数作用域
            let _this = this;
            // 拿到当前函数的参数数组
            let args = Array.prototype.slice.call(arguments, 0);
            // 开启倒计时定时器
            timer = setTimeout(function() {
                // 通过apply传递当前函数this,以及参数
                fn.apply(_this, args);
                // 默认300ms执行
            }, interval || 300)
        }
    }
    

    函数节流

    /**
     * @function throttle 函数节流
     * @param {Function} fn 需要节流的函数
     * @param {Number} interval 间隔时间
     * @return {Function} 经过节流处理的函数
     * */
    function throttle(fn, interval) {
        let timer = null; // 定时器
        let firstTime = true; // 判断是否是第一次执行
        // 利用闭包
        return function() {
            // 拿到函数的参数数组
            let args = Array.prototype.slice.call(arguments, 0);
            // 拿到当前的函数作用域
            let _this = this;
            // 如果是第一次执行的话,需要立即执行该函数
            if(firstTime) {
                // 通过apply,绑定当前函数的作用域以及传递参数
                fn.apply(_this, args);
                // 修改标识为false
                firstTime = false;
            }
            // 如果当前有正在等待执行的函数则直接返回
            if(timer) return;
            // 开启一个倒计时定时器
            timer = setTimeout(function() {
                // 通过apply,绑定当前函数的作用域以及传递参数
                fn.apply(_this, args);
                // 清除之前的定时器
                timer = null;
                // 默认300ms执行一次
            }, interval || 300)
        }
    }
    

    测试

    此处使用一个对象的方法,主要为了测试this指向绑定的问题,调用的时候传递参数问题等。

    function log(a,b) {
        console.log(a,b);
        console.log(this);
    }
    const throttleLog = throttle(log, 1000);
    const debounceLog = debounce(log, 1000);
    let a  = {
        b: throttleLog,
        c: debounceLog
    };
    document.body.onmousemove = function() {
        a.b('throttle', 'log');
        a.c('debounce', 'log');
    };
    
    函数防抖和函数节流测试

    什么是函数柯里化及有什么用?

    高阶函数

    在了解函数柯里化之前,我们需要先了解一下什么是高阶函数。
    通俗的说,函数可以作为参数传递到函数中,这个作为参数的函数叫回调函数,而拥有这个参数的函数就是高阶函数,回调函数在高阶函数中调用并传递相应的参数,在高阶函数执行时,由于回调函数的内部逻辑不同,高阶函数的执行结果也不同,非常灵活,也被叫做函数式编程。

    函数柯里化

    就是给函数分步传递参数,每次传递部分参数,并返回一个更具体的函数接收剩下的参数,这中间可嵌套多层这样的接收部分参数的函数,直至返回最后结果。

    函数柯里化有什么用?

    通过函数柯里化,我们可以实现参数复用、提前返回和延迟执行。通过函数柯里化,在不同的情况下传递不同的参数

    通过简单的案例理解一下

    了解函数柯里化有一个非常经典的案例,我这里也就不出丑了,直接拿这个经典案例来讲。
    将函数add(1, 2, 3); 输出三个参数相加结果 === 6,转变为 add(1)(2)(3); 输出三个参数相加结果 === 6。

    function add(a, b, c) {
        return a + b + c;
    }
    add(1, 2, 3); // 6
    

    这个时候,我们最简单的实现方法就是通过闭包来实现。

    function add(a) {
        return function(b) {
            return function(c) {
                return a + b + c;
            }
        }
    }
    add(1)(2)(3); // 6
    

    上边的案例就是一个最简单的函数柯里化,将函数的参数逐次传递,而不是一次传递进去。每次都接收下一个参数,当参数传递结束以后执行函数内的求和操作。

    简单实现函数柯里化通用函数
    ES5
    /**
     * @function curry 函数柯里化
     * @param {Function} fn 需要进行柯里化的函数
     * @param {Array|Null} args 参数数组
     * @return {Function} 柯里化函数,当参数长度足够时返回函数结果
     * */
    function curry(fn, args) {
        // 保存柯里化函数需要接收的参数数量
        let len = fn.length;
        // 保存每次传递进来的参数,第一次调用时默认为空数组
        args = args || [];
        return function() {
            // 拿到本次的参数数组
            let _args = Array.prototype.slice.call(arguments, 0);
            // 将本次的数组和之前的参树合并,下边的语句等于_args.unshift(...args);
            // 将原参数展开并逐个添加到新数组中。
            Array.prototype.unshift.apply(_args, args);
            // 如果当前的参数长度小于函数预期长度的话
            if(_args.length < len) {
                // 递归调用柯里化函数,将函数和参数继续传递下去
                return curry.call(null, fn, _args);
            }
            // 如果参数长度达到预期长度则直接调用函数
            return fn.apply(this, _args);
        }
    }
    
    ES6
    /**
     * @function curry 函数柯里化
     * @param {Function} fn 需要进行柯里化的函数
     * @param {Array|Null} args 参数数组
     * @return {Function} 柯里化函数,当参数长度足够时返回函数结果
     * */
    function curry(fn, args = []) {
        // 保存柯里化函数需要接收的参数数量
        let len = fn.length;
        return function (..._args) {
            // 将原参数展开并逐个添加到新的参数数组中。
            _args.unshift(...args);
            // 如果当前的参数长度小于函数预期长度的话
            if(_args.length < len) {
                // 递归调用柯里化函数,将函数和参数继续传递下去
                return curry.call(null, fn, _args);
            }
            // 如果参数长度达到预期长度则直接调用函数
            return fn.apply(this, _args);
        }
    }
    
    调用方式
    function add(a, b, c) {
        return a + b + c;
    }
    
    let addCurry = curry(add);
    // 第一种调用方式
    addCurry(1)(2)(3); // 6
    
    // 第二种调用方式
    let a = addCurry(1)(2);
    a(3); // 6
    
    // 第三种调用方式
    let b = addCurry(1);
    let c = b(2);
    let result = c(3); // 6
    

    关于函数柯里化更高阶的技巧:
    「前端进阶」彻底弄懂函数柯里化
    高阶函数应用 —— 柯里化与反柯里化


    谈谈模块化开发

    什么是模块化开发?

    模块就是完成单一的职责的功能函数或者代码块。
    模块化开发是指如何开发新的模块和复用已有的模块来实现应用的功能。

    为什么要模块化开发?

    1. 可维护性
    2. 可测试性
    3. 按需加载
    4. 代码复用
    5. 利于架构
    6. 利于协作

    对于我们前端来说,特别是解决了javascript中命名空间以及文件依赖的问题。

    模块化开发的规范

    CommonJS
    • 用于服务端模块化编程,Node.js就采用此规范;
    • 一个文件就是一个模块,require方法用来加载模块,该方法读取一个文件并执行,最后返回文件内部的module.exports对象;
    AMD
    • require.js(前端模块化管理的工具库)实现js文件的异步加载,避免网页失去响应;管理模块之间的依赖性,便于代码的编写和维护;require.js是使用创建script元素,通过指定script元素的src属性来实现加载模块的;
    • 依赖前置,尽早的执行模依赖块,执行顺序不一定;
    CMD
    • Sea.js 依赖就近,且在真正需要使用依赖模块时才执行该模块,顺序固定;
    • AMD和CMD最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块;
    • AMD依赖前置,js可以方便知道依赖模块是谁,立即加载;而CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块。
    ES6
    • ES6使用的是基于文件的模块。所以必须一个文件一个模块,不能将多个模块合并到单个文件中去。
    • ES6模块API是静态的,一旦导入模块后,无法再在程序运行过程中增添方法。
    • ES6模块采用引用绑定(可以理解为指针)。这点和CommonJS中的值绑定不同,如果你的模块在运行过程中修改了导出的变量值,就会反映到使用模块的代码中去。所以,不推荐在模块中修改导出值,导出的变量应该是静态的。
    • ES6模块采用的是单例模式,每次对同一个模块的导入其实都指向同一个实例。
      另外,ES6模块好处很多,但是并不支持按需加载的功能, 而按需加载又是Web性能优化中重要的一个环节。好在我们可以借助Webpack来弥补这一缺陷

    更多关于模块化开发的知识:
    浅谈模块化开发


    谈谈你对作用域链的理解

    什么是作用域?

    作用域就是变量与函数的可访问范围。在JavaScript中,变量的作用域有全局作用域和局部作用域,在ES6之后,又添加了块级作用域的概念,所以在JavaScript中有三种作用域。

    全局作用域
    var a = 5;
    function fn() {
        b = 10;
        console.log(a);
    }
    console.log(a);
    console.log(b);
    
    1. 定义在全局(window)下的变量拥有全局作用域;
    2. 所有末定义直接赋值的变量自动声明,并拥有全局作用域;
    3. 所有window对象上的属性,具有全局作用域。
    局部作用域
    function fn() {
        var b = 10;
        console.log(b);
    }
    console.log(b);
    
    1. 局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部;
    2. 块级作用域其实也可以算一种局部作用域。
    块级作用域
    1. 所声明的变量在指定块的作用域外无法被访问;
    2. 在一个函数内部;
    3. 在一个代码块(由一对花括号包裹)内部。
    let arr = [1, 2, 3, 4, 5];
    for(let i in arr) {
        console.log(i); // 1 2 3 4 5
    }
    console.log(i); // Uncaught ReferenceError: i is not defined
    
    // 对于for循环,圆括号内与花括号内是两个作用域。
    for(let j in arr) {
        let j = '郝晨光';
        console.log(j); // 郝晨光 * 5
    }
    

    什么是作用域链

    对于javascript而言,我们定义在局部作用域或者块级作用的变量,会存放在scope局部作用域对象上,而定义在全局作用域的变量,会存放在window/global这个全局对象上。
    当我们在局部/块级作用域中,调用一个外部的变量时,就会产生作用域链。
    当我们在一个函数内部访问当前作用域内不存在的变量时,就会逐层向外查找,如果一直没有找到就会报错。
    对于函数而言,函数在哪里创建,它就会从哪里开始向上查找变量;而不是函数在哪里调用。
    最常见的就是闭包,通过闭包定义的函数来访问函数的内部变量。

    let name = '郝晨光'; // 全局作用域
    function fn() {
        let age = 24; // 函数fn内部作用域
        console.log(name, age); // 郝晨光 24
        // console.log(sex); // Uncaught ReferenceError: sex is not defined
        return function() {
            let sex = '男';
            console.log(name, age, sex);  // 郝晨光 24 男
            return function() {
                console.log(name, age, sex);  // 郝晨光 24 男
            }
        }
    }
    

    深入理解作用域和作用域链:
    深入理解JavaScript作用域和作用域链
    JavaScript 作用域和作用域链


    什么是深拷贝?什么是浅拷贝?如何实现一个深拷贝函数?

    深拷贝和浅拷贝都是针对于引用类型(Object)而言,对于基本数据类型而言,赋值直接就是深拷贝。

    浅拷贝

    浅拷贝有两种定义,一种是直接赋值引用,另一种是只拷贝对象的第一层属性,更深层次的引用还是相同的;对于这两种,哪一种是正确的答案,这里不做多的解释,看个人理解,像我平时更倾向于第二种。

    实现一个浅拷贝
    基本实现
    function copy(target) {
        let result = {};
        for(let key in target) {
            result[key] = target[key];
        }
        return result;
    }
    let obj1 = {
        a: 'a',
        b: {
            a: 'a',
            b: 'b'
        }
    };
    let obj2 = copy(obj1);
    console.log(obj2 === obj1); // false
    console.log(obj2.b === obj1.b); // true
    
    ES6实现
    let obj1 = {
        a: 'a',
        b: {
            a: 'a',
            b: 'b'
        }
    };
    // 第一种方式
    let obj2 = Object.assing({}, obj1);
    // 第二种方式
    let obj3 = {...obj1};
    console.log(obj2 === obj1); // false
    console.log(obj3 === obj1); // false
    console.log(obj2.b === obj1.b); // true
    console.log(obj3.b === obj1.b); // true
    

    深拷贝

    对于浅拷贝有概念之后,就比较容易理解深拷贝了。
    深拷贝就是将原有对象重新拷贝一份,不论是修改哪一部分的值,都不会对原有对象造成影响。拷贝的永远是值,而不是引用。
    关于深拷贝请看本人的另一篇文章:
    浅谈JS深拷贝(深克隆)@郝晨光


    如果本文对您有帮助,可以看看本人的其他文章:
    前端常见面试题(十四)@郝晨光
    原生JS - 瀑布流布局@郝晨光
    原生JS - 图片懒加载@郝晨光

    结言
    感谢您的查阅,本文由郝晨光整理并总结,代码冗余或者有错误的地方望不吝赐教;菜鸟一枚,请多关照

    相关文章

      网友评论

        本文标题:前端常见面试题(十五)@郝晨光

        本文链接:https://www.haomeiwen.com/subject/pksyrctx.html