前端面试题

217 阅读18分钟

javascript有哪些数据类型

数字(Number)、字符串(String)、函数(Function)、数组(Array)、布尔值(Boolean)、对象(Object)、Undefined、Null、Symbol、BigInt

检测数据类型的方法

  • typeof():typeof只能检测数字、字符串、函数、布尔值、Undefined,检测不了引用类型,其中Array和Null的typeof结果都为Object
console.log(typeof 2);               // number
console.log(typeof true);            // boolean
console.log(typeof 'str');           // string
console.log(typeof []);              // object    
console.log(typeof function(){});    // function
console.log(typeof {});              // object
console.log(typeof undefined);       // undefined
console.log(typeof null);            // object
  • instanceof:只能正确判断引用数据类型
 
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true
  • Object.prototype.toString.call()
Object.prototype.toString.call(8)                 //'[object Number]'
Object.prototype.toString.call("add")            //'[object String]'
Object.prototype.toString.call({})               //'[object Object]'
Object.prototype.toString.call([])               //'[object Array]'

Null和Undefined的区别

  • 首先两者都是js的基本数据类型
  • Undefined表示未定义,一般在变量声明了但未赋值的情况下,则默认是Undefined
  • Null表示一个空对象,它是已经定义了的,用于赋值给一个可能会返回对象的变量

为什么typeof null的结果是Object

在JS中进行数据底层存储的时候是用二进制存储的,而它的前三位是代表存储的数据类型,而000是代表object类型也就是引用类型的数据。而null正好全是0,所以它巧妙的符合object类型的存储格式,所以在typeof检测的时候,它才会输出object

var、let和const的区别

  • 块级作用域:let和const拥有块级作用域(用{}围住的区域),只能在其作用域内访问,而var没有,块级作用域解决了ES5中的两个问题:
    • 内层变量可能覆盖外层变量
    • 用来计数的循环变量泄露为全局变量
  • 变量提升:var有变量提升,即未声明就可以提前使用,而let和const不可以
  • 给全局添加属性:浏览器的全局对象是window,Node的全局对象是global,var声明的变量是全局变量,并且会将该变量添加为全局对象的属性,而let和const不会
  • 重复声明:let和const声明过的变量不允许重复声明,但var声明过的变量是允许重复声明的,并且后面声明的会覆盖前面声明的
  • 初始值设置:let和var声明变量时无需立刻赋值,但const声明变量时必须同时赋初始值
  • 暂时性死区:在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
  • 指针指向:let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。

const对象的属性能被修改吗

  • 可以,const声明的变量只是不能改变其值,对于引用数据,就是不能改变它指向的内存地址,但是该地址保存的对象的属性是可以改变的

箭头函数与普通函数的区别

  • 箭头函数比普通函数更简洁
  • 箭头函数没有自己的this
  • 箭头函数通过继承来的this永远不会改变
  • 箭头函数无法通过call、apply和bind绑定this
  • 箭头函数没有自己的arguments
  • 箭头函数没有prototype
  • 箭头函数不能作为构造函数使用

this指向都有哪些情形

  • 常规函数:this即调用者(有明确主语则this就是该主语,无明确主语则this隐式地为window,DOM事件监听器的this为事件源e.currentTarget)
  • 箭头函数:this从父级作用域继承
  • 类的构造器中的this:为正在构造的实例本身 实例方法中的this为当前实例 静态函数中的this为类/构造函数本身

new操作符的实现原理

  • 先创建了一个新的对象newObj
  • 将对象的原型设置为构造函数的 prototype 对象
  • 将构造函数的this绑定到新对象,执行构造函数的代码,为这个新对象添加属性
  • 根据构建函数返回类型作判断,如果是值类型,返回这个新创建的对象。如果是引用类型,就返回这个引用类型的对象。 image.png

数组的操作API

  • push():将一个或多个元素添加至数组末尾,并返回该数组的长度,改变原数组
  • unshift():将一个或多个元素添加至数组开头,并返回数组长度,改变原数组
  • pop():从数组中删除最后一个元素,并返回该元素,改变原数组
  • shift():从数组中删除第一个元素,并返回该元素,改变原数组
  • splice(startIndex, deleteCount, item1, item2, ...):从startIndex开始删除deletCount个元素并原地添加新的元素item1,item2...,并以数组形式返回被删除的内容,改变原数组
  • reverse():反转数组,并返回该数组,改变原数组
  • sort():对数组进行排序,排序是将元素转化为字符串,然后比较它们的UTF-16代码单元值序列,并返回该数组,改变原数组
//升序a-b、降序b-a
var numbers = [4, 2, 5, 1, 3];
numbers.sort((a, b) => a - b);

![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e3b2dc1017954df8bea9be49eb5b782c~tplv-k3u1fbpfcp-watermark.image?)
//numbers.sort(compareFunction);
//compareFunction用来指定按某种顺序进行排列的函数。
//如果函数返回0,排序位置与原来一样
//如果函数返回负数,a在b前
//如果函数返回正数,a在b后
//如果省略,元素按照转换为的字符串的各个字符的 Unicode 位点进行排序。
  • concat():用于合并两个或多个数组,并返回一个合并后的新数组,不改变原数组
var new_array = old_array.concat( value1 , value2, ..., valueN)
//以old_array为基础,把括号内的数组元素依次并接到old_array数组后面
  • join():将数组元素通过给定字符拼接起来形成字符串,并返回该字符串,如果该数组只有一个项目,则无需添加拼接符直接返回,不改变原数组
arr.join(separator)    separator(可取值'-'' ''+'、...)如果省略默认以逗号分隔
  • indexOf()返回数组中首个给定元素的索引,如果不存在,返回-1
arr.indexOf(searchElement[, fromIndex])
//fromIndex:开始查找的位置
  • includes():判断数组中是否包含指定值,返回布尔值
arr.includes(valueToFind[, fromIndex])
  • slice(start, end):提取数组的某一部分,以数组形式返回提取的内容,不改变原数组,包头不包尾
  • forEach():对数组的每个元素执行一次给定的函数。无返回值
  • map()返回一个映射后的新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。
  • filter()返回一个新数组,其包含通过所提供函数实现的测试的所有元素(通过检测的意思返回的值的布尔值的状态是true)。
注意理解map和filter的规则:
const arr = [1,2,3,4,5]
arr.map(item=>true) //返回[true,true,true,true,true]
arr.filter(item+>false) //返回[],空数组
  • some()返回布尔值,测试数组中是不是至少有 1 个元素通过了指定的函数测试
  • every()返回布尔值,测试数组中所有元素是否都通过了指定的函数测试
  • find()返回第一个满足测试函数的元素,如果找不到就返回undefined
  • findIndex返回第一个通过了测试函数的成员的索引值,找不到返回-1
  • reduce()
-   reduce(批处理函数,pv初始值); 
-   reduce((上一个value(pv),当前value(cv),index,_arr){return value},pv初始值)
-   第一次执行reduce里面的处理器时, pv 和cv分别是数组的第一和第二个元素
-   后面每次执行 如果处理器函数体内没有return,则pv的值=undefinded,有return则是return的值
-   如果指定了pv初始值,则cv等于当前数组第一个元素
  • Array.from(伪数组名):伪数组转真数组

字符串的操作API

  • 检索:indexOf()、lastIndexOf()、includes()
  • 替换:replace()、replaceAll()
  • 截取:substring()、slice()
  • 分割:split()
  • 格式转换:toLowerCase()、toUpperCase()、trim()、trimEnd()、trimStart()
  • 特征匹配:startsWith()、endsWith()

节流

  • 核心:设置状态锁,一段时间内高频率触发,只有第一次成功
  • 理解:一段时间内只能触发一次,如果这段时间内触发多次事件,只有第一次生效会触发回调函数,一段时间过后才能再次触发(一定时间内只执行第一次)
  • 应用场景
    • ① 鼠标连续不断地触发某事件(如点击),只在单位时间内只触发一次;
    • ② 懒加载时要监听计算滚动条的位置,但不必每次滑动都触发,可以降低计算的频率,而不必去浪费 CPU 资源;
function throttle(fn,delay){
    let timer = null;
    return function () {
        if (!timer) {
            timer = setTimeout(() => {
                timer = null;
            }, delay)
            return fn();
        }
    }
}

防抖

  • 核心:设置延时器,一段时间内高频率触发,只有最后一次成功
  • 理解:在事件被触发时,延迟n秒后再触发回调函数,如果n秒内又触发了事件,则会重新开始计算时间(一定时间内最后一次生效)
  • 应用场景:(input输入框)用户在输入框中连续输入一串字符时,可以通过防抖策略,只在输入完后,才执行查询的请求,这样可以有效减少请求次数,节约请求资源
function debounce(fn,delay){
    let timer = null;
    return function (){
        if(timer){ 
            clearTimeout(timer)
        }
        timer = setTimeout(fn,delay);
    }
}

什么是重排,什么是重绘

  • 重排:当DOM的变化影响了元素的几何信息(位置和尺寸大小),浏览器就需要重新计算元素的几何属性,将其安放在页面中的正确位置,这个过程叫重排,重排也叫回流,简单来说,就是重新生成布局,重新排列元素
  • 下面情况会发生重排(不限于以下情况)
    • 页面初始渲染,这是开销最大的一次重排
    • 添加/删除可见的DOM元素
    • 改变元素位置
    • 改变元素尺寸,比如边距、填充、边框、宽度和高度等
    • 改变元素内容,比如文字数量,图片大小等
    • 改变元素字体大小
    • 改变浏览器窗口尺寸,比如resize事件发生时
    • 激活CSS伪类(例如::hover)
    • 设置 style 属性的值,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow
    • 查询某些属性或调用某些计算方法:offsetWidth、offsetHeight等,除此之外,当我们调用 getComputedStyle方法,或者IE里的 currentStyle 时,也会触发重排,原理是一样的,都为求一个“即时性”和“准确性”。
  • 重绘:一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。 关于重排的优化

用户输入URL后发生了什么

URL解析->DNS查询->TCP三次握手建立可靠连接->发送请求->响应请求->页面渲染->四次挥手断开连接

浏览器渲染内容流程

  • 图解

渲染流程.png

  • 描述
-   解析HTML,构建 DOM 树
-   解析 CSS ,生成 CSS 规则树
-   合并 DOM 树和 CSS 规则,生成 render 树
-   布局 render 树( Layout / reflow ),负责各元素尺寸、位置的计算
-   绘制 render 树( paint ),绘制页面像素信息
-   浏览器会将各层的信息发送给 GPUGPU 会将各层合成( composite ),显示在屏幕上

http和https的区别(高频)

  • HTTP 的URL 以http:// 开头,而HTTPS 的URL 以https:// 开头
  • HTTP 是不安全的,而 HTTPS 是安全的
  • HTTP 标准端口是80 ,而 HTTPS 的标准端口是443
  • 在OSI 网络模型中,HTTP工作于应用层,而HTTPS 的安全传输机制工作在传输层
  • HTTP 无法加密,而HTTPS 对传输的数据进行加密
  • HTTP无需证书,而HTTPS 需要CA机构wosign的颁发的SSL证书

常见HTTP响应状态码

  • 1开头:一般我们看不到,因为表示请求继续(请求内容已收到,还在进一步处理当中)
  • 2开头:2 开头的都是表示成功,本次请求成功了,只不过不一样的状态码有不一样的含义(语义化)
    • 200: 标准请求成功(一般表示服务端提供的是网页)
  • 3开头:3 开头也是成功的一种,但是一般表示重定向
    • 301:永久重定向
    • 302:临时重定向
    • 304:资源未被修改(使用的是缓存数据)
  • 4开头:4 开头表示客户端出现错误了
    • 401:未授权(你要登录的网站需要授权登录)
    • 403:服务器拒绝了你的请求(账户/角色/权限问题)
    • 404:服务器找不到你请求的URL(域名存在,路径不存在就是404,但是域名都不存在结果不是404)
  • 5开头:5 开头的表示服务端出现了错误
    • 500、502:服务器内部错误
    • 504:服务器繁忙

HTTP三次握手和四次挥手

三次握手确保稳定连接(双方能发能收),四次挥手确保可靠传输,都必须一次完成,否则作废重来

  • 三次握手
    • 第一步:客户端发送SYN报文到服务端发起握手,发送完之后客户端处于SYN_Send状态
    • 第二步:服务端收到SYN报文之后回复SYN和ACK报文给客户端
    • 第三步:客户端收到SYN和ACK,向服务端发送一个ACK报文,客户端转为established状态,此时服务端收到ACK报文后也处于established状态,此时双方已建立了连接 image.png
  • 四次挥手:刚开始双方都处于 establised 状态,假如是客户端先发起关闭请求,则:
    • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态。
    • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态。
    • 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
    • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态
    • 服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。 image.png

浏览器缓存机制

DNS缓存、CDN缓存。浏览器缓存、页面本地缓存等等,有一个良好的缓存策略可以减低重复资源的请求,降低服务器的开销,提高用户页面的加载速度。

  • 强缓
    • 在浏览器第一个请求资源时,服务器端的响应头会附上Expires这个响应字段,当浏览器在下一次请求这个资源时会根据上次的expires字段是否使用缓存资源(当请求时间小于服务端返回的到期时间,直接使用缓存数据)
    • cache-control字段优先级高于上面提到的 Expires,值是相对时间。 在cache-control中有常见的几个响应属性值,它们分别是(下面的属性背 max-age 和 no-cache就可以了)

image.png

  • 协缓
    • 如果浏览器在某次请求资源的时候发现本地缓存的资源过期了,这时候就会向服务器发送请求,并设置协商缓存
    • 浏览根据是否存在Last-Modified和Etag,来进行协商缓存,如果不存在,则直接向服务器发送请求,如果存在,则携带If-Modified-Since和If-None-Match发送请求,浏览器根据这两个请求头,判断数据是否更新,如果没有更新,就返回304,通知浏览器读取缓存就可,如果数据已经更新,返回200,把新数据返回给浏览器。
  • 示意图 HTTP协议缓存机制.png

如何理解Promise

Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大

Promise的状态

  • pending: 初始状态,不是成功或失败状态。
  • fulfilled: 意味着操作成功完成。
  • rejected: 意味着操作失败。

Promise.all和Promise.race

无论是all 还是 rece 当中有一个promise失败那么全部都会失败

  • Promise.all 可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值
  • 顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。

对async、await的理解

async 函数执行会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。 那如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)

深拷贝和浅拷贝的区别

  • 浅拷贝: 对于基本数据类型,拷贝的就是该基本数据类型的值,对于引用数据类型,拷贝的就是该引用数据类型的地址,所以对于引用类型,修改任何浅拷贝的副本会影响原来的数据
  • 深拷贝:递归到每个叶子结点(即value是基本数据类型或函数),深拷贝的副本修改不会相互影响

Object.assign()和扩展运算符实现的是深拷贝还是浅拷贝,两者的区别

  • 首先,两者都是浅拷贝
  • Object.assign()是以第一个参数作为目标对象,其他参数作为源对象,把所有源对象的属性覆盖或添加到目标对象,它会修改了一个对象,因此会触发 ES6 setter。
  • 扩展操作符(…)使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象中。它不复制继承的属性或类的属性,但是它会复制ES6的 symbols 属性。
  • 例如:
obj1 = {a:1, b:{c:3} }
obj2 = {}
Object.assign(obj2, obj1)
obj2.a = 10    //这里不会改变obj1的a属性
obj2.b.c = 10   //这里会改变obj1的b.c属性

如何实现深拷贝

  • lodash库中的_.cloneDeep()方法实现
  • JSON.stringify(),这种方式存在弊端,会忽略undefined、symbol和函数
  • jQuery.extend(),通过jquery这个方法可以实现
  • 拓展运算符,对第一层数据实现的是深拷贝,但除了第一层数据以外,实现的是浅拷贝
  • 手写循环递归
//简单方法一
function deepCopy(obj) {
    if (typeof (obj) !== 'object' || obj === null) {
       return obj
    } 
    else {
       let objCopy = Array.isArray(obj) ? [] : {}
       for (const key in obj) {
           objCopy[key] = deepCopy(obj[key])
       }
       return objCopy
    }
}
//详细方法二
function deepClone(obj, hash = new WeakMap()) {
  if (obj === undefined) return undefined // undefined 的情况
  if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
  if (obj instanceof Date) return new Date(obj);//日期对象的情况
  if (obj instanceof RegExp) return new RegExp(obj);//正则表达式的情况
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== "object") return obj;
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj);
  let cloneObj = new obj.constructor();
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj);
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  return cloneObj;
}

对原型和原型链的理解

  • 原型链:实例对象在查找属性时,如果查找不到,就会沿着__proto__去与对象关联的原型上查找,如果还查找不到,就去找原型的原型,直至查到最顶层,这也就是原型链的概念。
  • 原型:在js中,每个构造函数内部都有一个prototype属性,该属性的值是个对象,该对象包含了该构造函数所有实例共享的属性和方法。当我们通过构造函数创建对象的时候,在这个对象中有一个指针,这个指针指向构造函数的prototype的值,我们将这个指向prototype的指针称为原型。
  • 图解示例:

原型链示意图3.jpg

apply、call和bind的区别

  • apply、call、bind都可以为函数指定this
  • apply 和 call 就是传参方式不一样,apply 参数以一个数组的形式传入。但是两个都是会在调用的时候同时执行调用的函数。bind则会返回一个绑定了this的函数。
  • 实现一个bind(有能力看看)
const myBind = function(context,...args){
    const self = this;
    args = args?args:[];
    return function(...newArgs) {
        return self.apply(context,[...args,...newArgs])
    }
}
  • 实现一个call
const myCall = function(context,...args) {
    context = context || window;
    args = args ? args : [];
    const key = Symbol();
    context[key] = this;
    const result = context[key](...args);
    delete context[key];
    return result;
}
  • 实现一个apply
const myApply = function(context,args) {
    // 不传this默认指向window
    context = context || window;
    args = args ? args : [];
    // 给context新增一个独一无二的属性以免覆盖原有属性
    const key = Symbol();
    // this 指向调用它的对象
    context[key]=this;
    // 通过隐式绑定的方式调用函数
    const result = context[key](...args);
    // 删除添加的属性
    delete context[key];
    return result
}