1. 解释一下原型链?
在js中每个实例对象都有一个 __proto__属性,它指向该实例对象的构造函数的原型。该构造函数的原型也是一个对象,也有一个自己的__proto__属性,同样指向他的构造函数的原型,就这样一层一层形成了的链式结构称为原型链。
如果在当前对象中查找某个属性或方法时,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或方法为止,或者不存在的时候查找到原型链的顶端 null为止。
优点: 可以实现属性和方法的公用,节约内存。
缺点: 在原型链上查找是非常耗时的,如果对象的继承结构太深的时候,查找属性和方法的时候会对性能有一定的副作用,试图去访问一个不存在的属性或者方法的时候,也会遍历整个原型链。
场景: 我们可以通过原型链来实现继承,数据的共享,和方法的复用。
- 每个对象上都拥有一些共同的方法比如 toString()、valueOf() 等,toString()、valueOf() 是由 Object 上继承而来的,这一点也是使用的原型
- vue中组件的继承,也是使用的原型链
- 在Vue.prototype挂载一些属性和方法,供所有的组件去使用
2. instanceof的原理
主要用于检测构造函数的 prototype 是否出现在某个实例对象的原型链上,因此他主要是用于引用类型的数据类型判断,底层是通过遍历该实例对象的整个原型链去实现,如果原型链太长,可能性能会有一些问题。
object instanceof constructor
3.闭包
闭包是有权访问另一个函数作用域变量的函数。
最常见的场景是函数嵌套函数,内部函数访问外部函数的变量,这样就形成了一个闭包。
闭包优点主要有两点:
第一个是保存,子函数的引用父函数的某个数据,将其数据保存下来。
第二点是保护,是在第一点的基础上再去形成了不被销毁的栈内存。
缺点是因为被外界所引用,所以无法被垃圾回收机制回收,从而停留在堆内存中,造成内存泄露,我们日常开发应该及时的去释放变量置为null
应用场景是:
- 防抖节流,科里化
- 在 Vue 的组件定义中,会用到一个 data 函数来返回一个对象,这个函数就是一个闭包,在组件实例化的过程中,每个实例都会得到一个独立的数据对象
4.垃圾回收
垃圾回收是自动管理内存机制,他可以帮我们自动的清理不再使用的对象,从而释放内存空间。
js 中的垃圾回收机制主要有两种算法,主要有引用计数法和标记清除法,
-
引用计数,这个算法的原理是跟踪对象的引用次数,每当对象被引用的时候,引用次数会被加1,每当对象被取消引用的时候会被减1,当该对象引用次数为0的时候说明该对象不再被使用,可以被回收。但是这个算法可以实时回收,缺点是存在循环引用。
-
标记清除法,标记清除法分为标记阶段和清除阶段,在标记阶段,垃圾回收器会遍历所有的对象,标记那些所有还在被引用的对象,在清除阶段,垃圾回收会清除那些没有被标记的对象,也就是不在引用的对象。
V8引擎采用分代式垃圾回收,分为新生代和老生代,对于存活时间短,内存小,新的选新生代管理,存活时间长,内存大,老的对象被老生代管理。新生代通常只有1-8MB的容量,而老生代的容量要大的多。新生代主要采用了Scavenge 算法进行垃圾回收,老生代里面采用了标记清除法。
Scavenge 算法 指的是将内存一分为二,会有一块处于使用中,一块处于空闲状态,处于使用状态的空间称为from空间,处于空闲中的空间称为to空间,当我们创建一个对象的时候,这个对象会被分配到from空间,当from空间被填满的时候,垃圾回收器会开始工作,他会遍历from空间中的所有对象,找出那些还在被使用的对象,这些存活对象会被复制到To空间,而from空间中的垃圾对象则会被释放。然后from空间和to空间的角色互换,原来的from空间变成to空间,原来的to空间变成from空间。当一个对象被多次复制依然存活的时,他会被认为成是生命周期较长的对象,这种生命周期较长的对象会被晋升到老生代中,采用新的算法进行管理。还有一种情况就是当复制一个对象到空闲区的时候,占用的空闲区空间超过25%,那么这个对象会被直接晋升到老生代空间中,这是为了防止To空间被过度填满,影响后续的内存分配。他的主要优点是简单,高效,缺点是 1.内存利用率低 2.无法处理生命周期长的对象 3.复制操作的开销。所以在老生代里面采用了标记清理法,标记清理是只清除死亡对象,而Scavenge只复制活着的对象。
当垃圾回收器运行时,JS引擎会暂停代码的执行,这就是全停顿。
在 V8 新生代的垃圾回收中,因其空间较小,且存活对象较少,所以全停顿的影响不大。但老生代中,如果在执行垃圾回收的过程中,占用时间过久,需要等待执行完垃圾回收操作才能做其他事情,这将就可能会造成页面的卡顿现象。
全停顿垃圾回收是一次性清理所有的垃圾,期间不能进行其他的任务。在新生代中由于空间较小,所以全停顿的影响不大,而在老生代中,如果在执行垃圾回收的过程中,占用时间过久,需要等待执行完垃圾回收操作才能做其他事情,这将就可能会造成页面的卡顿现象。为了解决这个问题,V8 引擎采用了一些增量标记算法,使用增量标记算法可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样就不会让用户因为垃圾回收任务而感受到页面的卡顿了。
5.浏览器的缓存策略
浏览器的缓存策略是这样的,一般在第一次发送请求后,收到的响应的字段如果包含了强缓存的字段,强缓存的字段包括Expires 或者max-age,那么我们在第二次去网络请求的时候就会先根据Expires 或者max-age属性设置的时间判断缓存否在有效期内。如果在有效期内,则直接使用浏览器本地磁盘的数据。如果没有在有效期内就需要走协商缓存的过程, 将第一次请求响应获取到的last-Modifide 和Etag值发送给服务端,服务端如果发现两个值没有改变,资源没有改动,则返回304,浏览器收到此状态码则继续使用缓存中的数据,如果前面两个值发生了改变则返回200,将新的数据发给浏览器。
如果第一次获取的响应头的Cache-control 里面设置了 no-cache,第二次请求直接走上面一样的协商缓存过程,资源没有过期,服务端返回304,没有过期返回200。
浏览器的缓存主要是降低请求浏览器请求资源的时间,提高加载速度,降低服务器的负荷。
- Cache-control 除了上面的字段还可以设置
- no-cache :直接走协商缓存的过程
- no-store:设置了该字段表示禁止任何缓存,每次都会向服务端发起新的请求,拉取最新的资源;
- public:可以被客户端和代理服务器缓存
- private:只能被用户浏览器缓存
6.cookies,sessionStorage,localStorage的区别和使用
- 在存储大小的是cookies 是 4KB 左右。sessionStorage,localStorage 都是 5MB左右。
- 有效期方面:cookies 是在设定的过期时间前都是有效的,sessionStorage 是本窗口有效,关闭标签页或者关闭浏览器后数据被清除。localStorage,持久化存储,没有过期时间,数据会一直保留,直到手动删除。
- 与服务器通信方面,在浏览器会和服务端交互时,cookie会进行传递。而sessionStorage,localStorage,仅仅是存在本地不会和服务端进行通信的。
- cookie 存储一些身份登录信息,sessionStorage存储一些临时性数据,localStorage是做一些持久化的存储。
7. promise
promise是异步编程的解决方案,主要是为了用来解决回调地狱的问题而产生的。
promise 主要三个状态,分别是 pending fullfiled rejected,他从pending变成fullfiled,或者pending变成rejected,这些状态一旦改变就不可逆转。promise的优点是解决回调地狱的问题,然后他有更好的错误处理机制,他可以处理多个异步任务的并发。缺点是promise一旦开始就不可中断。promise的几个方法,Promise .all可以处理异步请求的并发。Promise .race 方法可以处理请求超时,.then 处理成功的回调,.catch 处理失败的回调。
8.事件循环
事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。 在 Chrome 的源码中,它开启一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。 过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。 根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。
下面是自己的理解
事件循环是一种异步执行机制,他主要来处理代码中的异步任务。 js代码执行是在渲染主线程中执行的,当代码的执行遇到一个异步任务时,比方说定时任务 ,事件监听时,浏览器会将这些异步任务先交其他线程去处理,等其他线程处理完成会将异步任务传递的回调函数包装成任务再将其添加微任务队列和其他队列。 当主线程把当前的调用栈的代码执行完,如果微任务队列里面有需要执行的任务,则会先从微任务队列中取任务,取出的顺序按照添加顺序,当微任务队列中执行完成,会从队列的优先级较高的任务队列中取出任务去执行,如果所有任务都执行完成,主线程进入休眠状态,这样循环往复的过程就叫事件循环。
9.new 的原理
new可以用来创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。
new操作符具体做了什么
- 创建一个空对象,该对象的原型为构造函数的原型对象。
- 将构造函数的 this 绑定到该空对象上
- 执行构造函数的代码,并将属性和方法添加到该空对象中。
- 如果构造函数显式返回一个对象,则返回该对象,否则返回前面创建的空对象。
手写
function myNew(Constructor, ...args){
var obj = Object.create(Contrutor.prototype);
var result = Contrutor.apply(obj,args);
return (typeof result === 'objct' && result !=== null) ? result :obj;
}
10.js的作用域
作用域是指程序代码中定义变量的区域,它规定了变量的可见性和生命周期。在 JavaScript 中,作用域可以分为全局作用域和函数作用域,块级作用域,它们之间存在嵌套关系,也就是所谓的作用域链。
全局作用域是指在程序中定义在最外层的变量,它在整个程序的任何位置都可以被访问到。函数作用域是指在函数中定义的变量,它们只能在该函数内部被访问到,函数外部无法访问。 ES6 中新增了块级作用域,最直接的表现就是新增的 let 和 const 关键词,使用这两个关键词定义的变量只能在块级作用域中被访问,有“暂时性死区”的特点,也就是说这个变量在定义之前是不能被使用的。
在函数中定义的变量也可以被嵌套在其他函数的作用域中,这样就形成了作用域链。 当程序执行到一个作用域时,它会先搜索该作用域中的变量,如果找到了就直接使用,否则就向上一级作用域继续搜索,直到找到全局作用域为止。这个搜索过程形成了作用域链,它保证了变量在正确的作用域中被访问和使用。
JavaScript 中的作用域还有几点特殊的地方,就是在函数中定义的变量,如果没有使用关键字 var 或 let 等声明,它就会变成全局变量。这种情况下,该变量会被添加到全局对象中,可以在任何作用域中访问和使用。
11.symbol这个新增的基础数据类型有什么用
Symbol 是在 ES6 中新增的基础数据类型,它的主要作用是创建一个唯一的标识符,用于对象属性名的命名、常量的定义等场景。
Symbol 还有一个重要的特点是,它不会出现在 for...in、for...of、Object.keys()、Object.getOwnPropertyNames() 等遍历对象属性的方法中,因此可以用来定义一些不希望被遍历到的属性,例如一些内部实现细节或隐藏属性
12.typeof null 为什么是object
在 JavaScript 最初的版本中,使用 32 位的值表示一个变量,其中前 3 位用于表示值的类型。000 表示对象,010 表示浮点数,100 表示字符串,110 表示布尔值,和其他的值都被认为是指针。 在这种表示法下,null 被解释为一个全零(32个0)的指针,也就是说它被认为是一个空对象引用,因此 typeof null 的结果就是 "object"。
因此在判断变量是否为 null 时,建议使用严格相等运算符(===)进行判断。
13.事件池
事件池(event pooling)是指浏览器在事件处理中,为了节省内存和提高性能,会对事件对象进行重复利用。 在浏览器中,每次触发事件都会创建一个对应的事件对象,并把它传递给回调函数。而在事件池机制中,浏览器会先检查是否存在空闲的事件对象,如果有,则直接从池中取出事件对象进行使用,避免了重复创建对象的开销,提高了性能。
需要注意的是,事件池机制只针对同一类型的事件。例如,当有多个鼠标点击事件同时发生时,浏览器会在内部为每个鼠标点击事件维护一个事件池,而不是把不同类型的事件混合在同一个事件池中进行管理。 值得一提的是,由于事件池机制的存在,我们在事件处理函数中不能异步获取事件对象,因为事件对象会在事件处理函数执行完成后被返回池中,如果此时异步代码仍然在使用事件对象,就会出现不可预料的后果。
14.es6新特性?箭头函数和普通函数有啥区别?箭头函数能当构造函数吗?
ES6(ECMAScript 2015)是 JavaScript 的一个新版本,引入了很多新的特性和语法,其中一些比较常用的包括:
- 块级作用域:通过 let 和 const 声明的变量只在当前块级作用域中有效。
- 箭头函数:使用 => 符号定义的函数,具有简化的语法和自动绑定 this 上下文的特点。
- 模板字符串:使用反引号 `` 和 ${} 操作符,可以方便地拼接字符串和变量。
- 解构赋值:可以将数组或对象的值解构赋给变量。
- 类和继承:引入了 class 和 extends 关键字,使得 JavaScript 支持面向对象编程。
- Promise 和 async/await:用于处理异步编程的新特性。
关于箭头函数和普通函数的区别,主要有以下几点:
- 箭头函数没有自己的 this 上下文,它的 this 上下文继承自外部作用域,因此不能使用 call()、apply() 或 bind() 方法改变 this 上下文。
- 箭头函数没有自己的 arguments 对象,如果需要获取函数参数,可以使用 rest 参数或者展开运算符。
- 箭头函数不能作为构造函数使用,不能使用 new 关键字创建对象。
15.promise.all 和 promise.allsettled区别
Promise.all() 方法,如果其中有一个 Promise 被 reject,则会立即 reject 并返回相应的错误信息。只有所有 Promise 都成功了才算成功,有一个失败了就算失败。
promise.allsettled区别 ,它会等到所有 Promise 都执行完毕,无论成功还是失败,都会把每个 Promise 的状态信息收集到一个数组里面返回。
16.substring和substr的区别
substring() 方法接收两个参数,起始位置和结束位置。它截取从起始位置到结束位置之间的字符,包括起始位置的字符,但不包括结束位置的字符。如果省略第二个参数,则截取到字符串末尾。
substr() 方法接收两个参数,起始位置和截取的字符数。它从起始位置开始截取指定数量的字符,如果省略第二个参数,则截取到字符串末尾。
17.js脚本异步加载如何实现 有什么区别
1.动态创建
- 使用 XMLHttpRequest 对象或 Fetch API 发送异步请求,并在请求成功后将响应文本解析为 JavaScript 代码,然后使用 eval() 函数或 Function() 构造函数来执行脚本。
使用这两种方式可以实现 JavaScript 脚本的异步加载,相比于同步加载脚本,异步加载具有以下区别:
- 异步加载可以提高页面的加载速度和响应性能,避免因 JavaScript 阻塞而造成页面卡顿的情况。
- 异步加载可以避免因加载脚本而造成的阻塞情况,使页面的其他资源可以更快地加载和呈现。
- 异步加载可以更灵活地控制脚本的加载顺序和执行时间,可以根据页面需要动态加载和卸载脚本,提高页面的可维护性和可扩展性。
18.for in/for of的区别
for...in 适用于遍历对象的可枚举属性,包括自有属性和继承属性,而 for...of 适用于遍历数组、字符串等可迭代对象的元素值。
19. js中如何判断数据类型
- 使用 typeof 操作符, typeof 操作符可以返回一个值的数据类型,它适用于除了 null 以外的所有值,需要注意的是,typeof null 返回 "object",这是一个历史遗留问题。
- 使用 instanceof 操作符,instanceof 操作符可以判断一个对象是否是某个构造函数的实例
- 使用 Object.prototype.toString 方法,需要注意的是,使用 Object.prototype.toString 方法判断基本类型值时,返回的是其包装对象的类型,而不是基本类型本身的类型。
20. splice和slice会改变原数组吗?怎么删除数组最后一个元素?
splice() 方法可以在数组中添加、删除或替换元素,并返回被删除的元素,它会改变原数组。
slice() 方法是从原数组中返回指定开始和结束位置的元素组成的新数组,它不会改变原数组。
splice() 方法删除数组的最后一个元素:
const arr = [1, 2, 3, 4];
arr.splice(-1, 1); // 从倒数第一个位置开始删除一个元素console.log(arr); // [1, 2, 3]
21. ==和===有什么区别
在 JavaScript 中,== 和 === 都用于比较两个值是否相等,但它们的比较方式不同。 == 运算符进行比较时,会先进行类型转换,然后再比较两个值是否相等。类型转换的规则比较复杂,但可以简单地概括为以下几点:
- 如果两个值类型相同,则直接比较它们的值。
- 如果一个值是 null,另一个值是 undefined,则它们相等。
- 如果一个值是数字,另一个值是字符串,则将字符串转换为数字后再比较。
- 如果一个值是布尔值,另一个值是非布尔值,则将布尔值转换为数字后再比较。
- 如果一个值是对象,另一个值是数字、字符串或布尔值,则将对象转换为原始值后再比较。
=== 运算符进行比较时,不进行类型转换,只有当两个值的类型和值都相等时才会返回 true。 一般来说,建议优先使用 === 运算符进行比较,因为它可以避免类型转换的问题,更加严格和安全。
22. requestAnimationFrame/requestIdleCallback,分别有什么用?
requestAnimationFrame 是浏览器提供的一种动画帧请求机制,它会在浏览器下一次绘制之前执行指定的回调函数。使用 requestAnimationFrame 可以实现更加流畅的动画效果,同时也可以减少页面的闪烁和卡顿。
function animate() {
// 在这里编写动画逻辑requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
requestIdleCallback 它的作用是在浏览器空闲时执行指定的回调函数。这个 API 的目的是让开发者能够在浏览器空闲时,进行一些比较耗时的任务,例如计算和渲染。这样做的好处是可以提高网页的性能和响应速度,同时也可以避免阻塞浏览器的主线程,导致用户体验不佳。
function doWork(deadline) {
while (deadline.timeRemaining() > 0) {
// 在这里编写任务逻辑
}
if (还有任务需要执行) {
requestIdleCallback(doWork);
}
}
requestIdleCallback(doWork);
需要注意的是,requestIdleCallback 的回调函数接受一个 IdleDeadline 参数,它包含了当前空闲时间的相关信息。开发者可以通过这个参数,根据浏览器的空闲时间进行任务调度和优化。 综上所述,requestAnimationFrame 适用于需要在下一次绘制之前执行的动画任务,而 requestIdleCallback 则适用于需要在浏览器空闲时执行的耗时任务。
23. let全局声明变量,window能取到吗?
使用 let 声明的变量不会挂在全局对象 window 上,因此无法通过 window.variableName 的方式访问。这与使用 var 声明的变量不同,var 声明的变量会被挂载在全局对象上,因此可以通过 window.variableName 的方式访问
24. 数组,map的foreach能否结束循环 ?
数组的 forEach 方法默认不支持提前结束循环,即无法使用类似于 break 或 return 的语法来跳出循环。但是可以使用抛出异常的方式来达到提前结束循环的效果。 需要注意的是,这种方式并不常用,通常可以使用 for 循环或 some、every 等数组方法来替代。
25. 如何合并对象?
- 使用 Object.assign() 合并对象
- 使用展开运算符 ... 合并对象 需要注意的是,如果合并的对象中有同名属性,则后面的属性值会覆盖前面的属性值。
26. 如何判断一个对象是不是空对象?
- 使用Object.keys()方法获取对象的属性列表,然后判断列表长度是否为0。
- 使用for...in循环遍历对象,如果有属性存在则不是空对象。
27. this 指向 ?
总结起来,this 的指向规律有如下几条
- 在函数体中,非显式或隐式地简单调用函数时,在严格模式下,函数内的 this 会被绑定到 undefined 上,在非严格模式下则会被绑定到全局对象 window/global 上。
- 一般使用 new 方法调用构造函数时,构造函数内的 this 会被绑定到新创建的对象上。
- 一般通过 call/apply/bind 方法显式调用函数时,函数体内的 this 会被绑定到指定参数的对象上。
- 一般通过上下文对象调用函数时,函数体内的 this 会被绑定到该对象上。
另外还有几个特殊的点: a .如果是DOM事件函数,this指向事件源 b. 箭头函数会根据其声明的地方来决定 this: c. 数组方法里面的this指向全局对象 d. setInterval回调函数中的this指向全局对象
28. let、const 和 var 的区别
-
变量提升 var声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined let和const不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错
-
暂时性死区 var不存在暂时性死区 let和const存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量
-
块级作用域 var不存在块级作用域 let和const存在块级作用域
-
重复声明 var允许重复声明变量 let和const在同一作用域不允许重复声明变量
-
修改声明的变量 var和let可以 const声明一个只读的常量。一旦声明,常量的值就不能改变
-
使用 能用const的情况尽量使用const,其他情况下大多数使用let,避免使用var
29. apply、call 和 bind 的作用、区别、实现
- call、apply、bind相同点:都是改变this的指向,如果第一个参数是nul或者undefined,会把全局对象作为this的值。
区别:
- 执行时机: apply() 和 call() 都会立即调用函数。 bind() 返回的是一个新的已绑定的函数,并不会立即执行,需要手动调用。
- 参数传递方式: apply() 接受参数是数组或者类数组形式。 call() 接受的是参数列表形式。 bind() 传入的也是参数列表,但是可以多次传入
30. 正向代理和反向代理的区别
正向代理: 客户端想获得一个服务器的数据,但是因为种种原因无法直接获取。于是客户端设置了一个代理服务器,并且指定目标服务器,之后代理服务器向目标服务器转交请求并将获得的内容发送给客户端。这样本质上起到了对真实服务器隐藏真实客户端的目的。实现正向代理需要修改客户端,比如修改浏览器配置。
反向代理: 服务器为了能够将工作负载分别到多个服务器来达到负载均衡等目的,当其受到请求后,会首先根据转发规则来确定请求应该被转发到哪个服务器上,然后将请求转发到对应的真实服务器上。这样本质上起到了对客户端隐藏真实服务器的作用。 一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。
31. javaScript 脚本异步加载如何实现,有什么区别?
正常浏览器遇到script标签会立即加载并执行指定的脚本,这个过程是会阻塞后续文档的加载。defer 和 async属性都是异步加载外部的JavaScript脚本文件,他们两个之间也有区别: async 下载 JS 文件的时候不会阻塞 DOM 树的构建,下载完就会立即执行,执行该 JS 代码会阻塞 DOM 树的构建。
defer 下载 JS 文件的时候不会阻塞 DOM 树的构建,下载完并不是立即执行,是要等待 DOM 树构建完毕后再执行此 JS 文件,要在DOMContentLoaded 事件触发之前完成
拓展: 为什么解析必须停止呢?
原因很简单,这是因为 Javascript 脚本可以改变 HTML 以及根据 HTML 生成的 DOM 树结构。例如,脚本可以通过使用 document.createElement( ) 来添加节点从而更改 DOM 结构。
preload:预加载的方式,向浏览器预先声明一个需要提前加载的资源,在需要使用的就可以直接获取,不必从网络获取了
prefetch 是一种利用浏览器的空闲时间加载页面将来可能用到的资源的一种机制,通常可以用于加载非首页的其他页面所需要的资源,以便加快后续页面的首屏速度
prefetch 加载的资源可以获取非当前页面所需要的资源,并且将其放入缓存至少 5 分钟(无论资源是否可以缓存)。并且,当页面跳转时,未完成的 prefetch 请求不会被中断;
prerender 在后台渲染某个需要提前渲染的整个页面。
preconnect
DOMContentLoaded事件 :该事件可以用来检测HTML页面是否完全加载完毕
32. 如何判断数组类型?
- 通过原型链做判断
obj.__proto__ === Array.prototype;
- 通过ES6的Array.isArray()做判断
- 通过instanceof做判断
obj instanceof Array
33. 深拷贝和浅拷贝
浅拷贝:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制)。浅拷贝只复制指向某个对象的指针(引用地址),而不复制对象本身,新旧对象还是共享同一块内存。
深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。
浅拷贝方法
1. 直接赋值
直接赋值是最常见的一种浅拷贝方式。例如:
var stu = {
name: 'xiejie',
age: 18
}
// 直接赋值
var stu2 = stu;
stu2.name = "zhangsan";
console.log(stu); // { name: 'zhangsan', age: 18 }
console.log(stu2); // { name: 'zhangsan', age: 18 }
2. Object.assign 方法
我们先来看一下 Object.assign 方法的基本用法。
该方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。
如下:
var stu = {
name: 'xiejie'
}
var stu2 = Object.assign(stu, { age: 18 }, { gender: 'male' })
console.log(stu2); // { name: 'xiejie', age: 18, gender: 'male' }
在上面的代码中,我们有一个对象 stu,然后使用 Object.assign 方法将后面两个对象的属性值分配到 stu 目标对象上面。
最终得到 { name: 'xiejie', age: 18, gender: 'male' } 这个对象。
通过这个方法,我们就可以实现一个对象的拷贝。例如:
const stu = {
name: 'xiejie',
age: 18
}
const stu2 = Object.assign({}, stu)
stu2.name = 'zhangsan';
console.log(stu); // { name: 'xiejie', age: 18 }
console.log(stu2); // { name: 'zhangsan', age: 18 }
在上面的代码中,我们使用 Object.assign 方法来对 stu 方法进行拷贝,并且可以看到修改拷贝后对象的值,并没有影响原来的对象,这仿佛实现了一个深拷贝。
然而,Object.assign 方法事实上是一个浅拷贝。
当对象的属性值对应的是一个对象时,该方法拷贝的是对象的属性的引用,而不是对象本身。
例如:
const stu = {
name: 'xiejie',
age: 18,
stuInfo: {
No: 1,
score: 100
}
}
const stu2 = Object.assign({}, stu)
stu2.name = 'zhangsan';
stu2.stuInfo.score = 90;
console.log(stu); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 90 } }
console.log(stu2); // { name: 'zhangsan', age: 18, stuInfo: { No: 1, score: 90 } }
3. ES6 扩展运算符
首先我们还是来回顾一下 ES6 扩展运算符的基本用法。
ES6 扩展运算符可以将数组表达式或者 string 在语法层面展开,还可以在构造字面量对象时,将对象表达式按 key-value 的方式展开。
例如:
var arr = [1, 2, 3];
var arr2 = [3, 5, 8, 1, ...arr]; // 展开数组
console.log(arr2); // [3, 5, 8, 1, 1, 2, 3]
var stu = {
name: 'xiejie',
age: 18
}
var stu2 = { ...stu, score: 100 }; // 展开对象
console.log(stu2); // { name: 'xiejie', age: 18, score: 100 }
接下来我们来使用扩展运算符来实现对象的拷贝,如下:
const stu = {
name: 'xiejie',
age: 18
}
const stu2 = {...stu}
stu2.name = 'zhangsan';
console.log(stu); // { name: 'xiejie', age: 18 }
console.log(stu2); // { name: 'zhangsan', age: 18 }
但是和 Object.assign 方法一样,如果对象中某个属性对应的值为引用类型,那么直接拷贝的是引用地址。如下:
const stu = {
name: 'xiejie',
age: 18,
stuInfo: {
No: 1,
score: 100
}
}
const stu2 = {...stu}
stu2.name = 'zhangsan';
stu2.stuInfo.score = 90;
console.log(stu); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 90 } }
console.log(stu2); // { name: 'zhangsan', age: 18, stuInfo: { No: 1, score: 90 } }
4. 数组的 slice 和 concat 方法
在 javascript 中,数组也是一种对象,所以也会涉及到深浅拷贝的问题。
在 Array 中的 slice 和 concat 方法,不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。
例如:
// concat 拷贝数组
var arr1 = [1, true, 'Hello'];
var arr2 = arr1.concat();
console.log(arr1); // [ 1, true, 'Hello' ]
console.log(arr2); // [ 1, true, 'Hello' ]
arr2[0] = 2;
console.log(arr1); // [ 1, true, 'Hello' ]
console.log(arr2); // [ 2, true, 'Hello' ]
// slice 拷贝数组
var arr1 = [1, true, 'Hello'];
var arr2 = arr1.slice();
console.log(arr1); // [ 1, true, 'Hello' ]
console.log(arr2); // [ 1, true, 'Hello' ]
arr2[0] = 2;
console.log(arr1); // [ 1, true, 'Hello' ]
console.log(arr2); // [ 2, true, 'Hello' ]
但是,这两个方法仍然是浅拷贝。如果一旦涉及到数组里面的元素是引用类型,那么这两个方法是直接拷贝的引用地址。如下:
// concat 拷贝数组
var arr1 = [1, true, 'Hello', { name: 'xiejie', age: 18 }];
var arr2 = arr1.concat();
console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ]
console.log(arr2); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ]
arr2[0] = 2;
arr2[3].age = 19;
console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 19 } ]
console.log(arr2); // [ 2, true, 'Hello', { name: 'xiejie', age: 19 } ]
// concat 拷贝数组
var arr1 = [1, true, 'Hello', { name: 'xiejie', age: 18 }];
var arr2 = arr1.slice();
console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ]
console.log(arr2); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ]
arr2[0] = 2;
arr2[3].age = 19;
console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 19 } ]
console.log(arr2); // [ 2, true, 'Hello', { name: 'xiejie', age: 19 } ]
5. jQuery 中的 $.extend
在 jQuery 中,$.extend(deep,target,object1,objectN) 方法可以进行深浅拷贝。各参数说明如下:
- deep:如过设为 true 为深拷贝,默认是 false 浅拷贝
- target:要拷贝的目标对象
- object1:待拷贝到第一个对象的对象
- objectN:待拷贝到第N个对象的对象
来看一个具体的示例:
<body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
const obj = {
name: 'wade',
age: 37,
friend: {
name: 'james',
age: 34
}
}
const cloneObj = {};
// deep 默认为 false 为浅拷贝
$.extend(cloneObj, obj);
obj.friend.name = 'rose';
console.log(obj);
console.log(cloneObj);
</script>
</body>
深拷贝方法
说完了浅拷贝,接下来我们来看如何实现深拷贝。
总结一下,大致有如下的方式。
1. JSON.parse(JSON.stringify)
这是一个广为流传的深拷贝方式,用 JSON.stringify 将对象转成 JSON 字符串,再用 JSON.parse 方法把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
示例如下:
const stu = {
name: 'xiejie',
age: 18,
stuInfo: {
No: 1,
score: 100
}
}
const stu2 = JSON.parse(JSON.stringify(stu));
stu2.name = 'zhangsan';
stu2.stuInfo.score = 90;
console.log(stu); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 100 } }
console.log(stu2); // { name: 'zhangsan', age: 18, stuInfo: { No: 1, score: 90 } }
这种方式看似能够解决问题,但是这种方法也有一个缺点,那就是不能处理函数。
这是因为 JSON.stringify 方法是将一个 javascript 值(对象或者数组)转换为一个 JSON 字符串,而 JSON 字符串是不能够接受函数的。同样,正则对象也一样,在 JSON.parse 解析时会发生错误。
例如:
const stu = {
name: 'xiejie',
age: 18,
stuInfo: {
No: 1,
score: 100,
saySth: function () {
console.log('我是一个学生');
}
}
}
const stu2 = JSON.parse(JSON.stringify(stu));
stu2.name = 'zhangsan';
stu2.stuInfo.score = 90;
console.log(stu); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 100, saySth: [Function: saySth] }}
console.log(stu2); // { name: 'zhangsan', age: 18, stuInfo: { No: 1, score: 90 } }
可以看到,在原对象中有方法,拷贝之后,新对象中没有方法了。
2. $.extend(deep,target,object1,objectN)
前面在介绍浅拷贝时提到了 jQuery 的这个方法,该方法既能实现浅拷贝,也能实现深拷贝。要实现深拷贝,只需要将第一个参数设置为 true 即可。例如:
<body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
const obj = {
name: 'wade',
age: 37,
friend: {
name: 'james',
age: 34
}
}
const cloneObj = {};
// deep 设为 true 为深拷贝
$.extend(true, cloneObj, obj);
obj.friend.name = 'rose';
console.log(obj);
console.log(cloneObj);
</script>
</body>
3. 手写递归方法
最终,还是只有靠我们自己手写递归方法来实现深拷贝。
示例如下:
function deepClone(target) {
var result;
// 判断是否是对象类型
if (typeof target === 'object') {
// 判断是否是数组类型
if (Array.isArray(target)) {
result = []; // 如果是数组,创建一个空数组
// 遍历数组的键
for (var i in target) {
// 递归调用
result.push(deepClone(target[i]))
}
} else if (target === null) {
// 再判断是否是 null
// 如果是,直接等于 null
result = null;
} else if (target.constructor === RegExp) {
// 判断是否是正则对象
// 如果是,直接赋值拷贝
result = target;
} else if (target.constructor === Date) {
// 判断是否是日期对象
// 如果是,直接赋值拷贝
result = target;
} else {
// 则是对象
// 创建一个空对象
result = {};
// 遍历该对象的每一个键
for (var i in target) {
// 递归调用
result[i] = deepClone(target[i]);
}
}
} else {
// 表示不是对象类型,则是简单数据类型 直接赋值
result = target;
}
// 返回结果
return result;
}
在上面的代码中,我们封装了一个名为 deepClone 的方法,在该方法中,通过递归调用的形式来深度拷贝一个对象。
下面是 2 段测试代码:
// 测试1
const stu = {
name: 'xiejie',
age: 18,
stuInfo: {
No: 1,
score: 100,
saySth: function () {
console.log('我是一个学生');
}
}
}
const stu2 = deepClone(stu)
stu2.name = 'zhangsan';
stu2.stuInfo.score = 90;
console.log(stu); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 100, saySth: [Function: saySth] }}
console.log(stu2); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 90, saySth: [Function: saySth] }}
// 测试2
var arr1 = [1, true, 'Hello', { name: 'xiejie', age: 18 }];
var arr2 = deepClone(arr1)
console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ]
console.log(arr2); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ]
arr2[0] = 2;
arr2[3].age = 19;
console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ]
console.log(arr2); // [ 2, true, 'Hello', { name: 'xiejie', age: 19 } ]
总结
- 深拷贝和浅拷贝的区别?如何实现
参考答案:
浅拷贝:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制)
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。
浅拷贝方法
- 直接赋值
- Object.assign 方法:可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。当拷贝的 object 只有一层的时候,是深拷贝,但是当拷贝的对象属性值又是一个引用时,换句话说有多层时,就是一个浅拷贝。
- ES6 扩展运算符,当 object 只有一层的时候,也是深拷贝。有多层时是浅拷贝。
- Array.prototype.concat 方法
- Array.prototype.slice 方法
- jQuery 中的 .extend(deep,target,object1,objectN) 方法可以进行深浅拷贝。deep 如过设为 true 为深拷贝,默认是 false 浅拷贝。
深拷贝方法
- $.extend(deep,target,object1,objectN),将 deep 设置为 true
- JSON.parse(JSON.stringify):用 JSON.stringify 将对象转成 JSON 字符串,再用 JSON.parse 方法把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。这种方法虽然可以实现数组或对象深拷贝,但不能处理函数。
- 手写递归
// 浅拷贝的实现;
function shallowCopy(object) {
// 只拷贝对象
if (!object || typeof object !== "object") return;
// 根据 object 的类型判断是新建一个数组还是对象
let newObject = Array.isArray(object) ? [] : {};
// 遍历 object,并且判断是 object 的属性才拷贝
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = object[key];
}
}
return newObject;
}
// 深拷贝的实现;
function deepCopy(object) {
if (!object || typeof object !== "object") return;
let newObject = Array.isArray(object) ? [] : {};
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] =
typeof object[key] === "object" ? deepCopy(object[key]) : object[key];
}
}
return newObject;
}
34. 介绍一下防抖和节流
防抖和节流都是为了解决频繁触发事件导致性能问题。 防抖(Debounce):防抖的原理是在事件被触发n秒后再执行回调函数,如果在这n秒内又触发了该事件,则重新开始计时。 节流(Throttle):节流的原理是在一段时间内只执行一次事件回调函数,如果在这段时间内又触发了该事件,则忽略该事件。
两者的区别在于:
- 防抖是在事件触发后等待一段时间再执行回调函数,而节流是在一段时间内只执行一次回调函数。
- 防抖的效果是只有最后一次事件会被处理,而节流的效果是在一段时间内事件只会被处理一次。
适用场景:
- 防抖适用于在输入框中输入文字后,等待用户输入暂停后再进行搜索请求,从而减少请求次数。
- 节流适用于页面滚动等频繁触发的事件,可以减少事件处理函数的执行次数,提高页面性能。
35. 事件冒泡和捕获的区别?默认是冒泡还是捕获?
默认是冒泡阶段。 捕获阶段是事件的触发触发最外层的元素,祖先元素,再触发父元素,最后触发的目标对象 而冒泡阶段是从目标元素开始,父元素,祖先元素,等往外层触发事件 默认是冒泡
36. 什么是事件代理?
事件代理是它通过将事件处理程序添加到父元素上,来管理子元素的事件处理。具体来说,事件代理是指将事件绑定到父元素上,当子元素触发事件时,该事件会冒泡到父元素,由父元素来处理该事件。这样做的好处是可以减少事件处理程序的数量,减轻页面的负担,提高代码的性能和可维护性。
事件代理的应用场景包括:
- 动态添加元素:当需要动态添加子元素时,如果每个子元素都添加事件处理程序,会使代码变得冗长且难以维护。此时,可以将事件处理程序添加到父元素上,由父元素来管理子元素的事件处理。
- 性能优化:当页面中有大量的元素需要添加事件处理程序时,使用事件代理可以减少事件处理程序的数量,从而提高页面的性能。
37. mouseover 和 mouseenter 的区别?
mouseover 从自身和子元素都会触发mouseover, 而mouseenter只有鼠标在自身上,不在子元素上面才能触发。mouseenter不会事件冒泡。
-mouseover 是进入父元素到子元素会触发子元素的mouseover,触发父元素的mouseout事件,同时这个子元素的mouseover事件也会冒泡给父元素。 mouseenter 是进入父元素会触发父元素的事件,进入子元素会触发子元素的事件,子元素触发这个事件的不会冒泡。
38. 打开了两个标签页是进程还是线程?
浏览器打开了两个标签页是进程。每个标签页都会在浏览器中独立运行,有自己的渲染进程、JavaScript 引擎等。现在这种情况进程开的有点多,浏览器可能考虑是将一个站点的标签页开一个进程。
39. 浏览器从输入网址到页面加载的整个过程 ?
- 浏览器自动补全协议、端口
- 浏览器自动完成url编码
- 浏览器根据url地址查找本地缓存,根据缓存规则看是否命中缓存,若命中缓存则直接使用缓存,不再发出请求
- 通过DNS解析找到服务器的IP地址
- 浏览器向服务器发出建立TCP连接的申请,完成三次握手后,连接通道建立
- 若使用了HTTPS协议,则还会进行SSL握手,建立加密信道。使用SSL握手时,会确定是否使用HTTP2
- 浏览器决定要附带哪些cookie到请求头中, 浏览器自动设置好请求头、协议版本、cookie,发出GET请求
- 服务器处理请求,进入后端处理流程。完成处理后,服务器响应一个HTTP报文给浏览器。
- 浏览器根据使用的协议版本,以及Connection字段的约定,决定是否要保留TCP连接。
- 浏览器根据响应状态码决定如何处理这一次响应
- 浏览器根据响应头中的Content-Type字段识别响应类型,如果是text/html,则对响应体的内容进行HTML解析,否则做其他处理
- 浏览器根据响应头的其他内容完成缓存、cookie的设置
- 浏览器开始从上到下解析HTML,若遇到外部资源链接,则进一步请求资源
- 解析过程中生成DOM树、CSSOM树,然后一边生成,一边把二者合并为渲染树(rendering tree),随后对渲染树中的每个节点计算位置和大小(reflow),最后把每个节点利用GPU绘制到屏幕(repaint)
- 在解析过程中还会触发一系列的事件,当DOM树完成后会触发DOMContentLoaded事件,当所有资源加载完毕后会触发load事件
40. cookie 里面都包含什么属性?
-
Domain 如果未指定,则默认为设置 cookie 的同一主机。此属性存在的唯一原因就是减少域的限制并使 cookie 在子域上可访问。例如,如果当前的域是 abc.xyz.com,并且在设置 cookie 时如果不指定 Domain 属性,则默认为 abc.xyz.com,并且 cookie 将仅限于该域。
-
Path 除了将 cookie 限制到域之外,还可以通过路径来限制它。
-
Expires/Max-age 该属性用来设置 cookie 的过期时间。若设置其值为一个时间,那么当到达此时间后,cookie 就会失效。
-
Secure 具有 Secure 属性的 cookie 仅可以通过安全的 HTTPS 协议发送到服务器,而不会通过 HTTP 协议
-
HTTPOnly cookie 只能通过服务端访问,不能通过客户端访问
41. cookie 如何设置跨域?
服务端设置: Access-Control-Allow-Credentials: true
前端请求的时候: credentials: 'include',
然后服务端还要设置指定的域名跨域访问资源(而不能设置 *)
42. 跨域方式
浏览器有一个重要的安全策略,称之为「同源策略」,
源=协议+主机+端口,两个源相同,称之为同源
同源策略是指,若页面的源和页面运行过程中加载的源不一致时,出于安全考虑,浏览器会对跨域的资源访问进行一些限制。
开发阶段:
对于前端开发而言,大部分的跨域问题,都是通过代理解决的
代理适用的场景是:生产环境不发生跨域,但开发环境发生跨域
因此,只需要在开发环境使用代理解决跨域即可,这种代理又称之为开发代理
在app的H5界面不存在跨域问题。只有在浏览器中才存在这个问题。
其他阶段:
CORS
它的全称是Cross-Origin Resource Sharing, 跨域资源共享。 因此实现CORS的关键就是服务器,只要服务器实现了CORS请求,就可以跨源通信了。
简单请求:
- 简单请求 HEAD GET POST
- 头部不超过几个字段
浏览器会直接发出CORS请求,它会在请求的头信息中增加一个Orign字段,该字段用来说明本次请求来自哪个源(协议+端口+域名),服务器会根据这个值来决定是否同意这次请求。如果Orign指定的域名在许可范围之内,服务器返回的响应就会多出以下信息头: 在简单请求中,在服务器内,至少需要设置字段:Access-Control-Allow-Origin
需要预检的请求
非简单请求的CORS请求会在正式通信之前进行一次HTTP查询请求,称为预检请求。
浏览器会询问服务器,当前所在的网页是否在服务器允许访问的范围内,以及可以使用哪些HTTP请求方式和头信息字段,只有得到肯定的回复,才会进行正式的HTTP请求,否则就会报错。
预检请求使用的请求方法是OPTIONS,表示这个请求是来询问的。他的头信息中的关键字段是Orign,表示请求来自哪个源。除此之外,头信息中还包括两个字段:
- Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法。
- Access-Control-Request-Headers: 该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段。
'Access-Control-Allow-Origin'
'Access-Control-Allow-Methods'
'Access-Control-Allow-Headers'
后端在请求的返回头部添加:Access-Control-Max-Age:number。它表示预检请求的返回结果可以被缓存多久,单位是秒。该字段只对完全一样的URL的缓存设置生效,所以设置了缓存时间,在这个时间范围内,再次发送请求就不需要进行预检请求了。
附带身份凭证的请求
JSONP
当需要跨域请求时,不使用AJAX,转而生成一个script元素去请求服务器,由于浏览器并不阻止script元素的请求,这样请求可以到达服务器。服务器拿到请求后,响应一段JS代码,这段代码实际上是一个函数调用,调用的是客户端预先生成好的函数,并把浏览器需要的数据作为参数传递到函数中,从而间接的把数据传递给客户端 JSONP有着明显的缺点,即其只能支持GET请求
43. Content-Type
表单
- application/x-www-form-urlencoded 最常见 POST 提交数据的方式。浏览器的原生 form 表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据。
- multipart/form-data 一种常见的 POST 数据提交的方式。我们在使用表单上传文件时,必须让 form 的 enctyped 等于这个值。 JSON
- application/json
文本和xml相关
- text/xml
- text/html:HTML 文档的内容类型。用于网页
- text/plain:纯文本,不含任何格式的文档
- text/css:CSS 样式表
JS 相关
- application/javascript text/javascript JavaScript 代码(过去常用 text/javascript,现在推荐 application/javascript)。
文件相关
- image/jpeg:JPEG 图像。
- image/png PNG 图像。
- application/pdf PDF 文档。
44. http 状态码
45. get post 的区别 ?
-
GET 请求是一个幂等的请求,而 Post 不是一个幂等的请求 一般 Get 请求用于对服务器资源不会产生影响的场景 而 Post ,一般用于对服务器资源会产生影响的情景
-
缓存 浏览器一般会对 Get 请求缓存,但很少对 Post 请求缓存。
-
get 请求把参数放在url后面,而post是放在请求体里面
-
长度的问题 浏览器由于对 url 长度的限制,所以会影响 get 请求发送数据时的长度。这个限制是浏览器规定的,并不是 RFC 规定的。
46. http 各版本之间的区别
http 1.0
http 1.0 的问题 每次请求都要三次握手,四次回收,连接的建立和销毁都会占用服务端和客户端的资源,也会多耗费时间,无法充分利用带宽(tcp有慢启动,拥塞控制)官方没有解决,开发者自己解决的在请求头加上 connection:keep-alive
在浏览器中 connectionId 里面 是一样的代表是用的一个TCP连接
http 1.1
http 1.1 默认开启长连接 可以多个请求使用TCP连接,避免了HTTP1.0的问题,
队头阻塞
队头阻塞:由于多个请求使用了同一个TCP连接,服务器必须按照请求到达的顺序进行相应。 举例子:就是客户端先后发了两个请求,第一个请求,由于服务端处理比较复杂,第二请求,服务端已经处理完了,但是第二个响应也不能返回,必须要等第一个响应回去后再返回,否则会导致客户端请求和响应对不上,这就是队头阻塞。队头阻塞发生在服务端。 队头阻塞的优化方式有: 通过减少文件数量,从而减少对头阻塞的几率 通过开辟多个TCP连接,实现真正的,有缺陷的并行传输。(一个域名终于对开6个TCP连接,要实现这个突破,就是把资源搞在不同的域中) 管道化并不是成功的模型,队头阻塞带来非常多的问题,现在的浏览器默认关闭这种问题。
优势
- 长连接
- 管道化
- 缓存处理,新增了 cache-control,用于客户端缓存
- 断点传输
上面两种方式无法解决队头阻塞问题的根源是请求头和请求体不允许拆分开,必须在一块传输。
http 2
-
二进制分帧 将传输的消息分为更小的二进制帧,每帧都有自己的标识序列,即便被随意打乱也能在另一端正确的组装
-
多路复用 可以允许一更小的单元传输数据,每个传输单元的数据为帧。每个请求或者相应的完整数据称为流,每个流都有自己的编号,每个帧会记录所属的流。 这样真正的解决共享TCP连接的队头阻塞的问题,实现了真正的多路复用。
基于二进制分帧,在同一域名下所有访问都是从同一个 tcp 连接中走,并且不再有队头阻塞问题,也无须遵守响应顺序
-
头部信息压缩 客户端和服务器同时维护一张头信息表(包括动态表和静态表),所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就能提高速度了。对于两张表都没有的情况,采用哈夫曼编码压缩后再传输,同时加到动态表里面。
-
服务端推送
允许在客户端主动请求的情况下,服务器预先把资源推给客户端。当客户端后续需要该资源的时,则主动从之前推送资源中寻找。 他是多给数据,比方我要html的信息,服务端把相关的其他css,js都推给我了。
47. XSS和CSRF是什么?如何防御
XSS
XSS是指跨站脚本攻击。攻击者利用站点的漏洞,在表单提交时,在表单内容中加入一些恶意脚本,当其他正常用户浏览页面,而页面中刚好出现攻击者的恶意脚本时,脚本被执行,从而使得页面遭到破坏,或者用户信息被窃取。
主要偷偷提交了脚本进去,然后另外的用户打开后执行了脚本。
要防范 XSS 攻击,需要在服务器端过滤脚本代码,将一些危险的元素和属性去掉或对元素进行HTML实体编码
CSRF(Cross-site request forgery,跨站请求伪造)
主要是cookie造成的。 它是指攻击者利用了用户的身份信息,执行了用户非本意的操作 CSRF 是跨站请求伪造,是一种挟制用户在当前已登录的Web应用上执行非本意的操作的攻击方法 它首先引导用户访问一个危险网站,当用户访问网站后,网站会发送请求到被攻击的站点,这次请求会携带用户的cookie发送,因此就利用了用户的身份信息完成攻击 防御 CSRF 攻击有多种手段:
- 不使用cookie
- 使用 CSRF Token 进行验证
- cookie中使用sameSite字段
- 服务器检查 referer 字段
48. 回流和重绘是什么,有什么区别?
回流:计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,浏览器会重新渲染部分或者全部文档的过程就称为回流
下面这些操作会导致回流:
- 元素的位置发生变化
- 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
- 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代
- 页面一开始渲染的时候(这避免不了)
- 添加或删除可见的DOM元素
- 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
- 获取offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight 会立即重排
重绘 当页面中某些元素的样式发生变化,自己的大小和位置不改变,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘。
1.颜色的修改 2. 文本方向的修改
- 阴影的修改
如何避免回流与重绘?
- 操作DOM时,尽量在低层级的DOM节点进行操作
- 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
- DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制
浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列 浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。
49. HTML文档的生命周期有哪些?
- DOM 完全加载和解析完毕,document 上的 DOMContentLoade
- 页面所有资源加载完毕,window 的load 事件被触发
- 在页面即将卸载时触发,开发者可以通过这个事件提醒用户保存数据或防止用户误离开页面。可以使用window 上的 beforeunload 事件
- 用户最终离开时,window 上的 unload 事件就会被触发
50. https 加密是什么样的?
超文本传输安全协议(Hypertext Transfer Protocol Secure,简称:HTTPS)是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,利用SSL/TLS来加密数据包。HTTPS的主要目的是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。
TLS/SSL的功能实现主要依赖三类基本算法:散列函数hash、对称加密、非对称加密。这三类算法的作用如下:
- 非对称加密验证身份和协商秘钥 RSA 2.对称加密算法采用协商的秘钥对数据加密 AES DES
- 基于散列函数验证信息的完整性(校验信息) MD5 SHA
非对称加密验证身份 对称加密加密数据 散列函数验证校验信息
51. 浏览器的内核是什么,包含什么,常见的有哪些?
- 渲染引擎 渲染引擎的职责就是渲染,即在浏览器窗口中显示所请求的内容
- JS 引擎 解析和执行 javascript 来实现网页的动态效果
Safari 浏览器 Webkit chrome Blink内核
52. 数组排序算法的原理?
插入排序 快速排序
53. 列举优化网络性能方法?
- 优化打包体积 利用一些工具压缩、混淆最终打包代码,减少包体积
- 多目标打包 利用一些打包插件,针对不同的浏览器打包出不同的兼容性版本,这样一来,每个版本中的兼容性代码就会大大减少,从而减少包体积
- 压缩 现代浏览器普遍支持压缩格式,因此服务端的各种文件可以压缩后再响应给客户端,只要解压时间小于优化的传输时间,压缩就是可行的
- CDN 利用 CDN 可以大幅缩减静态资源的访问时间,特别是对于公共库的访问,可以使用知名的 CDN 资源,这样可以实现跨越站点的缓存 对于除 HTML 外的所有静态资源均可以开启协商缓存,利用构建工具打包产生的文件 hash 值来置换缓存
- http2 开启 http2 后,利用其多路复用、头部压缩等特点,充分利用带宽传递大量的文件数据
- 雪碧图 对于不使用 HTTP2 的场景,可以将多个图片合并为雪碧图,以达到减少文件的目的
- defer、async 通过 defer 和 async 属性,可以让页面尽早加载 js 文件
- prefetch、preload 通过 prefetch 属性,可以让页面在空闲时预先下载其他页面可能要用到的资源 通过 preload 属性,可以让页面预先下载本页面可能要用到的资源
- 多个静态资源域 对于不使用 HTTP2 的场景,将相对独立的静态资源分到多个域中保存,可以让浏览器同时开启多个 TCP 连接,并行下载
54. 如何检查内存泄漏?
使用 Chrome 开发工具里面的 performance 和memory 去排查
主要的内存泄漏主要有:
- 意外的全局变量 (比方说你定义了一个函数,在函数里面定义变量你忘了带 var,声明成了 a = 20,这时a就是你意外声明的变量)
- 分离的dom (就是dom虽然从dom树上移除了,但是还是被某个全局变量引用着)
- 闭包
- 作为前端平时使用console.log在控制台打出相对应的信息可以说是非常常见 但如果没有去掉console.log可能会存在内存泄漏。 因为在代码运行之后需要在开发工具能查看对象信息,所以传递给console.log的对象是不能被垃圾回收水
- js中常用的定时器setInterval()、setTimeout().他们都是规定延迟一定的时间执行某个代码,而其中setInterval()和链式setTimeout()在使用完之后如果没有手动关闭,会一直存在执行占用内存,* 所以在不用的时候我们可以通过clearInterval()、clearTimeout()来关闭其对应的定时器,释放内存。
55. 实现寄生组合式继承?
1.ES6实现继承
class Animal {
constructor(type, name, age, sex) {
if (new.target === Animal) {
throw new TypeError("你不能直接创建Animal的对象,应该通过子类创建")
}
this.type = type;
this.name = name;
this.age = age;
this.sex = sex;
}
print() {
console.log(`【种类】:${this.type}`);
console.log(`【名字】:${this.name}`);
console.log(`【年龄】:${this.age}`);
console.log(`【性别】:${this.sex}`);
}
jiao() {
throw new Error("动物怎么叫的?");
}
}
class Dog extends Animal {
constructor(name, age, sex) {
super("犬类", name, age, sex);
// 子类特有的属性
this.loves = "吃骨头";
}
print() {
//调用父类的print
super.print();
//自己特有的代码
console.log(`【爱好】:${this.loves}`);
}
//同名方法,会覆盖父类
jiao() {
console.log("旺旺!");
}
}
const a = new Dog("旺财", 3, "公")
a.print();
2.原型链继承
function Parent() {
this.name = 'zhangsan';
this.children = ['A', 'B'];
}
Parent.prototype.getChildren = function() {
console.log(this.children);
}
function Child() {
}
Child.prototype = new Parent();
var child1 = new Child();
child1.children.push('child1')
console.log(child1.getChildren()); // Array ["A", "B", "child1"]
var child2 = new Child();
child2.children.push('child2')
console.log(child2.getChildren()); // Array ["A", "B", "child1", "child2"]
关键代码是 Child.prototype = new Parent(); ,让当先构造函数的原型指向父类的一个实例对象。
最大的问题是如果是父类的属性所有对象都会只共享一个,造成数据混乱的问题。
3.构造函数继承
function Parent(age) {
this.names = ['lucy', 'dom'];
this.age = age;
this.getName = function() {
return this.name;
}
this.getAge = function() {
return this.age;
}
}
function Child(age) {
// 核心代码
Parent.call(this, age);
}
var child1 = new Child(18);
child1.names.push('child1');
console.log(child1.names); // [ 'lucy', 'dom', 'child1' ]
var child2 = new Child(20);
child2.names.push('child2');
console.log(child2.names); // [ 'lucy', 'dom', 'child2' ]
```
核心就一点在父类的构造函数里面调用子类构造函数的方法。 好处是原型的属性不会被共享。 缺点是父类构造函数的原型不会被继承,因为上面的操作只是把父类的自身的几个属性拿过来了,父类自身的几个方法拿过来,但是父类的构造函数的property属性以及更上层由于没有设置原型链,所以就没有。
4.组合式继承
function Parent(name, age) {
this.name = name;
this.age = age;
this.colors = ['red', 'green']
console.log('parent')
}
Parent.prototype.getColors = function() {
console.log(this.colors);
}
function Child(name, age, grade) {
// 核心代码
Parent.call(this, name, age);// 创建子类实例时会执行一次
this.grade = grade;
}
// 核心代码
Child.prototype = new Parent(); // 指定子类原型会执行一次
Child.prototype.constructor = Child;// 校正构造函数
Child.prototype.getName = function() {
console.log(this.name)
}
var c = new Child('alice', 10, 4)
console.log(c.getName())
> "parent"
> "parent"
> "alice"
核心代码是前面两个结合到一块。
优点:
- 原型属性不会被共享 2 .可以继承父类的原型链上的属性和方法
缺点:
父类的方法会搞两份,子类上面有一份,他的原型(父类上面)也有一份,父类上面的一份是没有必要的。
5.寄生组合式继承
function Parent(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name); // 借用构造函数
this.age = age;
}
Child.prototype = Object.create(Parent.prototype); // 创建父类原型的副本
Child.prototype.constructor = Child; // 修复构造函数指向
const child1 = new Child("child1", 18);
child1.sayName(); // 输出: "child1"
寄生组合式继承的和前面的差别是使用Object.create(Parent.prototype) 解决了很多问题。
6.圣杯模式
56. 浏览器的渲染流程是什么?
- HTML 解析
第一步完成后,会得到 DOM 树和 CSSOM 树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。
- 样式计算
主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出它最终的样式,称之为 Computed Style。在这一过程中,很多预设值会变成绝对值,比如red会变成rgb(255,0,0);相对单位会变成绝对单位,比如em会变成px 这一步完成后,会得到一棵带有样式的 DOM 树。
- 布局
布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息。例如节点的宽高、相对包含块的位置。 大部分时候,DOM 树和布局树并非一一对应。 比如display:none的节点没有几何信息,因此不会生成到布局树;又比如使用了伪元素选择器,虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应。
- 分层
主线程会使用一套复杂的策略对整个布局树中进行分层。 分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。 滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果,也可以通过will-change属性更大程度的影响分层结果。
- 绘制
主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。 完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。
-
分块(后面三步都是在合成线程上执行)
-
光栅化
GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。 光栅化的结果,就是一块一块的位图
- 画
合成线程拿到每个层、每个块的位图后,生成一个个「指引(quad)」信息。 指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。 变形发生在合成线程,与渲染主线程无关,这就是transform效率高的本质原因。 合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像。