前端面试题锦集:第一期

2022-07-15 09:47:59 浏览数 (1)

封面图

面试只是起点,能力才是终局

声明变量 let, const ,var

var 声明变量存在变量提升的问题,容易造成全局变量污染。

let 声明变量只在当前作用域内有效,存在暂时性死区机制。

在变量未声明前对变量进行操作会报错,就是因为存在暂时性死区。只有在变量声明后才可对变量进行操作。

const 使用方法和let基本一致。但是const 保存当前变量的引用。意味着const声明一个Object类型的变量,依然可以修改该变量的属性

new 操作符执行的过程

使用new操作符会执行以下过程:

  1. 在内存中创建一个新对象。
  2. 这个新对象内部的[[prototype]]被赋值为构造函数的prototype属性。
  3. 构造函数内部的this被赋值给这个新对象。
  4. 执行构造函数内部的代码,给新对象添加属性。
  5. 如果构造函数返回非空对象,则返回该对象;否则返回创建的新对象。

Class类构造函数 和 构造函数的 区别

  1. 调用class类构造函数必须使用new操作符。
  2. 普通构造函数如果不使用new操作符,就以全局对象作为内部对象。

对象,函数,原型对象 ,prototype ,proto 和 [[Prototype]]的关系

这几个名词的关系比较容易混淆。

基本关系是:

每个函数本质上是一个对象。每个函数都有一个prototype属性和__proto__,prototype属性的指向是原型对象。[[Prototype]]可以理解为浏览器内部实现的一个原型对象的原型。可以使用__proto__属性访问到[[Prototype]]

this, call , apply , bind

this 的指向大致分为4种:

  • 作为对象的方法调用

当函数作为对象的方法被调用时,this 指向该对象

  • 作为普通函数调用

当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式,此时的 this 总是指 向全局对象。

  • 构造器调用

当用 new 运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的 this 就指向返回的这个对象。

  • Function.prototype.call 或 Function.prototype.apply 调用

跟普通的函数调用相比,用 Function.prototype.call 或 Function.prototype.apply 可以动态地改变传入函数的 this。

call 和 apply其实是Function的原型上的两个方法。也就是Function.prototype.call 或 Function.prototype.apply。

bind被用来修改函数内部的this指向,大部分浏览器已经内置了Function.prototype.bind。当然我们也可以手动实现一个bind

代码语言:javascript复制
Fucntion.prototype.bind = function(ctx){
  let self = this;
  return function(){
    return self.apply(ctx,arguments)
  }
}

有几个地方经常用到call或者apply:

  1. 类型判断
代码语言:javascript复制
const isType(type) => { 
  return Object.prototype.toString.call(obj) === `[object ${type}]`
}
  1. 类数组对象转为数组
代码语言:javascript复制
Array.prototype.slice.call(arrLike)
  1. 求数组中的最值
代码语言:javascript复制
function smallest(array){ 
  return Math.min.apply( Math, array ); 
} 
function largest(array){ 
  return Math.max.apply( Math, array ); 
} 

function smallest(){ 
  return Math.min.apply( Math, arguments ); 
} 
function largest(){ 
  return Math.max.apply( Math, arguments ); 
} 

高阶函数

高阶函数是指将函数作为参数或者返回值的函数。将函数作为参数可以将变化的部分封装起来,隔离代码中变化和不变的部分。

判断数据类型:

代码语言:javascript复制
const isType = (type) => {
  return (obj) => { Object.prototype.toString().call(obj) === `[object ${type} ]`}
}

节流函数

节流函数是限制函数调用频率的技术。比如 resize,mousemove,scroll事件。节流函数的实现原理是:将被执行函数用定时器延时一段时间后执行。如果本次没有执行完,则忽略调用函数的请求。

代码语言:javascript复制
  let throttle = function (fn,interval) {

    let timer = null
    let self = this

    return function (){
      let args = arguments;
      let firstTime = true;

      if(firstTime){
        fn.apply(self,args)
        return firstTime = false
      }
      if(timer){
        return 
      }

      timmer = setTimeOut(()=>{
        clearTimeout(timer)
        fn.call(self,args)
      },interval || 5000)

    }

  }

防抖函数

防抖这个技术点允许我们将多个相似的调用分成一组,或者可以理解为多个相同的事件最后只执行一次

代码语言:javascript复制
let debounce = function(fn){
  let timer = null
  return function(){
    timer = setTimeout(()=>{
      fn.call(this,argumnets)
    },500)
  }

}

防抖的应用场景:按钮重复点击search输入框鼠标事件

节流和防抖的区别在于:一个是定时执行,一个是只执行一次

class类中的constructor 和 super

  1. constructor 是类的构造函数,这个是非必需的。不定义构造函数相当于构造函数为空。
  2. 派生类的方法可以通过super关键字引用他们的原型。
  3. 在类构造函数中可以使用super调用父类的构造函数。
  4. 从这些方面来看,我们在定义React组件的时候,props实际上是构造函数的参数。

React组件中的props是什么

我们在定义React组件的时候,props实际上是构造函数的参数。

浅复制 和 深复制

浅复制只复制对象的引用地址,并不复制对象本身,新旧对象共用一个块儿内存。深拷贝会创建一个一模一样的对象,且新旧对象并不共享内存,修改新对象不会修改元对象。

浅拷贝方法:Object.assign(),Array.prorotype.concat(),Array.prototype.slice()

深拷贝方法:JSON.parse(),lodash._cloneDeep()

浏览器的事件循环模型

对象被分配在对内存中;基本类型的数据存放在栈内存;

一个Javascript运行时包含了一个待处理消息的消息队列。每个消息队列都关联着这个消息的回调函数。

在事件循环的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会移除队列,并作为输入参数来调用与之关联的函数。函数的处理会一致进行到执行栈为空为止;然后事件循环就会处理队列中的下一个消息。

异步非阻塞

异步非阻塞,我们一直在说异步非阻塞这个词。到底什么是异步非阻塞?

我觉得可以这样理解:

异步的目的是为了非阻塞。也就是不阻塞。

setTimeout, setInterval为什么是异步的?

这个问题很难从底层原理解释清楚,但是我们可以通过下面的代码了解到这个特点:

代码语言:javascript复制
console.log(1)
setTimeout(()=>{console.log('set-time-out')},3000)
console.log(2)

结果如下:

打印出1 之后,并没有执行setTimeout, 而是等到2打印出来之后,才打印set-time-out。

原理就是上面提到的浏览器的事件循环机制,简单点说就是当执行栈里的执行的代码遇到异步的代码时,会将它添加到消息队列中,等到执行栈里的代码执行完成后,会处理消息队列中的代码,依次循环往复。

任务 微任务 宏任务

(macro)task,宏任务可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:

(macro)task->渲染->(macro)task->...

宏任务包含:

  • script(整体代码)
  • setTimeout
  • setInterval
  • I/O
  • UI交互事件
  • postMessage
  • MessageChannel
  • setImmediate(Node.js 环境)

microtask,微任务可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。

所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。

微任务包含:

  • Promise.then
  • Object.observe
  • MutaionObserver
  • process.nextTick(Node.js 环境)

Promise基本原理

Promise的实现本质上是将回调函数封装在内部,其实现方式类似于发布订阅模式,then的时候将回调push到缓存数组中,resolve的时候遍历缓存数组执行回调函数。

依据Promise A 规范。三种状态,thenable, 执行器。逐渐完善

代码语言:javascript复制
//极简的实现
class Promise {
    callbacks = [];
    constructor(fn) {
        fn(this._resolve.bind(this));
    }
    then(onFulfilled) {
        this.callbacks.push(onFulfilled);
    }
    _resolve(value) {
        this.callbacks.forEach(fn => fn(value));
    }
}

0 人点赞