经典大厂前端面试题(含解析)函数篇(二)

金三银四就要到了。整理整理面试题。前端

整篇文章都与JS的函数相关。面试

从函数的定义开始

  1. 每一个函数实际上都是一个 Function 对象,即: (function(){}).constructor === Function
  2. 函数是 头等对象/一等公民
    1. 函数能够像任何其余对象同样具备属性和方法
    2. 能够赋值给变量(函数表达式
    3. 能够做为参数传递给函数(高阶函数
    4. 能够做为另外一个函数的返回值(闭包

定义函数的方式有 4 种:算法

  1. new Function(str);
  2. 函数表达式 var fn = function() {}
  3. 函数声明 function fn() {}
  4. 箭头函数 var fn = () => {}

PS:new Function 声明的对象是在函数建立时解析的,故比较低效chrome

什么是闭包?

MDN的定义:函数与对其状态即词法环境的引用共同构成闭包(closure)。也就是说,闭包可让你从内部函数访问外部函数做用域编程

在JavaScript,函数在每次建立时生成闭包。waht????(MDN说的...设计模式

小红书上的更好理解一点:闭包是指有权访问另一个函数做用域中的变量的函数数组

也就是说,这就是闭包:安全

function saySomething(){
        var name = 'mokou';
        return function () {
            console.log(name);
        }
    }

    var say = saySomething()
    say()
复制代码

闭包产生的缘由?

根据 JS 的垃圾回收机制(不提新生代和老生代),根据可达性算法:不可达就会被回收。bash

什么是不可达?简单来讲:内存中没有在内存中存放引用(即:没有指针指向堆)就视为不可达。(不懂堆栈的能够看下上一篇JS基础篇)微信

上面案例代码中:saySomething 方法的返回值的引用存在了 say 变量中,因此可达,故:引用不会被销毁,从而产生闭包。

说一个闭包的使用场景?

案例一:请求出错的提示框(多个请求同时出错通常都只有一个提示框)

实现思路:使用传说中的设计模式 单例模式

如下是单例模式的实现:

const Singleton = (function() {
        var _instance;
        return function(obj) {
            return _instance || (_instance = obj);
        }
    })();

    var a = new Singleton({x: 1});
    var b = new Singleton({y: 2});

    console.log(a === b);
复制代码

PS:上例还有一个优势:_instance 是私有的,外部不能更改(保证安全无污染/可信)

案例二:解决 varfor + setTimeout 混合场景中的BUG

BUG 展现:

for (var i=1; i<=5; i++) {
        setTimeout(function() {
            console.log(i);
        }, i*300 );
    }
复制代码

上例会打印:6 6 6 6 6

由于 var 是函数做用域(缘由1),而 setTimeout 是异步执行(缘由2),因此:当 console.log 执行的时候 i 已经等于 6 了(BUG产生)

在没有 letconst 的年代,经常使用的解决方式就是闭包

for (var i = 1; i <= 5; i++) {
        (function(j) {
            setTimeout(function() {
                console.log(j);
            }, j*300);
        })(i);
    }
复制代码

闭包的缺点?

缺点:

  1. 性能考量:闭包在处理速度和内存消耗方面对脚本性能具备负面影响(多执行了一个函数,多了一个内存指向)
  2. 可能内存溢出。(好比:在闭包中的 addEventListener 没有被 removeEventListener

函数表达式和函数声明的区别?

主要区别在

  1. 函数声明被提高到了函数定义(能够在函数声明以前使用)
  2. 函数表达式要根据定义的方式进行判断
    • 经过 var 定义:有变量声明提高
    • 经过 let 和 const 定义:没有变量提高

什么是变量提高?

JavaScript 中,函数及变量(经过var方式)的声明都将被提高到函数的最顶部。

案例:如下会输出什么结果?

var name = 'zmz';

    function say(){
        var name;
        console.log(name);

        var name = 'mokou';
        console.log(name);
    }

    say();
复制代码

答案是:先输出 undefined 再输出 mokou

由于在函数 say 内部也声明了一个 name(是经过 var)声明的,因此会声明提高,可是未赋值,因此首先输出的是 undefined,以后是正常流程,输出 mokou

PS:因为 var 的变量提高很不友好,因此在 ES6 中添加了 letconst (本章主要讲函数,暂略。)

函数定义和变量同名会怎么样?

在生成执行上下文时,会有两个阶段。

  1. 建立的阶段(具体步骤是建立 VO),JS 解释器会找出须要提高的变量和函数,而且给他们提早在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明而且赋值为 undefined
  2. 代码执行阶段:咱们能够直接提早使用。

在提高的过程当中:函数定义优先于变量提高,变量在执行阶段才会被真正赋值。

举例

console.log(typeof a === 'function')

    var a = 1;
    function a() {}

    console.log(a == 1);
复制代码

上例会打印 true true

说一下箭头函数?

箭头函数式 ES6 标准

  1. 箭头函数的的this,就是定义时所在的对象
  2. 一旦绑定了上下文,就不可改变(call、apply、bind 都不能改变箭头函数内部 this 的指向)
let obj = {
        x () {
            let y = () => {
                console.log(this === obj);
            }
            y();    // true
            // call、apply、bind 都不能改变箭头函数内部 this 的指向
            y.call(window); // true
            y.apply(window); // true
            y.bind(window)(); // true
            // 同时,被bind绑定过的方法,也是不可变的,(不会再次被 bind、call、apply改变this的指向)
        }
    }
复制代码
  1. 因为this指向问题,因此:箭头函数不能看成构造函数,不能使用new命令
  2. 箭头函数没有 arguments,须要手动使用 ...args 参数代替
  3. 箭头函数不能用做 Generator 函数

那其余函数的 this 指向问题呢?

  1. 以函数的形式调用(this指向 window)
function fn () {
        console.log(this, 'fn');
        function subFn () {
            console.log(this, 'subFn');
        }
        subFn(); // window
    }
    fn(); // window
复制代码
  1. 以方法的形式调用 (this指向 调用函数的对象)
var x = 'abc';
    var obj = {
        x: 123,
        fn: function () {
            console.log(this.x);
        }
    }
    obj.fn(); //  123
    var fn = obj.fn;
    fn(); // abc
复制代码
  1. callapplybind 的形式调用(更改指向,箭头函数除外)
  2. 以构造函数调用,(指向实例)
  • new的实例是 构造函数中return的对象 || this
// 构造函数中有 return对象 的状况
    function A() {
        return {
            a : 1
        }
    }
    A.prototype.say = function () {
        console.log(this, 'xx')
    }
    var a = new A();
    // a = {a: 1}
    // a.say === undefined
    // 构造函数中 没有return对象 的状况
    function A() {
        // 能够手动 return this
    }
    A.prototype.say = function () {
        console.log(this, 'xx')
    }
    var a = new A();
    a.say();
    // A {} "xx"
复制代码

call 和 apply 的不一样?

  1. 入参不一样
  2. 性能差别(call比apply快不少)

性能测试:如下测试环境为 chrome v73

function work(a, b, c) {}

    for (var j = 0; j < 5; j++) {
        console.time('apply');
        for (var i = 0; i < 1000000; i++) {
            work.apply(this, [1, 2, 3]);
        }
        console.timeEnd('apply');

        console.time('call');
        for (var i = 0; i < 1000000; i++) {
            work.call(this, 1, 2, 3);
        }
        console.timeEnd('call');
    }
    
    /*
        // apply: 69.355224609375ms
        // call: 8.7431640625ms

        // apply: 57.72119140625ms
        // call: 4.146728515625ms

        // apply: 50.552001953125ms
        // call: 4.12890625ms

        // apply: 50.242919921875ms
        // call: 4.720947265625ms

        // apply: 49.669921875ms
        // call: 4.054931640625ms
    */
复制代码

测试结果: call 比 apply快 10倍(大约是这样的)

缘由:.apply 在运行前要对做为参数的数组进行一系列检验和深拷贝,.call 则没有这些步骤

怎么实现 call ?

实现思路

  1. 前置知识点:
    1. 当函数以方法的形式调用时,this指向被调用的对象
    2. 函数的参数是值传递
    3. 引用类型可写
  2. myCall 的第一个参数(暂命名为that)做为 被调用的对象
  3. that上添加一个方法(方法名随意,暂命名fn
  4. 经过 that[fn](...args) 调用方法(此时this指向为that
  5. 删除掉第3步添加的方法

具体代码

Function.prototype.myCall = function(that, ...args) {
        let func = this;
        let fn = Symbol("fn");

        that[fn] = func;
        let res = that[fn](...args);
        delete that[fn];

        return res;
    }
复制代码

测试一下

function say(x,y,z) {
        console.log(this.name, x, y, z)
    }
    
    say.myCall({name: 'mokou'}, 1, 2, 3)

    // 打印 mokou 1 2 3
复制代码

怎么实现一个 bind ?

实现思路

  1. bind 只改变 this 指向,不执行函数,那么能够用闭包来实现
  2. 具体更改 this指向的问题能够借用 call 实现
Function.prototype.myBind = function(that) {
        if (typeof this !== 'function') {
            throw new TypeError('Error')
        }
        const _fn = this;
        return function(...args) {
            _fn.call(that, ...args)
        }
    }
复制代码

测试一下:

function say(x,y,z) {
        console.log(this.name, x, y, z)
    }

    const testFn = say.myBind({name: 'mokou'})
    testFn(1, 2, 3);

    // 打印 mokou 1 2 3
复制代码

说一下尾递归?

PS: 这个小题是半搬运的 @阮一峰 老师的博客

尾递归就是:函数最后单纯return函数,尾递归来讲,因为只存在一个调用记录,因此永远不会发生"栈溢出"错误。

ES6出现的尾递归,能够将复杂度O(n)的调用记录,换为复杂度O(1)的调用记录

测试:不使用尾递归

function Fibonacci (n) {
        if ( n <= 1 ) {return 1};
        // return 四则运算
        return Fibonacci(n - 1) + Fibonacci(n - 2);
    }
    Fibonacci(10) // 89
    Fibonacci(100) // 超时
    Fibonacci(100) // 超时
复制代码

测试:使用尾递归

function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
        if( n <= 1 ) {return ac2};
        return Fibonacci2 (n - 1, ac2, ac1 + ac2);
    }
    Fibonacci2(100) // 573147844013817200000
    Fibonacci2(1000) // 7.0330367711422765e+208
    Fibonacci2(10000) // Infinity
复制代码

蹦床函数(协程):解决递归栈溢出问题,将函数变成循环

function trampoline(f) {
        while (f && f instanceof Function) {
            f = f();
        }
        return f;
    }
复制代码

尾递归的优化:

function tco(f) {
        var value;
        var active = false;
        var accumulated = [];

        return function accumulator() {
            accumulated.push(arguments);
            // 除了第一次执行,其余的执行都是为了传参
            if (!active) { // 很重要,若是不使用 active关闭后续进入, sum函数超过会溢出
                // 在第一次进入进入递归优化时激活,关闭后续进入
                active = true;
                // 有参数就执行
                while (accumulated.length) {
                    // 调用f,顺便清除参数
                    value = f.apply(this, accumulated.shift());
                    // 因为while中又调用 f,f调用sum,而后sum在执行时给accumulated塞了一个参数
                    // 因此 while循环会在sum返回结果前一种执行,直到递归完成
                }
                active = false;
                return value;
            }
        };
    }

    var sum = tco(function(x, y) {
        if (y > 0) {
            // 此时的sum是accumulator
            // 执行sum等于给accumulator传参
            return sum(x + 1, y - 1)
        }
        else {
            return x
        }
    });

    sum(1, 100000)
复制代码

for in、 for of、forEach 各自的特色是什么?

  1. for in 遍历的是对象的可枚举属性
  2. for of 遍历的是对象的迭代器属性
  3. forEach 只能遍历数组,且不能中断(break等无效)

手写一个防抖函数?

防抖函数:

function debounce(fn, wait) {
        let timer = null;
        return (...args) => {
            clearTimeout(timer);
            timer = setTimeout(() => {
                fn.apply(this, args);
            }, wait);
        }
    }
复制代码

使用场景:输入框校验

手写一个节流函数

节流函数

function throttle(fn, wait = 300) {
        let flag = true;
        return (...args) => {
            if (!flag) return;
            flag = false;
            setTimeout(() => {
                fn.apply(this, args);
                flag = true;
            }, wait);
        }
    }
复制代码

使用场景:

  1. 延迟防抖函数:onscroll 时触发的事件
  2. 当即执行防抖函数:按钮的点击事件(某种状况下 once函数 更合适)

说一下 Class ?

ES6 的 class 能够看做只是一个语法糖,它的绝大部分功能,ES5 均可以作到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

Class 和 function 有什么不一样

  1. 类没有变量提高,
new B();
    class B {}
    // Uncaught ReferenceError: B is not defined
复制代码
  1. 类的全部方法,都不可枚举
class A {
        constructor() {
            this.x = 1;
        }
        static say() {
            return 'zmz';
        }
        print() {
            console.log(this.x);
        }
    }
    Object.keys(A); // []
    Object.keys(A.prototype); // []
复制代码
  1. 类的的全部方法,没有原型对象prototype
接例2
    console.log(A.say.prototype); // undefined
    console.log(new A().print.prototype); // undefined
复制代码
  1. 类不能直接使用,必须使用 new 调用。
接例2
    A();
    // Uncaught TypeError: Class constructor A cannot be invoked without 'new'
复制代码
  1. 类内部启用严格模式
class B {
        x = 1
    }
    // Uncaught SyntaxError: Identifier 'B' has already been declared
复制代码

ES5 怎么实现继承?

须要完成功能

  1. 继承 构造属性
  2. 继承 原型方法
  3. 纠正构造器

主流继承方案

function Parent () {
        this.name = 'mokou';
    }
    
    function Child() {
        Parent5.call(this);
        this.age = '18';
    }

    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
复制代码

继承优化(参考 Babel 的降级方案)

function inherits(subClass, superClass) {
        subClass.prototype = Object.create(superClass && superClass.prototype, {
            constructor: {
                value: subClass,
                writable: true,
                configurable: true
            }
        });
        if (superClass) Object.setPrototypeOf(subClass, superClass);
    }
复制代码

说一下 new 的过程?

  1. 建立一个空对象
  2. 新对象的__proto__指向构造函数的 prototype
  3. 绑定 this,指向构造方法
  4. 返回新对象

详细代码

function myNew() {
        var obj = new Object()
        var Con = [].shift.call(arguments)
        obj.__proto__ = Con.prototype
        var result = Con.apply(obj, arguments)
        return typeof result === 'object' ? result : obj
    }
复制代码

说一下原型链吧?

  1. 对象都有 __proto__, 它是一个访问器属性,指向了咱们不能直接访问到的内部属性 [[prototype]]
  2. 函数都有 prototype,每一个实例对象的 __proto__ 指向它的构造函数的 prototype
    • son.__proto__ === Son.prototype
  3. 属性查找会在原型链上一层一层的寻找属性
    • Son.prototype.__proto__ === Parent.prototype
  4. 层层向上直到一个对象的原型对象为 nullnull 没有原型,并做为这个原型链中的最后一个环节。
    • son.__proto__.__proto__........ === null

举例:

class Parent {}
    class Son extends Parent{}
    
    const log = console.log;
    
    const son = new Son();
    const parent = new Parent();
    
    log(son.constructor === Son)
    log(son.__proto__ === son.constructor.prototype)
    
    log(son.__proto__ === Son.prototype)
    log(Son.prototype.__proto__ === Parent.prototype)
    log(Parent.prototype.__proto__ === Object.prototype)
    log(Object.prototype.__proto__ === null)
    
    log(son.__proto__.__proto__.__proto__.__proto__ === null)

    log(Son.constructor === Function)
    log(Son.__proto__ === Parent)
    
    log(Parent.constructor === Function)
    log(Parent.__proto__ === Object.__proto__)
复制代码

PS:因为 __proto__ 的性能问题和兼容性问题,不推荐使用。

推荐

  • 使用 Object.getPrototypeOf 获取原型属性
  • 经过 Object.setPrototypeOf 修改原型属性
  • 经过 Object.create() 继承原型

PS: for inObject.keys 会调用原型 属性

  • 不调用不可枚举属性
  • isPrototypeOf 和 hasOwnProperty

说一下 静态属性/方法 ?

静态属性/方法:就是不须要实例化类,就能直接调用的 属性/方法。

综合上面ParentSon的例子

不论是 sonSon仍是Parent,它们都是对象,因此均可以直接赋值,也能在__proto__上赋值

因此静态属性/方式直接赋值就能够了

Parent.x = 1
    Parent.__proto__.x =2

    console.log(Parent.x)  // 1
    console.log(Parent.__proto__.x) // 2
复制代码

若是使用 ES6的 Class 定义一个类

class A {
        constructor() {
            this.x = 1;
        }
        static say() {
            console.log('zmz');
        }
        print() {
            console.log(this.x);
        }
    }

    A.say()
复制代码

后期规划

  1. 近期:Promise 相关、函数式编程相关
  2. 中期:源码相关
  3. 预计代码层面写的差很少了会写软件工程相关

关注微信号"前端进阶课" 回复"Vue"、"React"、"KOA"、"全栈" 有相关课程赠送哦。文章将不按期发送阅读红包,敬请期待

相关文章
相关标签/搜索