Web前端开发工程师_JavaScript进阶面试题
事件机制
DOM事件触发顺序是:捕获阶段,目标阶段,冒泡阶段
捕获阶段:就是某个页面元素触发事件后,浏览器会从顶级标签依次向下检查元素是否绑定了相同事件,且设置了捕获,如果有则触发,一直检查到触发事件的元素。
冒泡阶段:浏览器会从触发事件的元素开始向顶级标签依次检查,看元素是否有相同的事件,且未设置捕获,如果有则触发。
例如:
<!DOCTYPE html>
<html id='html'>
<style>
.box {
width: 150px;
height: 150px;
background-color: #f00;
}
.inner {
width: 100px;
height: 100px;
background-color: #0f0;
}
.insert {
width: 50px;
height: 50px;
background-color: #00f;
}
</style>
<body id='body'>
<div id='box'>
<div id='inner'>
<div id='insert'></div>
</div>
</div>
<script>
html.addEventListener('click', clickHandle, true);
body.addEventListener('click', clickHandle, true);
box.addEventListener('click', clickHandle, true);
inner.addEventListener('click', clickHandle, true);
html.onclick = body.onclick = box.onclick = inner.onclick = insert.onclick = clickHandle;
function clickHandle(){
console.log(this.id);
}
</script>
</body>
</html>
当我们点击 insert 元素时,在控制台输出如下内容:
html
body
box
inner
insert
inner
box
body
Html
事件委托是什么?为什么要使用它?
简介:利用浏览器事件冒泡的机制以及事件对象,将子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件委托。
优点:
不用为每一个子元素都绑定一个监听事件,减少了内存上的消耗。
实现了事件的动态绑定,比如说新增了一个子节点,不需要单独地为它添加一个监听事件,它所发生的事件会交给父元素中的监听函数来处理。
页面通讯
BroadcastChannel
它可以帮我们创建一个用于广播的通讯频道。当所有页面都监听同一频道的消息时,其中一个页面通过它发送的消息就会被其它所有页面收到。
使用:
// 创建广播频道
var bc = new BroadcastChannel("bufan");
// 通过 `onmessage` 方法来接受广播消息:
bc.onmessage = function (e){
console.log(`接收到的消息为:`, e.data);
}
// 监听错误
bc.onmessageerror = function(e){
console.warn("error:", e);
}
// 发送消息
bc.postMessage(mydata);
// 关闭监听
bc.close();
虽然好用,但是兼容性不太乐观

LocalStorage 作为前端最常用的本地存储,大家应该已经非常熟悉了;但StorageEvent这个与它相关的事件有些同学可能会比较陌生。
当 LocalStorage 变化时,会触发storage事件。利用这个特性,我们可以在发送消息时,把消息写入到某个 LocalStorage 中;然后在各个页面内,通过监听storage事件即可收到通知。
window.addEventListener("storage", function(e){
if(e.key == "ctc-msg"){
var data = JSON.parse(e.newValue);
console.log("监听到存储改变:", e.newValue);
}
})
需要注意的是:如果某个页面设置存储内容的值与第一次相同,那么不会触发 storage 事件。可以通过每次改变添加时间戳来让每次修改值都触发事件。
浏览器本地存储
sessionStorage:用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。
localStorage:用于持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
二者区别:
存储周期 – 上述介绍
共享范围:localStorage 同源页面共享存储的值;sessionStorage 仅在当前页面共享存储的值;
二者拥有相同的属性和方法:
setItem(key, value):设置存储的键值对
getItem(key):获取对应键名存储的内容
removeItem(key):删除对应键名存储的内容
clear():清空存储u内容
key(n):根据下标获取对应存储的键名
注意:存储对象或者数组时,需要将其更换为JSON字符串,然后再进行存储。
判断数据类型的方式
typeof
使用方式:typeof 要判断类型的内容
返回一个表示数据类型的字符串,返回结果包括:number、boolean、string、object、undefined、function,不能具体区分出 null、array、object
instanceof
使用方式:A instanceof 类型构造函数
返回一个布尔值,用来判断A是否为某类型构造函数的实例。如果返回 true,则证明是对应类型
Object.prototype.toString.call(要判断类型的内容):
返回一个字符串[object 数据类型对应构造函数名],是当前判断类型最准确且最常用的方式
作用域
定义:作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。说人话就是:指变量的取值范围。
常见的的作用域分为两类:
全局作用域
块级作用域(个人认为函数作用域也可以归类于块儿级作用域,所以这里写两个)
全局作用域代表的是浏览器环境,即在 window 上可以访问的属性,块级作用域指由 {} 包裹的内部空间。
由于作用域可以进行嵌套,属于内部的作用域可以访问它外部的作用域中声明变量。
将作用域可访问变量值的查找路径列出来,就是作用域链,目的是指导变量值的查找顺序。
函数作用域:词法作用域
函数作用域在声明时就确定了,不会随着调用环境的更改而更改。
var scope = "window";
function fn(){
console.log(scope);
}
function gn(){
var scope = "gn";
fn();
}
gn(); // window 因为fn只能访问自己作用域中声明的变量和全局的
call、apply、bind
call 和 apply 会执行函数,改变函数中的 this 为第一个接受的参数对象。作用一样,只是传参方式不同;
bind 方法用于创建固定 this 指向的函数或者固定部分参数的函数;
闭包
定义:
闭包就是能够读取其他函数内部变量的函数。
作用:
局部变量无法共享和长久的保存,而全局变量可能造成变量污染,所以我们希望有一种机制既可以长久的保存变量又不会造成全局污染。
特性:
1、函数嵌套函数
2、函数内部可以引用外部的参数和变量
3、参数和变量不会被垃圾回收机制回收
严格模式
在JS代码第一行或者函数体第一行添加 use strict; 即可开启严格模式。
优点:
消除JS语法的一些不合理、不严谨之处,减少一点怪异行为;
消除代码运行的不安全之处,保证代码安全运行;
提高编译器效率,增加运行速度
禁用了在ECMAScript未来版本中可能会用到的一些语法单词(比如class、export等),为未来新版本的JS做好铺垫
缺点:
严格模式在 IE10 以上版本的浏览器中才会被支持
当前网站的 JS 都会进行压缩,一些文件用了严格模式,而另一些没有。这时这些本来是严格模式的文件,被 merge后,这个串就到了文件的中间,不仅没有指示严格模式,反而在压缩后浪费了字节。
原型与原型链
JS中函数对象都有 prototype 属性指向函数对应原型对象;
通过 new 函数() 的方式创建的对象称为该函数的实例,实例具有 __proto__ 属性指向函数的原型对象;
对象可以读取原型上的属性;
通过更改对象的 __proto__ 属性可以设置对象的原型,让原型形成递进关系,由此形成的链式结构称为原型链。原型链提供了对象属性的查找方向–从当前元素开始,返回原型链中第一个查找到的属性值。
原型链的终点是 null,由(Object.prototype.__proto__)得到
一般而言创建一个实例对象,它的原型链为:
实例对象 --__proto__——> 构造函数的原型 --__proto__——> Object的原型 --__proto__——> null
完整原型图:

上图中还有两条隐藏的线,只有聪明的同学才能看见:
Function 可以通过 new 生成 Object构造函数
Object.__proto__ 指向Function的原型
JS中的鸡和蛋:对象是由构造函数产生的,函数本身也是对象,那么是先有的函数?还是先有的对象?
new操作符做了什么
创建空对象
将给对象的原型属性(__proto__)指向构造函数的原型
将构造函数中 this 绑定的属性赋予对象
返回该对象
function Person(name){
this.name = name;
}
Person.prototype.age = 23;
// var libai = new Person("libai");
// 相当于如下操作
var libai = {}
libai.__proto__ = Person.prototype;
Person.call(libai, "libai");
继承
寄生式组合继承:使用借用构造函数继承让子类继承父类构造函数定义的实例属性,使用寄生式继承让子类继承父类原型定义的共享属性
function Animal(type){
this.type = type;
}
Animal.prototype.run = function(){
console.log("running");
}
function Cat(name, type){
// 借用构造函数继承父类实例属性
Animal.call(this,type);
this.name = name;
}
// 寄生式继承父类原型属性
/*
function Temp(){}
Temp.prototype = Animal.prototype;
Cat.prototype = new Temp();
*/
Cat.prototype.__proto__ = Animal.prototype;
// 或者
Cat.prototype = Object.create(Animal.prototype);
var cat = new Cat();
cat.run();
浏览器输入网址到显示的过程
详细版:
浏览器会开启一个线程来处理这个请求,对 URL 分析判断如果是 http 协议就按照 Web 方式来处理;
调用浏览器内核中的对应方法,比如 WebView 中的 loadUrl 方法;
通过DNS解析获取网址的IP地址,设置 UA 等信息发出第二个GET请求;
进行HTTP协议会话,客户端发送报头(请求报头);
进入到web服务器上的 Web Server,如 Apache、Tomcat、Node.JS 等服务器;
进入部署好的后端应用,如 PHP、Java、JavaScript、Python 等,找到对应的请求处理;
处理结束回馈报头,此处如果浏览器访问过,缓存上有对应资源,会与服务器最后修改时间对比,一致则返回304;
浏览器开始下载html文档(响应报头,状态码200),同时使用缓存;
文档树建立,根据标记请求所需指定MIME类型的文件(比如css、js),同时设置了cookie;
页面开始渲染DOM,JS根据DOM API操作DOM,执行事件绑定等,页面显示完成。
简洁版:
浏览器根据请求的URL交给DNS域名解析,找到真实IP,向服务器发起请求;
服务器交给后台处理完成后返回数据,浏览器接收文件(HTML、JS、CSS、图象等);
浏览器对加载到的资源(HTML、JS、CSS等)进行语法解析,建立相应的内部数据结构(如HTML的DOM);
载入解析到的资源文件,渲染页面,完成。
防抖和节流
防抖 – 在一定时间内触发事件就执行一次定义好的行为,如果在这段时间内再次触发事件,则时间重新计算,且不执行定义好的行为。
// 防抖
function debounce(func, wait) {
var timer = null;
return function() {
if(timer){
clearTimeout(timer);
}
timer = setTimeout(function() {
func();
timer = null;
}, wait)
}
}
// 立即执行的防抖
function debounce(func, wait) {
var timer = null;
return function() {
timer ? clearInterval(timer) : func();
timer = setTimeout(function() {
timer = null;
}, wait)
}
}
节流 – 在一定时间内触发事件就执行一次定义好的行为,如果在这段时间内再次触发事件,不重新计算时间,且不执行定义好的行为
// 节流
// 时间戳版,先执行
function throttle(func, wait) {
var startTime = Date.now();
return function() {
var now = Date.now();
if(now - startTime > wait){
func();
startTime = now;
}
}
}
// 定时器版
function throttleImmediately(func, wait) {
var timer = null;
return function() {
if(!timer){
// func(); // 先执行,再等待
timer = setTimeout(function() {
func(); // 先等待,再执行
timer = null;
}, wait)
}
}
}
AJAX请求封装
function ajax(params){
// if(!params.url){
// alert("请传入url地址");
// }
// 对象赋默认值
params = Object.assign({
url: location.href,
type: "GET",
dataType: "json",
timeout: 10000,
contentType: "application/x-www-form-urlencoded",
data: {},
success: function(){},
error: function(){}
}, params);
// 1. 声明核心对象
var xhr = new XMLHttpRequest();
// 2. 设置预期返回值类型
xhr.responseType = params.dataType;
// 3. 设置各种监听
xhr.timeout = params.timeout;
xhr.onloadstart = params.beforeSend;
xhr.onload = function (){
params.success(xhr.response);
}
xhr.onerror = function (){
params.error(xhr.status);
};
xhr.onloadend = params.complete;
// 将对象形式参数改成form形式
// name:value => name=value
var str = new URLSearchParams(params.data).toString();
if(params.type.toUpperCase() === "GET"){
// 4. 设置请求相关
xhr.open("GET", params.url + (str ? "?"+str : ""));
// 5. 发送请求
xhr.send(null);
}else {
xhr.open("POST", params.url);
xhr.setRequestHeader("Content-Type", params.contentType);
if(/json/.test(params.contentType)){
xhr.send(JSON.stringify(params.data));
}else if(/www/.test(params.contentType)){
xhr.send(str);
}else {
xhr.send(params.data);
}
}
}
TCP三次握手和四次挥手
通俗来讲,三次握手主要是让客户端与服务端确认自己是否具有收发消息的能力。
将其抽象化:
由客户端发起
Client:喂?收到消息请回复 ------1------> Server
第一次:服务端确认了自己接收消息的能力
Client <-----2------- Server:我收到你发来的消息了,你收到我消息请回复
第二次:客户端确认了自己可以收/发消息的能力
Client:OK,我也能收到你发来的消息 ------3------> Server
第三次:服务端确认自己具有发送消息的能力
四次挥手让双方都知道对方要断开连接:
由客户端发起
Client:我要断开连接了 ----1---> Server
第一次:客户端关闭给服务端发送数据的通道
Client <------2------- Server:好的
第二次:服务端知道客户端不会发数据了,关闭接受数据的通道
Client <------3------- Server:我也要断开连接了
第三次:服务端关闭发送数据的通道
Client:好的 ----4---> Server
第四次:客户端知道服务端不会发消息了,关闭接受数据的通道
注意:上述对话内容是通过报文发送的,不发送的是正常连接时传输的数据(非报文)。
为什么是四次挥手:握手的时候,C 和 S 打个招呼,S 可以直接把自己发送的信息和对 C 的回应信息一起带上,但是挥手的时候,C 说我要断开了,S 可能还有没发完最后的数据,因此需要先回应一下 C,我收到你的断开的请求了,但是你要等我把最后的内容给你,所以这里分开了 2 步: (1)回应 C; (2)发送自己的最后一个数据
JS中的同步和异步任务
同步任务:前一个任务结束后再执行后一个任务,程序的执行顺序与任务的排列顺序是一致的、同步的。比如做饭的同步做法:我们要烧水煮饭,等水开了(10分钟之后),再去切菜,炒菜。
异步任务:并行处理多件任务,在做这一个任务的同时,你还可以去处理其他任务。比如做饭的异步做法,我们在烧水的同时,利用这10分钟,去切菜,炒菜。
JavaScript 本身是单线程的,即它在同一个时间只能做一件事。所有任务需要排队,前一个任务结束,才会执行后一个任务。这样所导致的问题是: 如果某一段 JS 执行的时间过长,这样就会造成后面的代码执行不连贯,导致页面渲染加载阻塞的感觉。
为了解决这个问题,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程。于是,JS 中出现了同步和异步。
JS中的异步任务一般分为以下三种类型:
1,DOM事件:如 click、resize 等
2,资源加载:如 load、error 等
3,定时器:包括 setInterval、setTimeout 等
4,网络资源请求:Ajax、Fetch 等
JS 在执行代码时,会按照代码的书写顺序,将每一句代码压入调用堆栈中执行,如果遇到异步代码,它们会被添加到浏览器提供的异步线程中,等到它们的触发条件满足,被压入任务队列等候执行。当页面代码执行完毕,事件轮循(event-loop)就会按次序读取任务队列中等候执行的异步任务,将它们推入调用堆栈(call stack)中执行。
GET和POST的区别
语义上:GET 用与获取数据,POST 用于提交数据
GET 请求的参数会在地址栏显示,而 POST 请求会把请求的数据放在HTTP请求体中,不在地址栏显示,相对安全
GET 参数有长度限制(受限于URL长度,具体的数值取决于浏览器和服务器的限制,最长2048字节),而 POST 无限制
GET 请求参数只能以 name=value&... 形式,POST 参数有更多的数据格式
GET 请求会被浏览器主动缓存
网络安全
XSS攻击:注入恶意代码攻击。比如input框里面别人如果输入一段恶意的脚本代码直接解析,就有可能造成数据的不安全(解决方法:1.cookie 设置 httpOnly; 2.转义页面上的输入内容和输出内容)
CSRF:跨站点请求伪造。(解决办法:1.get 不修改数据;2.不被第三方网站访问到用户的 cookie; 3. 设置白名单,不被第三方网站请求;4.请求加校验)
内容有些多,大家可以先点赞收藏,但一定要去看。当你把大厂面试题知识点都看完并且理解了,以后就没有你过不了的前端面试。资源都帮你整理好了,还不学就有些说不过去了。
不凡学院最新前端学习路线图

2022年不凡学院前端全套教程




