JS高级面试题
1.JS高级面试题
事件委托
- 事件委托本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会传递到父节点;因此可以把子节点的监听函数定义在父节点上(这样父节点就是绑定事件的节点),父节点可以通过事件对象获取目标节点(就是触发事件的节点),由父节点的监听函数统一处理多个子元素的事件,这种方式就是事件委托。
- 使用事件委托可以不必为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗,此外,使用事件委托还可以实现事件的动态绑定,比如说新增一个子节点,并不需要为它添加一个监听事件,事件委托会直接交给父元素的监听函数来处理。
原型
- 在JS中,每个对象都有一个原型,原型是一个对象,它包含了共享的属性和方法,可以被其他对象继承和访问。
- 当你访问一个对象的属性和方法时,如果该对象自身没有这个属性或方法,JS会去它的原型链上查找,原型链是一个连接对象和原型的链式结构,每个对象都有一个隐式指向其原型的链接;如果沿原型链查找一直找到Object原型对象它指向null都没找到的话,说明该对象的原型链上没有该属性或方法,那么就无法访问。
- 构造函数的所有实例都可以访问构造函数原型中的方法和属性。也就是把共有的东西统一存放到一个共享空间。这样可以避免相同函数重复声明,减少空间占用,节省内存
- 所有的函数天生自带一个属性 prototype ,构造函数也是函数,也有一个属性 Prototype , 函数名.prototype => 原型
- 所有的对象天生自带一个属性 __ proto __ ,实例对象也是对象,也有一个属性 __ proto __ , 实例对象.proto => 原型
- 所有的构造函数(自定义,内置),都是Function的实例对象
原型和原型链的区别
Promise
- Promise是异步编程的一种解决方案,本质上是一个构造函数,new一个Promise对象用来封装一个异步操作可以获取其成功或失败的值,Promise实例被创建后回调函数会立即执行。
- Promise有三种状态:pending(等待中)、fulfilled(成功的)、rejected(失败的) 状态一旦改变就不会再变。
resolve => Promise中使用resolve参数修饰 得到的结果是一个成功的promise,promise的结果就是resolve修饰的内容
reject => Promise中使用reject参数修饰 得到的结果是一个失败的是promise,promise的结果就是reject修饰的内容 - Promise的.then()和.catch()操作都能返回一个Promise对象
.then => 获取到的是一个成功的promise返回结果
.catch => 获取到的是一个失败的promise返回结果
.finally => 甭管成功还是失败 它都能执行 - ES7 提出来了promise的新的获取结果的方法
使用async,await来获取promise的返回结果
await只能出现在async函数中。
async的await只能获取成功promise 如果await的promise失败了,就会抛出异常,需要通过try...catch来捕获处理 - Promise可以实现在异步操作执行完后,用链式调用的方式执行回调函数从而解决回调地狱的问题;Promise可以支持多个并发的请求,获取并发请求的数据。Promise可以解决异步的问题,但本身不能说 Promise是异步的
- Promise.all()方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调;可以批量处理多个promise对象:
如果多个promise的结果都是成功的,那么就返回一个成功的promise,结果就是多个promise的结果组成的一个数组
如果有一个失败的promise 那么就返回一个失败的promise 它的结果就是这个失败的promise的结果
如果有多个失败的promise那么就返回一个失败的promise 它的结果就是最快失败的promise的结果 - Promise.race()方法也可以批量处理多个promise对象,它返回的结果是最快的那个promise的结果,如果最快的是成功的promise那么返回的就是一个成功的promise,如果最快的是失败的promise那么返回的就是一个失败的promise
promise和async await捕获错误的方式
箭头函数 箭头函数与普通函数的区别
箭头函数是通过匿名函数进化得来的,当使用箭头函数时,function省略不写,变成箭头,当且仅当函数的参数只有一个的情况下,小括号可以省略不写,当函数的返回值,只有一句话的时候,{}花括号和return可以省略不写
箭头函数的特点:
- 箭头函数本身没有this,会捕获其所在上下文的this,作为自己的this,使用call,apply,bind并不会改变箭头函数中的this指向
- 箭头函数没有arguments,用后面的三点运算符进行替换或者用rest参数,同时没有super和new.target
- 箭头函数是匿名函数,不能作为构造函数,箭头函数内部没有constructor方法,也没有prototype属性,所以不支持new操作
- 箭头函数不用function关键字声明,它属于表达式函数,因此不存在函数提升
ES6新增的属性和方法
新增特性
- 新增了具备块级作用域的声明变量的方式let和const
- 新增了一种基本数据类型Symbol,返回的是一个唯一的值
- 新增了箭头函数
- 新增了对象和数组的解构赋值
- 新增了三点运算符,作用是将可迭代对象展开到其单独的元素中;可迭代对象包括任何能用for of 循环进行遍历的对象,比如数组、字符串、Set、Map、DOM节点等。
- 引入了类的概念,类的所有方法都定义在类的prototype属性上面
- 引入了 Promise 对象,用于处理异步操作和处理回调,避免了回调地狱问题。可以使用.then()、.catch()、.finally()等方法链式调用异步操作。
- 新增了Object.assign()方法,用于将源对象的所有可枚举属性复制到目标对象, 可以实现对象的合并
- Object.keys()、Object.values()、Object.entries():分别获取对象的键、值、键值对数组
- 新增了可以使用类似数组的迭代方法(如 map、forEach、filter、reduce 等)来处理对象
- 新增了一些数组方法 比如Array.isArray()判断是否是真数组,Array.from()将伪数组转换成真数组,Array.of()将一组值转为数组
- ES6新增模块化语法,支持导入导出模块
- ES6新增了 Set 和 Map 数据结构
- ES6原生提供 Proxy 构造函数,用来生成 Proxy 实例,用于数据代理
- ES6新增了生成器(Generator)和遍历器(Iterator)
const、var、let的区别
- var 具有函数作用域,可以重复声明,提升变量,可以在函数内或全局范围内使用和修改
- let 和 const 具有块级作用域,不允许重复声明,不会提升变量,let 可以在块级作用域内部赋值和修改,const 一旦赋值就不能修改。
- var 和 let可以不需要初始值,const必须要有初始值
for in 和 for of的区别
- 使用for of遍历的内容必须要有迭代器
- for in一般用于遍历对象也可以遍历数组,遍历的是对象的属性或者数组的下标,for of一般用于遍历数组,map,set等,不能遍历对象
- for in一般遍历的是key,for of一般遍历的是value
深拷贝和浅拷贝
-
进行深拷贝时,会拷贝所有的属性,并且如果这些属性是对象,也会对这些对象进行深拷贝,直到最底层的基本数据类型为止;深拷贝后的对象与原对象没有关系
深拷贝的方法:- for in 遍历
- JSON数据转换的方式
- 引用lodash.js 创建深拷贝 const newObj = _.cloneDeep(obj)
-
浅拷贝只会拷贝对象的第一层属性,如果这些属性是对象,则不会对这些对象进行拷贝,而是直接复制对象的引用。这意味着,对于浅拷贝后的对象,如果原对象的对象属性的属性值发生了变化,浅拷贝后的对象的对象属性的属性值也会跟着发生变化
浅拷贝的方法:- for in 遍历+递归的方式
- 三点运算符
- Object.assign() 可以把任意多个的源对象自身的可枚举属性拷贝给目标对象
- 引用lodash.js 创建浅拷贝 const newObj = _.clone(obj)
浅拷贝和深拷贝 需要你手写一个深拷贝
闭包
- 闭包是一种结构
- 闭包的形成条件:
- 在函数的内部有一个直接或者间接的函数
- 内部函数访问外部函数的局部变量
- 定义一个变量 用于接受外部函数的返回值
- 闭包的特点:
- 外部函数的局部变量一直存在于内存空间当中 可以延长变量的声明周期
- 可以让外部作用域访问函数里面的局部变量
- 闭包的缺点:变量一直才能在于内存空间当中,对于低版本的IE浏览器来说,容易造成内存泄漏
- 注意点:因为闭包所使用的变量会一直占用内存 使用完毕后最好手动销毁
(原型链)a.b做了什么
1.先看a
在当前作用域中查找a,如果当前作用域找不到,那么就去外部作用域查找...一直找到全局作用域
2.找到了a再看点
如果这个a是一个复杂数据类型,那么都可以点
如果这个a是一个基本数据类型 那么看a是否能转换成包装类
number,boolean,string可以转换 即可以点
null,undefined不可以转换 不能点 报错 TypeError错误
3.如果a可以点再看b
此时这个a要么是一个复杂类型要么是一个可以转换成包装类的基本数据类型
先在a自己身上查找这个b 找到了 那么就使用 找不到那么就按照原型链进行查找直到找到Object对象指向为null都还没找到b这个属性的话,那么就说明在a的原型链上不存在b这个属性,a就无法访问b,否则可以进行a.b
防抖与节流
- 防抖与节流本质上是优化高频率执行代码的一种手段。浏览器的 resize、scroll、keypress、mousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能
- 防抖debounce
- 原理:当频繁触发某事件时,若在设定时间内重复触发,会重新开始计时,直到过了设定的时间没有再次触发了,事件才会处理函数一次
- 应用场景:登录、发短信等按钮避免用户点击次数太多,以致于发送了多次请求,需要防抖;调整浏览器窗口大小时,resize触发次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖;
- 企业版防抖 引入lodash.js文件
- 节流throttle
- 原理:在持续触发某事件时,一定的时间内只会执行一次函数,目的是减少一定事件内的触发频次
- 应用场景:鼠标连续不断地触发某事件(如鼠标移动)单位时间内只触发一次;监听滚动事件,不让它频发地触发,要使用节流
- 企业版节流 引入lodash.js文件
函数柯里化
- 柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数并且能够返回结果的新函数的技术。
1、函数原始状态接受多个参数,可以是一个、两个、三个......
2、转换成另外一个函数,这个函数可以接受小于原始函数的参数个数,并且等到累计接受参数个数与原始函数接受参数个数一致时,返回结果。
参考文章:详解函数柯里化
- 柯里化的好处:
参数复用:柯里化可以实现参数复用;
可以提前固定参数确认一些信息;
延迟执行:当参数个数达不到时不返回结果,返回函数
数组扁平化
- 数组扁平化指的是将一个多层嵌套的数组转换为一维数组
- 数组扁平化的实现:参考实现方式
- 普通的递归实现
- 利用reduce函数迭代
- 扩展运算符实现
- toString()和split方法共同处理
- ES6中的flat方法
- 正则和JSON方法共同处理
微任务和宏任务
- 本质是由于JavaScript采用单线程的设计模式,主线程用来同步执行代码,当主线程执行时遇到了异步事件,会将异步事件加入到异步队列中,等主线程同步代码执行完毕后再来执行异步代码
- 异步代码又分为微任务和宏任务;宏任务主要包括script代码,setTimeOut,setInterval,DOM事件,Ajax请求;微任务主要包括Promise,async/await,process.nextTick等。微任务的执行优先级要高于宏任务
- 在微任务中process.nextTick的优先级最高
异步函数
- 异步函数就是使用async所修饰的函数;async和await是ES7中增加来对promise操作的方法,是ES7系列提供的语法糖,await不能单独使用一定要结合 async 来使用。
- async会返回一个promise对象,async函数调用不会造成代码的阻塞;当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
- 返回的结果如果非promise且代码没有报错,那么得到的是一个成功的promise,他的结果就是return后面的内容;如果代码有报错,那么得到的是一个失败的promise,他的结果就是异常信息
- 返回的结果如果是一个成功的promise,那么返回的也是一个成功的promise,他的结果就是promise的结果;如果是一个失败的promise,那么返回的也是一个失败的promise,他的结果就是promise的结果
参考文章:我们需要知道的 JS 异步编程
图片懒加载
- 定义:在网页中视图之外的图片默认不加载,随着页面的滚动,图片进入了显示的范围,则触发图片的加载显示
- 目的:提高页面的加载速度,让用户体验感更好并且节省流量;比如在一些网站有大量的商品图片信息,如果打开网页时让所有图片一次性加载完成,需要处理很多次网络请求,等待加载时间比较长,用户体验感很差
- 原理:初始化时,图片标签的src不是真实的图片地址;而统一使用白图或者透明图代替;由于所有图片都使用这一张图片,只会发送一次请求,不会增加性能负担。然后将图片的真实路径绑定给一个自定义属性,例如data-url;然后监听滚动事件当图片元素进入视口时,就将src替换为真正的url地址。
- 实现方式:
- 滚动监听+scrollTop+offsetTop+innerHeight
- 滚动监听+getBoundingClientRect().top+window.innerHeight 当图片距离顶部的距离小于浏览器的窗口的时候,那么就图片加载显示
- 使用监听器来监听图片与视口的交叉 intersectionObserver() 监听图片与视口的交叉 => o.observe(img);监听到的图片是否交叉 => imgItem.isIntersecting;当为true时就用真正的图片路径替换src,然后使用o.unobserve(img) 不再监听视口交叉;这种方式比上面的方式更好,因为监听到后就可以关闭,而上面方式需要频繁监听scroll事件,会有一定的性能损耗;但是Intersection Observer API会有浏览器兼容问题!!
参考文章:图片懒加载的原理和三种实现方式
路由懒加载
- 路由懒加载就是需要哪一个组件才会加载哪一个组件
- 如果使用静态引入那么在打开页面的时候全部组件都运行一遍会极大地影响页面运行效率
- 所以就需要动态引入也叫异步引入路由信息,异步引入会得到promise的内容,在promise的返回结果当中存在渲染函数叫做 default,将这个default的运行那么对应的组件就会渲染页面
history路由和hash路由的区别
- hash路由模式
- 监听浏览器的hashchange事件 => window.onhashchange()
- 根据window.location.hash 获取路由的url
- 然后匹配路由表中的路由信息
- 匹配成功后就调用对应的component组件,渲染页面
- history路由模式
- 原理:两个方法一个事件
- window.history.pushState() => 添加一个历史记录
- window.history.replaceState() => 替换一个历史记录
- window.onpopstate() => 监听浏览器的前进与后退
-window.historystate => 存储历史记录
-window.history.state.path => 获取历史记录的path路径
- 当调用组件渲染页面时就添加历史记录 监听到浏览器的前进与后退时就获取历史记录的path然后重新渲染页面
- 原理:两个方法一个事件
- hash路由的跳转路径中有#和history路由没有 history路由在上线时要额外配置,否则会报错
token
- token是用于用户身份验证和授权的一种方式,它是由一段加密信息构成的字符串
- 用户登录成功时服务器会生成一个Token返回给用户,之后在应用程序中的每次请求中,都会带上这个token,服务器会对这个token进行解密和验证,以确定用户的身份和权限
- token 的有效期是有限的,一旦超过有效期就会失效,让该用户重新登录
跨域
- 跨域是是因为浏览器的同源策略限制,是浏览器的一种安全机制,在服务端之间是不存在跨域的。
- 所谓同源指的是两个页面具有相同的协议、域名和端口,三者有任一不相同即会产生跨域。
- 在浏览器中,< script >、< img >、< iframe >、< link >等标签都可以加载跨域资源,而不受同源限制
- 解决跨域的方法
- 通过JSONP的方式,JSONP本质上是使用script的方式来解决跨域,它只支持GET数据请求,不支持POST请求
- 使用代理服务器的方式,代理服务器与浏览器同源,不存在跨域问题,代理服务器与服务器也不存在跨域问题,所以可以利用代理服务器进行转发
- 使用CORS解决跨域,它允许浏览器向非同源的服务器发送请求
Ajax
- Ajax 全称为“Asynchronous Javascript And XML”级(异步 JavaScript 和 XML),是一种创建交互式网页应用的网页开发技术,用来实现前后端交互
- 其核心是XMLHTTPRequest对象,由浏览器提供。通过这个对象可以实现在不重新加载页面的情况下与Web服务器交换数据,使得程序能够更快地回应用户的操作,也就是说ajax可以实现网页的局部刷新
- 由于每次发出ajax请求都要写很多相同的代码,一个页面一般都有好几个请求,那么会有冗余代码,不仅影响前端开发效率,还影响代码可维护性和可读性,所以可以对ajax进行封装
- 因为js是单线程的编程语言,一般情况下ajax都会用异步的方式,否则会阻塞后续js代码执行,出现卡顿,影响效率
- 可以使用promise进一步完善对原生ajax的封装,使用resolve来接收服务器返回的数据,然后用.then方法获取返回结果;同时也可以利用 async/await 语法进一步简化代码
- axios就是一种通过Promise对ajax的封装,是一个基于Promise的Http库,可以在浏览器和Node.js中使用,可以直接调用.then方法获取服务器返回的数据;axios可以自动转换JSON数据,不需要再用JSON.parse()方法,并且支持配置ajax的请求与响应拦截器,比如可以往里面添加nprogress进度条组件优化用户体验;请求拦截器:发送请求的时候,携带一些信息,比如说token,就是令牌,在用户向服务端请求数据时进行校验,提高安全性;响应拦截器:接收到数据的时候,进行数据过滤、对状态码判断等对应的操作
- axios是一个第三方库,需要通过npm安装后使用;而ajax是JavaScript的内置对象,无需安装就可以使用,直接new操作
ajax常见的请求方式
● GET:表示向服务器获取资源
● POST:表示向服务器提交信息,通常用于产生新的数据,比如注册
● PUT:表示希望修改服务器的数据, 通常用于修改某数据
● DELETE:表示希望删除服务器的数据
● OPTIONS:发生在跨域的预检请求中,表示客户端向服务器申请跨域提交
GET和POST请求数据区别
- 使用Get请求时,参数在URL中显示,直接拼接在请求路径后,用一个?间隔,而使用Post请求方式,则请求参数放在xhr.send()里面,并且Post请求需要请求头
- 使用Get请求时只能携带查询字符串格式;而使用Post请求方式,原则上不限制格式, 但是需要在请求报文的 content-type 做出配置
- 使用Get请求发送数据量小(2kb左右),Post请求发送数据量大
- 使用Get请求是明文发送,因为在传输过程,数据被放在请求的URL中,URL中数据会被记录到日志文件,泄漏信息的风险,相对不安全;使用Post请求是暗文发送,Post的所有操作对用户来说都是不可见的,相对安全
JS常见的设计模式
单例模式
- 作用:确保一个类只有一个实例对象,让对象唯一存在
- 应用场景:在需要共享资源或管理全局状态的情况下,如数据库连接池、线程池、全局配置等
组合模式
- 作用: 将拥有相同功能的实例对象存储到一个队列当中,一起执行
- 应用场景:客户端可以忽略组合对象与单个对象的差异;优化处理树形结构、文件目录、菜单、导航
策略模式
- 作用:将一些相似的算法都封装成起来然后暴露给外部使用,使得这些算法可以独立于客户端变化
- 应用场景:在需要根据不同的策略来完成特定任务的情况下,如排序算法、支付方式选择等
观察者模式
- 作用:定义对象间的一种一对多依赖关系,当一个对象的状态发生变化时,它的所有依赖对象都会收到通知并自动更新。
- 应用场景:在需要实时更新或同步信息的情况下,如用户界面中的事件处理、消息订阅等
工厂模式
- 作用:一种用来创建对象的设计模式
- 应用场景:需要批量生产同种类的对象的时候