初级—>中级的前端面试题

271 阅读6分钟

前言:

今天来梳理一下前端的几个高频面试题,我想正常前端程序员一般应该都知道答案吧。来看看今天的主要内容

this三种指向

this:谁调用我,我就指向谁

  1. 全局函数:this指向window
  2. 对象方法:this指向对象
  3. 构造函数:this指向new创建的空对象

this指向详解

 /* 
    环境对象 this : 谁'调用'我,我就指向谁 
     普通函数;  函数名()          this指向window
     对象方法:   对象名.方法名()   this指向对象
     构造函数;  new 函数名()      this指向new创建实例对象
       * 小技巧: 没点没new是window, 有new是实例,有点是点左边的对象
  */

      //作用域链
      let obj = {
        name: "张三",
        eat: function() {
          //1级链
          console.log(this) //1.obj
          function fn() {
            //2级链
            console.log(this) //2.window
          }
          fn()
        }
      }

      let eat = obj.eat
      obj.eat()

call、apply、bind区别

上下文调用的三种方法:

  1. 函数名.call()
  2. 函数名.apply()
  3. 函数名.bind()

相同点:都是修改函数this指向

不同点

  1. 传参方式不同:call用于单个参数,apply用于多个参数
  2. 执行机制不同:
  • (1)call与apply会立即执行,bind不会立即执行
  • (2)call、apply用一次修改一次;bind一次修改终身有效

call()调用函数

众所周知,默认情况下,函数内部的this无法被修改,如果想要动态的修改函数内部的this指向,则需要使用上下文调用的方法(上下文:函数作用域;上下文指向:修改函数作用域内部指向) call()的用法:语法:函数名.call(修改的this,参数1,参数2......)

function fn(a, b) {
            console.log(this);
            console.log(a + b);
        }
        fn()
        new fn()
        //函数名.call(修改的this,参数1,参数2......)
        fn.call({
            name: '隔壁老王'
        }, 20, 30)

call()的应用场景:万能数据类型检测。

  1. typeof数据检测数据类型有两种数据类型检测不了,分别是数组和null得到的都是'object'
  2. 万能数据类型检测:Object.prototype.toString.call(数据)
 //值类型
        let str = 'abc'
        let num = 123
        let bol = true
        let und = undefined
        let nul = null

        //引用类型
        let arr = [10, 20, 30]
        let fn = function () {}
        let obj = {
            name: '张三'
        }
        console.log(typeof str); //string
        console.log(typeof num); //number
        console.log(typeof bol); //boolean
        console.log(typeof und); //undefined
        console.log(typeof nul); //object
        console.log(typeof arr); //object
        console.log(typeof fn); //function
        console.log(typeof obj); //object

        /* 
        万能的数据类型检测原理
        (1)Object.prototype.toString()内部会返回的this的固定类型,得到固定格式字符串'[object, 数据类型]'
        (2)使用Object原型中的toString()要想得到数据类型,只需要把this的指向修改成你想检测的对象
        */

        console.log(Object.prototype.toString.call(str)); //[object String]
        console.log(Object.prototype.toString.call(num)); //[object Number]
        console.log(Object.prototype.toString.call(bol)); //[object Boolean]
        console.log(Object.prototype.toString.call(und)); //[object Undefined]
        console.log(Object.prototype.toString.call(nul)); //[object Null]
        console.log(Object.prototype.toString.call(arr)); //[object Array]
        console.log(Object.prototype.toString.call(fn)); //[object Function]
        console.log(Object.prototype.toString.call(obj)); //[object Object]

apply()调用函数

语法:函数名.apply(修改的this, 数组/伪数组);apply会自动遍历数组和伪数组,然后逐一传参

function fn(a, b) {
            console.log(this);
            console.log(a + b);
        }
        fn()
        new fn()
        //(1) 函数名.call(修改的this,参数1,参数2......)
        fn.call({
            name: '隔壁老王'
        }, 20, 30)
        //(2)函数名.apply(修改的this,数组/伪数组)
        // apply会自动遍历数组和伪数组,然后逐一传参
        fn.apply({
            name: '王五'
        }, [50, 60])

apply的应用场景:伪数组转真数组(伪数组:有数组的三要数(下标、元素、长度),不能使用数组的方法,本质是对象)

let obj = {
            0: 10,
            1: 20,
            2: 30,
            length: 3
        }
        console.log(obj);
 //需求:有时候需要使用真数组 需要把伪数组转换成真数组
let arr = []
//arr.push.apply(arr,伪数组)
//这里使用apply不是为了改变this的指向,而是利用apply自动遍历数组传参的特性把伪数组转换成真数组
arr.push.apply(arr, obj)
        console.log(arr);
//不过有了ES6新语法之后就很少用了
//ES6: 伪数组转换真数组,固定静态方法 Array.from(伪数组)
let newArr = Array.from(obj)
        console.log(newArr);

bind()调用函数

语法:函数名.bind(修改的this);bind()不会立即执行函数,而是得到一个修改之后的函数,用于定时器、事件处理函数

function fn(a, b) {
            console.log(this);
            console.log(a + b);
        }
        fn()
        new fn()
        //(1) 函数名.call(修改的this,参数1,参数2......)
        fn.call({
            name: '张三'
        }, 20, 30)
        //(2)函数名.apply(修改的this,数组/伪数组)
        // apply会自动遍历数组和伪数组,然后逐一传参
        fn.apply({
            name: '李四'
        }, [50, 60])

        //函数名.bind(修改的this)
        //bind不会立即执行函数,而是得到一个修改this指向之后的一个新函数
        let newFn = fn.bind({
            name: '王五'
        })
        newFn(22, 33)

bind()应用场景:修改定时器的this(定时器中的this默认指向window,如果要修改定时器的this,就需要使用bind)

setTimeout(function () {
      console.log(this);
    }.bind({name: '张三'}), 2000)

闭包

  • 什么是闭包:
  1. 第一种接地气的说法:闭包是一个访问其他函数内部变量的函数
  2. 第二种听起来高大上一点:闭包是 函数 + 上下文引用
  • 闭包作用:解决变量污染
function fn() {
      //局部变量
      let num = 10
      //fn1 + num 的组合就是闭包
      function fn1() {
        console.log(num);
        console.log('age');
      }
      fn1()
    }
    fn()

断点调试一下,更加清晰,红框所示就是闭包 image.png

递归

  • 什么是递归:函数自己调用自己
  • 递归场景:(1)深拷贝(2)遍历dom树
  • 什么是深拷贝和浅拷贝
  1. 浅拷贝:拷贝地址,修改拷贝后的数据对原数据有影响
  2. 深拷贝:拷贝数据,修改拷贝后的数据对原数据无影响
  • 浅拷贝深拷贝的两种实现方式
  1. json方式:let newObj = JSON.parse(JSON.stringify(js对象))(json底层自动深拷贝)
  2. 递归函数:见下文 浅拷贝与json方式深拷贝代码
let obj = {
            name: '张三',
            age: 20,
            sex: '男',
            hobby: ['吃饭', '睡觉', '打工']
        }

        //浅拷贝:拷贝地址
        /* let newObj = obj

        newObj.name = '李四'
        console.log(obj, newObj); */

        //json深拷贝
        //(1)JSON.stringify(js对象):把js对象 —> json字符串(json底层自动深拷贝)
        let newObj = JSON.parse(JSON.stringify(obj))
        newObj.name = '李四'
        newObj.hobby[0] = '打豆豆'
        console.log(obj, newObj);

递归方式进行深拷贝,在代码里进行说明。

let obj = {
            name: '张三',
            age: 20,
            sex: '男',
            hobby: ['吃饭', '睡觉', '学习'],
            star: {
                name: "迪丽热巴",
                height: '175cm'
            }
        }

        //深拷贝函数封装
        function kaobei(obj, newObj) {
        //key in遍历对像
            for (let key in obj) {
            //如果检测到是对象属性是数组,那么进行递归,
            //直到把数组完全复制一份再进行下一个对象属性遍历
                if (obj[key] instanceof Array) {
                    //声明新数组
                    newObj[key] = []
                    //这里key值是hobby,重新传参进行递归,将hobby复制到新的数组里
                    kaobei(obj[key], newObj[key])
                } else if (obj[key] instanceof Object) {
                    //声明空对象
                    newObj[key] = {}
                    //对象的拷贝跟数组方式一样
                    kaobei(obj[key], newObj[key])
                } else {
                //将obj里的属性值给newObj里的属性
                    newObj[key] = obj[key]
                }
            }
        }
        //(1)创建一个空对象
        let newObj = {}
        //(2)调用封装函数
        kaobei(obj, newObj)
        //现在修改newobj不会导致obj也一起修改了
        newObj.name = '李四'
        newObj.hobby[0] = '打豆豆'
        console.log(obj, newObj);

这里打断点会有更加直观的体会。

image.png