2021大家都在写年末终结的时候,我在找工作~
3年+web前端开发,离职原因公司欠薪。
一个月的求职经历,工作总算落定下来,把面试过的题目梳理一下,记录一下!
愿所有的人生低谷,都能成为蓄势待发!!!
CSS
什么是重绘?什么是回流?通过哪些元素可以触发?如何做优化?
回流也可以叫重排:当DOM的变化影响了元素的几何信息(DOM对象的位置和尺寸大小), 浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做回流 触发:添加或者删除可见的DOM元素 元素尺寸改变——边距、填充、边框、宽度和高度
重绘:当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。
触发:改变元素的color、background、box-shadow等属性
优化:
1.减少dom的操作,减少dom的访问次数
2.基于vue数据影响视图模式,避免设置大量的style大量内联样式,最好使用class ‘
3.避免使用tabel布局,某一个触发回流就导致整个table触发回流
4.尽量少使用display:none,替换为visibility:hidden
元素水平垂直居中,多说几种?
1.绝对定位(已知居中元素宽高)
div {
height: 200px;
width: 400px;
position: absolute;
right: 0;
bottom: 0;
top: 0;
left: 0;
margin: auto;
}
2.使用transform属性(居中元素未知宽高)
div {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
3.绝对定位+负边距位移(已知居中元素宽高)
div {
height: 200px;
width: 400px;
position: absolute;
top: 50%;
left: 50%;
margin-top: -100px;
margin-left: -200px;
}
4.使用flex-box
div {
display: flex;
justify-content: center;
align-items: center;
}
5.网格布局
div {
display:grid;
place-item:center;
}
6.单行文字居中
div {
height: 100px;
line-height: 100px;
text-align: center;
}
BFC/IFC?
BFC 概念:决定了元素如何对其内容进行定位,以及其它元素和相互作用。BFC提供了一种环境 ,html在这个环境按照一定规则进行布局,里面的子元素不会在布局上影响外面的元素。
创建BFC:
1、float的值不是none。
2、position的值不是static或者relative。
3、display的值是inline-block、table-cell、flex、table-caption或者inline-flex
4、overflow的值不是visible
BFC作用?
1、防止外边距重叠。margin塌陷
2、清除浮动的影响块级子元素浮动,如果块级父元素没有设置高度,其会有高度塌陷的情况发生原因:子元素浮动后,均开启了BFC,父元素不会被子元素撑开。
外边距重叠
两个或多个块级盒子的垂直相邻边界会重合当两个外边距相遇,
他们会合成一个计算方法:
a、全部都为正值,取最大者;
b、不全是正值,则都取绝对值,然后用正值减去最大值;
c、没有正值,则都取绝对值,然后用0减去最大值。
IFC(inline Formatting Context)“行级格式化上下文”
(1)内部的盒子会在水平方向,一个个地放置;
(2)IFC的高度,由里面最高盒子的高度决定;
(3)当一行不够放置的时候会自动切换到下一行;
px、em、rem区别?
像素px是相对于显示器屏幕分辨率而言的
em相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸。
EM特点
em的值并不是固定的; em会继承父级元素的字体大小。
注意:任意浏览器的默认字体高都是16px。所有未经调整的浏览器都符合: 1em=16px。那么12px=0.75em,10px=0.625em。为了简化font-size的换算,需要在css中的body选择器中声明Font-size=62.5%,这就使em值变为 16px*62.5%=10px,
rem 区别在于使用rem为元素设定字体大小时,仍然是相对大小,但相对的只是HTML根元素
如何选 对于只需要适配少部分手机设备,且分辨率对页面影响不大的,使用px即可 。 对于需要适配各种移动设备,使用rem,例如只需要适配iPhone和iPad等分辨率差别比较挺大的设备。
rem原理:在做响应式布局的时候,通过调整HTML的字体大小,页面上所有使用rem单位的元素都会做相应的调整。
JS
js数据类型。区别?深拷贝的实现方式2种以上
- 基本数据类型
Number、String、Boolean、Null、 Undefined、Symbol(ES6) 、BigInt(ES10,用来表示表示大于 2^53 - 1) 特点:存放在栈中,数据大小确定,内存空间大小可以分配,它们是直接按值存放的,所以可以直接按值访问
- 引用数据类型
Object类 对象,数组,以及函数等 特点: 名存在栈内存中,值存在于堆内存中,但是栈内存会提供一个引用的地址指向堆内存中的值
深拷贝实现方式 1.递归递归去复制所有层级属性 2.JSON对象的parse和stringify 3.通过lodash函数库实现
说下作用域、作用域链?
作用域链
概念:指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限
1.全局作用域
在该作用域中的对象在代码的任何地方都能访问,其生命周期伴随着页面的生命周期。
2.函数作用域/局部作用域
变量在函数内声明,变量为局部作用域,只能在函数内部访问。 函数执行结束后,函数内部的变量会被销毁。
3.块级作用域
比如使用一对大括号包裹一段代码,比如函数、判断语句、循环语句, 甚至单独的一个{}都可以看作一个块级作用域
块级作用域可通过let和const声明,声明后的变量再指定块级作用域外无法被访问
作用域链:
当所需要的变量在所在的作用域中查找不到的时候, 它会一层一层向上查找,直到找到全局作用域还没有找到的时候, 就会放弃查找。这种一层一层的关系,就是作用域链。
事件轮询,执行顺序是怎样的?
首先我们要知道 由于js是单线程的脚本,异步事件要基于回调来实现的 而event loop 就是异步回调的实现原理
事件执行顺序:
1、所有任务都在主线程上执行,形成一个执行栈。
2、主线程发现有异步任务,如果是微任务就把他放到微任务的消息队列里, 如果是宏任务就把他放到宏任务的消息队列里。
3、执行栈所有同步任务执行完毕。
4、执行微任务队列,之后再执行宏任务队列。
5、轮询第4步
-
宏任务:宏任务的时间颗粒度比较大,执行时间是不能精确控制的,对一些高实施性的需求就不太符合 macrotask 包括:setTimeout/setInterval、postMessage、script、ajax、读取文件等。
-
任务:一个需要异步执行的函数,执行时机是在主函数结束之后,当前宏任务结束之前。 microtask 包括:Promise.then、await后面的代码、process.nextTick(Node.js)、
宏任务:宏任务是指消息队列中等待被主线程执行的事件。
微任务:微任务也是需要异步执行的事件。执行时间是在主函数执行结束之后,当前宏任务结束之前。
原型、原型链?
原型概念:对象中固有的proto属性,该属性指向对象的prototype原型属性。
原型链概念:当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的终点为null
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。 hasOwnProperty() 指示对象自身属性中是否具有指定的属性,该方法会忽略掉那些从原型链上继承到的属性。
说下继承 es5 es6?
1.原型链继承(继承属性)
存在的问题:
原型中包含的引用类型属性将被所有实例共享
子类在实例化的时候不能给父类构造函数传参
function Animal() {
this.colors = ['black', 'white']
}
Animal.prototype.getColor = function() {
return this.colors
}
function Dog() {}
Dog.prototype = new Animal()
let dog1 = new Dog()
dog1.colors.push('brown')
let dog2 = new Dog()
console.log(dog2.colors)// ['black', 'white', 'brown']
2.借用 构造函数 实现继承(使用call改变this指向)
存在的问题: 解决了原型链继承的 2 个问题,引用类型共享问题以及传参问题。但是由于方法必须定义在构造函数中,所以会导致每次创建子类实例都会创建一遍方法。
function Animal(name) {
this.name = name
this.getName = function() {
return this.name
}
}
function Dog(name) {
Animal.call(this, name)//核心 拷贝了父类的实例属性和方法 this指向dog
}
Dog.prototype = new Animal()
3.组合继承 组合继承,指的是将原型链和借用构造函数的技术组合到一起
基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性
缺点:new两次 call、new
function Animal(name) {
this.name = name
this.colors = ['black', 'white']
}
Animal.prototype.getName = function() {
return this.name
}
function Dog(name, age) {
Animal.call(this, name)
this.age = age
}
Dog.prototype = new Animal()//拿到getName方法
Dog.prototype.constructor = Dog
let dog1 = new Dog('奶昔', 2)
dog1.colors.push('brown')
let dog2 = new Dog('哈赤', 1)
console.log(dog2) // { name: "哈赤", colors: ["black", "white"], age: 1 }
4.寄生式继承
Object.create 将Animal原型方法继承在Dog.prototype上
function Animal(name) {
this.name = name
this.colors = ['black', 'white']
}
Animal.prototype.getName = function() {
return this.name
}
function Dog(name, age) {
Animal.call(this, name)
this.age = age
}
//Dog.prototype = new Animal()//拿到getName方法
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog
let dog1 = new Dog('奶昔', 2)
dog1.colors.push('brown')
let dog2 = new Dog('哈赤', 1)
console.log(dog2) // { name: "哈赤", colors: ["black", "white"], age: 1 }
5.ES6中 Class ... extends 关键字实现继承
class Animal {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Dog extends Animal {
constructor(name, age) {
super(name)
this.age = age
}
}
防抖和节流你是怎么理解的?说下思路?
防抖----触发高频事件后 n 秒后函数只会执行一次,如果 n 秒内高频事件再 次被触发,则重新计算时间
函数防抖的要点,是需要一个 setTimeout 来辅助实现,延迟运行需要执行的代码。 如果方法多次触发,则把上次记录的延迟执行代码用 clearTimeout 清掉,重新开始计时。 若计时期间事件没有被重新触发,等延迟时间计时完毕,则执行目标代码。
function debounce(fn,wait){
var timer = null;
return function(){
if(timer !== null){
clearTimeout(timer);
}
timer = setTimeout(fn,wait);
}
}
function handle(){
console.log(Math.random());
}
window.addEventListener("resize",debounce(handle,1000));
节流----连续发生的事 几秒执行一次
原理: 设置canRun作为是否执行的标志。每次触发onresize,都判断canRun的值(true执行,false不执行)。 第一次应该执行,所以设置canRun初始值为true。当第一次执行后,设置canRun为false(防止下次执行),并设置计时器,以恢复canRun的值。
var canRun = true;
window.onresize = function() {
if (!canRun){
return;
}
canRun = false;
setTimeout(function() {
console.log('节流');
canRun = true;
}, 1000);
}
说下模块化?
AMD
1、异步加载
2、管理模块之间的依赖性,便于代码的编写和维护。
环境:浏览器环境 应用:requireJS是参照AMD规范实现的
// 语法:
1、导入:require(['模块名称'], function ('模块变量引用'){// 代码});
2、导出:define(function (){return '值');
//a.js
define(function(){
return {
a:'hello world'
}
});
// b.jsrequire(['./a.js'],
function (moduleA){
console.log(moduleA.a); // 打印出:hello world
});
CMD
// 1、CMD是在AMD基础上改进的一种规范,和AMD不同在于对依赖模块的执行时机处理不同,CMD是就近依赖,而AMD是前置依赖。
// 环境:浏览器环境
// 应用:seajs是参照UMD规范实现的,requireJS的最新的几个版本也是部分参照了UMD规范的实现
// 语法:
// 1、导入:define(function(require, exports, module) {});
// 2、导出:define(function (){return '值');
// a.js
define(function (require, exports, module){
exports.a = 'hello world';
});
// b.js
define(function (require, exports, module){
var moduleA = require('./a.js');
console.log(moduleA.a); // 打印出:hello world
});
CommonJS
用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。
//commonJS.js
let n = 'commonjs'
function add(x, y) {
return x + y
}
function desc(str) {
return str
}
module.exports = {
n,
add,
desc
}
//
const { n, add, desc } = require('./commonJS.js')
console.log(desc(33333))
console.log(add(10,3),n)
ES Module(es6)
import export 用法略
数组相关方法?
1.改变数组的方法
pop()删除最后一个
push()追加末尾
shift()删除第一个
unshift()添加到第一个
sort()
splice(index,howmany,要添加的元素)
reverse()
copyWithin() es6语法
fill()
join()
2.不改变原数组的方法
contact()
join()
slice()
map()
every()
some()
filter()
reduce()
redureRight()
3.遍历相关的方法:
forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数。数组中的每个值都会调用回调函数 array.forEach(function(当前元素, 索引, 元素所属数组对象), thisValue) 注:不会改变原数组 没有返回值 forEach无法使用 break,continue 跳出循环,使用 return 时,效果和在 for 循环中使用 continue 一致;
map():通过指定函数处理数组的每个元素,并返回处理后的数组 (不会改变原数组/ ,返回新数组)
some():用于检测数组中的元素是否满足指定条件,有一个满足的返回true,惰性查找,找到之后不在执行 (不会改变原数组)
every(): 用于检测是所有的元素都满足条件返回true (不会改变原数组/,和some正好相反)
filter(): 创建一个新数组,新数组里面的元素都是符合条件的元素 (不会改变原数组 )
for...of 创建一个循环来迭代可迭代的对象 for (variable of iterable) 注:只会遍历当前对象的属性,不会遍历其原型链上的属性,for of 方法不支持遍历普通对象 可以使用break、continue、return来中断循环遍历 for in
Array.find() 返回数组中符合条件的第一个元素,若数组中没有元素符合要求则返回undefined,不改变原数组
Array.findIndex() 返回数组中符合条件的第一个元素的下标,若数组中没有元素符合要求则返回-1,不改变原数组
Array.includes() 判断数组是否含有某一个元素,有则返回true,没有则返回false,参数:第一个参数为查询的元素,第二个参数为查询的下标(可不填,负数为倒着数)
Array.indexOf() 查询某一元素的下标,若数组中没有该元素则返回-1,参数:第一个参数为查询元素,第二个参数为开始查询位置(选填)
Array.isArray() 判断对象是否为数组,是返回true,不是返回false
Array.valueOf() 返回数组对象的原始值 let arr = [1,2,3] console.log(arr.valueOf()) //[1,2,3]
Array.entries()、Array.keys()、Array.values() 遍历数组,它们都返回一个遍历器对象,entries是对键值对遍历,values是对键值遍历,keys是对键名遍历
Array.flat(num)数据扁平化 ,多维数组转换为一维数组,对原数组没有影响。num为转换的层数,若不确定可以写Infinity,会跳过空位
Array.flatMap() 相当于先对一个数组执行map方法,然后对返回结果再执行flat方法返回新数组,不改变原数组
Array.fill()
Array.of()用于构造数组 let arr = Array.of(1,2,3) console.log(arr) //[1,2,3]
Array.from() console.log(Array.from(“abcdef”)) //[‘a’,‘b’,‘c’,‘d’,‘e’,‘f’]
reduce() 第一个参数callback函数: pre为上次return的值,next为数组的本次遍历的项 第二个参数为初始值,也是第一个pre
数组去重怎么做?
1.//利用filter 去重
ar arr = [1, 2, 2, 3, 4, 5, 5, 6, 7, 7,6,3,4,56,2,{},{}];
var arr2 = arr.filter((x, index,self)=>self.indexOf(x)===index)
2.//去重复2
let res = [...new Set(arr)] //...扩展运算符
Array.from(new Set(arr))
3.reduce+includes
3.let res1 = arr.reduce((pre, cur) => {
return pre.includes(cur) ? pre : [...pre, cur]
}, [])
4.最简单方法(indexOf 方法)
实现思路:新建一个数组,遍历要去重的数组,当值不在新数组的时候(indexOf 为 -1)就加入该新数组中;
var newArr = [];
arr.forEach(function(v){
if(newArr.indexOf(v) === -1){
// 将这个数据,写入到新数组中
newArr.push(v)
}
})
console.log( newArr );
5.利用双重for循环去重
var arr = ['h', 'e', 'l', 'l','l', 'o', '2', 1, 2, 1,1, 3, 2];
function noRepeat(arr) {
for (var i = 0; i < arr.length; i++) {
for (var j = i + 1; j < arr.length; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1); //因为从arr中删除了一个元素所以跳过了第i个元素后一个元素 所以要进行j-- 将下标减一
j--;
}
}
}
return arr;
}
var result = noRepeat(arr);
console.log(result); //[ "h", "e", "l", "o", "2", 1, 2, 3 ]
数据类型判断
基本类型 可以通过typeof ,引用类型、 null、Date通过typeof判断会不准确
instanceof 左操作数是一个类,右操作数是标识对象的类。
如果左侧的对象是右侧类的实例,则返回true (内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。)
constructor 判断对象的构造函数是谁
坑,如果我创建一个对象,更改它的原型,constructor就会变得不可靠了
function Fn(){};
Fn.prototype=new Array();
var f=new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
Object.prototype.toString.call() 使用 Object 对象的原型方法 toString
var a = Object.prototype.toString;
console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));
什么是闭包?
概念:闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用链域,将函数内部的变量和方法传递到外部
闭包的特性:
1.函数内再嵌套函数
2.内部函数可以引用外层的参数和变量
3.参数和变量不会被垃圾回收机制回收
function aaa() {
var a = 0;
return function () {
alert(a++);
};
}
var fun = aaa();
fun(); //1
使用场景:
- 函数节流防抖
- 闭包作用回调函数
- 封装对象的私有属性和方法
- 类和继承 结果需要缓存的场景
缺点:
- 使函数内部变量存在内存中,内存消耗大
- 滥用闭包可能会导致内存泄漏
- 闭包可以在父函数外部改变父函数内部的值,慎操作
介绍下this指向?
this对象是是执行上下文中的一个属性,它指向最后一次调用这个方法的对象,在全局函数中,this等于window,而当函数被作为某个对象调用时,this等于那个对象。 在实际开发中,this 的指向可以通过四种调用模式来判断。
- 函数调用,当一个函数不是一个对象的属性时,直接作为函数来调用时,this指向全局对象。
- 方法调用,如果一个函数作为一个对象的方法来调用时,this指向这个对象。
- 构造函数调用,this指向这个用new新创建的对象。
- 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。apply接收参数的是数组,call接受参数列表,
` bind方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this指向除了使用new时会被改变,其他情况下都不会改变。
new关键字的时候发生了什么?
首先创建了一个新的空对象
设置原型,将对象的原型设置为函数的prototype对象。
让函数的this指向这个对象,执行构造函数的代码(为这个新对象添加属性)
判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
手写new 略
声明函数作用提升?声明变量和声明函数的提升有什么区别?
变量声明提升:变量在进入执行上下文就已完成。只要变量在代码中进行了声明,无论在哪个位置声明,js引擎都会把它的声明放在范围作用域最顶部。
函数的两种创建方式
- 函数声明
- 函数表达式
函数声明:提升会在编译阶段把函数和函数体整体都提前到执行环境顶部,所以我们可以在函数声明之前调用这个函数
sum(10, 10); //20
function sum(num1, num2) {
return num1 + num2;
};
预编译阶段把代码编译后其实是这样的
var sum;
sum = function (num1, num2) {
return num1 + num2;
}
sum(10, 10); //20
变量or函数声明 :函数声明会覆盖变量声明,但不会覆盖变量赋值。
同一个名称标识a,即有变量声明var a,又有函数声明function a(){},不管二者声明顺序,函数声明会覆盖变量声明,也就是说,此时a的值是声明的函数function a() {} 注意:如果在变量声明的同时初始化a,或是之后对a进行赋值,此时a的值变量的值
var a = 1;
function b() {
a = 10;
return;
function a() {
}
}
b();
console.log(a);
//编译后:
var a = 1;
function b() {
var a;
a = function () {
};
a = 10;
return;
}
b();
console.log(a); //1
es6相关
es6新增语法
const/let
变量的解构赋值(包含数组、对象、字符串、数字及布尔值,函数参数),
剩余运算符(...rest);
模板字符串(${data});
扩展运算符(数组、对象);
箭头函数;
函数默认参数Set和Map数据结构;
Proxy/Reflect;
Promise;
async函数;
Class;Module语法(import/export)。
新增symbol类型 表示独一无二的值,用来定义独一无二的对象属性名;
promise相关?
(1)Promise
Promise 状态:
pending: 初始状态,既不是成功,也不是失败状态。
fulfilled: 意味着操作成功完成。
rejected: 意味着操作失败。
new Promise实例,而且要return new Promise要传入函数,函数有resolve reject两个参数,成功时执行resolve 失败的时候执行reject, .then监听结果.
手写promise: 略
(2)Promise.all()
多个 Promise 任务同时执行。如果全部成功执行,则以数组的方式返回所有 Promise 任务的执行结果。 如果有一个 Promise 任务 rejected,则只返回 rejected 任务的结果
(3)Promise.race()
多个 Promise 任务同时执行,返回最先执行结束的 Promise 任务的结果,不管这个 Promise 结果是成功还是失败
(4)Promise.allSettled()
Promise.allSettled() ES11新增 可以收集所有结果 不论成功或者失败
(5) Promise.any()
只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态,如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态,跟Promise.race()方法很像,只有一点不同,就是不会因为某个 Promise 变成rejected状态而结束
说下async/await?
特点:
-
async函数返回的是 Promise 对象
-
await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)
-
await必须写在async函数中
-
async函数内部return语句返回的值,会成为then方法回调函数的参数
任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行怎么能不中断呢?
方法一:把第一个await放在try...catch结构里面
方法二:await后面的 Promise 对象再跟一个catch方法,这样可以处理前面可能出现的错误
let var const区别?
var 支持变量提升(可以通过window.变量名的方式访问)
let 不支持变量提升、不能重复声明、有块级作用域、有暂存性死区
const 常量 不支持重复声明、不能修改、有块级作用域
set和map有什么区别?
Map对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。构造函数Map可以接受一个数组作为参数。
Set对象允许你存储任何类型的值,无论是原始值或者是对象引用。它类似于数组,但是成员的值都是唯一的,没有重复的值。
vue2相关
你了解的 MVVM、MVC?
MVVM
model数据模型 view 视图层 viewmodel连接model和view,数据会绑定到viewmodel上并自动将数据渲染到页面上,视图变化会通知更新viewmodel。view通过事件绑定影响model,model通过数据绑定影响view
MVC
m-model数据层,v-view视图层,c-controller业务逻辑
- view传送指令到controller
- controller完成业务逻辑后,要求model改变状态
- model将新的数据发送到view,用户得到反馈
响应式数据原理:
vue2:
对象:数据劫持结合发布-订阅者模式,通过Object.definePropery来劫持各个属性的setter和getter, 在数据变动的时候发布消息给订阅者,触发相应的监听回掉。
数组:为了性能缘故,数组使用数组增强的方式,覆盖原数组默认的方法,保证数组在新增或者删除数据时, 通过dep通知所有订阅者(watcher)更新。( 数组的索引和长度是无法监控的)
- 监听器 Observer ,用来劫持并监听所有属性(转变成setter/getter形式),如果属性发生变化,就通知订阅者
- 订阅器 Dep,用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理
- 订阅者 Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图
- 解析器 Compile,可以解析每个节点的相关指令,模版中变量换成数据等。对模板数据和订阅器进行初始化
- MVVM作为数据绑定的入口,和observe、compile和watcher三者,通过observer监听数据变化, 通过complie来解析模版指令,最终利用Watcher搭起Observer和compile通信桥梁,达到数据变化->视图更新,视图交互变化->model变更双向绑定的效果。
vue3.0 使用的proxy对对象代理,从而实现数据劫持。es6语法。兼容性差
vue生命周期
- beforeCreate 组件实例被创建之初,组件的属性生效之前
- created 组件实例已经完成创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用
- beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用
- mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
- beforeUpdate 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
- update 组件数据更新之后
- activited keep-alive 专属,组件被激活时调用
- deactivated keep-alive 专属,组件被销毁时调用
- beforeDestory 组件销毁前调用
- destoryed 组件销毁后调用
组件通讯的方式?多说几种?
第一种:props $emit
父传子:通过 自定义参数 可以把父组件的消息传递给子组件
parent.vue <child :title="title"></child>
子组件通过props接受:// child.vue
props: {
title: {
type: String,
default: '',
}
}
子传父:$emit触发事件
this.$emit('change', '123')
父组件接受:$on监听
<child @change="changedata"></child>
第二种:EventBus
可以实现在任意2个组件之间通信。只用于简单、少量业务的项目。
1.通过导出一个 Vue 实例,然后再需要的地方引入
// main.js
Vue.prototype.$eventBus = new Vue()
2.使用 EventBus 订阅和发布消息
// 订阅的地方
this.$eventBus.$on('update', val => {})
// 发布信息的地方this.$eventBus.$emit('update', '参数')
3.移除事件监听this.$eventBus.$off('update', {})
第三种:provide/inject
provide 和 inject 主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。 provide/inject 组合以允许一个祖先组件向其所有子孙后代注入一个依赖,可以注入属性和方法
// 父组件 index.vue
data() {
return {
title: '123',
}
}
provide() {
return {
detail: {
title: this.title,
change: (val) => {
console.log( val )
}
}
}
}
// 子组件 detail.vue
inject: ['detail'],
mounted() {
console.log(this.detail.title) // 123
this.detail.title = 'hello world' // 虽然值被改变了,但是父组件中 title 并不会重新渲染 this.detail.change('改变后的值') // 执行这句后将打印:改变后的值
}
注:provide 和 inject 的绑定对于原始类型来说并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。这也就是为什么在孙子组件中改变了 title,但是父组件不会重新渲染的原因。
第四种:Vuex进行全局的数据管理
Vuex是通过全局注入store对象,来实现组件间的状态共享。一般用在大型复杂的项目中(多级组件嵌套),需要实现一个组件更改某个数据,多个组件自动获取更改后的数据进行业务逻辑处理。
“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )
(1)Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化, 那么相应的组件也会相应地得到高效更新。
(2)改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化
State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中
第五种: Vue.observable
Vue2.6 中新增的 API,用来让一个对象可以响应。我们可以利用这个特点来实现一个小型的状态管理器
//store.js
import Vue from 'vue'
export const state = Vue.observable({
count: 0,
})
export const mutations = {
increment() {
state.count++
}
decrement() {
state.count--
}}
// parent.vue
<template>
<p>{{ count }}</p>
</template>
<script>
import { state } from '../store'
export default {
computed: {
count() {
return state.count
}
}
}
</script>
// child.vue
import { mutations } from '../store'
export default {
methods: {
handleClick() {
mutations.increment()
}
}
}
第六种:$refs 来直接操作子组件的方法和属性.
this.$refs.list.getList()
第七种: $parent/$children
说下 v-for/v-if优先级?
对于v-for/v-if的优先级也是有调整的, 在Vue2中v-for优先级高于v-if,在Vue3的更新之后v-if的优先级是高于v-for的。
可以查看源码的处理逻辑。
说下key的作用、原理?
key 的作用主要是 为了实现高效的更新虚拟 DOM,提高性能 其原理是vue在patch的过程中通过key可以精准的判断两个节点是否是同一个,从而避免频繁的更新元素,使得整个patch过程更加高效,减少DOM操作量,提高性能。
在pathVnode前会进行sameVnode(oldVnode, newVnode)
v-for时为什么不建议用index作为key?
如果你的列表顺序会改变,别用 index 作为 key,和没写基本上没区别,因为不管你数组的顺序怎么颠倒,index 都是 0, 1, 2 这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。
用组件唯一的 id(一般由后端返回)作为它的 key,实在没有的情况下,可以在获取到列表的时候通过某种规则为它们创建一个 key,并保证这个 key 在组件整个生命周期中都保持稳定。
千万别用随机数作为 key,不然旧节点会被全部删掉,新节点重新创建千万别用随机数作为 key,不然旧节点会被全部删掉,新节点重新创建
keep-alive 的了解?
keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染,一般结合路由和动态组件一起使用,用于缓存组件
- 有include 和 exclude 属性 ,include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
- 当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated
v-model 的原理?
v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素,使用不同的属性并抛出不同的事件 。
以 input 表单元素为例:
<input v-model='something'>相当于<input v-bind:value="something" v-on:input="something = $event.target.value">
路由模式 hash、history、abstract?
- hash:
实现原理 基于 location.hash 来实现的 location.hash 的值就是 URL 中 # 后面的内容
特点: 1.hash 值的改变,都会在浏览器的访问历史中增加一个记录,因此我们能通过浏览器的回退、前进按钮控制hash 的切换
2.可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值
3.我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转
- history:
实现原理: HTML5 提供了 History API 来实现 URL 的变化
API 有以下两个:history.pushState() 和 history.repalceState()。 这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。 唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录
window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);特点: pushState 和 repalceState 两个 API 来操作实现 URL 的变化 可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转 history.pushState() 或history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转
- abstract
支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.
query和params区别?
query语法:
this.route.query.id; 这是接受参数
params语法:
this.route.params.id; 这是接受参数
query 来接收, 一个用 params 接收 ,总结就是谁发得 谁去接收 query 在刷新页面得时候参数不会消失,而 params 刷新页面得时候会参数消失,可以考虑本地存储解决 query 传得参数都是显示在url 地址栏当中,而 params 传参不会显示在地址栏
说下路由守卫?
全局守卫、单个路由守卫、组件内部守卫
- 全局守卫
全局前置守卫beforEach:
router.beforeEach((to, from, next) => {
if (to.meta.permission) {
if (sessionStorage.getItem("token")) {
next();
} else {
alert("请先登录");
next("/login");
}
} else {
next();
}
});
全局后置钩子afterEach(少用)
router.afterEach((to, from) => {
// to and from are both route objects.
});
- 单个路由守卫beforeEnter
// 首页模块路由
{
path: "/index",
name: "index",
meta: { permission: true },
component: () => import("../views/Index.vue"),
beforeEnter: function(to, from, next) {
if (sessionStorage.getItem("token")) {
next();
} else {
alert("请先登录");
next("/login");
}
}
},
- 组件内部守卫beforeRouteEnter
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
if (sessionStorage.getItem("token")) {
next();
} else {
alert("请先登录");
next("/login");
}
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 可以访问组件实例 `this`
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
},
beforeRouteEnter 是进入前
beforeRouteUpdate 是路由变化时
beforeRouteLeave 是离开后。这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。
注意:beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。
守卫参数: to: 要进入的目标路由(去哪儿) from: 要离开的路由(从哪来) next: 是否进行下一步(要不要继续)
注: 写next()相当于 next(true) 继续执行 next(false)终止执行 next(path)跳转 例如:next("/login")
后置钩子afterEach没有next参数
computed与watch的区别?
- computed 计算属性
属性的结果会被缓存,当computed中的函数所依赖的属性没有发生改变的时候, 那么调用当前函数的时候结果会从缓存中读取- watch 属性监听 是一个对象,键是需要观察的属性,值是对应回调函数,主要用来监听某些特定数据的变化, 从而进行某些具体的业务逻辑操作,监听属性的变化
watch 支持异步 computed不支持
使用场景:
computed:当一个属性受多个属性影响的时候使用
watch:当一条数据影响多条数据的时候使用
怎么理解Vue中的diff算法?
在js中,渲染真实DOM的开销是非常大的, 比如我们修改了某个数据,如果直接渲染到真实DOM,会引起整个dom树的重绘和重排。那么有没有可能实现只更新我们修改的那一小块dom而不要更新整个dom呢? 此时我们就需要先根据真实dom生成虚拟dom, 当虚拟dom某个节点的数据改变后会生成有一个新的Vnode, 然后新的Vnode和旧的Vnode作比较,发现有不一样的地方就直接修改在真实DOM上,然后使旧的Vnode的值为新的Vnode。
diff的过程就是调用patch函数,比较新旧节点,一边比较一边给真实的DOM打补丁。在采取diff算法比较新旧节点的时候,比较只会在同层级进行。
在patch方法中,首先进行树级别的比较 new Vnode不存在就删除 old Vnode old Vnode 不存在就增加新的Vnode
都存在就执行diff更新 当确定需要执行diff算法时,比较两个Vnode,包括三种类型操作:属性更新,文本更新,子节点更新 新老节点均有子节点,则对子节点进行diff操作,调用updatechidren 如果老节点没有子节点而新节点有子节点,先清空老节点的文本内容,然后为其新增子节点 如果新节点没有子节点,而老节点有子节点的时候,则移除该节点的所有子节点 老新老节点都没有子节点的时候,进行文本的替换 updateChildren 将Vnode的子节点Vch和oldVnode的子节点oldCh提取出来。
oldCh和vCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。 如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠, 一旦StartIdx>EndIdx表明oldCh和vCh至少有一个已经遍历完了,就会结束比较。
什么情况下使用nextTick?
如果用户想在数据变化后操作到最新的DOM,那么在同步代码中是获取不到最新DOM的,因为DOM还没重新渲染,为什么还没重新渲染,待会会详细说明。
nextTick,将操作最新DOM的代码写在回调中,回调会延迟到下次DOM更新循环后执行,这样操作的肯定是更新后的DOM
然后$nextTick是对nextTick的封装,nextTick函数不单止用在这里,还用在nextTick(flushQueueWatcher)
这是什么,是将更新DOM的回调推入callbacks数组中,nextTick(flushQueueWatcher)是在queueWatcher时调用的,修改数据时,update函数被触发,就会queueWatcher,然后你接着$nextTick(fn),这样 callbacks数组中,更新DOM的回调就在fn之前,就会先被执行。
当同步代码都执行完了,会去检查微任务队列中是否有事件存在,如果存在,则会依次执行微任务队列中事件对应的回调,直到微任务队列清空,然后去宏任务队列中取出一个事件,把对应的回调加入当前执行栈,当执行栈中所有任务执行完毕后,再次检查微任务队列中是否有事件存在,无限重复这个过程,叫浏览器的事件循环。
两个不同的宏任务之间穿插着UI的重渲染,那么我们只需在微任务中把所有需要更新的数据更新,即执行 flushCallbacks ,情况Callbacks数组中的回调,开头是flushQueueWatcher,作用是执行并清空queue中的watcher的run方法,对表达式或渲染函数重新求值,生成最新的DOM,然后才是用户通过$nextTick注册的回调,这些回调就可以操作到最新的DOM。微任务结束后,执行宏任务,即浏览器将最新的DOM渲染到页面,所以只需要一次UI重渲染就能得到最新的DOM。
vue中的性能优化?
- 路由懒加载
- v-show 复用dom
- v-for遍历的时候避免使用v-if
- 长列表的性能优化(纯粹为了数据展示,不需要响应式可以
- 使用Object.freeze冻结数据 。如 this.users = Object.freeze(users)
- 事件销毁事件销毁,组件销毁时会自动解绑他的全部指令和监听器,仅限于组件本身的事件。
- 自己定义的事件及定时器需要自行销毁
- 子组件分割 重业务写在自组件里
- 函数式组件
- 临时变量 针对computed
- 仅仅渲染可视区 虚拟列表
- 异步组件、动态组件
- 延迟装载 defer
- keep-alive
- ssr
- 图片资源懒加载
- 第三方插件的按需引入
- computed 和 watch 区分使用场景
- SEO优化
- 打包优化
- 骨架屏
- key保证唯一
- 开启gzip
- productionsourceMap 设为false 不然打出来的包比较大
为什么要使用异步组件?
节省打包出的结果,异步组件分开打包,采用jsonp的方式进行加载,有效解决文件过大的问题。 核心就是包组件定义变成一个函数,依赖 import() 语法,可以实现文件的分割加载。
Vue插槽?
插槽:组件内部留一个或多个的插槽位置,可供组件传对应的模板代码进去。插槽的出现,让组件变的更加灵活。
- 默认插槽:就是指没有名字的插槽,子组件未定义的名字的插槽,父级将会把未指定插槽的填充的内容填充到默认插槽中,默认插槽只能只有一个。
- 具名插槽: 定义插槽名字,指定插入的位置的内容
- 作用域插槽 :作用域插槽其实就是带数据的插槽,即带参数的插槽,简单的来说就是子组件提供给父组件的参数,该参数仅限于插槽中使用,父组件可根据子组件传过来的插槽数据来进行不同的方式展现和填充插槽内容。
小程序相关
如何通过用户授权获取手机号?
wx.login-->获取登录凭证(code 有效期)--->后端用code换session_key和openid -->调用getPhoneNumer-->将encrypted和iv传给后端-->后端进行解密得到json (含手机号)
说下小程序登录流程?
wx.login-->获取登录凭证(code)--->后端用code换session_key和openid-->自定义登录状态关联openid、session_key-->根据openid查询登录状态-->自定义登录状态存入storage-->wx.request()发请求携带自定义的登录状态
说下小程序中的路由?
wx.navigateTo(url:'')打开新页面
wx.redirectTo()页面重定向 关闭当前页面,跳转到应用内的某个页面
wx.navigateBack()页面返回 关闭当前页面,返回上一页面或多级页面
wx.switchTab()Tab 切换
wx.reLaunch() 关闭所有页面,打开到应用内的某个页面
小程序生命周期?
onLoad()页面加载时触发 只会调用一次
onShow() 页面显示/切入前台时触发
onReady() 页面初次渲染完成时触
onHide() 页面隐藏/切入后台时触发
onUnload() 页面卸载时触发
小程序传参?
- 全局变量实现数据传递
- 使用data-item 通过e.currentTarget.dataset.item获取
- 使用路由加参数的方式
小程序的双向绑定和vue的异同?
大体相同,但小程序直接this.data的属性是不可以同步到视图的,必须调用this.setData()方法!
H5
h5页面在微信环境分享,显示图片的问题?
1 .微信公众平台配置 公众号设置—>功能设置—-> 填写js接口安全域名(不能配置端口号) 下载校验文件放到域名目录 2.引入JS文件 需要借助微信js-sdk 显示图标 3.通过config接口注入权限验证配置
wx.config({ debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数, 可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: '', // 必填,公众号的唯一标识 timestamp: , // 必填,生成签名的时间戳 nonceStr: '', // 必填,生成签名的随机串 signature: '',// 必填,签名 jsApiList: [] // 必填,需要使用的JS接口列表 });
仅支持线上调试
Webpack/前端工程化
webpack与grunt、gulp的不同?
三者都是前端构建工具,grunt和gulp在早期比较流行,现在webpack相对来说比较主流,不过一些轻量化的任务还是会用gulp来处理,比如单独打包CSS文件等。 grunt和gulp是基于任务和流(Task、Stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程。
webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能。
所以,从构建思路来说,gulp和grunt需要开发者将整个前端构建过程拆分成多个
Task,并合理控制所有Task的调用关系;webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工 对于知识背景来说,gulp更像后端开发者的思路,需要对于整个流程了如指掌 webpack更倾向于前端开发者的思路
什么是bundle,什么是chunk,什么是module?
bundle:是由webpack打包出来的文件
chunk:代码块,一个chunk由多个模块组合而成,用于代码的合并和分割
module:是开发中的单个模块,在webpack的世界,一切皆模块,一个模块对应一个文件, webpack会从配置的entry中递归开始找出所有依赖的模块
Loader和Plugin的不同?
不同的作用:
Loader直译为"加载器"。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader。 所以Loader的作用是让webpack拥有了加载和解析非JavaScript文件的能力。 Plugin直译为"插件"。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
不同的用法:
Loader在module.rules中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options) Plugin在plugins中单独配置。 类型为数组,每一项是一个plugin的实例,参数都通过构造函数传入。
有哪些常见的Loader?他们是解决什么问题的?
- file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
- url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
- source-map-loader:加载额外的 Source Map 文件,以方便断点调试
- image-loader:加载并且压缩图片文件
- babel-loader:把 ES6 转换成 ES5
- css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
- style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
- eslint-loader:通过 ESLint 检查 JavaScript 代码
有哪些常见的Plugin?
- define-plugin:定义环境变量
- commons-chunk-plugin:提取公共代码
- uglifyjs-webpack-plugin:通过UglifyES压缩ES6代码
- HtmlWebpackPlugin
- OptimizeCSSPlugin
网络安全、http协议
三次握手/四次挥手?
- 三次握手
第一步:客户端发送SYN报文到服务端发起握手,发送完之后客户端处于SYN_Send状态 第二步:服务端收到SYN报文之后回复SYN和ACK报文给客户端 第三步:客户端收到SYN和ACK,向服务端发送一个ACK报文,客户端转为established状态, 此时服务端收到ACK报文后也处于established状态,此时双方已建立了连接
- 四次挥手
刚开始双方都处于 establised 状态,假如是客户端先发起关闭请求,则:
第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态。
第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态。
第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态
服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态
为什么连接的时候是三次握手,关闭的时候却是四次握手?
为什么连接的时候是三次握手,关闭的时候却是四次握手?
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。 其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时, 很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端, “你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了, 我才能发送FIN报文,因此不能一起发送。故需要四步握手
http如何实现缓存?
强缓存策略和协商缓存策略 强缓存==>Expires(过期时间)/Cache-Control(no-cache)(优先级高) 协商缓存 ==>Last-Modified/Etag(优先级高) Etag适用于经常改变的小文件 Last-Modefied适用于不怎么经常改变的大文件
强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本, 区别只在于协商缓存会向服务器发送一次请求。它们缓存不命中时, 都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略 是一起合作使用的。浏览器首先会根据请求的信息判断,强缓存是否命中, 如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求, 使用协商缓存,如果协商缓存命中的话,则服务器不返回资源, 浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。...
说下XSS和CSRF?
- XSS(Cross-Site Scripting) 跨站脚本攻击
1.反射型XSS
概念:当用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。Web服务器将注入脚本,比如一个错误信息,搜索结果等,未进行过滤直接返回到用户的浏览器上。
反射型 XSS 的攻击步骤: 攻击者构造出特殊的 URL,其中包含恶意代码。 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
防范: 对url的查询参数进行转义后再输出到页面。 app.get('/welcome', function(req, res) { //对查询参数进行编码,避免反射型 XSS攻击 res.send(${encodeURIComponent(req.query.type)}); });
2.DOM 型 XSS
概念:就是前端 JavaScript 代码不够严谨,把不可信的内容插入到了页面。在使用 .innerHTML、.outerHTML、.appendChild、document.write()等API时要特别小心,不要把不可信的数据作为 HTML 插到页面上,尽量使用 .innerText、.textContent、.setAttribute() 等
DOM 型 XSS 的攻击步骤:
攻击者构造出特殊数据,其中包含恶意代码。 用户浏览器执行了恶意代码。 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
防范: 对输入内容进行转义
3.存储型XSS
概念:恶意脚本永久存储在目标服务器上。当浏览器请求数据时,脚本从服务器传回并执行,影响范围比反射型和DOM型XSS更大。存储型XSS攻击的原因仍然是没有做好数据过滤:前端提交数据至服务端时,没有做好过滤;服务端在接受到数据时,在存储之前,没有做过滤;前端从服务端请求到数据,没有过滤输出。
存储型 XSS 的攻击步骤:
攻击者将恶意代码提交到目标网站的数据库中。 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
防范: 前端数据传递给服务器之前,先转义/过滤(防范不了抓包修改数据的情况) 服务器接收到数据,在存储到数据库之前,进行转义/过滤 前端接收到服务器传递过来的数据,在展示到页面前,先进行转义/过滤
其它防范:
输入内容长度控制
输入内容限制(限定不能包含特殊字符或者仅能输入数字等)
其它安全:HTTP-only Cookie: 禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
验证码:防止脚本冒充用户提交危险操作
- CSRF 跨站点伪造
攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账。造成的问题包括:个人隐私泄露以及财产安全
典型的CSRF攻击流程: 登录了一个受信任的网站A,并且本地存放了Cookie; 攻击者诱导 不关闭A的情况下,访问了危险网站B; 站点B向站点A发送了一个请求,浏览器会默认携带站点A的Cookie信息。 站点A接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是无辜的受害者发送的请求。 站点A以受害者的名义执行了站点B的请求。 攻击完成,攻击者在受害者不知情的情况下,冒充受害者完成了攻击。
预防CSRF攻击:
- 尽量使用post请求,get比较容易被利用
- 加入验证码(确保是用户所为)
- 检测Referer(并不安全,Referer可以被更改(根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址)
- 请求中添加token并验证
-
- 服务端给用户生成一个token,加密后传递给用户 - 用户在提交请求时,需要携带这个token - 服务端验证token是否正确
- JWT
- Anti CSRF Token
http和https?
概念: HTTP:超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。 设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。它可以使浏览器更加高效。 HTTP 协议是以明文方式发送信息的,如果黑客截取了 Web 浏览器和服务器之间的传输报文,就可以直接获得其中的信息。
HTTPS:是以安全为目标的 HTTP 通道,是 HTTP 的安全版。HTTPS 的安全基础是 SSL。SSL 协议位于 TCP/IP 协议与各种应用层协议之间,为数据通讯提供安全支持。SSL 协议可分为两层:SSL 记录协议(SSL Record Protocol),它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。SSL 握手协议(SSL Handshake Protocol),它建立在 SSL 记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等
HTTP 与 HTTPS 的区别?
1、HTTPS 协议需要到 CA (Certificate Authority,证书颁发机构)申请证书,一般免费证书较少,因而需要一定费用。 (以前的网易官网是http,而网易邮箱是 https 。)
2、HTTP 是超文本传输协议,信息是明文传输,HTTPS 则是具有安全性的 SSL 加密传输协议。
3、HTTP 和 HTTPS 使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4、HTTP 的连接很简单,是无状态的。HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。(无状态的意思是其数据包的发送、传输和接收都是相互独立的。无连接的意思是指通信双方都不长久的维持对方的任何信息。)
http1.0、http1.1、http2.0的区别?
1和1.0相比,1.1可以一次传输多个文件
http1.x解析基于文本,
http2.0采用二进制格式,新增特性 多路复用、header压缩、服务端推送(静态html资源)
在浏览器中输入URL并回车后都发生了什么?
一、解析URL
URL用来表示某个资源的地址,浏览器首先对拿到的URL进行识别,抽取出域名字段。
二、DNS解析
DNS实际上是一个域名和IP对应的数据库
三、浏览器与网站建立TCP连接(三次握手)
第一次握手:客户端向服务器端发送请求(SYN=1) 等待服务器确认;
第二次握手:服务器收到请求并确认,回复一个指令(SYN=1,ACK=1);
第三次握手:客户端收到服务器的回复指令并返回确认(ACK=1)。
四、请求和传输数据
五、浏览器渲染页面
客户端拿到服务器端传输来的文件,找到HTML和MIME文件,通过MIME文件,浏览器知道要用页面渲染引擎来处理HTML文件。
1. 浏览器会解析html源码,然后创建一个 DOM树。
在DOM树中,每一个HTML标签都有一个对应的节点,并且每一个文本也都会有一个对应的文本节点。
2. 浏览器解析CSS代码,计算出最终的样式数据,形成css对象模型CSSOM。
首先会忽略非法的CSS代码,之后按照浏览器默认设置——用户设置——外链样式——内联样式——HTML中的style样式顺序进行渲染。
3. 利用DOM和CSSOM构建一个渲染树(rendering tree)。
渲染树和DOM树有点像,但是是有区别的。
DOM树完全和html标签一一对应,但是渲染树会忽略掉不需要渲染的元素,比如head、display:none的元素等。
待补充:
setTimeout和promise的区别?
promise和async 区别?
webpack对性能优化?
h5在安卓ios遇到的兼容问题?
微前端qian kun父子怎么通讯的?
vue3.0 reactive响应式,什么情况下响应式会丢?
h5和原生怎么通讯的?
token过期了怎么处理?
flex:1代表什么?