平平无奇古哥哥

V1

2023/05/23阅读:22主题:橙心

前端面试:JS高频面试题「2023」

本文收录于我写的《前端面试手册》,想获得更好的阅读体验请访问在线地址:https://gugiegie.gitee.io/frontend

1. 怎么理解闭包,有哪些使用场景?

简单点来说就是函数里面定义了函数,内部函数可以使用外部函数的变量。当然也可以稍微深入一点从词法作用域的角度来讲一下闭包形成的原因。

闭包常见应用场景:

  1. 自执行函数。
  2. 面试常考的循环点击问题。
  3. 高阶函数。
  4. 变量封装,比如Redux的createStore函数。

过度使用闭包可能会导致内存泄漏和性能问题,在使用闭包时需要谨慎。

2. 什么是事件循环?宏任务和微任务有哪些,执行顺序是怎么样的?

  1. JS里的任务分为同步任务和异步任务,异步任务又包括宏任务和微任务。
  2. 同步任务先执行,异步任务会被添加到任务队列等待执行。
  3. 每次循环会先执行宏任务,后执行微任务,第一次循环执行的是同步任务和微任务。
  4. 微任务相当于是同步任务和宏任务的收尾。

宏任务有:网络请求、浏览器事件、setTimeout、setInterval、setImmediate。

微任务有:Promise、process.nextTick、MutationObserver。

举个例子:

console.log('A1');

setTimeout(() => {
  console.log('A2');
  Promise.resolve().then(() => {
    console.log('A3');
  });
}, 0);

Promise.resolve().then(() => {
  console.log('A4');
});
// 执行顺序为 A1 -> A4 -> A2 -> A3

3. 讲一下你对原型和原型链的理解?

  1. 每个对象,都有__proto__属性,称为隐式原型;
  2. 每个函数,都有prototype属性,称为显示原型;
  3. 对象的隐式原型和其构造函数的显示原型,指向的是同一个引用。
  4. 对象有原型,对象的原型又是个对象,也有原型,这样一直往上,就形成了原型链;
  5. 当我们访问一个对象的属性时,如果该对象本身不存在这个属性,那么就会沿着原型链往上查找,直到找到该属性或原型链的末尾为止。

应用场景:

  1. 实现继承:可以将一个对象的原型设置为另一个对象,从而实现继承。
  2. 扩展原型对象,比如在Vue项目里,往Vue的原型上添加方法,然后在组件里通过this去调用。

4. JS有哪些数据类型?怎么判断数据类型?

JS中数据类型分为两大类:

  • 基本类型:包括Undefined、Null、Boolean、Number、String和Symbol(ES6新增)。

  • 引用类型:包括Object、Array和Function、Date、RegExp等。

typeof运算符可以判断基本数据类型和函数类型,但是它不能准确地判断对象的具体类型,包括普通对象、数组、函数等。

如果要判断对象的具体类型,可以使用instanceof运算符或者Object.prototype.toString方法。

判断数组:

var arr = [123];
console.log(arr instanceof Array); // true

可以使用Object.prototype.toString方法来判断一个变量的具体类型:

var obj = {};
console.log(Object.prototype.toString.call(obj)); // "[object Object]"

var arr = [];
console.log(Object.prototype.toString.call(arr)); // "[object Array]"

在判断数据类型时,还需要注意一些特殊情况,例如NaN、Infinity和- Infinity的类型均为number,因此不能直接使用typeof运算符来判断这些特殊值的类型。

5. 引用类型和基本类型有什么区别?

  1. 存储位置不同,值类型存在栈中,而引用类型存在堆中。
  2. 基础类型的复制是按值复制,改变一个变量不会对另一个变量产生影响;而引用类型的复制是按引用复制,改变一个变量,另一个也变了。看代码:
// 值类型
let a = 10;
let b = a;
b = 5;
console.log(a);  // 10

// 引用类型
let xiaoming = {
    name'xiaoming',
    age18
}
let xiaoli = xiaoming;
xiaoli.name = 'xiaoli',
console.log(xiaoming.name);  // xiaoli
  1. 基础类型的比较是按值比较,当两个变量的值相等时,它们被认为是相等的。而引用类型的比较是按引用比较,即只有当两个变量引用同一个对象时,它们才被认为是相等的。

6. 深拷贝是什么,有哪些应用场景?

深拷贝是指Copy一个对象,属性和值都跟原对象一样,但是引用地址不一样,互不干扰。

深拷贝实现方式有:

  1. JSOM序列化方式,这种方式有比较多的缺陷。
  2. 递归,需要考虑循环引用。

深拷贝最常见的应用场景就是不可变数据,比如在React项目里使用shouldComponentUpdate生命周期钩子结合不可变数据做性能优化。

在项目里使用深拷贝时,一般是使用Lodash的_clone方法来实现,或者使用不可变数据的库。

7. 什么是变量提升?

JS中的变量提升是指变量和函数声明会被提升到作用域的顶部。

简单点说就是可以在变量和函数声明之前使用它们而不会报错,但是变量这时候的值是undefined,而函数调用正常。举个例子:

  1. 变量提升:
console.log(myName); // undefined
var myName = 'XiaoMing';
  1. 函数提升:
sayHello(); // "Hello, XiaoMing!"
function sayHello({
  console.log('Hello, ' + myName + '!');
}

8. 怎么改变this指向?

  1. call、apply:这两个方法都可以显式地更改this指向,区别在于,所传递的参数不同,call方法接受一个参数列表,而apply方法接受一个参数数组。
  2. bind:bind和call、apply的区别是bind方法不会立即调用函数,而是返回一个新的函数。
  3. 箭头函数:箭头函数继承外部函数的this,可以解决回调函数的this指向问题。

9. 你对箭头函数有哪些了解?

  1. 箭头函数语法简洁,可以在一行代码中声明一个函数。
const sum = (a, b) => a + b;
  1. 箭头函数可以不使用return语句来返回结果,如果函数只有一条语句,则该语句的结果会自动作为返回值。
const double = (x) => x * 2;
  1. 箭头函数没有自己的this,它的this继承自外层作用域的this,也就是说箭头函数内部的this与外层作用域中的this相同,可以避免this指向的问题。

10. let和const的优点?

  1. 可以产生块级作用域:使用let和const声明的变量拥有块级作用域,也就是说,在花括号({})中声明的变量只能在该花括号内部访问,而在外部无法访问,可以避免变量污染和命名冲突等问题。

  2. 不会发生变量提升:使用let和const声明的变量不会像var声明的变量一样发生变量提升。

  3. 使用const声明的变量是常量,不可修改,可以确保代码的可靠性和安全性。

11. 讲一下你对JS异步的理解?

关于异步,需要讲清楚从callback -> Promise -> async/await的发展过程。

  1. callback

回调函数是最早被广泛使用的一种方式。通过将耗时的操作封装在一个函数中,并将该函数作为参数传递给异步操作,异步操作完成后会调用该函数,从而达到异步编程的效果。但是嵌套层数太多的回调函数会造成回调地狱,不利于代码阅读和异常处理。

  1. Promise

Promise是ES6中新增的一种异步编程方式,它可以更好地管理异步操作的状态,并且可以避免回调函数嵌套的问题。Promise有三种状态:pending(进行中)、fulfilled(已完成)和rejected(已拒绝),在异步操作完成后,Promise对象的状态会从pending变为fulfilled或rejected,同时可以通过then()和catch()方法来处理异步操作的结果。

  1. async/await

async/await是ES8中新增的异步编程方式,它使用起来更加简洁和直观,可以将异步操作看作同步操作来处理,提高了代码的可读性和可维护性。async/await基于Promise实现,使用async关键字声明异步函数,并使用await关键字等待异步操作的结果。

12. Symbol有哪些使用场景?

Symbol的主要特点是具有唯一性,即每个Symbol类型的值都是独一无二的,不会与其他值相等。

使用场景:

  1. 作为对象属性的键名,由于Symbol的唯一性,它可以作为对象属性的键名,避免键名冲突的问题。例如:
const obj = {};
const s1 = Symbol();
const s2 = Symbol();
obj[s1] = 'foo';
obj[s2] = 'bar';
console.log(obj); // { [Symbol()]: 'foo', [Symbol()]: 'bar' }
  1. 定义常量,使用Symbol定义常量可以避免命名冲突,同时也能够保证常量的唯一性。例如:
const MY_CONSTANT = Symbol('my_constant');
console.log(MY_CONSTANT); // Symbol(my_constant)
  1. 实现私有属性或方法,由于Symbol类型的值无法被外部访问,因此可以将Symbol类型的值用作私有属性或方法的键名。

  2. 作为迭代器,Symbol迭代器可以为对象定义默认迭代器,使对象可以被 for...of 循环遍历。例如:

const obj = {
  [Symbol.iterator]: function*({
    yield 1;
    yield 2;
    yield 3;
  }
};
for (const x of obj) {
  console.log(x);
}
// 输出:
// 1
// 2
// 3

13. 讲一下你对JS垃圾回收的理解?

  1. 怎么理解自动

C语言是手动来管理内存,这样的好处是精细,对于优秀程序员来说能把性能优化到极致,缺点是开发成本和难度比较大。

像Java、Go、Python、JS等这些高级语言,绝大多数都是自动垃圾回收,性能呢,也还可以,也不用程序员去手动管理内存那么费劲。

而Rust呢,既不是手动也不是自动,它是依靠规则去约束,只要你按它的所有权和借用规则来写代码,就可以实现内存安全。

  1. 有哪些回收策略

引用计数、标记清除、分代回收、增量标记。暂时就先记住这几个关键词,后续我会发文章专门讲解。

  1. 垃圾回收有什么用

举个简单的例子,就像我们平时点的360里垃圾清理一样,垃圾回收的目的就是清除不用的变量,减少内存占用。

  1. 前端怎么避免内存泄漏
    1. 事件处理器和定时器要记得清除
    2. 限制递归深度:当递归深度过深时,可能会占用过多内存,导致程序崩溃。因此需要在编写递归算法时设置递归深度限制。

14. 为什么会出现跨域问题,解决跨域有哪些方案,你选择哪个?

出现跨域问题是由于浏览器的同源策略,即不允许在同一个域名、协议和端口下的页面使用另一个域名、协议和端口下的资源。

解决跨域问题的方案:

  1. CORS:CORS是后端设置响应头来允许跨域的。客户端发送跨域请求时,会先发送一个OPTIONS请求,请求服务器确认是否允许跨域请求,如果允许,则发送真正的请求。

  2. 代理:因为同源协议只存在于浏览器,而服务器向服务器发送请求是没有跨域问题的。所以可以使用服务器端代理来避免浏览器跨域限制。在开发环境下,一般是配置脚手架里的devServer来设置代理,生产环境可以直接把前端页面当作静态文件放到后端项目里,或者在生产环境下用Nginx代理。

  3. JSONP:利用script标签的src属性不受同源策略限制的特点,缺陷是不支持Post方法。JSONP现在已经很少使用了,但是一些老旧的第三方API可能是以JSONP形式提供的。

15. 从输入一个url到显示页面发生了哪些事情?

  1. DNS解析:浏览器会先通过DNS服务器获取URL对应的IP地址。
  2. TCP连接:浏览器会向目标服务器发起TCP连接请求。
  3. 发送HTTP请求TCP连接建立后,浏览器会向服务器发送HTTP请求。
  4. 服务器响应:服务器接收到HTTP请求后,会根据请求返回相应的内容和状态码。
  5. 浏览器解析渲染:浏览器接收到服务器返回的响应后,会先进行解析,将HTML转换为DOM树,将CSS转换为CSSOM树,并且执行JS代码。然后,浏览器会将DOM树和CSSOM树结合起来,生成渲染树,并根据渲染树进行布局和绘制,最终呈现出完整的页面。
  6. 连接结束:浏览器断开与服务器的连接,页面显示完成。

16. 什么是强缓存和协商缓存?

首先,浏览器缓存其实是由后端设置的。

  1. 强缓存

强缓存是浏览器直接从本地缓存中读取数据,而不向服务器发送请求的一种缓存策略。

当浏览器向服务器请求资源时,服务器会在响应头中返回一个Cache-Control或Expires字段,用于控制浏览器缓存的过期时间和缓存策略。

  1. 协商缓存

协商缓存是浏览器向服务器发送请求,由服务器根据资源的最后修改时间或者 ETag(实体标签)来判断资源是否更新,从而决定是否返回新的资源或者告知浏览器使用缓存的资源。

协商缓存是一种更加灵活的缓存策略,可以在缓存过期后减少不必要的请求,从而提高网站性能和加载速度。

当浏览器向服务器请求资源时,服务器会在响应头中返回一个Last-Modified 和/或ETag字段,用于表示资源的最后修改时间或实体标签。

浏览器在下一次请求该资源时,会在请求头中添加If-Modified-Since和/或 If-None-Match 字段,分别用于表示上次请求资源时的Last-Modified和/或ETag值。

服务器接收到请求后,会根据If-Modified-Since和/或If-None-Match的值来判断资源是否更新。如果资源未更新,则返回304 Not Modified状态码,告知浏览器使用缓存的资源;如果资源已更新,则返回新的资源。

17. 你了解哪些设计模式?

常见的设计模式有多达23种,但是一般是指那些纯class编程的语言,比如Java那种。

JS里常见的模式有单例模式、观察者模式、发布订阅模式、适配器模式、策略模式。

比如Vue的响应式就是使用了观察者模式、EventBus就是发布订阅模式、Axios实现Node平台和浏览器平台通用就使用了适配器模式。

分类:

前端

标签:

前端

作者介绍

平平无奇古哥哥
V1