Skip to main content

JS详解面试题46题

1、对原型链的理解

JavaScript中的每个对象都有一个内部的[[Prototype]]属性,也称为原型(prototype),它指向另一个对象或者null。当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript会沿着它的原型链一层层向上查找,直到找到该属性或方法或者原型链到达顶端(即Object.prototype,它是所有对象的祖先)为止。

通过原型链,我们可以实现继承、属性的共享和方法的复用。当我们创建一个对象时,可以通过指定它的原型来继承原型上的属性和方法。在查找属性或方法时,如果对象本身没有这个属性或方法,就可以在其原型上查找。这样可以避免在每个对象上都复制一份相同的属性和方法,从而节省内存。

需要注意的是,如果在原型链上的某个属性被修改了,那么所有继承自该原型的对象都会受到影响。因此,应该谨慎地修改原型上的属性和方法。另外,通过对象的constructor属性,可以访问到其构造函数,从而可以判断一个对象的类型。

2、map 和 Object 的区别

关键词:键值区别、长度大小获取、原型、顺序、循环

1、Object的键只能是字符串或Symbol,但是Map的键可以是任意值

2、Map的长度大小可以通过size来获取,但是Object则需要通过Object.keys(obj).length

3、Object的键值可能会和原型上的属性重名,我们可以使用Object.create(null)来设置没有原型的对象

4、Object的key的顺序是先判断数字开头的key,在判断字符串,而Map的顺序是在插入时决定的

5、Map结构的数据可以使用for of来循环,而Object则需要手动遍历,用for in

6、Map在频繁增删键值对的场景下表现更好。

综上所诉:如果需要存储不同的键值并且保持插入顺序的话用Map会更加灵活

3、this 指向

关键词:函数执行上下文、箭头函数、new

规则:箭头函数>new>显式调用>隐式调用>默认绑定

解释:this是javascript中的一个关键字,多数情况下指向的是调用它的对象

首先,this应该指向的是一个对象(函数执行上下文对象),其次这个对象指向的是调用它它的对象,如果调用它的不是一个对象或者对象不存在,那么它指向的就是全局对象(严格模式下为undefined)

其实,this在函数被调用时就确定了,它的指向就是函数被调用的地方,而不是它声明的地方(除箭头函数外)。当函数被调用时,会创建一个执行上下文,它包含了函数在哪里被调用(调用栈)、函数调用的方式、以及函数的参数等信息,this就是一个记录它的一个属性,在函数执行的过程中会被用到

1.、默认绑定

函数的浏览器在全局环境中直接使用代表的是全局对象Window,在node环境下则是Global,如果是严格模式下,则是undefined

2、隐式绑定

理解:谁调用就指向谁

3、显式绑定

**apply、call、bind都是可以改变函数指向,**但是 call 和 apply 是直接进行函数调用 ,不会执行函数,而是返回一个新的函数,这个新的函数已经自动绑定了新的 this 指向,需要我们手动调用。 call 方法接受的是参数列表,而 apply 方法接受的是一个参数数组

const target = {}
fn.call(target, 'arg1', 'arg2')
fn.apply(target, ['arg1', 'arg2'])
fn.bind(target, 'arg1', 'arg2')()

如果传入了null或者undefined作为this指向,则this会使用默认绑定规则

并且使用多次bind绑定,永远取第一次this的指向

4、new绑定

函数作为构造函数使用 new 调用时, this 绑定的是新创建的构造函数的实例

5、特殊this指向

1.箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this

2.数组方法:例如forEach、find、findIndex、map、some、every都可以改变this指向(第二个参数)

3.立即执行函数:就是默认的全局对象 window

4.setTimeout和setInterval:其实,延时效果(setTimeout)和定时效果(setInterval)都是在全局作用域下实现的。无论是 setTimeout 还是 setInterval 里传入的函数,都会首先被交到全局对象手上。因此,函数中 this 的值,会被自动指向 window

6、不在函数中的场景,可分为浏览器的 标签里,或 Node.js 的模块文件里。

​ 1) 在 标签里,this 指向 Window。

​ 2) 在 Node.js 的模块文件里,this 指向 Module 的默认导出对象,也就是 module.exports。

4、闭包的作用、原理和使用场景

一个函数和对其周围状态的引用捆绑在了一起(或者说函数被引用包围),这样的组合就是闭包。也就是说闭包就是一个让你可以访问外层函数的作用域。通俗来说,闭包其实就是一个可以访问其他函数内部变量的一个函数,即定义在函数内部的函数,或者说闭包就是一个内嵌函数。

通常来说,函数内部的变量是无法在外部进行访问的(即全局变量和局部的变量的区别),因此使用闭包的作用实现了,能在外部访问某个函数内部的变量的功能,让这些变量的值始终可以保存在内存当中。

从直观上讲,闭包这个概念为JavaScript中访问函数内变量提供了途径和便利。这样做的好处有很多,比如闭包实现缓存等。

2、作用域链的概念

当访问一个变量时,代码编辑器会首先在当前作用域去查找,如果没还找到,就会继续往父级作用域去查找,直到找到该变量或者不存在父级作用域内,这样的链路操作就是作用域链。

3、闭包的本质是什么?

当前环境内存在对于父级作用域的引用

4、闭包的使用场景

  • 在定时器、事件监听、Ajax请求、web workers、任何异步中,只要使用了回调函数,实际上就是使用闭包,防抖和节流,以及vue底层都使用了
// 定时器
setTimeout(function handler(){
console.log('1');
}1000);
// 事件监听
document.getElementById(app).addEventListener('click', () => {
console.log('Event Listener');
});
  • 作为函数参数的传递的形式
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 这是闭包
fn();
}
foo(); // 输出2,而不是1
  • 立即执行函数:创建了闭包,保存了全局作用域和当前作用域,因此可以输出全局的变量。
function memorize(fn) {
const cache = {}
function foo(args) {
const key = JSON.stringify(args)
let result = cache[key]
if (!result) {
cache[key] = args
result = fn(args)
}
return { cache, result }
}
foo.cache = cache
return foo
}

function add(a) {
return a + 1
}

const added = memorize(add)
console.log("", added(1))
console.log("", added(2))
console.log("", added(3))
console.log("", added(4))
adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(2) // 输出: 3 当前: cache: { '[1]': 2, '[2]': 3 }

5、常见的闭包的循环输出的问题

for(var i = 1; i <= 5; i ++){
setTimeout(function() {
console.log(i)
}, 0)
}
输出56
解决办法:
1、利用立即执行函数
for(var i = 1; i <= 5; i ++){
(function(j){
setTimeout(function() {
console.log(j)
}, 0)
})(i)
}
2、使用es6的let
3、使用定时器的第三个参数
for(var i=1;i<=5;i++){
setTimeout(function(j) {
console.log(j)
}, 0, i)
}
定时器的第三个参数,这些参数会作为回调函数的附加参数存在

5、正向代理和反向代理的区别

正向代理和反向代理是两种不同类型的代理服务器,它们在网络通信中扮演不同的角色。

  1. 正向代理(Forward Proxy):
    • 正向代理作为客户端的代理,代表客户端向其他服务器发送请求。客户端需要通过正向代理来访问其他服务器,因为直接访问会受到限制或阻止。
    • 举个例子,当你在公司内部网络中访问互联网时,你可能需要通过公司的正向代理服务器来访问外部网站,因为公司的网络设置了防火墙或者其他安全限制。
  2. 反向代理(Reverse Proxy):
    • 反向代理作为服务器的代理,代表服务器接收客户端的请求并将请求转发到内部的服务器。客户端不知道自己实际正在与哪个服务器通信,因为所有的请求都是发送到反向代理服务器。
    • 举个例子,当你访问一个网站时,你实际上在与反向代理服务器通信,它会将你的请求转发到后端的多个服务器上,然后将结果返回给你。

总的来说,正向代理是代表客户端发出请求,而反向代理是代表服务器接收请求。它们的主要区别在于代理的角色不同,以及它们在网络通信中的位置和功能不同。

6、箭头函数和普通函数的区别

区别:

1、箭头函数比普通函数更加简洁:

箭头函数返回值只有一句,直接可以省略大括号。如果不需要返回值直接加上void就可以了

2、箭头函数没有自己的this

箭头函数不会创建自己的this,所以它没有this,它只会继承自己作用域的上一层,所以箭头函数在创建的时候this指向就已经定义好了

3、箭头的函数的this指向永远不会被改变

4、call、apply、bind等方法不会改变箭头函数this的指向

5、箭头函数不能作为构造函数使用:因为箭头函数没有自己的this,所以会报错

6、箭头函数没有自己的arguments:箭头函数没有自己的argumnets对象,在箭头函数中访问的实际上获得的是它外层函数的arguments值

7、箭头函数没有prototype

8、箭头函数不能作为Genrator函数,不能使用yeild关键字

7、ES6新特性

1、let、const

2、模版字符串

3、解构赋值

4、函数设置默认值

5、...运算符

6、箭头函数

7、for of

  1. for of 不能遍历对象,可以遍历数组
  2. for in 遍历得到的是下标或者是对象的key
  3. 可迭代对象可以使用for of
  4. img
  5. for of 不同与forEach,前者可以使用break、continue、return配合使用,也就是可以随时跳出循环

8、class类,原型链的语法糖的表现形式

9、导入导出 export default 、import

10、Promise

11、async 和 await

12、Symbol 新的基本类型

13、Set集合

8、promise.all和promise.allSettled的区别

他们都是promise的静态方法,用与处理多个promise实例。

区别:

1、返回值不同:Promise.all的返回值是一个对象,当所有的Promise实例都成功时,返回的Promise对象的状态都是fulfilled,其中包含了应该所有Promise实例结果的数组;当有任何一个Promise实例失败的时候,返回的Promise对象的状态是rejected,并返回一个被拒绝的Promise的结果;而Promise.allSettled的返回值是一个Promise对象,当所有的Promise实例执行完成后(不管成功还是失败),返回的Promise对象的状态为fulfilled,其结果包含所有Promise实例执行结果的数组,数组的每一个元素都是一个对象,包含Promise实例的状态和结果信息

2、处理方式不同:Promise.all在所有实例resolved或者有一个实例rejected,就会立即终止Promise实例的执行,即使还有Promise没有完成;而Promise.allSettled会等到所有Promise完成,无论是成功还是失败。

status:‘’,value或者status,reason

使用:Promise.all可以使用在例如两个接口不互相依赖,可以同时执行请求,减少请求的时间

9、subString、subStr区别

两个方法都用于截取字符串,但是用法不同:

  1. substring 方法:
  • substring 方法用于从字符串中提取子字符串,并返回提取的子字符串。
  • 它接收两个参数,分别表示要提取的子字符串的起始位置和结束位置(不包括结束位置的字符)。
  • 如果省略第二个参数,则会提取从起始位置到字符串末尾的所有字符。
  • 如果第一个参数大于第二个参数,则 substring 会自动交换这两个参数的位置
const str = "Hello, World!";
console.log(str.substring(1, 4)); // "ell"
console.log(str.substring(4, 1)); // "ell",自动交换参数位置
console.log(str.substring(7)); // "World!",省略第二个参数
  1. substr 方法:
  • substr 方法也用于从字符串中提取子字符串,并返回提取的子字符串。
  • 它接收两个参数,第一个参数表示要提取的子字符串的起始位置,第二个参数表示要提取的字符数。
  • 如果第二个参数为负数,则表示从字符串末尾开始计算字符数。
  • 示例:
javascriptconst str = "Hello, World!";
console.log(str.substr(1, 4)); // "ello"
console.log(str.substr(7)); // "World!",省略第二个参数
console.log(str.substr(-6, 5)); // "World",负数表示从末尾开始计算字符数

10、symbol作用和使用场景

解释:Symbol是ES6中引入的新数据类型,表示的是一个唯一得常量,通过Symbol函数来创建对应的数据类型,可以在创建的时候添加变量描述,该变量会被强制转换为字符串

用法:常量值和对象属性

  1. 避免常量值重复
  2. 避免对象属性覆盖:给函数的参数赋值,如果参数是一个对象,且包含相同属性,就可以利用Symbol来赋值和读取

11、JS脚本异步加载

1.当script上没有defer或者async的时候,浏览器在执行脚本的时候会阻塞后续文档的加载。当存在defer或async的时候都是异步加载,它们不会阻塞页面的解析:

区别:

1、执行顺序:多个async属性不会保证执行的顺序,多个defer属性会保证执行的顺序

2、脚本是否并行执行:async会异步下载解析脚本(即加载后续文档元素的过程中和js脚本的加载是并行进行);而defer表示延迟加载,脚本会先不执行,而是等到文档解析完成后再去执行。

2.JavaScript脚本延迟加载的方式有哪些?

延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。

一般有以下几种方式:

  • **defer 属性:**给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
  • **async 属性:**给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
  • **动态创建 DOM 方式:**动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
  • **使用 setTimeout 延迟方法:**设置一个定时器来延迟加载js脚本文件
  • 让 JS 最后加载:将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。

12、typeof和intanceof区别

关键词:递归、proto、 prototype 例如: L instanceof R instanceof运算时,会递归L的__proto__原型链,查看是否存在右侧的prototype原型。

function myInstanceOf(left,right){
if (typeof left !== 'object' || left === null || typeof right !== 'function') {
return false;
};
let proto = Object.getPrototypeOf(left) // 获取左边对象的原型
let prototype = right.prototype
// 判断构造函数的 prototype 对象是否在对象的原型链上
while(true){
if(!proto) return false
if(proto===prototype) return true
proto = Object.getPrototypeOf(proto)
}
}

typeof与instanceof 都是判断数据类型的方法,区别如下:

● typeof会返回一个运算数的基本类型,instanceof 返回的是布尔值

● instanceof 可以准确判断引用数据类型,但是不能正确判断原始数据类型

● typeof虽然可以判断原始数据类型(null 除外),但是无法判断引用数据类型(function 除外)

**扩展:**typeof的原理

typeof原理: 不同的对象在底层都表示为二进制,在Javascript中二进制前(低)三位存储其类型信息

  • 000: 对象
  • 010: 浮点数
  • 100:字符串
  • 110: 布尔
  • 1: 整数

typeof null 为"object", 原因是因为 不同的对象在底层都表示为二进制,在Javascript中二进制前(低)三位都为0的话会被判断为Object类型,null的二进制表示全为0,自然前三位也是0,所以执行typeof时会返回"object"。 一个不恰当的例子,假设所有的Javascript对象都是16位的,也就是有16个0或1组成的序列,猜想如下:

Array: 1000100010001000
null: 0000000000000000

typeof [] // "object"
typeof null // "object"

因为Array和null的前三位都是000。为什么Array的前三位不是100?因为二进制中的“前”一般代表低位, 比如二进制00000011对应十进制数是3,它的前三位是011。

还有一种情况

function foo() {};
typeof foo; // 'function'

这样看来,function 也是JavaScript的一个内置类型。然而查阅规范,就会知道,它实际上是 object 的一个"子类型"。具体来说,函数是“可调用对象”,它有一个内部属性[[call]],该属性使其可以被调用。typeof 可以用来区分函数其他对象。

typeof可以对基本数据类型进行类型判断,例如number, string, object, boolean, function, undefined, symbol,但是在判断object类型的时候,而不能细致的具体到哪一种object,如果要想具体到哪一种object,可

推荐使用:Object.prototype.toString.call()

Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('hi') // "[object String]"
Object.prototype.toString.call({a:'hi'}) // "[object Object]"
Object.prototype.toString.call([1,'a']) // "[object Array]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(() => {}) // "[object Function]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"

13、forEach、map是否可以break

map 和forEach无法使用return 或break跳出循环;但是可以抛出throw new Error(),通过try catch去捕获这个错误终止循环

14、如何判断数组类型

1、通过Object.prototype.toString.call()
Object.prototype.toString.call(obj).slice(8,-1) === 'Array'
2、通过ES6Array.isArray()来判断
Array.isArray(obj)
3、通过原型链做判断
obj.__proto__ ===Array.prototype
4、通过instanceof来判断
obj instanceof Array
5、通过Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)

15、操作数组元素的方法有哪些

改变数组的方法:fill、pop、push、shift、unshift、reverse、splice、sort

不改变数组的方法:concat、every、some、filter、find、map、findIndex、forEach、indexOf、includes、join、lastIndexOf、reduce、reduceRight、slice、some

数组转化成字符串的方法:toLocalString、toString、valueOf、join

添加返回数组长度,而删除则返回数组的删除的那一项

sort不传参数的情况下回造成不准确的情况,因为会调用Sting转型函数

sort的比较函数的原理是第一个参数排在第二个参数前面,则返回-1;如果相等则返回0;第一个参数排在第二个参数后面,则返回1

slice返回该截取的数组

splice删除、替换、插入,会返回被删除的数组

reduce正序执行,reduceRight倒序执行

1Object.prototype.toString.call(obj).slice(8,-1) ---'Array'
2.Array.isArray()
3.obj.__proto__===Array.prototype
4.obj instanceof Array
5.Array.prototype.isPrototypeof(obj)

16、sort排序算法的本质?

sort排序算法分为两种,插入排序和快排序

当没有参数传入的时候,使用的是默认排序,将排序的数据转化成字符串,通过unicode去排序,当然比较函数也可以自定义,自定义函数需要有返回值

function compare(left,right){
if(left<right){
return -1
}else if(left>right){
return 1
}else{
return 0
}
}

当数组长度小于等于10的时候,使用插入排序;大于10的时候,使用快排序

插入排序:虽然插入排序的复杂度是n2,但是由于数据量很小,因此是常量的复杂度,效率很高,而且插入排序是一个稳定的排序算法。

function InsertionSort(arr,from,to){
for(let i = from+1;i<to;i++){
let element = arr[i]
for(let j = i-1;j>=from;j--){
let tmp = arr[j]
let order = comparefn(tmp,element)
if(order>0){
a[j+1] = tmp
}else{
break
}
}
a[j+1] = element
}
}

快速排序:sort对于长度大于1000的数组,采用的是快排与插入排序混合的方式进行排序的,因为,当数据量很小的时候,插入排序效率优于快排

17、如何拷贝一个对象?如何实现深浅拷贝

浅拷贝:一个新的对象对原始对象的属性值进行精确拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值;如果是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,那另一个对象也会发生改变。

实现浅拷贝的方法:

1、直接赋值

2Object.assign()实现浅拷贝

注意:同名属性会被后面的属性覆盖;如果只有一个参数时,会将参数转化成对象直接返回;如果第一个参数是null或者undefined,会报错,因为不能转化成对象;不会拷贝对象的继承属性,包括可枚举属性,但是可以拷贝Symbol类型的属性

3、扩展运算符

4、数组方法实现数组浅拷贝
1. Array.prototype.slice() --直接不传参数就相当于浅拷贝
2. Array.prototype.concat() --直接不传参数就相当于浅拷贝
5、手写实现浅拷贝
实现原理:对于基本类型的拷贝;基本引用类型,开辟一个新的存储,并且拷贝对象属性
function shallowCopy(obj){
if(!obj && typeof obj !== 'object') return
// 判断obj的类型是数组还是对象
let newObject = Array.isArray(obj)?[]:{}
// 遍历 object,并且判断是 object 的属性才拷贝
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
newObject[key] = obj[key]
}
}
return newObject
}

深拷贝:对于简单数据类型直接拷贝它的值,对于引用数据类型,在堆中新开辟一个内存地址用于存放复制的对象,并把原有的对象类型数据拷贝过来,这两个对象相互独立,属于两个不同的内存地址,修改其中一个,另一个也不会发升改变

function cloneForce(x) {
const uniqueList = []; // 用来去重
let root = {};
const loopList = [
{
parent: root,
key: undefined,
data: x,
}
];
while(loopList.length) {
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const data = node.data;
// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let res = parent;
if (typeof key !== 'undefined') {
res = parent[key] = {};
}

// 数据已经存在
let uniqueData = find(uniqueList, data);
if (uniqueData) {
parent[key] = uniqueData.target;
continue;
}
// 数据不存在
// 保存源数据,在拷贝数据中对应的引用
uniqueList.push({
source: data,
target: res,
});

for(let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
loopList.push({
parent: res,
key: k,
data: data[k],
});
} else {
res[k] = data[k];
}
}
}
}
return root;
}
//find函数用来遍历uniqueList
function find(arr, item) {
for(let i = 0; i < arr.length; i++) {
if (arr[i].source === item) {
return arr[i];
}
}
return null;
}

总结: ●**浅拷贝:**浅拷贝指的是将一个对象的属性值复制到另一个对象,如果有的属性的值为引用类型的话,那么会将这个引用的地址复制给对象,因此两个对象会有同一个引用类型的引用。浅拷贝可以使用 Object.assign 和展开运算符来实现。 ●**深拷贝:**深拷贝相对浅拷贝而言,如果遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象可以使用 JSON 的两个函数来实现,但是由于 JSON 的对象格式比 js 的对象格式更加严格,所以如果属性值里边出现函数或者 Symbol 类型的值时,会转换失败

18、splice和slice会改变原数组吗?如何删除数组最后一个元素?

1、splice会改变原数组,并以数组形式返回被修改的内容

2、slice不会改变数组,一个含有被提取元素的新数组

3、删除数组最后一个元素的方法

  1. arr.pop()
  2. slice(0,-1)
  3. arr.splice(-1,1)
  4. a.length-1或者**delete arr[arr.length-1]

19、0.1+0.2为什么不等于0.3?

1.二进制转换的过程中精度丢失
2.计算的过程中精度丢失

解决:
1.将数字转化成整数
2.使用第三方库
3.使用tofixed

20、== 和 === 的区别

  • 使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
  • 使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。

21、解释 requestAnimationFrame/requestIdleCallback,分别有什么用

requestAnimationFrame 和 requestIdleCallback 都是浏览器提供的用于在下一次浏览器重绘之前执行指定函数的方法,它们的作用类似。它们的区别在于:

  1. 回调函数的执行时机:requestAnimationFrame 的回调函数在下一次浏览器重绘之前执行,通常为每秒60次(60fps);而requestIdleCallback 的回调函数在浏览器空闲时执行,即在浏览器没有其它任务时执行,通常为每秒几次(不确定,取决于浏览器)。
  2. 回调函数的参数:requestAnimationFrame 的回调函数会被传入一个时间戳参数,表示当前帧开始渲染的时间;而requestIdleCallback 的回调函数会被传入一个 IdleDeadline 对象,包含了当前空闲时间的一些信息。 因此,当需要在下一次浏览器重绘之前执行某些操作时,应该使用requestAnimationFrame;当需要在浏览器空闲时执行一些较为耗时的操作时,应该使用requestIdleCallback。使用这两个方法可以避免造成页面卡顿或阻塞,提高用户体验。需要注意的是,由于requestIdleCallback的回调函数可能会在多次空闲时间内执行,因此应该根据实际情况合理地控制回调函数的执行次数和执行时间,以避免占用过多的浏览器资源

22、解释 requestAnimationFrame/requestIdleCallback,分别有什么用

RAF和RIC都是浏览器用于在下一次浏览器重绘之前执行指定函数的方法,他们的作用类似

RAF是用下一次浏览器重绘之前执行,通常时间为60s,而RIC是需要在浏览器空闲去执行的,如果没有空闲时间是不会执行的

23、谈下事件循环机制?

首先,浏览器的所有任务都是在一条线程上处理,即单线程

这样的坏处是,当执行一条任务时,这条任务执行的时间很长或者无响应,就会阻塞后面的任务,所以浏览器给出了一个方案,就是分为同步任务和异步任务,

异步任务是包括宏任务和微任务

其中常见的宏任务有全局的Script、settimeout、setInterval、IO操作、UI交互操作、以及nodejs中的setimmediate

常见的微任务有:Promise.then、async,await promise的语法糖、Process.nexTick、mutaionObserver

浏览器的EventLoop:

首先浏览器在执行代码的时候有两个概念,一个是执行栈,一个是任务队列

栈是先进后出,也就是函数的执行的时候,是后进去的先执行,pop()

任务队列是先进先出,也就是当有宏任务或者微任务的时候,都会放进各自的任务队列里去等待执行,shift()

总结:

在浏览器执行代码的过程中,首先执行全局的同步的代码,执行的过程中,如果发现有宏任务或者微任务,就会放进各自的任务队列去等待完成,随着同步的任务执行完成,先会去查看微任务队列是否存在任务,如果不存在就会开始将宏任务中的第一个任务取出,去执行;如果存在那就回优先执行微任务队列的任务;当执行宏任务的时候,如果宏任务中也包含微任务的话,那就会继续把微任务放进微任务队列,然后依次去执行微任务队列,直到所有的宏任务和微任务都执行完成,浏览器的一次事件循环就结束了

说一下node环境中的事件循环

首先node中也含有像浏览器中的一些异步操作,例如:IO操作:readFIleAsyc、process.nextTick、setImmdeiate

还有就是node中其实是利用libuv去执行js脚本,以及node,他们是怎么操作的呢?

首先v8引擎会去处理js脚本,node处理解析后的代码,而libuv库就是去处理nodeAPi的执行,将他们分配给不同的线程去执行,形成一个eventloop,最后以异步的形式去返回结果给v8引擎

一共六个阶段

timer阶段:处理定时器等,回调是在poll阶段执行

io操作阶段:处理io事件,回调也是在poll阶段执行

idle,prepare阶段:处理系统事件,可以忽略

poll阶段:处理timer的回调和io的回调

check阶段:处理setlmmdeiate的回调

close阶段:处理close的回调

其中poll阶段比较复杂,分为几种情况:

如果没有设定timer时间(timer时间为下一次触发时间减去当前时间),

则poll队列是否为空,如果不为空的话,就会遍历去执行所有的回调,直到回调完成或者达到系统限制(这里的系统限制指的是系统的递归深度,默认为1000);

如果为空的话,则会判断setlmmdeiate是否有回调需要执行,如果有则会进去check阶段去执行回调;如果没有回调要切执行,它就会等待回调被加入到队列中,这个等待时间是有限制的,不会无限的等待下去。

如果有设定timer时间且poll队列为空的话,则会判断是否有timer超时,如果有的就会回到timer阶段执行回调,如果队列不为空,则执行回调

24、介绍一下防抖和节流

防抖:防抖就是事件被触发n秒后再执行回调函数,可能在n秒内又触发了这个事件,则会重新计时,这样就确保了最后一次触发事件的n秒后去执行回调函数,从而减少回调函数的调用次数

节流:节流就是在一段时间内只会执行一次回调函数,如果在这段时间内又触发了这个事件,则忽略该事件,这样就确保了最后一次触发事件的n秒后去执行回调函数,从而减少回调函数的调用次数

时间戳版可能无法指定时间去执行回调函数

防抖:在触发事件的n秒后函数才会被执行,如果n秒内又触发了事件,则会重新重新计算函数执行的时间

事件被触发后 N 秒后再执行回调函数,如果 N 秒内事件再次被触发则重新计算执行回调函数时间。简单可以总结为延时执行,最后一次事件触发后执行回调函数

函数防抖常用在连续的事件但只需要触发一次的场景

  • 搜索框输入,只需要用户输完再发送请求
  • 手机号和邮箱的输入检查
  • 窗口大小resize,只需要窗口调整完成之后再计算大小
// 函数防抖的实现
function debounce(fn, wait) {
let timer = null
return function (...args) {
let ctx = this
if (timer) {
clearTimeout(timer)
timer = null
}
timer = setTimeout(() => {
fn.apply(ctx, args)
}, wait)
}
}
clearTimeout()清除了定时器之后timer会变成一个类型为Number的数字,所以timer为true,下次就不生效了。所以要设置为null

节流:每隔一段时间,只执行一次函数

限制了函数再一定时间只能执行一次

就像坐地铁,过闸机时,每个人进入后3秒关闭,才能等下一个人进入

函数节流常用在需要间隔时间执行一次回调的场景

  • 滚动加载的底部监听,加载更多
  • 搜索框的联想功能
  • 防止按钮重复点击,多次提交表单
function throttle(fn, wait) {
let timer = null
return function (...args) {
const ctx = this
if (!timer) {
timer = setTimeout(() => {
fn.apply(ctx, args)
timer = null
}, wait)
}
}
}
function throttle(fn, wait) {
let curTime = Date.now()
return function (...args) {
let ctx = this
let nowTime = Date.now()
// 如果两次时间间隔超过了指定时间,则执行函数。
if (nowTime - curTime >= wait) {
curTime = Date.now()
return fn.apply(ctx, args)
}
}
}

//验证
function Fn() {
console.log('111');
}
let obj = throttle(Fn,1000)

24、事件冒泡和捕获的区别?默认是冒泡还是捕获?

事件流: 描述的是从页面中接收事件的顺序。事件被触发时会在元素节点之间按照特定的顺序传播,这个过程就是事件流,即捕获阶段、目标阶段、冒泡阶段---事件的响应也是遵循先捕获后冒泡的规则(最新答案)

  • 例如:首先给div注册单击事件,当你单击了div时,也就是单击body、单击了html,单击了document。

事件捕获:网景最早提出:document对象首先接收到click事件,然后事件沿着DOM树逐级向下传播,直到传播到事件的实际目标。(由外向内)

  • 事件捕获的过程 (window->document->html->body->div-text)

事件冒泡:IE最早提出:事件由具体的元素(文档中嵌套层次最深的哪个节点)接收,然后逐级向上传播到DOM最顶层节点的过程 。(由内向外)

  • 事件冒泡的过程 (text->div->body->html->document->window)

W3C统一:制定统一的标准------先捕获再冒泡。IE、Firefox、Chrome、Opera和Safari即支持冒泡又支持捕获。目前低版本的IE浏览器只能支持冒泡流(IE6,IE7,IE8均只支持冒泡流)建议使用冒泡流

  • 注意: js代码中只能执行捕获或者冒泡的其中一个阶段

  • onclick和attachEvent 只能得到冒泡阶段

  • addEventListener(type、listener、useCapture) 第三个参数如果是true ,表示在事件捕获阶段调用事件处理程序,如果是false(不写默认是false) 则表示的是事件冒泡阶段调用事件处理程序。

  • 实际开发中我们很少使用事件捕获,我们更关注事件冒泡

  • 有些事件是没有事件冒泡的,比如blur、focus、mouseenter、mouseleave

  • 如何阻止事件冒泡

阻止默认行为

  • 阻止默认行为 例如 表单的提交会跳转页面,a标签被单击后,默认会进行页面跳转
  • addEventListener() 注册事件 ,只有一种阻止默认行为的方式 ,e.preventDefault()
  • 传统的注册方式 onclick ,有三种注册方式(preventDefault(),returnValue,return false;)

阻止事件冒泡

事件冒泡本身的特性,会带来坏处,与好处 要怎么利用冒泡这一特性

  • e.stopPropagation( ) //有兼容性,只支持高版本的浏览器

  • return false

  • event.preventDefault()

  • e. cancelBubble=true; // 只支持 IE6-8, 低版本浏览器

冒泡的应用---事件委托也叫事件代理:事件代理就是利用事件冒泡或事件捕获的机制把一系列的内层元素事件绑定到外层元素---事件代理既可以通过事件冒泡来实现,也可以通过事件捕获来实现

  • 作用:减少操作DOM的次数,提高程序的性能
  • 动态新创建的子元素,也拥有事件。

25、什么是事件代理?

也就是通过事件捕获或者事件冒泡,把一系列内层元素的事件绑定到外层元素,作用是减少操作的Dom的次数,提高程序的性能并且在动态添加或删除列表项时也不需要重新绑定事件处理程序,这使得代码更加简洁、灵活且易于维护。

比如动态创建子元素,给ul绑定事件,那每一个子元素也会有事件

26、mouseover 和 mouseenter 的区别?

  • mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡过程。对应的移除事件是 mouseout
  • mouseenter:当鼠标移入元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡。对应的移除事件是 mouseleave
  • 通过图片进行分析 冒泡 情况img
  • hover 事件调用顺序mouseover -> mouseenter -> mousemove(hover进去之后移动会触发) -> mouseout -> mouseleave

27、浏览器缓存策略

  • Service Worker:运行在浏览器背后的独立线程,它可以自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的
  • 内存缓存:基于内存的缓存,读取高效速度快,但是一旦关闭网页,内存就释放了。
  • 磁盘缓存:基于磁盘的缓存,容量大,读取慢
  • 推送缓存:推送缓存,http2中的内容,缓存在会话session中的

缓存策略

浏览器HTTP缓存策略分为两种:强缓存和协商缓存,都是通过设置 HTTP Header 来实现的。

  • 强缓存: 不会向服务器发送请求,直接从缓存中读取资源,强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control对比:Cache-Control优先级高于Expires

  • Expires: 缓存过期时间,用来指定资源到期的时间,是服务器端具体的时间点 Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效

  • Cache-Control: HTTP/1.1 的产物,比如当设置Cache-Control:max-age=300,单位是s,代表5分钟内再次请求就会走强缓存

  • 协商缓存: 协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程 协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag对比:ETag更精确,性能上Last-Modified好点

  • Last-Modified: http1.0 原理:浏览器第一次访问资源时,服务器会在response头里添加Last-Modified时间点,这个时间点是服务器最后修改文件的时间点,然后浏览器第二次访问资源时,检测到缓存文件里有Last-Modified,就会在请求头里加If-Modified-Since,值为Last-Modified的值,服务器收到头里有If-Modified-Since,就会拿这个值和请求文件的最后修改时间作对比,如果没有变化,就返回304,如果小于了最后修改时间,说明文件有更新,就会返回新的资源,状态码为200

  • ETag: http1.1 原理:与Last-Modified类似,只是Last-Modified返回的是最后修改的时间点,而ETag是每次访问服务器都会返回一个新的token,第二次请求时,该值埋在请求头里的If-None-Match发送给服务器,服务器在比较新旧的token是否一致,一致则返回304通知浏览器使用本地缓存,不一致则返回新的资源,新的ETag,状态码为200

  • 如果什么缓存策略都不设置: 这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间

28、浏览器内核是什么?包含什么?常见的有哪些?

浏览器内核是由js引擎和渲染引擎组成的

(1) IE 浏览器内核:Trident 内核,也是俗称的 IE 内核; (2) Chrome 浏览器内核:统称为 Chromium 内核或 Chrome 内核,以前是 Webkit 内核,现在是 Blink内核; (3) Firefox 浏览器内核:Gecko 内核,俗称 Firefox 内核; (4) Safari 浏览器内核:Webkit 内核; (5) Opera 浏览器内核:最初是自己的 Presto 内核,后来加入谷歌大军,从 Webkit 又到了 Blink 内核; (6) 360浏览器、猎豹浏览器内核:IE + Chrome 双内核; (7) 搜狗、遨游、QQ 浏览器内核:Trident(兼容模式)+ Webkit(高速模式); (8) 百度浏览器、世界之窗内核:IE 内核;

29、打开了两个标签页是进程还是线程?

浏览器从关闭到启动,然后新开一个页面至少需要:1个浏览器进程,1个GPU进程,1个网络进程,和1个渲染进程,一共4个进程

后续如果再打开新的标签页:浏览器进程,GPU进程,网络进程是共享的,不会重新启动,然后默认情况下会为每一个标签页配置一个渲染进程,但是也有例外,比如从A页面里面打开一个新的页面B页面,而A页面和B页面又属于同一站点的话,A和B就共用一个渲染进程,其他情况就为B创建一个新的渲染进程

所以,最新的Chrome浏览器包括:1个浏览器主进程,1个GPU进程,1个网络进程,多个渲染进程,和多个插件进程

  • 浏览器进程: 负责控制浏览器除标签页外的界面,包括地址栏、书签、前进后退按钮等,以及负责与其他进程的协调工作,同时提供存储功能

  • GPU进程:负责整个浏览器界面的渲染。Chrome刚开始发布的时候是没有GPU进程的,而使用GPU的初衷是为了实现3D CSS效果,只是后面网页、Chrome的UI界面都用GPU来绘制,这使GPU成为浏览器普遍的需求,最后Chrome在多进程架构上也引入了GPU进程

  • 网络进程:负责发起和接受网络请求,以前是作为模块运行在浏览器进程一时在面的,后面才独立出来,成为一个单独的进程

  • 插件进程:主要是负责插件的运行,因为插件可能崩溃,所以需要通过插件进程来隔离,以保证插件崩溃也不会对浏览器和页面造成影响

  • 渲染进程:负责控制显示tab标签页内的所有内容,核心任务是将HTML、CSS、JS转为用户可以与之交互的网页,排版引擎Blink和JS引擎V8都是运行在该进程中,默认情况下Chrome会为每个Tab标签页创建一个渲染进程

我们平时看到的浏览器呈现出页面过程中,大部分工作都是在渲染进程中完成,所以我们来看一下渲染进程中的线程

渲染进程中的线程

  • GUI渲染线程:负责渲染页面,解析html和CSS、构建DOM树、CSSOM树、渲染树、和绘制页面,重绘重排也是在该线程执行
  • JS引擎线程:一个tab页中只有一个JS引擎线程(单线程),负责解析和执行JS。它跟GUI渲染线程不能同时执行,只能一个一个来,如果JS执行过长就会导致阻塞掉帧
  • 计时器线程:指setInterval和setTimeout,因为JS引擎是单线程的,所以如果处于阻塞状态,那么计时器就会不准了,所以需要单独的线程来负责计时器工作
  • 异步http请求线程: XMLHttpRequest连接后浏览器开的一个线程,比如请求有回调函数,异步线程就会将回调函数加入事件队列,等待JS引擎空闲执行
  • 事件触发线程:主要用来控制事件循环,比如JS执行遇到计时器,AJAX异步请求等,就会将对应任务添加到事件触发线程中,在对应事件符合触发条件触发时,就把事件添加到待处理队列的队尾,等JS引擎处理

30、浏览器从输入网址到页面加载的整个过程

(1)解析URL:

  1. 首先会对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。
  2. 如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。
  3. 如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程。

(2)缓存判断:浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。

(3)DNS解析:为了获取真正的IP地址

  1. 下一步首先需要获取的是输入的 URL 中的域名的 IP 地址,首先会判断本地是否有该域名的 IP 地址的缓存,如果有则使用,如果没有则向本地 DNS 服务器发起请求。
  2. 本地 DNS 服务器也会先检查是否存在缓存,如果没有就会先向根域名服务器发起请求,获得负责的顶级域名服务器的地址后,再向顶级域名服务器请求,然后获得负责的权威域名服务器的地址后,再向权威域名服务器发起请求,最终获得域名的 IP 地址后
  3. 本地 DNS 服务器再将这个 IP 地址返回给请求的用户。用户向本地 DNS 服务器发起请求属于递归请求,本地 DNS 服务器向各级域名服务器发起请求属于迭代请求。

(4)获取MAC地址:当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相与,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 ARP 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。

(5)TCP三次握手:为了建立网络连接,下面是 TCP 建立连接的三次握手的过程,

  1. 首先客户端向服务器发送一个 SYN 连接请求报文段和一个随机序号,
  2. 服务端接收到请求后向服务器端发送一个 SYN ACK报文段,确认连接请求,并且也向客户端发送一个随机序号。
  3. 客户端接收服务器的确认应答后,进入连接建立的状态,同时向服务器也发送一个ACK 确认报文段,服务器端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了。

(6)HTTPS握手:对数据进行加密传输

如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。

  1. 首先由客户端向服务器端发送使用的协议的版本号、一个随机数和可以使用的加密方法。
  2. 服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证书。
  3. 客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。
  4. 服务器端接收后,使用自己的私钥对数据解密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把秘钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输。

(7)返回数据:当页面请求发送到服务器端后,服务器端会返回一个 html 文件作为响应,浏览器接收到响应后,开始对 html 文件进行解析,开始页面的渲染过程。

(8)页面渲染:浏览器首先会根据 html 文件构建 DOM 树,根据解析到的 css 文件构建 CSSOM 树,如果遇到 script 标签,则判端是否含有 defer 或者 async 属性,要不然 script 的加载和执行会造成页面的渲染的阻塞。当 DOM 树和 CSSOM 树建立好后,根据它们来构建渲染树。渲染树构建好后,会根据渲染树来进行布局。布局完成后,最后使用浏览器的 UI 接口对页面进行绘制。这个时候整个页面就显示出来了。

(9)TCP四次挥手:就是为了断开网络连接

  1. 若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。
  2. 服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。
  3. 客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。
  4. 当服务端收到确认应答后,也便进入 CLOSED 状态。

三次握手的步骤:(抽象派)

客户端:hello,你是server么?
服务端:hello,我是server,你是client么
客户端:yes,我是client
建立连接成功后,接下来就正式传输数据
然后,待到断开连接时,需要进行四次挥手(因为是全双工的,所以需要四次挥手)

四次挥手的步骤:(抽象派)

主动方:我已经关闭了向你那边的主动通道了,只能被动接收了
被动方:收到通道关闭的信息
被动方:那我也告诉你,我这边向你的主动通道也关闭了
主动方:最后收到数据,之后双方无法通信

31、前端模块化机制?

https://juejin.cn/post/6844903744518389768

解释:模块化的开发方式可以提高代码复用率,方便进行代码的管理。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。目前流行的js模块化规范有CommonJS、AMD、CMD、UMD以及ES6的模块系统;除此之外,IIFE(立即执行函数)也是实现模块化的一种方案。

本文将介绍其中的六个: ● **IIFE:**立即执行函数

1、定义 (function () {    // 声明私有变量和函数     return {        // 声明公共变量和函数    } })(); 2、使用 var module = (function(){  var age = 20;  var name = 'JavaScript'    var fn1 = function(){    console.log(name, age)  };    var fn2 = function(a, b){    console.log(a + b)  };    return {    age,    fn1,    fn2,  }; })(); module.age;           // 20 module.fn1();         // JavaScript 20 module.fn2(128, 64);  // 192

● **CommonJS:**Node.js 采用该规范

  1. 定义

  2. CommonJS 是社区提出的一种 JavaScript 模块化规范,它是为浏览器之外的 JavaScript 运行环境提供的模块规范,Node.js 就采用了这个规范

  3. CommonJS 规范加载模块是同步的,只有加载完成才能继续执行后面的操作。不过由于 Node.js 主要运行在服务端,而所需加载的模块文件一般保存在本地硬盘,所以加载比较快,而无需考虑使用异步的方式。

  4. 基本语法

暴露模块:module.exports = value或exports.xxx = value

引入模块:require(xxx),如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径

此处我们有个疑问:CommonJS暴露的模块到底是什么? CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性

// example.js var x = 5; var addX = function (value) {  return value + x; }; module.exports.x = x; module.exports.addX = addX; var example = require('./example.js');//如果参数字符串以“./”开头,则表示加载的是一个位于相对路径 console.log(example.x); // 5 console.log(example.addX(1)); // 6

require命令用于加载模块文件。require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错

  1. 特点

  2. 文件即模块,文件内所有代码都运行在独立的作用域,因此不会污染全局空间;

  3. 模块可以被多次引用、加载。第一次被加载时,会被缓存,之后都从缓存中直接读取结果。

  4. 加载某个模块,就是引入该模块的 module.exports 属性,该属性输出的是值拷贝,一旦这个值被输出,模块内再发生变化不会影响到输出的值。

  5. 模块加载顺序按照代码引入的顺序。

  6. 优缺点

  7. CommonJS 的优点:

  8. 使用简单

  9. 很多工具系统和包都是使用 CommonJS 构建的;

  10. 在 Node.js 中使用,Node.js 是流行的 JavaScript 运行时环境。

  11. CommonJS 的缺点

  12. 可以在 JavaScript 文件中包含一个模块

  13. 如果想在 Web 浏览器中使用它,则需要额外的工具

  14. 本质上是同步的,在某些情况下不适合在 Web 浏览器中使用

  15. 模块的加载机制

  16. CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。这点与ES6模块化有重大差异(下文会介绍),请看下面这个例子:

// lib.js var counter = 3; function incCounter() {  counter++; } module.exports = {  counter: counter,  incCounter: incCounter, };

上面代码输出内部变量counter和改写这个变量的内部方法incCounter。

// main.js var counter = require('./lib').counter; var incCounter = require('./lib').incCounter; console.log(counter);  // 3 incCounter(); console.log(counter); // 3

上面代码说明,counter输出以后,lib.js模块内部的变化就影响不到counter了。这是因为counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值

  1. require****加载过程

  2. 优先从缓存中加载

  3. 如果缓存中没有,检查是否是核心模块,如果是直接加载

  4. 如果不是核心模块,检查是否是文件模块,解析路径,根据解析出的路径定位文件,然后执行并加载;

  5. 如果以上都不是,沿当前路径向上逐级递归,直到根目录的node_modules目录。

AMD:

  1. 定义

  2. CommonJS 的缺点之一是它是同步的,AMD 旨在通过规范中定义的 API 异步加载模块及其依赖项来解决这个问题。AMD 全称为 Asynchronous Module Definition,即异步模块加载机制。它规定了如何定义模块,如何对外输出,如何引入依赖。

  3. AMD规范重要特性就是异步加载。所谓异步加载,就是指同时并发加载所依赖的模块,当所有依赖模块都加载完成之后,再执行当前模块的回调函数。这种加载方式和浏览器环境的性能需求刚好吻合。

  4. 基本语法

1、AMD 规范定义了一个全局函数 define,通过它就可以定义和引用模块,它有 3 个参数: 	define(id?, dependencies?, factory); 其包含三个参数: ●	id:可选,指模块路径。如果没有提供该参数,模块名称默认为模块加载器请求的指定脚本的路径。 ●	dependencies:可选,指模块数组。它定义了所依赖的模块。依赖模块必须根据模块的工厂函数优先级执行,并且执行的结果应该按照依赖数组中的位置顺序以参数的形式传入工厂函数中。 ●	factory:为模块初始化要执行的函数或对象。如果是函数,那么该函数是单例模式,只会被执行一次;如果是对象,此对象应该为模块的输出值。 2、除此之外,要想使用此模块,就需要使用规范中定义的 require 函数: 	require(dependencies?, callback); 其包含两个参数: ●	dependencies:依赖项数组; ●	callback:加载模块时执行的回调函数。 
  1. 优缺点

  2. AMD 的优点

  3. 异步加载导致更好的启动时间

  4. 能够将模块拆分为多个文件

  5. 支持构造函数

  6. 无需额外工具即可在浏览器中工作

  7. AMD 的缺点:

  8. 语法很复杂,学习成本高

  9. 需要一个像 RequireJS 这样的加载器库来使用 AMD

CMD:

  1. 定义

  2. CMD 全称为 Common Module Definition,即通用模块定义。CMD 规范整合了 CommonJS 和 AMD 规范的特点。sea.js 是 CMD 规范的一个实现 。

  3. 基本语法

CMD 定义模块也是通过一个全局函数 define 来实现的,但只有一个参数,该参数既可以是函数也可以是对象: define(factory); 如果这个参数是对象,那么模块导出的就是对象;如果这个参数为函数,那么这个函数会被传入 3 个参数: define(function(require, exports, module) {  //... }); 这三个参数分别如下: (1)require:一个函数,通过调用它可以引用其他模块,也可以调用 require.async 函数来异步调用模块; (2)exports:一个对象,当定义模块的时候,需要通过向参数 exports 添加属性来导出模块 API; (3)module 是一个对象,它包含 3 个属性:  ●	uri:模块完整的 URI 路径;  ●	dependencies:模块依赖;  ●	exports:模块需要被导出的 API,作用同第二个参数 exports。 下面来看一个例子,定义一个 increment 模块,引用 math 模块的 add 函数,经过封装后导出成 increment 函数: define(function(require, exports, module) {  var add = require('math').add;  exports.increment = function(val) {    return add(val, 1);  };  module.id = "increment"; }); 
  1. 特点

  2. CMD 最大的特点就是懒加载,不需要在定义模块的时候声明依赖,可以在模块执行时动态加载依赖。除此之外,CMD 同时支持同步加载模块异步加载模块

  3. AMD和CMD的主要区别是什么?

  4. AMD 需要异步加载模块,而 CMD 在加载模块时,可以同步加载(require),也可以异步加载(require.async)。

  5. CMD 遵循依赖就近原则,AMD 遵循依赖前置原则。也就是说,在 AMD 中,需要把模块所需要的依赖都提前在依赖数组中声明。而在 CMD 中,只需要在具体代码逻辑内,使用依赖前,把依赖的模块 require 进来。

UMD:

  1. 定义

  2. UMD 全程为 Universal Module Definition,即统一模块定义。其实 UMD 并不是一个模块管理规范,而是带有前后端同构思想的模块封装工具。

  3. UMD 是一组同时支持 AMD 和 CommonJS 的模式,它旨在使代码无论执行代码的环境如何都能正常工作,通过 UMD 可以在合适的环境选择对应的模块规范。比如在 Node.js 环境中采用 CommonJS 模块管理,在浏览器环境且支持 AMD 的情况下采用 AMD 模块,否则导出为全局函数。

  4. 一个UMD模块由两部分组成: ● 立即调用函数表达式 (IIFE):它会检查使用模块的环境。其有两个参数:root 和 factory。 root 是对全局范围的 this 引用,而 factory 是定义模块的函数。 ● **匿名函数:**创建模块,此匿名函数被传递任意数量的参数以指定模块的依赖关系。

  5. 执行过程

(function (root, factory) {  if (typeof define === 'function' && define.amd) {    define([], factory);  } else if (typeof exports === 'object') {    module.exports,    module.exports = factory();  } else {    root.returnExports = factory();  } }(this, function () {  // 模块内容定义  return {}; })); 它的执行过程如下: 1、先判断是否支持 Node.js 模块格式(exports 是否存在),存在则使用 Node.js 模块格式; 2、再判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块; 3、若两个都不存在,则将模块公开到全局(Window 或 Global)。 
  1. 特点

  2. UMD 的优点: ● 小而简洁; ● 适用于服务器端和客户端

  3. UMD 的缺点: ● 不容易正确配置

● **ES6 Module:**内置的 JavaScript 模块系统

  1. 定义

  2. ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性,CommonJS 加载的是拷贝

  3. 基本语法

export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。 /** 定义模块 math.js **/ var basicNum = 0; var add = function (a, b) {    return a + b; }; export { basicNum, add }; /** 引用模块 **/ import { basicNum, add } from './math'; function test(ele) {    ele.textContent = add(99 + basicNum); } 如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。 // export-default.js export default function () {  console.log('foo'); } // import-default.js import customName from './export-default'; customName(); // 'foo' 模块默认输出, 其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
  1. ES6 模块与 CommonJS 模块的差异

它们有两个重大差异:

① CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

② CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

下面重点解释第一个差异,我们还是举上面那个CommonJS模块的加载机制例子:

// lib.js export let counter = 3; export function incCounter() {  counter++; } // main.js import { counter, incCounter } from './lib'; console.log(counter); // 3 incCounter(); console.log(counter); // 4复制代码

ES6 模块的运行机制与 CommonJS 不一样。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块

总结:

  • CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
  • AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
  • CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重
  • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案

32、ES Module 和 commonjs 的区别?

es6module是值的引用,commonjs是值的拷贝

es6是编译的时候输出接口,commonjs是运行才加载

es6是异步加载,commonjs是同步加载,因为commonjs是运行在node中,文件在本地,而es6是在浏览器运行,所以需要支持异步加载

es6支持tree-shaking,commonjs不支持

esmodule的数据是同步的

33、cookie、localStorage、sessionStorage、IndexedDB 的区别?

4k 5M 5M

34、cookie 里面都包含什么属性?

  1. Domain:设置主机名,设置浏览器允许哪些主机可以访问cookie;作用,减少域的限制并可以在子域里访问

  2. Path:指定访问cookie的必须存在请求url中的路径,除了使用域之外,还可以使用路径

  3. Expires/max-size:设置cookie的过期时间,如果没有设置,那么就是和session一起失效

  4. secure:https才可以访问

  5. httponly:使cookie只能通过服务端访问,并且客户端无法通过js去获取 很重要,可以防止xss攻击

35、Cookie能跨域吗?如何设置?

同一域名下,cookie是可以的共享的,而不同域名下,默认情况下是不共享的

可以跨域,需要设置

  1. 响应头设置Access-Control-Allow-Credentials: true,表示允许发送cookie,与前端的withCredentials配合使用
  2. 响应头设置Access-Control-Allow-Origin:域名,请求域名,表示允许该域名下的请求访问资源

36、对 CORS 的理解

首页是一种机制,跨域资源共享机制,也就是域,协议,端口不同时,会发起一个http请求,cors需要服务器和浏览器同时支持

浏览器把cors分为简单请求和非简单请求

简单请求不会触发cors预检请求,满足以下条件就是简单请求

  1. 请求方法是以下方法之一
  2. HEAD
  3. GET
  4. POST
  5. HTTP的头信息不超出以下几种字段

简单请求的过程:

​ 对于简单请求,浏览器会直接发出cors请求,它会子啊头信息中增加一个origin字段,该字段表明请求来自于哪个源(协议+域名+端口),服务器会根据这个值来决定是否同意这个请求,如果origin指定的域名在许可范围内,服务器返回的响应头就会多出以下信息

​ Access-Control-Allow-Origin:跟orgin一致

​ Access-Control-Allow-Credentials:true // 表示可以允许发送cookie

​ Access-Control-Expose-Headers:FooBar // 指定返回其他字段的值

​ content-type:text-html; charset=utf-8// 表示文档类型

如果Orign指定的域名不在许可范围之内,服务器会返回一个正常的HTTP回应,浏览器发现没有上面的Access-Control-Allow-Origin头部信息,就知道出错了。这个错误无法通过状态码识别,因为返回的状态码可能是200。

在简单请求中,在服务器内的响应头,至少需要设置字段:Access-Control-Allow-Origin

非简单请求的过程

​ 一般是DELETE或则PUT等这些请求,也就是非简单请求的时候,会在通信前进行一次预检请求,也就是Options请求

浏览器会询问服务器,当前网页是否在服务器的允许访问的范围内,以及可以使用哪些http请求方式和头信息字段,只要得到了肯定的答复,才会进行正式的请求,否则就会报错,返回403

预检请求使用的方法是Options,这个请求就是来询问服务器,它的头信息里有一个origin字段,表示来自哪个源,除此之外,来包括

'Access-Control-Allow-Origin'

Access-Control-Request-Method:用来列出浏览器的CORS请求会用到哪些HTTP方法。

Access-Control-Request-Headers: 该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段

服务器收到浏览器的预检请求之后,会根据头信息的三个字段来进行判断,如果返回的头信息中含有Access-Control-Allow-Origin这个字段就是允许跨域请求,如果没有,就是不同意这个预检请求,就会报错。返回403

Access-Control-Allow-Origin: http://api.bob.com  // 允许跨域的源地址
Access-Control-Allow-Methods: GET, POST, PUT // 服务器支持的所有跨域请求的方法
Access-Control-Allow-Headers: X-Custom-Header // 服务器支持的所有头信息字段
Access-Control-Allow-Credentials: true // 表示是否允许发送Cookie
Access-Control-Max-Age: 1728000 // 用来指定本次预检请求的有效期,单位为秒

只要服务器通过了预检请求,在以后每次的CORS请求都会自带一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

减少OPTIONS请求次数:

​ OPTIONS请求次数过多就会损耗页面加载的性能,降低用户体验度。所以尽量要减少OPTIONS请求次数,可以后端在请求的返回头部添加:Access-Control-Max-Age:number。它表示预检请求的返回结果可以被缓存多久,单位是秒。该字段只对完全一样的URL的缓存设置生效,所以设置了缓存时间,在这个时间范围内,再次发送请求就不需要进行预检请求了。

CORS中Cookie相关问题: 在CORS请求中,如果想要传递Cookie,就要满足以下三个条件: **●**在请求中设置 withCredentials 默认情况下在跨域请求,浏览器是不带 cookie 的。但是我们可以通过设置 withCredentials 来进行传递 cookie

Access-Control-Allow-Origin

Access-Control-Allow-Credentials

37、如何检查内存泄露?

使用性能分析器可视化内存消耗

识别分离的 DOM 节点

38、https 加密是怎样的?

1. 什么是HTTPS协议?

​ 超文本传输安全协议(Hypertext Transfer Protocol Secure,简称:HTTPS)是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,利用SSL/TLS来加密数据包。HTTPS的主要目的是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。

HTTP协议采用明文传输信息,存在信息窃听、信息篡改和信息劫持的风险,而协议TLS/SSL具有身份验证、信息加密和完整性校验的功能,可以避免此类问题发生。

安全层的主要职责就是对发起的HTTP请求的数据进行加密操作 和 对接收到的HTTP的内容进行解密操作。

2. TLS/SSL的工作原理

​ (1)散列函数hash

常见的散列函数有MD5、SHA1、SHA256。该函数的特点是单向不可逆,对输入数据非常敏感,输出的长度固定,任何数据的修改都会改变散列函数的结果,可以用于防止信息篡改并验证数据的完整性。

特点:在信息传输过程中,散列函数不能都实现信息防篡改,由于传输是明文传输,中间人可以修改信息后重新计算信息的摘要,所以需要对传输的信息和信息摘要进行加密,并用于防止信息篡改并验证数据的完整性

(2)对称加密

对称加密的方法是,双方使用同一个秘钥对数据进行加密和解密。但是对称加密的存在一个问题,就是如何保证秘钥传输的安全性,因为秘钥还是会通过网络传输的,一旦秘钥被其他人获取到,那么整个加密过程就毫无作用了。 这就要用到非对称加密的方法。

常见的对称加密算法有AES-CBC、DES、3DES、AES-GCM等。相同的秘钥可以用于信息的加密和解密。掌握秘钥才能获取信息,防止信息窃听,其通讯方式是一对一。

特点:对称加密的优势就是信息传输使用一对一,需要共享相同的密码,密码的安全是保证信息安全的基础,服务器和N个客户端通信,需要维持N个密码记录且不能修改密码。

(3)非对称加密

非对称加密的方法是,我们拥有两个秘钥,一个是公钥,一个是私钥。公钥是公开的,私钥是保密的。用私钥加密的数据,只有对应的公钥才能解密,用公钥加密的数据,只有对应的私钥才能解密。我们可以将公钥公布出去,任何想和我们通信的客户, 都可以使用我们提供的公钥对数据进行加密,这样我们就可以使用私钥进行解密,这样就能保证数据的安全了。但是非对称加密有一个缺点就是加密的过程很慢,因此如果每次通信都使用非对称加密的方式的话,反而会造成等待时间过长的问题。

常见的非对称加密算法有RSA、ECC、DH等。秘钥成对出现,一般称为公钥(公开)和私钥(保密)。公钥加密的信息只有私钥可以解开,私钥加密的信息只能公钥解开,因此掌握公钥的不同客户端之间不能相互解密信息,只能和服务器进行加密通信,服务器可以实现一对多的的通信,客户端也可以用来验证掌握私钥的服务器的身份。

特点:非对称加密的特点就是信息一对多,服务器只需要维持一个私钥就可以和多个客户端进行通信,但服务器发出的信息能够被所有的客户端解密,且该算法的计算复杂,加密的速度慢。

综合上述算法特点,TLS/SSL的工作方式就是客户端使用非对称加密与服务器进行通信,实现身份的验证并协商对称加密使用的秘钥。对称加密算法采用协商秘钥对信息以及信息摘要进行加密通信,不同节点之间采用的对称秘钥不同,从而保证信息只能通信双方获取。这样就解决了两个方法各自存在的问题。

3. 数字证书是什么?

​ 现在的方法也不一定是安全的,因为没有办法确定得到的公钥就一定是安全的公钥。可能存在一个中间人,截取了对方发给我们的公钥,然后将他自己的公钥发送给我们,当我们使用他的公钥加密后发送的信息,就可以被他用自己的私钥解密。然后他伪装成我们以同样的方法向对方发送信息,这样我们的信息就被窃取了,然而自己还不知道。为了解决这样的问题,可以使用数字证书。

首先使用一种 Hash 算法来对公钥和其他信息进行加密,生成一个信息摘要,然后让有公信力的认证中心(简称 CA )用它的私钥对消息摘要加密,形成签名。最后将原始的信息和签名合在一起,称为数字证书。当接收方收到数字证书的时候,先根据原始信息使用同样的 Hash 算法生成一个摘要,然后使用公证处的公钥来对数字证书中的摘要进行解密,最后将解密的摘要和生成的摘要进行对比,就能发现得到的信息是否被更改了。

39、HTML文档的生命周期有哪些?[https://juejin.cn/post/6946079427671359502]

- DOMContentLoaded浏览器已经加载完成HTML数据,并且构建了DOM树。但是如样式、图片之类的外部文件有可能仍未加载完成。所以,可以再此时寻找DOM元素,以及初始化接口。在遇到<script>标签之后,脚本会阻塞 DOMContentLoaded,浏览器将等待它们执行结束,因为脚本有可能修改页面的DOM,所以DOMContentLoaded是在解析完脚本以后才执行。额外说明:

- 如果在样式后面有一个脚本,那么该脚本必须等待样式表加载完成。原因是,脚本可能想要获取元素的坐标和其他与样式相关的属性。
- DOMContentLoaded 事件发生在 document 上, 必须使用 addEventListener 来监听它。

- document.**addEventListener**('DOMContentLoaded', **function**(){ **alert**( 'DOMContentLoaded' ); })

- onload浏览器已经加载完全部的HTML以及图片、样式等外部资源,因此可以使用 window.onload
- beforeunload 和 unload

- beforeunload:用户准备离开,可以提醒用户是否保存了数据,确认要离开页面。

- document.**addEventListener**("beforeunload", **alert**('确认离开?'));

- unload:用户马上离开。此时仍可以处理一些操作,如发送统计数据等。如果存在iframe,父iframe会在子iframe卸载前卸载。父beforeunload => 子beforeunload => 父onunload => 子unload,使用window.onunload

40、如何解决跨域问题?

(1)CORS

(2)JSONP

jsonp的原理就是利用 <script>标签没有跨域限制,通过 <script>标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。

1)原生JS实现:

<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
document.head.appendChild(script);
// 回调执行函数
function handleCallback(res) {
alert(JSON.stringify(res));
}
</script>

// 服务端返回如下(返回时即执行全局函数):
handleCallback({"success": true, "user": "admin"})

2)Vue axios实现:

this.$http = axios; this.$http.jsonp('http://www.domain2.com:8080/login', {  params: {},  jsonp: 'handleCallback' }).then((res) => {  console.log(res);  })

后端node.js代码:

var querystring = require('querystring'); 
var http = require('http');
var server = http.createServer();
server.on('request', function(req, res) {
var params = querystring.parse(req.url.split('?')[1]);
var fn = params.callback; // jsonp返回设置
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(fn + '(' + JSON.stringify(params) + ')');
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');

JSONP的缺点:

  • 具有局限性, 仅支持get方法
  • 不安全,可能会遭受XSS攻击

(3)postMessage 跨域

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

用法:postMessage(data,origin)方法接受两个参数:

  • data**: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。**
  • origin**: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。**

1)a.html:(domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" rel="external nofollow"  rel="external nofollow"  style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向domain2传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
};
// 接受domain2返回数据
window.addEventListener('message', function(e) {
alert('data from domain2 ---> ' + e.data);
}, false);
</script>

2)b.html:(domain2.com/b.html)

<script>
// 接收domain1的数据
window.addEventListener('message', function(e) {
alert('data from domain1 ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false);
</script>

(4)nginx代理跨域

https://juejin.cn/post/6844903734804217863

nginx代理跨域,实质和CORS跨域原理一样,通过配置文件设置请求响应头Access-Control-Allow-Origin…等字段。

1)nginx配置解决iconfont跨域

浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

location / {
add_header Access-Control-Allow-Origin *;
}

2)nginx反向代理接口跨域

跨域问题:同源策略仅是针对浏览器的安全策略。服务器端调用HTTP接口只是使用HTTP协议,不需要同源策略,也就不存在跨域问题。

实现思路:通过Nginx配置一个代理服务器域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域访问。

nginx具体配置:

#proxy服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}

(5)nodejs 中间件代理跨域

node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

1)非vue框架的跨域

使用node + express + http-proxy-middleware搭建一个proxy服务器。

  • 前端代码:
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
  • 中间件服务器代码:
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
app.use('/', proxy({
// 代理跨域目标接口
target: 'http://www.domain2.com:8080',
changeOrigin: true,
// 修改响应头信息,实现跨域并允许带cookie
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
res.header('Access-Control-Allow-Credentials', 'true');
},
// 修改响应信息中的cookie域名
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}));
app.listen(3000);
console.log('Proxy server is listen at port 3000...');

2)vue框架的跨域

node + vue + webpack + webpack-dev-server搭建的项目,跨域请求接口,直接修改webpack.config.js配置。开发环境下,vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域。

webpack.config.js部分配置:

module.exports = {
entry: {},
module: {},
...
devServer: {
historyApiFallback: true,
proxy: [{
context: '/login',
target: 'http://www.domain2.com:8080', // 代理跨域目标接口
changeOrigin: true,
secure: false, // 当代理某些https服务报错时用
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}],
noInfo: true
}
}

(6)document.domain + iframe跨域

此方案仅限主域相同,子域不同的跨域应用场景。实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

1)父窗口:(domain.com/a.html)

<iframe id="iframe" src="http://child.domain.com/b.html" rel="external nofollow" ></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>

2)子窗口:(child.domain.com/a.html)

<script>
document.domain = 'domain.com';
// 获取父窗口中变量
console.log('get js data from parent ---> ' + window.parent.user);
</script>

(7)location.hash + iframe跨域

实现原理:a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。

1)a.html:(domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" rel="external nofollow"  rel="external nofollow"  style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 向b.html传hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);

// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>

2)b.html:(.domain2.com/b.html)

<iframe id="iframe" src="http://www.domain1.com/c.html" rel="external nofollow"  style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>

3)c.html:(http://www.domain1.com/c.html)

<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>

(8)window.name + iframe跨域

window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

1)a.html:(domain1.com/a.html)

var proxy = function(url, callback) {
var state = 0;
var iframe = document.createElement('iframe');
// 加载跨域页面
iframe.src = url;
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
iframe.onload = function() {
if (state === 1) {
// 第2次onload(同域proxy页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
state = 1;
}
};
document.body.appendChild(iframe);
// 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
};
// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
alert(data);
});

2)proxy.html:(domain1.com/proxy.html)

中间代理页,与a.html同域,内容为空即可。

3)b.html:(domain2.com/b.html)

通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

(9)WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。

原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

1)前端代码:

<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js" rel="external nofollow" ></script>
<script>
var socket = io('http://www.domain2.com:8080');
// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>

2)Nodejs socket后台:

var http = require('http');
var socket = require('socket.io');
// 启http服务
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
// 监听socket连接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 断开处理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});

41、Content-Type 值有哪些?

常见的 Content-Type 属性值有以下四种:

(1)application/x-www-form-urlencoded:浏览器的原生 form 表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据。该种方式提交的数据放在 body 里面,数据按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL转码。

(2)multipart/form-data:该种方式也是一个常见的 POST 提交方式,通常表单上传文件时使用该种方式。

(3)application/json:服务器消息主体是序列化后的 JSON 字符串。

(4)text/xml:该种方式主要用来提交 XML 格式的数据。

服务器向客户端发送数据格式类型:XML、HTML、JSON

42、常见状态码的含义?

101 切换请求协议,从 HTTP 切换到 WebSocket

200 请求成功,有响应体,服务器成功返回网页

301 永久重定向:会缓存

302 临时重定向:不会缓存

303 正常请求重定向,使用get重定向

304 协商缓存命中

400 请求错误

401 用户认证失败

403 服务器禁止访问

404 资源未找到,请求的网页不存在

500 服务器端错误

502 服务器请求超时

503 服务器停机维护,或者nginx设置了限速

504 服务器请求超时,通常是代码执行时间超时

43、get 和 post 的区别? 6 种

Post 和 Get 是 HTTP 请求的两种方法,其区别如下:

  • 应用场景:

  • GET 请求是一个幂等的请求,一般 Get 请求用于对服务器资源不会产生影响的场景,比如说请求一个网页的资源。而 Post 不是一个幂等的请求,一般用于对服务器资源会产生影响的情景,比如注册用户这一类的操作。

  • 是否缓存:

  • 因为两者应用场景不同,浏览器一般会对 Get 请求缓存,但很少对 Post 请求缓存。

  • 发送的报文格式:

  • Get 请求的报文中实体部分为空,Post 请求的报文中实体部分一般为向服务器发送的数据。

  • 安全性:

  • Get 请求可以将请求的参数放入 url 中向服务器发送,这样的做法相对于 Post 请求来说是不太安全的,因为请求的 url 会被保留在历史记录中。

  • 请求长度:

  • 浏览器由于对 url 长度的限制,所以会影响 get 请求发送数据时的长度。这个限制是浏览器规定的,并不是 RFC 规定的。

  • 参数类型:

  • post 的参数传递支持更多的数据类型。

44、HTTP 2.0 有哪些改进?5种

  • 二进制协议:

  • HTTP/2 是一个二进制协议。在 HTTP/1.1 版中,报文的头信息必须是文本(ASCII 编码),数据体可以是文本,也可以是二进制。HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧",可以分为头信息帧和数据帧。 帧的概念是它实现多路复用的基础。

  • 多路复用:

  • HTTP/2 实现了多路复用,HTTP/2 仍然复用 TCP 连接,但是在一个连接里,客户端和服务器都可以同时发送多个请求或回应,而且不用按照顺序一一发送,这样就避免了"队头堵塞"【1】的问题。

  • 数据流:

  • HTTP/2 使用了数据流的概念,因为 HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的请求。因此,必须要对数据包做标记,指出它属于哪个请求。HTTP/2 将每个请求或回应的所有数据包,称为一个数据流。每个数据流都有一个独一无二的编号。数据包发送时,都必须标记数据流 ID ,用来区分它属于哪个数据流。

  • 头信息压缩:

  • HTTP/2 实现了头信息压缩,由于 HTTP 1.1 协议不带状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent ,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。HTTP/2 对这一点做了优化,引入了头信息压缩机制。一方面,头信息使用 gzip 或 compress 压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就能提高速度了。

  • 服务器推送:

  • HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送。使用服务器推送提前给客户端推送必要的资源,这样就可以相对减少一些延迟时间。这里需要注意的是 http2 下服务器主动推送的是静态资源,和 WebSocket 以及使用 SSE 等方式向客户端发送即时数据的推送是不同的。

【1】队头堵塞:

队头阻塞是由 HTTP 基本的“请求 - 应答”模型所导致的。HTTP 规定报文必须是“一发一收”,这就形成了一个先进先出的“串行”队列。队列里的请求是没有优先级的,只有入队的先后顺序,排在最前面的请求会被最优先处理。如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待,结果就是其他的请求承担了不应有的时间成本,造成了队头堵塞的现象。

45、XSS和CSRF是什么?如何防御?

1. 什么是 XSS 攻击?

(1)概念

XSS 攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。

XSS 的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。

攻击者可以通过这种攻击方式可以进行以下操作:

  • 获取页面的数据,如DOM、cookie、localStorage;
  • DOS攻击,发送合理请求,占用服务器资源,从而使用户无法访问服务器;
  • 破坏页面结构;
  • 流量劫持(将链接指向某网站);

(2)攻击类型

XSS 可以分为存储型、反射型和 DOM 型:

  • 存储型指的是恶意脚本会存储在目标服务器上,当浏览器请求数据时,脚本从服务器传回并执行。
  • 反射型指的是攻击者诱导用户访问一个带有恶意代码的 URL 后,服务器端接收数据后处理,然后把带有恶意代码的数据发送到浏览器端,浏览器端解析这段带有 XSS 代码的数据后当做脚本执行,最终完成 XSS 攻击。
  • DOM 型指的通过修改页面的 DOM 节点形成的 XSS。

1)存储型XSS的攻击步骤:

  1. 攻击者将恶意代码提交到⽬标⽹站的数据库中。
  2. ⽤户打开⽬标⽹站时,⽹站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
  3. ⽤户浏览器接收到响应后解析执⾏,混在其中的恶意代码也被执⾏。
  4. 恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。

这种攻击常⻅于带有⽤户保存数据的⽹站功能,如论坛发帖、商品评论、⽤户私信等。

2)反射型XSS的攻击步骤:

  1. 攻击者构造出特殊的 URL,其中包含恶意代码。
  2. ⽤户打开带有恶意代码的 URL 时,⽹站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
  3. ⽤户浏览器接收到响应后解析执⾏,混在其中的恶意代码也被执⾏。
  4. 恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。

反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库⾥,反射型 XSS 的恶意代码存在 URL ⾥。

反射型 XSS 漏洞常⻅于通过 URL 传递参数的功能,如⽹站搜索、跳转等。 由于需要⽤户主动打开恶意的 URL 才能⽣效,攻击者往往会结合多种⼿段诱导⽤户点击。

3)DOMXSS****的攻击步骤:

  1. 攻击者构造出特殊的 URL,其中包含恶意代码。
  2. ⽤户打开带有恶意代码的 URL。
  3. ⽤户浏览器接收到响应后解析执⾏,前端 JavaScript 取出 URL 中的恶意代码并执⾏。
  4. 恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。

DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执⾏恶意代码由浏览器端完成,属于前端JavaScript ⾃身的安全漏洞,⽽其他两种 XSS 都属于服务端的安全漏洞。

2. 如何防御 XSS 攻击?

可以看到XSS危害如此之大, 那么在开发网站时就要做好防御措施,具体措施如下:

  • 可以从浏览器的执行来进行预防,一种是使用纯前端的方式,不用服务器端拼接后返回(不使用服务端渲染)。另一种是对需要插入到 HTML 中的代码做好充分的转义。对于 DOM 型的攻击,主要是前端脚本的不可靠而造成的,对于数据获取渲染和字符串拼接的时候应该对可能出现的恶意代码情况进行判断。
  • 使用 CSP ,CSP 的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行,从而防止恶意代码的注入攻击。
  1. CSP 指的是内容安全策略,它的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现。
  2. 通常有两种方式来开启 CSP,一种是设置 HTTP 首部中的 Content-Security-Policy,一种是设置 meta 标签的方式 <meta http-equiv="Content-Security-Policy">
  • 对一些敏感信息进行保护,比如 cookie 使用 http-only,使得脚本无法获取。也可以使用验证码,避免脚本伪装成用户执行一些操作。

3. 什么是 CSRF 攻击?

(1)概念

CSRF 攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。

CSRF 攻击的本质是利用 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。

(2)攻击类型

常见的 CSRF 攻击有三种:

  • GET 类型的 CSRF 攻击,比如在网站中的一个 img 标签里构建一个请求,当用户打开这个网站的时候就会自动发起提交。
  • POST 类型的 CSRF 攻击,比如构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单。
  • 链接类型的 CSRF 攻击,比如在 a 标签的 href 属性里构建一个请求,然后诱导用户去点击。

4. 如何防御 CSRF 攻击?

CSRF 攻击可以使用以下方法来防护:

  • 进行同源检测,服务器根据 http 请求头中 origin 或者 referer 信息来判断请求是否为允许访问的站点,从而对请求进行过滤。当 origin 或者 referer 信息都不存在的时候,直接阻止请求。这种方式的缺点是有些情况下 referer 可以被伪造,同时还会把搜索引擎的链接也给屏蔽了。所以一般网站会允许搜索引擎的页面请求,但是相应的页面请求这种请求方式也可能被攻击者给利用。(Referer 字段会告诉服务器该网页是从哪个页面链接过来的)
  • 使用 CSRF Token 进行验证,服务器向用户返回一个随机数 Token ,当网站再次发起请求时,在请求参数中加入服务器端返回的 token ,然后服务器对这个 token 进行验证。这种方法解决了使用 cookie 单一验证方式时,可能会被冒用的问题,但是这种方法存在一个缺点就是,我们需要给网站中的所有请求都添加上这个 token,操作比较繁琐。还有一个问题是一般不会只有一台网站服务器,如果请求经过负载平衡转移到了其他的服务器,但是这个服务器的 session 中没有保留这个 token 的话,就没有办法验证了。这种情况可以通过改变 token 的构建方式来解决。
  • 对Cookie 进行双重验证,服务器在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串,然后当用户再次向服务器发送请求的时候,从 cookie 中取出这个字符串,添加到 URL 参数中,然后服务器通过对 cookie 中的数据和参数中的数据进行比较,来进行验证。使用这种方式是利用了攻击者只能利用 cookie,但是不能访问获取 cookie 的特点。并且这种方法比 CSRF Token 的方法更加方便,并且不涉及到分布式访问的问题。这种方法的缺点是如果网站存在 XSS 漏洞的,那么这种方式会失效。同时这种方式不能做到子域名的隔离。
  • 在设置 cookie 属性的时候设置 Samesite ,限制 cookie 不能作为被第三方使用,从而可以避免被攻击者利用。Samesite 一共有两种模式,一种是严格模式,在严格模式下 cookie 在任何情况下都不可能作为第三方 Cookie 使用,在宽松模式下,cookie 可以被请求是 GET 请求,且会发生页面跳转的请求所使用。

46、回流和重绘是什么,有什么区别?

dom结构发发生改变

重绘也就是样式发生改变

操作DOM时,尽量在低层级的DOM节点进行操作

  • 不要使用table布局, 一个小的改动可能会使整个table进行重新布局
  • 使用CSS的表达式
  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。
  • 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
  • 避免频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中
  • 将元素先设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制。

浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列

浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。