2023前端大厂面试题之JavaScript篇(1)

系列文章:
2023前端大厂面试题之JavaScript篇(1)
2023前端大厂面试题之JavaScript篇(2)
2023前端大厂面试题之JavaScript篇(3)
2023前端大厂面试题之JavaScript篇(4)
2023前端大厂高频面试题之CSS篇
2023前端大厂高频面试题之Vue篇(1)
2023前端大厂高频面试题之Vue篇(2)
2023大厂高频面试题之HTTP篇
2023前端大厂高频面试题之浏览器篇
2023大厂高频面试题之操作系统篇
2023大厂高频面试题之计算机网络篇
2023大厂高频面试题之项目篇

📒博客首页:若年封尘
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
🙏作者水平很有限,如有不妥,不吝赐教!
❤️期待一起交流,共同进步!

js为什么是单线程的

DOM 操作:JavaScript 最主要的用途之一是操作浏览器中的文档对象模型(DOM),即网页的结构和内容。多线程在处理 DOM 操作时可能会导致不可预测的结果,在同一时间同时操作DOM,一个增加,一个删除,JS就不知道到底要干嘛了。通过单线程模型,可以确保 DOM 操作的顺序性,避免潜在的冲突。

防止并发问题(竞态条件):多个线程在同一时间修改共享数据,可能导致数据不一致或错误。通过将 JavaScript 设计为单线程,可以避免这些潜在的并发问题。

虽然 JavaScript 本身是单线程的,但是现代的浏览器和 Node.js 等环境采用了一些技术,如 随着HTML5到来js也支持了多线程Web Worker 但是也是不允许操作DOM和 Node.js 的事件循环机制,以实现部分并发性、异步编程,从而更好地利用多核处理器。

Web Worker

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。
推荐阅读:Web Worker 使用教程MDN文档

js中的同步和异步

同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务。
异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。异步模式:每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。
JavaScript的异步机制:
(1)所有同步任务都在主线程上执行,行成一个执行栈
(2)主线程之外,还存在一个任务队列,只要异步任务有了结果,就会在任务队列中放置一个事件
(3)一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面还有哪些事件,那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
(4)主线程不断的重复上面的第三步

异步编程的实现方式

1.回调函数:这是异步编程最基本的方法。
回调函数:当一个函数作为参数传入另一个参数中,并且它不会立即执行,只有当满足一定条件后该函数才可以执行,这种函数就称为回调函数。它的优点是简单、容易理解和部署,缺点是多个回调函数嵌套的时候会造成回调地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
2.事件监听:事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
3.发布/订阅:假定存在一个"信号中心",某个任务执行完成,就向信号中心"发布"一个信号,其他任务可以向信号中心"订阅"这个信号,从而知道什么时候自己可以开始执行,又称"观察者模式"。
4.Promise:在程序中的意思就是承诺我过一段时间后会给你一个结果。可以将嵌套的回调函数作为链式调用。每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。f1().then(f2);//f1的回调函数f2但有时会造成多个 then 的链式调用,造成代码语义不够明确。
Promise的三种状态:Pending(初始);Fulfilled(成功);Rejected(失败)
5.Generator:ES6 提供的一种异步编程解决方案,当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。
6.async/await(异步终极解决方案):generator 和 promise 实现的一个自动执行的语法糖。

Promise

JS是单线程的,使用回调函数异步编程的话会造成回调地狱问题,Promise就是用来解决这个问题的异步编程解决方案。
状态:pending(初始态)、fulfilled(已成功)、rejected(已失败)。
实例方法:then()是实例状态发生改变时的回调函数、catch()用于指定发生错误时的回调函数、finally()用于指定不管 Promise 对象最后状态如何,都会执行的操作。
构造函数方法:all()、race()、allSettled()、resolve()、reject()、try()。
Promise.all()方法用于将多个 Promise实例,包装成一个新的 Promise实例,都成功才成功,有一个失败即失败:

const p = Promise.all([p1, p2, p3]);

Promise.race()方法率先改变的 Promise 实例的返回值则传递给p的回调函数;
Promise.allSettled()方法只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束;
Promise.resolve()用于将现有对象转为 Promise对象,参数可以分成四种情况,分别如下:
1.参数是一个 Promise 实例,promise.resolve将不做任何修改、原封不动地返回这个实例
2.参数是一个thenable对象,promise.resolve会将这个对象转为 Promise对象,然后就立即执行thenable对象的then()方法
3.参数不是具有then()方法的对象,或根本就不是对象,Promise.resolve()会返回一个新的 Promise 对象,状态为resolved
4.没有参数时,直接返回一个resolved状态的 Promise 对象
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s) {
  console.log(s)
});
// 出错了

Promise和async/await

1.编程风格:
Promise 使用了链式调用(.then().catch()),通常需要编写嵌套的回调函数,这可能导致回调地狱(Callback Hell)。
async/await 提供了可读性更高、更优雅的方式来编写异步代码,使代码看起来更像同步代码,而不需要嵌套回调。

2.错误处理:
在 Promise 中,错误处理通常通过 .catch() 或在每个 .then() 中使用第二个回调函数来处理。
在 async/await 中,可以使用 try…catch 来捕获和处理错误,使错误处理更直观。

3.代码结构:
使用 Promise 时,你需要创建一个新的 Promise 实例并定义执行的异步操作,然后在链式的 .then() 和 .catch() 中处理结果和错误。
async/await 使得代码更线性,你可以在函数内使用 await 关键字等待异步操作完成,然后直接处理结果或错误。

4.错误传播:
Promise 中的错误会在整个链中传递,需要在适当的位置处理。
async/await 允许你使用 try…catch 捕获错误,避免错误在整个链中传播。

5.并行操作:
使用 Promise.all() 可以同时执行多个异步操作,并等待它们全部完成。
async/await 通常用于串行执行异步操作,等待一个完成后再执行下一个。

async await与Promise一样,是非阻塞的。async await是基于Promise实现的,可以说是改良版的Promise,它不能用于普通的回调函数。

闭包closure

闭包概念
在js中函数内部可以直接读取全局变量的,但在函数外部无法读取函数内部的局部变量。所以就有了闭包的概念,闭包的话就是能够在一个嵌套的函数中,内层函数中访问到其外层函数的作用域,从而使这个外层函数的变量可以被外部访问到,它最大的作用就是延续函数内部某些变量的生命周期。 本质上,闭包是将函数内部和函数外部连接起来的桥梁。

函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B就被称为闭包。(一个嵌套的function,里面的function读取了外面这个function里的变量。)

为什么会导致内存泄漏问题?
闭包可能导致内存泄漏问题,因为闭包可以使函数维持对外部作用域中变量的引用,导致这些变量无法被垃圾回收,即使在原本应该被销毁的情况下也是如此。这可能会导致内存占用不断增加,最终影响应用程序的性能。

导致闭包内存泄漏的情况:
未释放资源: 如果闭包函数持续引用一些资源,比如 DOM 元素、计时器或网络请求,即使这些资源在闭包外部已经不再需要,它们也不会被垃圾回收,从而导致内存泄漏。

循环引用: 当闭包函数与其外部作用域中的对象之间存在循环引用时,这些对象可能无法被垃圾回收,因为闭包会保持对这些对象的引用。这种情况下,即使你认为这些对象不再需要,它们仍然会占用内存。

闭包未被释放: 如果闭包本身没有被正确地释放,它会继续存在并引用其外部作用域中的变量,从而阻止这些变量被垃圾回收。

避免闭包导致内存泄漏的方法:
及时释放资源: 在不再需要时,手动清除闭包函数中引用的资源,如取消计时器、移除事件监听器,或释放其他类型的资源。

避免循环引用: 尽量避免在闭包函数和外部对象之间创建循环引用关系,以确保对象在不需要时能够被垃圾回收。

手动解除引用: 当你确定不再需要一个闭包函数时,手动将其设置为 null,从而解除对外部作用域中变量的引用。

使用适当的范围: 如果你只需要临时的访问,考虑在不需要闭包的情况下执行操作,以确保及时释放资源。

全局变量过多会造成什么问题?

生存周期长,越多消耗内存越大
容易引发命名冲突
不利于排查错误和调试

闭包的特点:
1.让外部访问函数内部变量成为可能;
2.让这些变量的值始终保持在内存中,不会在外部函数调用后被自动清除;
3.可以避免使用全局变量,防止全局变量污染;
4.没有用的局部变量就会被销毁内存,而上面闭包中的变量a被内层的函数一直访问着,不会被销毁。
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

使用闭包实现每次输出加1:
直接funA()()问题出在你每次调用 funA() 的时候都返回了一个新的闭包函数,而每个闭包函数都有自己独立的 a 变量。因此,每次调用闭包函数时都是在独立的作用域中进行计数,而不是共享同一个变量。

如果你希望实现每次调用递增的效果,可以将 funA() 的返回值赋给一个变量,然后多次调用这个变量,而不是每次都调用 funA()。

function funA() {
    var a = 0;
    return function() {
        console.log(++a);
    };
}
funA()() // 输出:1
funA()() // 输出:1
funA()() // 输出:1

var closureA = funA(); // 将 funA() 的返回值赋给变量 closureA
closureA(); // 输出:1
closureA(); // 输出:2
closureA(); // 输出:3
function A() {
  var name = "zagiee"; // name 是一个被 A 创建的局部变量
  function B() { // B() 是内部函数,一个闭包
      console.log(name); // 使用了父函数中声明的变量
  }
  B();
}
A();//zagiee
function funA(){
  var a = 10;  // funA的活动对象之中;
  return function(){   //匿名函数的活动对象;
        alert(a);
  }
}
var b = funA();
b();  //10
funA()();//10

参考题目

回调地狱

回调函数中嵌套回调函数的情况就叫做回调地狱。回调地狱就是为是实现代码顺序执行而出现的一种操作,如何解决回调地狱:
1.通过Promise对象进行链式编程来解决

  • Promise构造函数接收一个函数作为参数,我们需要处理的异步任务就写在该函数体内,该函数的两个参数是resolve,reject。异步任务执行成功时调用resolve函数返回结果,反之调用reject。
  • Promise对象的then方法用来接收处理成功时响应的数据,catch方法用来接收处理失败时相应的数据。
  • Promise的链式编程可以保证代码的执行顺序,前提是每一次在then做完处理后,一定要return一个Promise对象,这样才能在下一次then时接收到数据。
var promise = new Promise((resolve, reject) => {/* executor函数 */
    // ... some code
    if (/* 异步操作成功 */){
        resolve(value);
    } else {
        reject(error);
    }
});
promise.then((value) => {
    //success
}, (error) => {
    //failure
})

2.async/await语法糖

  • async关键字,他作为一个关键字放到声明函数前面,表示该函数为一个异步任务,不会阻塞后面函数的执行。
  • await关键字只能在使用async定义的函数中使用​,await后面可以直接跟一个 Promise实例对象,await可以直接拿到Promise中resolve中的数据。

原型

①所有引用类型,对象都有一个__proto__(隐式原型)属性,属性值是一个普通的对象
②所有函数都有一个prototype(显式原型)属性,属性值是一个普通的对象
构造函数
(可以用来new的函数就叫构造函数,箭头函数不能用来当做构造函数)的prototype和其实例的__proto__是指向同一个地方的,这个地方就叫做原型对象

var a = [1,2,3];
a.__proto__ === Array.prototype; // true

function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.sayName = function() {
  console.log(this.name)
}
const person1 = new Person('小明', 20)
console.log(Person.prototype === person1.__proto__) // true
console.log(Function.prototype === Person.__proto__) // true
console.log(Function.prototype === Object.__proto__) // true,function Object()其实也是个函数,所以他是Function构造函数的实例
console.log(Function.prototype === Function.__proto__) // true
console.log(Person.prototype.constructor === Person) // true
console.log(Person.prototype.__proto__ === Object.prototype) // true
console.log(Function.prototype.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__ === null) // true
console.log(Person.prototype.__proto__ === Function.prototype) // false

Function是一个构造函数,他有显式原型对象,这个显式原型对象是对象(而非函数)的一个实例,所以他有隐式原型,指向他的构造函数的显式原型,也就是Function.prototype.__proto__ === Object.prototype,一个具体函数Person的隐式原型也是Object.prototype:Person.prototype.__proto__ === Object.prototype
在这里插入图片描述
此图出处

原型链

  • 当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链
  • Object.prototype其实也有__proto__,指向null,那才是原型链的终点
function Parent(month){
    this.month = month;
}
var child = new Parent('Ann');
console.log(child.month); // Ann
console.log(child.father); // undefined

在child中查找某个属性时,会执行下面步骤:
在这里插入图片描述
constructor和prototype是成对的,你指向我,我指向你。
在这里插入图片描述

原型链面试题:

原型继承

原型继承:实例可以使用构造函数上的prototype中的方法。

function Person(name) { // 构造函数
  this.name = name
}
Person.prototype.sayName = function() { // 往原型对象添加方法
  console.log(this.name)
}
const person = new Person('zagiee') // 实例
// 使用构造函数的prototype中的方法
person.sayName() // zagiee

console.log(Person instanceof Function) // true
console.log(Person instanceof Object) // true
console.log(person instanceof Person) // true
console.log(person instanceof Object) // true

防抖和节流

防抖 (debounce):防止抖动,单位时间内事件触发会被重置,避免事件被误伤触发多次。有个需要频繁触发的函数在规定时间内,只让最后一次生效,前面不生效。
在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。代码实现重在清零 clearTimeout。

<button id="btn">按钮</button>

function debounce(fn, delay) {
  //记录上一次的延时器
  let timer = null;
  return function () {
    // 清除上一次延时器
    if (timer) {
        clearTimeout(timer);
    }
    //重新设置新的延时器
    timer = setTimeout(function () {
      fn.apply(this);
    },delay);
  }
}
document.getElementById('btn').onclick = debounce(function () {console.log('点击事件被触发了' + Date.now())},1000);

防抖的应用场景:

  • 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
  • 表单验证
  • 按钮提交事件:防止多次提交按钮,只执行最后提交的⼀次
  • 浏览器窗口缩放,resize事件(如窗口停止改变大小之后重新计算布局)等。

节流(throttle)控制流量,单位时间内事件只能触发一次。一个函数执行一次后,只有大于设定的执行周期后才会执行第二次。有个需要频繁触发的函数,处于优化性能角度,在规定时间内,只让函数触发的第一次生效,后面不生效。所以节流会稀释函数的执行频率。代码实现重在开锁关锁

function throttle(fn, wait) {
  //记录上一次函数触发时间
  let lastTime = 0;
  return function () {
    let nowTime = Date.now();
    if (nowTime - lastTime > wait) {
      //修正this指向问题
      fn.call(this);
      //同步时间
      lastTime = nowTime;
    }
  };
}
document.onscroll = throttle(function () {console.log('scroll事件被触发了' + Date.now())},200);

节流的应用场景:

  • 按钮高频点击
  • 滚动加载更多
  • 表单重复提交
  • 拖拽事件
  • onScoll
  • 计算鼠标移动的距离(mousemove)

宏任务与微任务(事件循环机制)

宏任务:当前调用栈中执行的代码成为宏任务。(主代码块,定时器等等)。
e.g.:I/O、setTimeout、setInterval、setImmediate、requestAnimationFrame

微任务: 当前(此次事件循环中)宏任务执行完,在下一个宏任务开始之前需要执行的任务,可以理解为回调事件。
e.g.:Promise.then catch finally、async await、process.nextTick、MutationObserver、queueMicrotask

await 会阻塞下面的代码(即加入微任务队列)

async function fn1 (){
    console.log(1)
    await fn2()
    console.log(2) // 阻塞
}

async function fn2 (){
    console.log('fn2')
}

fn1()
console.log(3)
//输出结果为:1,fn2,3,2

宏任务中的事件放在callback queue(有多个)中,由事件触发线程维护;微任务的事件放在微任务队列(只有一个)中,由js引擎线程维护。

运行机制:

  1. 在执行栈中执行第一个宏任务(只有一个任务:执行主线程的js代码)。
  2. 执行过程中遇到微任务,将微任务添加到微任务队列中(微任务队列只有一个)。
  3. 当前宏任务执行完毕,立即执行微任务队列中的所有任务。
  4. 当前微任务队列中的任务执行完毕,检查渲染,GUI线程接管渲染。
  5. 渲染完毕后,js线程接管,开启下一次事件循环,执行下一次宏任务(事件队列中取)。

js是单线程所有主线程的同步任务先执行,然后执行微任务队列的程序,最后执行宏任务队列,秉承先进先出的原则。
深入理解JavaScript事件循环机制

宏任务与微任务面试题:

console.log('start')

setTimeout(() => {
  console.log('setTimeout')
}, 0)

new Promise((resolve) => {
  console.log('promise')
  resolve()
})
  .then(() => {
    console.log('then1')
  })
  .then(() => {
    console.log('then2')
  })

console.log('end')
//输出:
start 
promise
end
then1
then2
setTimeout
(
async ()=>{
console.log(1);

    setTimeout(()=>{
        console.log(2);
    },0)

    await new Promise((resolve,reject)=>{
        console.log(3);
    }).then(res=>{
        console.log(4);
    })

    console.log(5);
}
)()
// 输出:
1 3 2
async  function  async1 ()  {//函数创建未执行,所以不输出
    console.log('async1 start');
    await  async2();//此处输出async2后在微队列等待执行栈完成任务后执行。
    console.log('async1 end')
}
async  function  async2 ()  {
    console.log('async2')
}
console.log('script start');
setTimeout(function ()  {//定时器,将任务放在宏任务中
    console.log('setTimeout')//微队列执行完成后执行宏队列,输出setTimeout。
},  0);
async1();//调用时才会执行
new  Promise(function (resolve)  {
    console.log('promise1');//输出promise1完成后,Promise.then推送至微队列排队
    resolve()
}).then(function ()  {
    console.log('promise2')
});
console.log('script end')//输出script end,此时执行栈清空,开始执行微队列输出async1 end和promise2

//输出:
script start
async1 start
async2
promise1
script end
async1 end
promise2
undefined
setTimeout

总结:
1.先执行同步和立即执行任务,比如说console.log()、new Promise()
2.再依次执行微任务,比如说thenable函数和catchable函数
3.当微任务执行完成后开始执行宏任务,比如说定时器、事件回调等

设计模式

JavaScript设计模式es6(23种)

  • 22
    点赞
  • 233
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

若年封尘

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值