JavaScript高频问题
- JS的组成部分
- ECMA Script:JS的核心内容,基础语法。如
var
,for
, 数据类型等。 - 文档对象模型(DOM):DOM将HTML页面规划为元素构成的文档。
- 浏览器对象模型(BOM):对浏览器窗口进行访问和操作(如提供浏览器特性,获取浏览器API)。
- ECMA Script:JS的核心内容,基础语法。如
- JS内置对象
String, Boolean, Number, Array, Object, Function, Math, Date, RegExp
…- 常用内置对象:
Math: abs(), sqrt(), max(), min(), ...
Date: new Date(), getYear(), getHour(), ...
String: concat(), length, slice(), split(), ...
- 操作数组的方法
push(), pop(), sort(), shift(), unshift(), reverse(), concat(), join(), map(), filter, every(), some(), reduce(), isArray(), findIndex(), splice()
- 哪些方法会改变原数组:
push(), pop(), shift(), unshift(), sort(), reverse(), splice()
- 检测数据类型
typeof
:判断基础数据类型,无法判断引用数据类型(class)。typeof 12345
instanceof()
:只能判断引用数据类型,无法判断基本数据类型。[] instanceof Array
constructor
:几乎可以判断基本数据类型和引用数据类型。('abc').constructor === String
Object.prototype.toString.call()
:解决以上方式的问题。Object.prototype.toString.call(2/'abc'/true/[]/{})
- 闭包特性
- 函数嵌套函数,内部函数被外部函数返回并保存下来时会产生闭包。
输出
1
2
3
4
5
6
7function fn(param) {
return function() {
console.log(param);
}
}
var foo = fn('abcd');
foo()abcd
。 - 特性:
- 可以复用变量且该变量不会污染全局(变量始终保存在内存中,不会被垃圾回收机制回收)。
- 缺点:闭包较多时对内存占用较多,导致页面性能下降。只有在IE浏览器中会导致内存泄露。
- 使用场景:防抖、节流、需避免全局污染时。
- 函数嵌套函数,内部函数被外部函数返回并保存下来时会产生闭包。
- 前端内存泄漏
- JS中已分配内存空间的对象为能及时释放清除,会导致长期占用内存的现象,导致内存资源可用性变差,最终导致运行效率降低,甚至内存泄漏。
- JS的垃圾回收机制:自动释放长时间不用的内存资源。
- 导致内存泄漏的原因:为声明直接赋值的变量、未清空的定时器、过度闭包、引用元素未被清除等。
- 事件委托(事件代理)
- 利用事件冒泡机制,将子元素事件(event)绑定到父元素上。
- 若子元素阻止事件冒泡(
event.stop()
),则委托不成立。 addEventListener('click', func_name, false)
默认false,false控制事件冒泡,true为事件捕获。- 好处:提高性能,减少事件绑定,减少内存占用。
- 基本数据类型与引用数据类型
- 基本数据类型:
String, Number, Boolean, undefined, null
等保存在栈中,直接保存数据。 - 引用数据类型(复杂数据类型):
Object, Function, Array
等保存在堆中,类似于C语言的指针类型(或C++引用),保存引用数据类型的地址。加入声明两个引用类型变量引用同一个地址时,改变其中一个另一个也改变。
- 基本数据类型:
- 原型链
- 原型就是一个普通对象,用于构造函数的实例,共享属性和方法;所有实例中引用的原型都是同一个对象。
1
2
3
4
5
6
7
8
9function Person() {
this.say = function() {
console.log('Hello');
}
}
var p1 = new Person();
var p2 = new Person();
p1.say(); //p1和p2的say方法相互独立,需要存两个实例。
p2.say(); - 使用prototype可以把方法挂在原型上,不同实例共享同一内存。
1
2
3
4
5Person.prototype.look = function() {
console.log('Look');
}
p1.look();//p1和p2的look方法指向同一内存。
p2.look(); __proto__
可以理解为指针,实例对象中的属性指向构造函数的原型(prototype)。- 参照C++虚函数表机制。
- JS中若把属性定义在Prototype上,只读时内存共享,一旦需要写入则创建拷贝(遮蔽)。
- 原型链:一个实例对象在调用属性、方法时一次按照实例本身(
Person
)、构造函数原型(Person.prototype
)、原型的原型(Object.prototype
)上去查找。
- 原型就是一个普通对象,用于构造函数的实例,共享属性和方法;所有实例中引用的原型都是同一个对象。
new
操作符- 创建空对象。
- 把空对象和构造函数通过原型链连接。
- 把构造函数的
this
绑定到新的空对象。 - 根据构造函数返回的类型判断,若为值则返回对象值,若为引用则返回引用。
- JS的继承机制
- 原型链继承:方便简洁,容易理解;对象实例继承所有属性和方法,无法像父类构造函数传参。
Child.prototype = new Parent()
- 借用构造函数继承:在子类构造函数内部调用父类构造函数;使用
apply()
或call()
方法将父对象构造函数绑定在子对象上。在子类中写Parent.call(this, param)
。解决了原型链实现继承无法向父类传参的问题;方法都在构造函数中定义,无法实现函数复用,且父类原型中定义的方法对子类不可见,所有类型只能使用构造函数模式。 - 组合式继承:解决了原型链继承和构造函数继承的缺点;然而无论什么情况下都会调用两次父类构造函数(一次是创建子类原型时,另一次在子类构造函数内部)。
- ES6的class类继承:Class通过
extends
关键字实现继承,实质是先创造出父类this对象在调用子类构造函数修改this。子类构造方法中必须调用super方法且只有在调用了super后才能使用this。简单易懂;对于不支持ES6的浏览器无法适用。
- 原型链继承:方便简洁,容易理解;对象实例继承所有属性和方法,无法像父类构造函数传参。
- JS的设计原理
- JS引擎:编译器。
- 运行上下文:浏览器中可调用的API(外部接口)。
- 调用栈:函数调用顺序的数据结构。用于同步执行、递归、错误堆栈等场景。(JS是单线程,只有一个主线程)
- 事件循环:由于JS是单线程,为防止卡死引入了异步机制。事件循环为调用栈为空时,从任务队列里取出一个回调,压入调用栈执行,重复这个过程。把耗时任务交给“外部系统 (浏览器内核的 Web APIs 或 Node 的 libuv)”,等结果好了,再通过 事件循环 (Event Loop) 安排执行回调函数。
- 回调:是一个被当作参数传递的函数,在合适的时候被调用。在异步编程里,回调通常是“异步任务完成后要执行的代码”。
- JS中this的指向
- 全局对象中
this
的指向:指向window。 - 全局作用域或普通函数中的
this
:指向window。 this
永远指向最后调用它的对象。(非箭头函数)new
关键词会改变this的指向。apply
,call
以及bind
都可以改变this
的指向。(非箭头函数)- 箭头函数中的
this
在定义时已被确定,箭头函数无this,看外层是否有函数,有就是外层函数的this,没有就是window。 - 匿名函数中
this
永远指向window。(匿名函数的执行环境具有全局性)。
- 全局对象中
- Script标签中
async
与defer
- 没有async与defer时:浏览器会立刻加载并执行指定的脚本。
- 有
async
:加载和渲染后面元素的过程将与Script的加载和执行并行执行(异步)。 - 有
defer
:加载Script与加载渲染元素异步完层,执行则在所有元素解析完成之后。
setTimeout
的最小执行时间- setTimeout:4ms
- setInterval: 10ms
- 由HTML5规定(浏览器可能会影响)
- ES6和ES5的区别
- ES5: ECMA Script 5(ECMAScript2009);
- ES6: ECMA Script 6(ECMAScript2015);
- ES6的新特性
- 新增块级作用域(let,const)
- 不能在同一个作用域内重复声明。
- 不存在变量提升。
- 存在暂时性死区问题
- 新增定义类(class)
- 新增一种数据类型(symbol):代表独一无二的值,即使内容一致。
- 新增结构赋值:从数据或者对象中取值赋给变量。
- 新增函数参数默认值
- 新增数组API
- 新增对象和数组的扩展运算符
- 新增Promise
- 表示未来才会完成的值,用于解决回调地狱的问题。
1
2
3
4
5
6
7
8
9
10
11const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("任务完成"); // 或 reject("出错了")
}, 1000);
});
p.then(result => {
console.log(result);
}).catch(error => {
console.error(error);
}); - 自身有all,reject,resolve,rece这些方法。原型上有then,catch方法。
- 把异步操作序列话,按预期返回结果。
- 三种状态:pending(初始),fulfilled(操作成功),rejected(操作失败)。一旦状态转变就会凝固。
- async与await:同步代码做异步操作,必须搭配使用。是Promise的语法糖。在函数面前添加async会自动返回一个Promise。遇到await暂停执行,等待Promise完成再继续。
- 表示未来才会完成的值,用于解决回调地狱的问题。
- 新增模块化(import,export)
- 新增set和map数据结构
- 新增generator
- 新增箭头函数
- 不能作为构造函数使用,不能用new。
- 箭头函数没有arguments。
- 不能使用call,apply,bind去改变this的执行
- 无prototype。
- 新增块级作用域(let,const)
call, apply, bind
的区别- 都是改变this指向和函数调用。
- call与apply类似,只是传参方法不同。
- call传参数列表。
- apply传一个数组。
- bind传参后不会立即执行,会返回一个改变了this指向的函数,这个函数可传参,通过
bind()()
来调用。 - call方法性能比apply好一些,bind无法构造函数。
- 递归的问题
- 递归:函数内部调用函数本身(递归函数必须有退出条件,否则会导致栈溢出)。
- 深拷贝的实现
- 完全拷贝一个新的对象,在堆中重新开辟新的空间。拷贝的对象被修改后,原对象不受影响。
- 主要针对引用数据类型。
- 扩展运算符:只能针对第一层深拷贝,对于多层对象只是浅拷贝。
1
2
3
4
5let obj1 = {
name: "jack",
age: 23
};
let obj2 = {...obj}; JSON.parse(JSON.stringify())
:原对象中若有函数则不会被拷贝过来。- 利用递归函数:
1
2
3
4
5
6
7
8
9
10
11function copy(origin, isDeepCopy=true) {
let result = {};
if (origin instanceof Array) {
result = [];
}
for (let key in origin) {
let v = origin[key];
result[key] = (!!isDeepCopy && typeof v === 'object' && v !== null) ? copy(v, isDeepCopy) : v;
}
return result;
}
- 事件循环
- JS是一个单线程脚本语言。
- 主线程:唯一线程上任务。
- 执行栈:任务栈,存储上下文与执行顺序。
- 任务队列:任务调度。
- 宏任务/微任务:异步任务,微任务优先级更高。
- 主线程先执行同步任务,再执行任务队列里的微任务或宏任务。执行完再去查询任务队列,或者执行主任务。
- AJAX
- 创建交互式网页应用的开发技术,再不重新加载整个页面的情况下与服务器交换数据并更新部分内容。
- 通过XMLHttpRequest对象向服务器发送异步请求,返回数据后通过JS操作DOM的形式更新页面。
- 过程:
- 创建XMLHttpRequest对象。
- 通过XMH对象里的open()方法与服务器建立连接。
- 构建请求所需的数据,并通过XMH的send()方法发送至服务器。
- 通过XMH对象的onReadyStateChange事件监听服务器与本地的通信状态。
- 接受并处理服务器响应的结果。
- 将处理结果更新到HTML页面中。
GET
与POST
的区别- GET一般获取数据,POST一般提交数据。
- GET参数在URL中,安全性较差。
- POST参数在body中,数据长度无限制。
- GET请求刷新服务器或退回无影响,POST请求退回时重新提交数据。
- GET请求会被缓存,POST不会。
- GET会被保存在浏览器的历史记录中,POST不会。
- GET只能进行url编码,POST支持多种数据结构。
- Promise的内部原理与优缺点
- Promise对象封装了一个异步操作且可以获取成功或失败的结果。
- Promise主要解决回调地狱的问题,若之前异步任务较多且之间包含依赖关系就只能用回调函数解决,这样会导致代码可读性差、可维护性差形成了回调地狱。
- Promise的三种状态:Pending,Fulfilled,Rejected。只能从Pending变为后者其中之一且不可逆。
- Promise无法被取消,一旦创建立即执行,无法中断。
- 如果不设置回调,Promise内部抛出的错误无法反馈到外面。
- 原理:
- 构造一个Promise实例需要传递一个函数作为参数,该函数有两个形参切都是函数类型,一个是resolve另一个是reject。
- Promise上还有then方法,用来指定状态发生改变时的确定操作,resolve执行第一个函数,reject执行第二个函数。
- Promise与async、await的区别
- 都是异步处理请求方式。
- Promise是ES6,async和await是ES7.
- async与await都是基于Promise实现的,都是非阻塞的。
- 优缺点:
- Promise是一个返回对象需要用then和catch来捕获,且书写方式是链式的容易造成代码重叠不好维护。而async和await通过try-catch来捕获异常。
- async await最大的优点是让代码看起来像同步一样,只要遇到await就会立刻返回结果然后再执行后面的操作。而Promise.then()的方式会出现请求还没返回就执行了后面操作的情况。
- 浏览器的存储方式
- cookies:H5标准前的本地存储方式;兼容性好,请求头自带cookie;存储量小容易造成资源浪费,使用麻烦(需要封装)。
- localStorage:H5加入的标准方式,以key-value的形式存储。操作方便、永久存储、兼容性好;保存值的类型可能被限定,浏览器隐私模式下不可读取,不能被爬虫。
- sessionStorage:类似于localStorage;当前页面关闭后会被立即清理,会话级别的存储方式。
- indexedDB:H5标准存储方式,key-value存储,可以快速读取,适合web场景。
- token的存储位置
- token:验证身份的令牌,一般是用户通过账号密码登录后,服务端把这些凭证通过加密等一系列操作后得到的字符串。
- 若存在localStorage里,后期每次请求接口都需要把它当作一个字段传给后台,容易被XSS攻击。
- 若存在cookie中会自动发送,不能跨域,会有CSRF攻击。
- 一般存在localStorage/sessionStorage中,具体看需求。
- token的登录流程
- 客户端用账号密码请求登录。
- 服务端收到请求后验证账号密码。
- 验证成功后,服务端签发一个token发送给客户端。
- 客户端收到token保存后。
- 后续客户端每次向服务端发送资源请求时,都需要携带这个token。
- 服务端收到请求后验证token。成功则返回资源。
- 页面渲染的过程
- DNS解析-建立TCP连接-发送HTTP请求-服务端处理请求-渲染页面-断开TCP
- 渲染页面:
- 获取HTML和CSS资源,把HTML解析成DOM树。
- 解析CSS为CSSOM。
- 把DOM和CSSOM合并为渲染树。
- 把渲染树每个节点渲染到页面上(绘制)。
- DOM树与渲染树
- DOM树是HTML标签的一一对应,包括head和隐藏元素。
- 渲染树不包含head和隐藏元素。
- 精灵图与base64
- 精灵图:多张图拼接后压缩,前端若要请求多张图片可以只请求一次。
- base64:单张图片二进制信息转译为字符串(字符串大小可能比原图大)。
- svg格式
- 基于XML语法格式的图像格式,可缩放矢量图。
- svg可直接插入页面中,用JS或css对其进行操作。
<svg></svg>
- 也可以作为图像文件被引入。
- 可以转为base64引入。
- JWT
- JSON Web Token通过JSON形式在web应用中的令牌,可以在各方之间安全的把信息作为JSON对象传输。
- 用作信息传输、授权。
- 如果用session来验证会导致服务端负载过大,不利于负载均衡。
- JWT认证流程:
- 前端把账号密码发送给后端接口。
- 后端核对信息,把用户id等信息作为JWT负载,把它和head分别进行base64编码后拼接的签名,形成一个JWT Token。
- 前端每次请求时都会把JWT放在请求头的Authorization位置。
- 后端检查是否存在,如果存在就验证JWT的有效性(签名是否正确,token是否过期等)。
- 验证后后端使用JWT中包含的用户信息进行其他的操作,并返回对应结果。
- JWT简洁、可跨语言使用。
- npm的底层环境
- node package manager,node的包管理和分发工具,已经成为分发node的标准。是JS的运行环境。
- 组成:网站、注册表(每一个包基本信息的数据库),命令行工具。
- HTTP协议规定包含的响应头和请求头
- 请求头信息:
- Accept:浏览器告诉服务器所支持的数据类型。
- Host:浏览器告诉服务器像访问的主机。
- Referer:浏览器告诉服务器我从哪里来(防盗链)。
- User-Agent:浏览器类型、版本。
- Date:浏览器告诉服务器我是什么时候访问的。
- Connection:连接方式。
- Cookie
- X-Request-With:请求方式。
- 响应头信息:
- Location:告诉浏览器你要去找谁。
- Server:告诉浏览器服务器的类型。
- Content-Type:告诉浏览器返回的数据类型。
- Refresh:控制定时刷新。
- 请求头信息:
- 浏览器的缓存策略
- 强缓存(本地缓存):不发起请求,直接使用缓存中的内容,浏览器把JS,CSS,image等内容存到内存中,下次访问时直接从内存中取。
- 弱缓存(协商缓存):需要向后台发请求,通过判断来决定是否使用协商缓存,如果请求内容没有变化,则返回304,浏览器使用缓存里的内容。
- 强缓存的触发:HTTP1.0用时间戳响应标头;HTTP1.1用Cache-Control响应标头。
- 弱缓存的触发:HTTP1.0用if-modified-since响应头last-modified;HTTP1.1用if-none-match响应头Etag。
- 同源策略
- 浏览器的基本(核心)安全策略,URL中协议、域名以及端口号完全相同即为同源。不一样则产生跨域。
- 三个允许跨域加载资源的标签:img,link,script。
- 跨域是可以正常发送请求的,后端也会返回结果,只不过该结果被浏览器拦截。
- 解决跨域的方式:JSONP(服务端允许),CORS(服务端和客户端同时配置),websocket,反向代理(中转站)。
- 防抖和节流
- 都是应对页面中频繁触发事件的优化方案。
- 防抖:触发事件后,在一段时间内只执行最后一次。如果在这段时间内事件又被触发,就重新计时。
- 频繁和服务端交互
- 输入框自动保存时间
- 节流:触发事件后,在固定时间间隔内只执行一次。不管事件触发多少次,都会按照时间间隔来执行。
- 页面滚动监听 → 每 200ms 计算一次位置。
- 鼠标拖拽 → 每 100ms 更新一次位置。
- JSON
- JSON是纯字符串形式数据,适合在网络中传输,不提供任何方法。
- JSON数据存储在.json文件中,或者以字符串形式在数据库、cookie中。
- JS提供了
JSON.parse()
,JSON.stringify()
方法。 - 使用场景:定义接口、序列化、生成token、配置文件。
- 数据请求失败的做法
- 在渲染数据的地方给默认值。
- if判断,没有数据可以隐藏。
- 无感登录
- 无感刷新token,token失效时自动更新。
- 在响应拦截器中拦截,token过期后调用刷新token的接口。(常用)
- 后端返回过期的时间,前端判断token的过期时间,主动调用刷新token的接口。
- 写定时器,定时刷新token。
- 流程:
- 登录成功后保存token和refresh_token。
- 响应拦截器中对401状态码引入刷新token的api方法调用。
- 替换保存本地新的token。
- 把错误对象里的token替换。
- 再次发送未完成请求。
- 如果refresh_token也过期,清除所有token返回登录页面。
- 大文件上传
- 分片上传:把需要上传的文件按一定规则分割成大小相同的数据块。
- 初始化分片上传任务,返回本次上传的唯一标识。
- 按照一定规则把各个数据块上传。
- 发送完成后服务端判断数据完整性,若完整则合并。
- 断点续传:
- 服务端返回从哪里开始/浏览器自己处理。
- 分片上传:把需要上传的文件按一定规则分割成大小相同的数据块。
var
,let
与const
的区别- 作用域:
- var在函数内声明则只作用于函数内部,其他情况都是全局作用域。
- let/const是在花括号内有效。
- 变量提升:
- var声明会被提升(hoist),初始化为undefined。声明之前访问为undefined。
- let/const也会被提升,但是会有TDZ(Temporary Dead Zone),在声明之前访问会报错。
- 重复声明:
- var可以重复声明同名变量。
- let/const在同一作用域内不允许重复声明。
- 是否可以重新赋值:
- var/let:可以重新赋值。
- const:声明后必须立刻赋初值,且不能重复赋值。
- 对象/数据的const特殊性:const只是保证变量绑定(引用)不可辨,但对象/数组的内容可以被改变。
- 作用域: