likes
comments
collection
share

我的2023前端面试准备-js篇

作者站长头像
站长
· 阅读数 12

书接上文,这篇是关于js篇的面试题,js部分的话有些是我用自己的话总结出来的,脑子边想边敲出来,如果不正确的希望有小伙伴指出

1、JS中的this指向,普通函数和箭头函数的区别

this指向与函数调用方式和调用位置有关,与定义的位置无关,分为以下几种情况

  • 默认绑定:声明一个函数foo,直接用foo()方式调用,this指向全局
function foo(){
   console.log(this)
 }
 foo() //window
  • 隐式绑定:通过一个对象进行调用,this指向这个对象
const obj = {
      name: '233',
      age: 23,
      title: 'gegd',
      foo:function(){
        console.log(this)
      }
  }
 obj.foo() //obj
  • 显示绑定:通过call、apply、bind明确指明this绑定的对象

apply:apply(thsiObj,[arg1,arg2,...])

call:call(thisObj,arg1,arg2,....)

bind:bind()方法总是创建一个新的绑定函数(怪异函数对象),这个函数的this指向bind()的第一个参数,其余参数作为新函数的参数

   function foo() {
      console.log(arguments)
      console.log(this)
    }
    foo.apply("thisObj1",[1,2,3,4]) //thisObj1
    foo.call("thisObj2",1,2,3,4) //thisObj2
    const _foo = foo.bind("thisObj3",1,2,3,4) //thisObj3
    _foo() //thisObj3

我的2023前端面试准备-js篇

  • 4、通过new绑定:通过new创建出来的对象,构造函数里面的this指向这个对象

new绑定步骤:

①创建一个对象

②将这个对象的__proto__指向构造函数的prototype

将this指向这个对象

④执行构造函数中的代码

⑤若函数无返回其他对象,则返回这个新创建的对象

2、普通函数和箭头函数的区别

①this:普通函数的this指向与这个函数是怎么调用的有关系,箭头函数中是没有绑定this的,他的this指向他上一层作用域里面的this

   const bar = function () {
      const _bar = {
        __bar: () => {
          console.log(this)
        }
      }
      return _bar.__bar()
    }
    bar() //window

②arguments:箭头函数中没有arguments这个参数,在箭头函数中访问这个arguments实际上是获取他外层函数的arguments

 const bar = function (name, age) {
      const _bar = {
        __bar: (_name, _age) => {
          // console.log(this)
          console.log(arguments)
        }
      }
      return _bar.__bar("_黄", '_23')
    }
    bar("黄", 23) 

我的2023前端面试准备-js篇

③prototype:箭头函数没有prototype这个属性,所以他不能作为构造函数使用去new一个对象

④其他:箭头函数写法可以更简洁,参数只有一个可以省略括号,返回值只有一句可以省略大括号且必须省略return;call/apply/bind不能改变箭头函数的this指向,要么指向window要么指向上一层作用域的this

3、JS代码执行过程(执行上下文,作用域(链),变量提升等内容)

①js引擎在代码执行之前,先在堆内存中,创建一个GO全局对象,这个对象除了常见的全局属性Date、Array、setTimeout等还有一个window属性,指向自己

②同时js引擎内部还有一个执行上下文栈(ECS),是一个用来执行代码的调用栈,全局代码为了执行会创建一个全局执行上下文(GEC),放入ECS中

③GEC放入ESC的过程有以下俩部分:1、代码执行之前,在parser将js代码转换成AST的过程中,会将所有定义在全局的变量和函数声明放到GO对象中,变量不会赋值,但函数声明会在GO对象中保存一个指向该函数(FO)的指针,这个过程就叫变量提升函数声明提升 2、代码执行过程中,对变量赋值并执行对应的函数

④每一个执行上下文都会关联一个VO对象,变量和函数声明都会被添加到VO对象中,GEC的VO对象就是堆内存中的GO对象

⑤全局代码执行过程到一个函数的时候,会在ECS中创建一个函数执行上下文(FEC),压入到ECS中,FEC关联的VO对象叫AO,AO会使用函数的arguments来作为初始化,并将参数作为他的初始值,在函数中的代码执行完成之后,这个FEC就会出栈,继续执行后面的代码

作用域 和作用域链:作用域是可访问函数、对象、变量的集合,最外层函数和最外层函数定义的变量拥有全局作用域,那些没有定义直接赋值的变量也放在全局作用域中,局部作用域是函数内定义的变量,一般内层作用域可以访问外层作用域,反之则不行,let和const声明的变量也拥有块级作用域。

作用域链:每一个执行上下文都会有关联一个作用域链,这个作用域链是一个对象列表,用来在变量标识符进行求值,当进入一个执行上下文是,这个作用域链就会被创建,然后根据代码类型添加一系列对象,当我们查找一个变量的时候,首先会在自己的作用域查找,找不到再继续沿着作用域链查找,直到找到全局作用域也没有的话,就会返回undefined

4、变量提升和函数提升的区别

代码执行之前,会将var定义的变量和函数声明提升到当前作用域的前面,变量提升只提升声明不提升赋值,函数声明只提升声明不提升调用,在函数声明之前是可以调用的,但函数表达式没有函数提升一说,不能在表达式之前调用

5、原型和原型链

每一个js对象都会有一个内置对象__proto__,指向另外一个对象,当我们通过key来访问某一个属性的时候,会触发对象的[Get]操作,如果在当前对象找不到该属性的话,就去他的__proto__找,获取这个内置属性的方法有两个,一个是通过obj.proto,一个是使用Object.getPrototypeOf()获取

每一个函数都会有prototype对象,称之为原型,他的作用是当这个函数作为构造函数new一个新的对象时,会将Fun.prototype赋值给新对象的__proto__

function Function() { }
const foo = new Function()
console.log(foo.__proto__ === Function.prototype) //true

原型对象上都会有一个construtor属性,打印出来这个属性是指向构造函数本身的

function Function() { }
const foo = new Function()
console.log(Function.prototype.constructor)

我的2023前端面试准备-js篇

使用构造函数创建对象的过程在内存中的表示如下图所示

暂时无法在飞书文档外展示此内容

由于Function.prototype也是一个对象,所以也拥有__proto__属性,当我们需要查找的属性在Function.prototype也没有的话,回去Function.prototype.__proto__上面去查找,Function.prototype.proto === Object.prototype,直到找到Object.prototype.__proto__也找不到的话就返回undefined,这个查找的过程就形成了原型链,原型链的最上层是Object.prototype.proto,他指向null

6、闭包&闭包场景

当我们在一个函数里面创建另一个函数,并在里面的函数去访问外部函数的变量,这个过程就形成了闭包,闭包的作用第一个是用来创建私有变量,当外部函数把内部的函数return出来,通过在外部调用这个闭包函数,可以访问到函数内部的变量,以此来创建私有变量,第二个作用是在函数执行完成之后,被闭包引用的变量不会被GC回收,因为内存中还保留着对这个变量的引用

闭包常见的应用场景譬如:防抖节流、柯里化

7、什么是内存泄露,哪些操作会造成内存泄露,如何解决

内存泄漏是由于对象,变量,定时器、闭包等这些内存不需要而长期没有被释放,一直存在内存中,没有办法被GC回收,最终导致内存泄漏

导致内存泄露的操作:

①使用JS获取DOM对象,之后该对象从页面移除,保存的js对象一直存在着对这个对象的引用,不能被回收

②没有及时清除定时器:使用了setInterval()或setTimeout(),并在定时器中引用了外部的变量,在不需要使用的时候必须移除

③定义了一些全局变量,在不使用的时候应该把这个变量删除或者赋值为null

④没有正确的使用闭包,我们应该把闭包需要使用的外部变量定义在外部函数中,而不是定义在全局或者是外部函数外,并且在使用闭包的时候只保留一些必要的引用,并且在不需要用到的时候赋值为null或者删除

8、数组的常用方法

pop()、push()、shift()、unshift()、reserve()、sort()、slice()、splice()、filter()、map()、reduce()、toString()、toLocalString()、join()、cancat()

9、防抖&节流

防抖:事件在触发之后,在N秒时候才会执行,如果重复触发的话,则重新开始计时,一般用在按钮多次点击提交,表单验证输入等场景

节流:对于频繁操作的事件,每N秒只会执行一次,一般用在拖拽,缩放窗口,拉动滚动条等事件中

代码实现见常考手写题

10、js实现继承的方法(es5(组合借生,寄生组合),es6(class))

  1. 原型链 继承 基于原型链创建对象之间的继承关系。通过将子类的原型指向父类的实例,使得子类可以访问父类的属性和方法。但是在继承过程中,子类共享了父类的属性和方法,如果在子类中修改了继承的属性,会影响到其他子类实例。
javascript复制代码
function Parent() {
  this.name = 'Parent';
}

function Child() {}
Child.prototype = new Parent();

var child = new Child();
console.log(child.name); // 输出: "Parent" 
  1. 构造函数 继承 使用父类的构造函数来增强子类实例,即在子类的构造函数中调用父类的构造函数,从而继承父类的属性和方法。这种方式解决了原型链继承中属性共享的问题,但无法继承父类原型上的方法。
javascript复制代码
function Parent() {
  this.name = 'Parent';
}

function Child() {
  Parent.call(this);
}

var child = new Child();
console.log(child.name); // 输出: "Parent" 
  1. 组合 继承 (伪经典继承): 结合了原型链继承和构造函数继承的特点。使用原型链实现对父类原型上方法的继承,使用构造函数继承实现对父类属性的继承。

(将子类的原型指向父类的实例,继承父类原型上的方法,在子类的构造函数中去调用父类的构造函数,继承父类的方法)

javascript复制代码
function Parent() {
  this.name = 'Parent';
}

function Child() {
  Parent.call(this);
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;

var child = new Child();
console.log(child.name); // 输出: "Parent" 
  1. 原型式 继承 借助一个临时构造函数创建一个中间对象,然后将该中间对象的原型指向父类对象。通过这种方式可以创建一个新的对象,并且可以在该对象上添加额外的属性和方法。这种继承方式共享了父类对象的属性和方法。
javascript复制代码
function createObject(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}

var parent = { name: 'Parent' };
var child = createObject(parent);
console.log(child.name); // 输出: "Parent" 
  1. ES6 类 继承 利用ES6中引入的class关键字来定义类,使用extends关键字实现继承。子类通过继承父类,可以直接使用父类的属性和方法,并且可以自定义自己的属性和方法。
javascript复制代码
class Parent {
  constructor() {
    this.name = 'Parent';
  }
}

class Child extends Parent {
  constructor() {
    super();
  }
}

var child = new Child();
console.log(child.name); // 输出: "Parent" 

11、for..in和for...of的区别

for..in...会遍历到对象的原型链上,包括原型链上的可枚举属性,性能比较差,for..of...只会遍历可迭代对象,遍历出来的值是每个下标对应的值L

12、如何将类数组转为真实数组

①Array.from()

②遍历类数组,添加到一个新的数组中

③Array.prototype.slice()

④Array.prototype.splice()

⑤剩余参数[...arguments]

function foo(arg1, arg2, ...args) {
    console.log(arguments)
    console.log(Array.from(arguments))
    console.log(Array.prototype.slice.call(arguments))
    console.log(Array.prototype.splice.call(arguments, 0))
    console.log(Array.prototype.splice.cancat([], arguments))
}
foo(99, 88, 77, 66, 55, 44)

13、判断两个对象是否相等

①使用全等运算符”===“,可以判断两个比较对象的引用是否相同,相同的话返回true

②使用Object.keys获取对象的键,通过遍历判断对象里面键相同的属性值是否也相同,这种方法不会去比较对象的地址是否指向同一个

③使用工具库lodash里面的isEqual方法,与②类似,可以对复杂类型多层比较

④JSON.stringify,如果对象里面只有基本数据类型可以使用该法对对象转化成字符串进行比较

⑤Object.is(),与===类似,可以对地址的引用进行判断,但是Object.is(+0,-0)返回的是false,而使用全等返回的是true,使用Object.is(NaN,NaN)返回的是true

14、Promise,Promise.all,Promise.race,Promise.allsettle的区别

  • 什么是promise,promise是一个用来代表异步操作结果的对象,是一种异步编程的解决方
  • 解决的困境:在Promise出现之前,我们解决异步编程的方式是通过在回调里面嵌套,这种方式可能会随着我们业务逻辑的增加导致回调地狱,代码可读性很差且难以维护,所以Promise的写法更加简洁直观,且可以直接对异常结果进行处理
  • Promise是一个构造函数,当我们创建一个Promise对象的时候,需要往构造函数里面去传入一个executor回调函数,这个回调函数会被立即执行,并且在调用resolve()的时候会去执行.then里面的代码,调用resolve()的时候执行.catch里面的代码
  • Promise有三种状态,pending,fullfilled,rejected,状态的改变只能由pending=>fullfilled或由pending=>rejected,且状态一旦确定下来就不能再变了,执行executor里面的resolve的时候状态变为fullfilled,并将resolve()里的参数作为.then()回调函数里面的参数,执行reject()的时候状态变为rejected(),
  • resolve里面的值的类型分为以下几种:①传入一个普通的值和对象,这个值作为.then回调函数里面的参数②如果传入的是一个新的Promise,这个新的Promise的状态会决定原来Promise的状态③如果传入的是一个thenable对象,也就是实现一个then方法的对象,这个then的状态会决定原来Promise的状态
  • then方法是Promise的一个实例方法,接受两个参数,一个是状态变为已决议的时候调用的,一个是状态变为拒绝的时候调用的。
  • .then()方法本身返回的也是一个Promise,他的Promise的状态是由.then()方法的返回值决定的,①返回一个普通值,这个普通值作为链式调用中的.then()的回调里面的参数②返回一个新的Promise,这个.then()返回Promise的状态由这个新的Promise的状态来决议③返回一个返回一个thenable对象,这个链式调用中.then()返回的Promise的状态由这个thenable对象中的then方法决议

   ps:如果在.then()中抛出一个异常,那么这个then方法返回的Promise变成reject状态

  • .catch方法与.then类似,两者都是Promise的实例方法,都可以被多次调用
  • .finally()也是Promise的实例方法,是Promise无论变成fullfilled还是rejected最终都会执行的一个方法,并且不接收参数
  • .then,.catch,.finally都是Promise的实例方法,Promise还有一些类方法

   ①Promise.resolve(),等价于new Promise((resolve,reject)=>{resolve()}

    ②Promise.reject(),等价于new Promise((resolve,reject)=>{reject()},reject()参数的状态不管是什么,最终都会去执行.catch方法

    ③Promise.all(),all方法的参数是将多个Promise以数组形式传递进去,这个方法会返回一个新的Promise,当所有的Promise的状态都变为fullfilled的时候,新的Promise的状态也会变为fullfilled,并将suoyouPromise的返回值形成数组返回,当有一个变为rejectd状态时,新的Promise的状态也变为rejected

    ④Promise.allSettled(),all方法一旦有一个promise的状态变成reject状态就确定下来了,有些成功状态的promise的返回值也查看不了,allSettled方法不管你的Promise状态是成功还是失败,最后.allSettled都会返回他们的状态和返回值,并放到数组中作为新的Promise的.then回调里面的参数

    ⑤Promise.race(),race有竞赛的意思,也是包裹Promise传入一个数组,哪个Promise状态先确定下来就以那个Promise的状态作为最终的状态

    ⑥Promise.any(),any有任意的意思,他会等到有一个Promise状态变为fullfilled,就去使用那个Promise的结果

15、async和await与Promise

async定义的函数是一个异步函数,这个异步函数的返回值是一个Promise,

  • 如果返回的是一个普通值a,则这个普通值到包裹在一个Promise.resolve中
  • 如果返回的是一个Promise,异步函数的状态由这个Promise决定
  • 如果返回的是一个thenable对象,则异步函数的状态由这个对象中的then方法决定
  • 如果在async中抛出了异常,不会报错,而是把错误信息作为这个异步函数的reject来传递
  • await关键字可以在async函数中使用,他后面的表达式一般会返回一个Promise,只有当Promise的状态变为fullfilled的时候才会去继续执行后面的代码

16、什么是Iterable对象,和Array有什么区别

  • 迭代器是帮助我们对一些数据结构进行遍历的对象,它内部需要有一个next函数,返回一个包含done和value书香的对象

  • 可迭代对象:需要符合以下几点

    •   ①实现一个[Symbol.interator] ②这个函数必须返回一个迭代器用来迭代我们的对象(调用next方法)
      • 可迭代对象可以通过obj[Symbol.interator]拿到一个函数,执行这个函数的时候可以返回一个迭代器,这个迭代器可以调用next()方法拿到可迭代对象里面的值
      • 可迭代对象可以使用for...of...进行遍历
    •   目前已经有很多原生对象也实现了可迭代协议,创建出来的对象也属于可迭代对象,比如String,Array,Set,Map,NodeList,arguments
    • 我的2023前端面试准备-js篇
  • 与数组相比,迭代器对象的主要区别如下:

    • 数据结构不同: 数组是一种特定的数据结构,具有有序的、可变长度的列表形式。而迭代器对象可以适用于任何数据结构,只要实现了Iterable接口即可。
    • 访问方式不同: 对于数组,我们可以使用索引来访问其中的元素,例如array[0]。而对于迭代器对象,我们通过调用next()方法来逐个获取元素。
    • 实时计算数据: 迭代器对象通常是基于惰性计算的原则,只在需要时才会生成下一个元素。这意味着它可以处理无穷大或者非常庞大的数据集,而不会一次性全部加载到内存中,从而节省了内存资源。
    • 更灵活的遍历方式: 数组的遍历方式通常是线性的,从第一个元素到最后一个元素。而迭代器对象可以自定义遍历的方式,例如可以跳过某些元素、反向遍历等。
    •   总之,迭代器对象提供了一种更抽象、更通用的方式来遍历数据集,相比于数组更加灵活、节省资源,并且可以适用于各种不同的数据结构。
    •   补充:如果想要让一个可迭代对象在遍历的时候终端,可以在迭代器对象中添加一个return函数,并返回一个{done:true}对象

17、简述Object.defineProperty

如果我们想要对一个对象的属性进行精准控制的话,比如是否可以通过delete删除,是否可以被Object.keys或者for..in..遍历到,那就可以通过Object.defineProperty来精准的添加或修改某个属性

Object.defineProperty接收三个参数,第一个参数就是要修改的对象,第二个参数是要添加或修改的属性,第三个参数是对这个属性的属性描述符,这个方法会把修改后的对象返回

属性描述符有两种:数据属性描述符和存取(访问器)属性描述符

这两种描述符都有对应的四种特性,其中两种是共有的,另外两种是互斥的

共有的:

①[configurable]:用来决定属性是否可以通过delete删除、是否可以修改他的特性、是否可以转化成另外一种属性描述符

数据属性描述符,通过obj.pros这种方式添加或修改属性的话,[configurable]默认是true,通过Onject.defineProperty添加或修改的属性[configurable]默认是true

②[enumerable]:用来决定属性是否可以被Object.keys获取,或者是否可以被for..in..遍历,通过obj.pros的方式,默认值是true,否则默认值是false

数据属性描述符独有的:

①[writable]:是否可以修改,当我们直接在一个对象上定义某个属性时,这个属性的[[Writable]]为true;通过属性描述符定义的话默认值是false

②value:该属性的值,默认情况下是undefined

存取属性描述符独有的:

①[get]:获取属性时会执行的函数

②[set]:设置属性值的时候会执行的函数

18、事件循环(宏任务&微任务)

javsScript是单线程的,同一时刻只能做一件事情,对于一些耗时的操作比如dom操作,定时器,网络请求等,浏览器会将他们放在队列中排队去等待执行上下文栈为空的时候,在放入栈中去执行,js中的队列又分为微任务队列和宏任务队列,每次执行宏任务队列的任务时,会先去判断微任务队列是否为空,不为空的话就先去执行微任务队列的任务,这个循环的过程就称为事件循环,一般setTimeout,setInterval,dom操作,网络请求等会被放到宏任务中,promise.then(),queueMicrotask()会被放到微任务队列中

19、垃圾回收机制

浏览器内核分为webCore和jsCore,其中jsCore是用来执行我们的js代码的,chrome里面的jsCore指的就是V8引擎,js代码执行的时候都需要为他们分配内存,js这门语言的内存管理是由V8引擎帮我们管理的,我们不需要手动进行管理,当我们内存中的对象变量等不再需要的时候,js引擎帮我们对这些对象进行释放,以便浏览器回收,这就是垃圾回收机制(GC回收)

GC判断js中的变量不再需要使用,常用的几种算法如下:

引用计数:GC会去查找那些有被引用到的对象,如果被引用了,这个对象的引用就+1,如果对象的引用时是0就说明可以被回收,这个方法可能会导致循环引用

标记清楚:会去设置一个根对象,GC会定期从这个根对象开始去查找有引用到的对象,给他们做标记,没有引用到的对象就说明是不可用的可以被回收,这个方法解决了循环引用的问题

标记整理:类似于标记清楚,但是把那些有被引用到的对象放到一个连续的存储空间中,避免内存碎片化

闲时收集:只会在CPU空闲的时候才会运行

增量收集:如果一次性去遍历一个大的对象集,可能会需要花费比较多的时间,导致代码运行造成一个比较大的延迟,所以可以将垃圾收集工作分成几个部分来做,对这些任务逐一处理,这样就是几个小的延迟而不是一个大的延迟

分代收集:浏览器会把对象分为新的和旧的,对于那些一出现就马上被取消的对象,称之为新的,而有一些对象在内存中存在了很久一直没有被回收的,称之为旧的对象,浏览器会逐渐减少对他们的检查频次

20、Map和Set区别

这两个是es6新增的两个数据结构,与之对应的是weakSet、weakMap

Set:Set是与数组类似的一个数据结构,当时里面的对象是不能重复的,用来给数组去重

常见的方法:size()、add()、delete()、has()、forEach()、clear()

WeakSet:存取不重复的元素,与set的区别是①WeakSet存储的数据只能是对象 数据类型,不能是基本数据类型 ②对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,GC可以对这个对象进行回收,不能被遍历,造成弱引用的对象不能被销毁

常见的方法:add()、delete()、has()

Map:用于存储映射关系,对象存储映射关系时属性只能是字符串或者是Symbol,Map的键可以是其他复杂类型比如对象,可以被遍历

常见的方法:size()、set()、get()、has()、delete()、clear()、forEach()

WeakMap:也是存储键值对,weakMap的键只能是对象类型,不接受基本数据类型,键对对象的引用也是弱引用,若果没有引用引用这个对象的话,那么这个对象可以被GC回收,也是不能被遍历的,用在深拷贝中解决循环引用的问题

21、forEach和map的区别:

forEach()方法会针对每一个元素执行提供的函数,对数据的操作会改变原数组,该方法没有返回值

map()方法不会改变原数组的值,有返回值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值

22、数组去重的方式

①set数据结构里面的元素不能重复,不能去重引用数据类型的元素

②双重for循环,使用两个for循环,判断是否有相同元素,有的话就是用splice(x,1)删除这个重复的元素

③使用indexOf去重,创建新对象,对数组进行遍历,对每个元素都是用indexOf判断,把不重复的元素加到新的数组中

④与③类似,使用includes去判断

⑤使用filter配合indexOf去重,indexOf()如果存在的话会返回元素对应的下标,如果不是重复值的话,indexOf返回的值跟你下标对应的值是一样的,把他们作为一个新的数组返回

23、let和const怎么转化成es5

转载自:https://juejin.cn/post/7270503053358694440
评论
请登录