2022前端面试手册——JavaScript相关
JavaScript的基本数据类型有哪些
- String
- Number
- Boolean
- Symbol
- Undefined
- Null
- Object
Ajax 如何使用
一个完整的AJAX请求包括五个步骤:- 创建XMLHTTPRequest对象
- 使用open方法创建http请求,并设置请求地址, xhr.open(get/post,url,async,true(异步),false(同步))经常使用前三个参数
- 设置发送的数据,用send发送请求
- 注册事件(给 ajax 设置事件)
- 获取响应并更新页面
如何判断一个数据是NaN
NaN 非数字 但是用 typeof 检测是 number 类型
利用 NaN 的定义, 用 typeof 判断是否为 number 类型并且判断是否满足 isNaN
利用 NaN 是唯一一个不等于任何自身的特点 n!==n
Js 中 null 与 undefined 区别相同点
: 用 if 判断时,两者都会被转换成 false不同点
: number 转换的值不同 number(null)为 0 number(undefined)为NaN- Null 表示一个值被定义了,但是这个值是空值
- Undefined 变量声明但未赋值
闭包是什么? 有什么特性? 对页面会有什么影响
闭包
可以简单理解成: 定义在一个函数内部的函数,其中一个内部函数,在包含它们的外部函数之外被调用时形成闭包。
特点:- 函数嵌套函数
- 函数内部可以引用外部的参数和变量
- 参数和变量不会被垃圾回收机制回收
使用:
- 读取函数内部的变量
- 这些变量的值始终保持在内存中,不会在外层函数调用后被自动清除
优点:
- 变量长期驻扎在内存中
- 避免全局变量的污染
- 私有成员的存在
缺点: 会造成内存泄露
Js 中常见的内存泄漏
- 意外的全局变量
- 被遗忘的计时器或回调函数
- 脱离 DOM 的引用
- 闭包
事件委托是什么? 如何确定事件源(Event.target 谁调用谁就是事件源)
JS 高程上讲: 事件委托就是利用事件冒泡
,只制定一个时间处理程序,就可以管理某一类型的所有事件。事件委托
,称事件代理,是 js 中很常用的绑定事件的技巧,事件委托就是把原本需要绑定在子元素的响应事件委托给父元素,让父元素担当事件监听的职务,事件委托的原理是 DOM 元素的事件冒泡什么是事件冒泡?
一个事件触发后,会在子元素和父元素之间传播,这种传播分为三个阶段- 捕获阶段: 从 window 对象传导到目标节点(从外到里)
- 目标阶段: 在目标节点上触发
- 冒泡阶段: 从目标节点传导回 window 对象(从里到外)
本地存储与 cookie 的区别
- cookie: 非常小,它的大小限制为4KB左右,它的主要用途保存登录信息等。
- localStorage: 5MB左右, 可以将一部分数据在当前会话中保存下来。
- sessionStorage: 同localStorage,区别是页面关闭,sessionStorage中的数据就会被清空。
三者的异同: 数据的生命期 与服务器端通信
- cookie: 一般由服务器生成,可设置失效时间。如果在浏览器端生成Cookie,默认是关闭浏览器后失效。每次都会携带在 HTTP 头中,如果使用 cookie 保存过多数据会带来性能 问题
- localStorage: 除非被清除,否则永久保存。仅在客户端(即浏览器)中保存,不参与和服务器的通信
- sessionStorage: 仅在当前会话下有效,关闭页面或浏览器后被清除。
ES6 新特性
const 和 let、模板字符串、箭头函数、函数的参数默认值、对象和数组解构、for…of 和 for…in、ES6 中的类let 与 var 与 const 的区别
var 声明的变量会挂载在 window 上,而 let 和 const 声明的变量不会
var 声明的变量存在变量提升,let 和 const 不存在变量提升
let 和 const 声明会形成块级作用域
const 一旦声明必须赋值,不能用 null 占位,声明后不能再修改,如果声明的是复合类型数据,可以修改属性数组方法有哪些请简述
arr.push
() 从后面添加元素,返回值为添加完后的数组的长度arr.pop
() 从后面删除元素,只能是一个,返回值是删除的元素arr.shift
() 从前面删除元素,只能删除一个返回值是删除的元素arr.unshift
() 从前面添加元素, 返回值是添加完后的数组的长度arr.splice
(i,n) 删除从i(索引值)开始之后的那个元素,返回值是删除的元素arr.concat
() 连接两个数组,返回值为连接后的新数组str.split
() 将字符串转化为数组arr.sort
() 将数组进行排序,返回值是排好的数组,默认是按照最左边的数字进行排序,不是按照数字大小排序的arr.reverse
() 将数组反转,返回值是反转后的数组arr.slice
(start,end) 切去索引值 start 到索引值 end 的数组,不包含 end 索引的值,返回值是切出来的数组arr.forEach
(callback)遍历数组,无return 即使有return,也不会返回任何值,并且会影响原来的数组arr.map
(callback) 映射数组(遍历数组),有 return 返回一个新数组arr.filter
(callback) 过滤数组,返回一个满足要求的数组
Json 如何新增/删除键值对
新增: obj.a = 1
删除: delete obj.a什么是面向对象请简述
面向对象
是一种思想,是基于面向过程而言的,就是说面向对象是将功能等通过对象来实现,将功能封装进对象之中,让对象去实现具体的细节
Js 本身是没有 class 类型的,但是每个函数都有一个 prototype 属性, prototype 指向一个对象,当函数作为构造函数时,prototype 就起到类似于 class 的作用
面向对象有三个特点:封装
(隐藏对象的属性和实现细节,对外提供公共访问方式),继承
(提高代码复用性,继承是多态的前提),多态
(是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象)
普通函数和构造函数的区别
- 构造函数也是一个普通函数,创建方式和普通函数一样,但是构造函数习惯上首字母大写
- 调用方式不一样,普通函数直接调用,构造函数要用关键字 new 来调用
- 调用时,构造函数内部会创建一个新对象,就是实例,普通函数不会创建新对象
- 构造函数内部的 this 指向实例,普通函数内部的 this 指向调用函数的对象(如果没有对象调用,默认为 window)
- 构造函数默认的返回值是创建的对象(也就是实例),普通函数的返回 值由 return 语句决定
- 构造函数的函数名与类名相同
请简述原型/原型链/(原型)继承
原型:
任何对象实例都有一个原型,也叫原型对象,这个原型对象由对象的内置属性_proto_指向它的构造函数的 prototype 指向的对象,即任何对象都是由一个构造函数创建的,但是不是每一个对象都有 prototype, 只有方法才有 prototype原型链:
原型链基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。原型链的核心就是依赖对象的_proto_的指向,当自身不存在的属性时,就一层层的扒出创建对象的构造函数,直至到 Object 时,就没有_proto_指向了原型继承:
利用原型中的成员可以被和其相关的对象共享这一特性,可以实现继承Promise 的理解
Promise
是一种解决异步编程的方案,相比回调函数和事件更合理和更强大。从语法上讲,promise 是一个对象,从它可以获取异步操作的消息
promise 有三种状态:pending
初始状态也叫等待状态,fulfiled
成功状态,rejected
失败状态;状态一旦改变,就不会再变。创造 promise 实例后,它会立即执行
特点:- Promise 对象的状态不受外界影响
- Promise 的状态一旦改变,就不会再变,任何时候都可以得到这个结果,状态不可以逆
缺点:
- 无法取消 Promise,一旦新建它就会立即执行,无法中途取消
- 如果不设置回调函数,Promise 内部抛出的错误,不会反映到外部
- 当处于 pending(等待)状态时,无法得知目前进展到哪一个阶段, 是刚刚开始还是即将完成
我们用 Promise 来解决什么问题?
- 回调地狱,代码难以维护, 常常第一个的函数的输出是第二个函数的输入这种现象
- promise 可以支持多并发的请求,获取并发请求中的数据
请简述 async 的用法
Async 就是 generation 和 promise 的语法糖,async 就是将 generator的*换成 async,将 yield 换成 await
函数前必须加一个 async,异步操作方法前加一个 await 关键字,意思就 是等一下,执行完了再继续走注意
: await 只能在 async 函数中运行, 否则会报错
Promise 如果返回的是一个错误的结果,如果没有做异常处理,就会报 错,所以用 try..catch 捕获一下异常就可以了一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?
DNS域名解析
- 在客户端和浏览器,本地DNS之间的查询方式是递归查询;
- 在本地DNS服务器与根域及其子域之间的查询方式是迭代查询
建立TCP连接
- 第一次握手, 建立连接。
- 客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;
- 然后,客户端进入SYN_SEND状态,等待服务器的确认;
- 第二次握手: 服务器收到SYN报文段。
- 服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);
- 同时,自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;
- 服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态
- 第三次握手: 客户端收到服务器的SYN+ACK报文段。
- 然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,
- 这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
- 如果是HTTPS,SSL握手
- 第一次握手, 建立连接。
发送HTTP请求,服务器处理请求并返回响应结果
关闭TCP连接,四次挥手
- 第一次分手
- 主机1(可以使客户端,也可以是服务器端,设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;
- 此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
- 第二次分手
- 主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;
- 主机1进入FIN_WAIT_2状态;主机2告诉主机1,我”同意”你的关闭请求;
- 第三次分手
- 主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;
- 第四次分手
- 主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;
- 主机2收到主机1的ACK报文段以后,就关闭连接;
- 此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。
- 第一次分手
HTTP报文浏览器解析渲染页面
- 构建DOM树
- 渲染进程将 HTML 内容转换为能够读懂DOM 树结构。
- 样式计算
- 渲染引擎将 CSS 样式表转化为浏览器可以理解的styleSheets,计算出 DOM 节点的样式。
- 布局阶段
- 创建布局树,并计算元素的布局信息。
- 分层
- 对布局树进行分层,并生成分层树。
- 栅格化
- 为每个图层生成绘制列表,并将其提交到合成线程。合成线程将图层分图块,并栅格化将图块转换成位图。
- 显示
- 合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上。
- 构建DOM树
Get请求传参长度的误区
HTTP 协议 未规定 GET 和 POST 的长度限制
GET 的最大长度显示是因为浏览器和 web 服务器限制了 URI 的长度不同的浏览器和 WEB 服务器,限制的最大长度不一样要支持 IE,则最大长度为 2083byte,若只支持 Chrome,则最大长度 8182byte补充 get 和 post 请求在缓存方面的区别
get 请求类似于查找的过程,用户获取数据,可以不用每次都与数据库连接,所以可以使用缓存
post 做的一般是修改和删除的工作,所以必须与数据库交互,所以不能使用缓存JS 的 new 操作符做了哪些事情
new 操作符新建了一个空对象,这个对象原型指向构造函数的prototype,执行构造函数后返回这个对象。改变函数内部 this 指针的指向函数(bind,apply,call 的区别)
通过 apply 和 call 改变函数的 this 指向,他们两个函数的第一个参数都是一样的表示要改变指向的那个对象,第二个参数,apply 是数组,而 call 则是 arg1,arg2…这种形式。
通过 bind 改变 this 作用域会返回一个新的函数,这个函数不会马上执行JS 的各种位置,比 clientHeight,scrollHeight,offsetHeight ,以及 scrollTop, offsetTop,clientTop 的区别?
clientHeight
: 表示的是可视区域的高度,不包含 border 和滚动条offsetHeight
: 表示可视区域的高度,包含了 border 和滚动条scrollHeight
: 表示了所有区域的高度,包含了因为滚动被隐藏的部分。clientTop
: 表示边框 border 的厚度,在未指定的情况下一般为0scrollTop
: 滚动后被隐藏的高度,获取对象相对于由 offsetParent 属性指定的父坐标(css定位的元素或body元素)距离顶端的高度。
JS 拖拽功能的实现
首先是三个事件,分别是mousedown
,mousemove
,mouseup
当鼠标点击按下的时候,需要一个tag标识此时已经按下,可以执行 mousemove 里面的具体方法。clientX
,clientY
标识的是鼠标的坐标, 分别标识横坐标和纵坐标,并且我们用offsetX
和offsetY
来表示元素的元素的初始坐标JS 中的垃圾回收机制
必要性:
由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript 程序每次创建字符串、 数组或对象时,解释器都必须分配内存来存储那个实体。标记清除
: 垃圾回收器在运行的时候会给存储在内存中的变量都加上标记(所有都加),然后去掉环境变量中的变量,以及被环境变量中的变量所引用的变量(条件性去除标记),删除所有被标记的变量,删除的变量无法在环境变量中被访问所以会被删除,最后垃圾回收器,完成了内存的清除工作, 并回收他们所占用的内存。引用计数法
: 每个值没引用的次数, 当声明了一个变量,并用一个引用类型的值赋值给改变量, 则这个值的引用次数为 1;相反的,如果包含了对这个值引用的变量又取得了另外一个值,则原先的引用值引用次数就减 1;当这个值的引用次数为 0 的时候,说明没有办法再访问这个值了,因此就把所占的内存给回收进来。JS 监听对象属性的改变
- ES5 中可以通过 Object.defineProperty 来实现已有属性的监听缺点: 如果 id 不在 user 对象中,则不能监听 id 的变化
1
2
3
4Object.defineProperty(user,'name',{
set:function(key,value){
}
}) - 在 ES6 中可 以通过 Proxy 来实现即使有属性在 user 中不存在,通过 user.id 来定义也同样可以这样监听这个属性的变化
1
2
3
4var user = new Proxy({},{
set:function(target,key,value,receiver){
}
})
- ES5 中可以通过 Object.defineProperty 来实现已有属性的监听
自己实现一个 bind 函数
原理: 通过 apply 或者 call 方法- 初始版本
1
2
3
4
5
6
7
8Function.prototype.bind = function(obj,arg) {
var arg = Array.prototype.slice.call(arguments,1);
var context = this;
return function(newArg){
arg = arg.concat(Array.prototype.slice.call(newArg));
return context.apply(obj,arg);
}
} - 考虑到原型链
1
2
3
4
5
6
7
8
9
10
11
12
13Function.prototype.bind = function(obj,arg) {
var arg = Array.prototype.slice.call(arguments,1);
var context = this;
var bound = function(newArg) {
arg = arg.concat(Array.prototype.slice.call(newArg));
return context.apply(obj,arg);
}
var F = function() {}
//这里需要一个寄生组合继承
F.prototype = context.prototype;
bound.prototype = new F();
return bound;
}
- 初始版本
实现 JS 中所有对象的深度克隆(包装对象,Date 对象,正则对象)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31function deepClone(obj, hash = new WeakMap()) {
//先把特殊情况全部过滤掉 null undefined date reg
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.has(obj)) return hash.get(obj);
// 不直接创建空对象的目的:克隆的结果和之前保持相同的所属类
// 同时也兼容了数组的情况
let newObj = new obj.constructor;
hash.set(obj, newObj) // 制作一个映射表
//判断是否有 key 为 symbol 的属性
let symKeys = Object.getOwnPropertySymbols(obj);
if (symKeys.length) {
symKeys.forEach(symKey => {
newObj[symKey] = deepClone(obj[symKey], hash);
});
}
for (const key in obj) {
if (obj.hasOwnProperty(key)) { // 不拷贝原型链上的属性
newObj[key] = deepClone(obj[key], hash); // 递归赋值
}
}
return newObj;
}