1.函数防抖与节流
-
概念(作用相同:限制任务触发的频率)
函数防抖debounce:在特定的时间内,没有触发特定条件,就执行一次任务
函数节流throttle:在特定的时间内,无论触发多次条件,仅执行一次任务
-
两者的区别:
函数防抖有可能在很长时间内一次任务都不执行,只有最后一次延时时间达到之后执行一次
函数节流在特定时间内会固定触发一次任务,并且是规律的
-
应用场景
函数防抖:
关键字搜索,限制接口调用频率
表单验证,验证邮箱的格式,停止输入时再做验证
函数节流:
onresize(页面放大/缩小)、 onscroll(滚动页面)、 onmousemove(鼠标移动),(一般用于分页加载数据)对于高频事件一般要做触发频率的显示
// 函数防抖(固定时间内没有触发条件,就执行一次)
// 函数防抖(固定时间内没有触发条件,就执行一次)
async inputHandle () {
clearTimeout(this.timer)
this.timer = setTimeout(async () => {
let res = await request('goods/qsearch', 'get', {
query: this.keyword
})
this.searchResult = res.data.message
}, 1000)
}
// 函数节流(固定时间内无论触发几次,仅执行一次)
keywordSearch () {
if (this.isLoading) {
// 终止后续代码的执行,终止请求
return
}
this.isLoading = true
setTimeout(async () => {
let res = await request('goods/qsearch', 'get', {
query: this.keyword
})
this.searchResult = res.data.message
// 重新打开发送请求的开关
this.isLoading = false
}, 1000)
}
2.修改点击事件的执行顺序
执行双击事件(dblclick)时,除了触发双击,也触发了两次单击事件(click)
单击(click):mousedown,mouseout,click;
双击(dblclick):mousedown,mouseout,click , mousedown,mouseout,click,dblclick;
<button onclick="single(event)" ondblclick="double(event)">按钮</button>
<script>
var time = 200; //300以上,双击才生效
var timeOut = null;
function single (e) {
clearTimeout(timeOut); // 清除第一个单击事件
timeOut = setTimeout(function () {
console.log('单击');
// 单击事件的代码执行区域
// ...
}, time)
}
function double (e) {
clearTimeout(timeOut); // 清除第二个单击事件
console.log('双击')
// 双击的代码执行区域
// ...
}
</script>
// 释义
关于time=200,大家知道js的事件循环机制,点击事件会添加一个任务队列。time=0,也会添加一个任务队列。那么time=0与time=200有什么区别呢?
因为第一次单击事件后,主线程没有任何任务,就会立马执行这个单击事件的任务。待第二次单击的时候,假设距离第一次单击事件是150ms, 如果你的定时器小于150ms, 那么第一次的任务队列就会执行完。要想不执行第一次的任务队列,那么定时器时间间隔就必须大于两次单击的时间间隔了。 所以,这个200是酌情值,大于间隔就行。常用值有:200、300、500、1000。第一次单击任务不执行了,是被定时器延时,然后第二次点击的时候给清除了。那么第二次点击事件呢?在两次单击之后,会立马执行一个双击事件,双击事件的一开头就把这个第二次点击事件给清除了。这就是双击事件的大概过程。
3.BFC
-
什么是BFC:块级格式化上下文
-
特性:
BFC是一个块级元素,块级元素在垂直方向上依次排列。
BFC是一个独立的容器,内部元素不会影响容器外部的元素。
属于同一个BFC的两个盒子,外边距margin会发生重叠,并且取最大外边距。
计算BFC高度时,浮动元素也要参与计算。
-
如何创建BFC(条件):
给父级元素添加以下任意样式
overflow: hidden;
display: flex;
display: inline-flex;
display: inline-block;
position: absolute;
position: fixed;
表格单元格:table-cell
-
BFC有什么作用:
1.解决外边距的垂直塌陷问题
2.解决当父级元素没有高度时,子级元素浮动会使父级元素高度塌陷的问题
3.解决子级元素外边距会使父级元素塌陷的问题(也可以通过将子级元素的margin-top改为父级元素的padding-top来解决)
4.BFC可以阻止标准流元素被浮动元素覆盖(可实现文字环绕)
4.重排(reflow)和重绘(repaint)
重排必然导致重绘,重绘不一定重排
原因:
添加、删除或更改DOM元素:这些操作会导致浏览器重新计算元素的位置、大小等信息,从而触发重排。
修改元素的尺寸、位置、边距、填充、边框等样式属性:这些操作同样会导致浏览器重新计算元素的布局信息,从而触发重排。
修改页面的字体大小、样式等:这些操作会影响页面的渲染,从而触发重排或重绘。
用户交互事件:这些事件会触发页面的重排和重绘。例如,窗口大小改变会导致页面重新布局,从而触发重排;滚动则会导致页面重新绘制,从而触发重绘。
如何减少重排和重绘:
1.避免频繁的DOM操作
2.使用CSS3动画
3.避免使用table布局
4.避免频繁的重复样式
5.使用position:absolute或fixed定位
6.将样式表放在头部等方法
5.display属性常见的属性值及含义是什么?
display是CSS中的一个重要属性,用于定义元素在页面上的显示方式
1. block:将元素显示为块级元素。块级元素会独占一行,相邻块级元素会另起一行显示。宽度默认为父元素的100%。
2. inline:将元素显示为内联元素。内联元素不会独占一行,相邻内联元素会在同一行显示。宽度默认由内容决定。
3. inline-block:将元素显示为内联块级元素。内联块级元素不会独占一行,可以设置宽度、高度等属性。
4. none:将元素隐藏,不占据任何空间。元素及其内容不会在页面上显示。
5. flex:将元素显示为弹性容器。可以使用flex布局来控制子元素的排列方式。
6. grid:将元素显示为网格容器。可以使用grid布局来控制子元素的排列方式。
7. table:将元素显示为表格。可以使用表格相关的CSS属性控制元素的布局。
8. inline-table:将元素显示为内联表格。
9. table-cell:将元素显示为表格单元格。
10. table-caption:将元素显示为表格标题。
6. flex弹性布局
-
容器样式属性:
以下 6 个属性设置在容器上:
flex-direction:决定主轴的方向。
flex-wrap:如果一条轴线排不下,如何换行。
flex-flow:flex-direction 属性和 flex-wrap 属性的简写属性。
justify-content:定义项目在主轴上的对齐方式。
align-items:定义项目在交叉轴上如何对齐。
align-content:定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。
-
项目样式属性
以下6个属性设置在项目上。
order:定义项目的排列顺序。数值越小,排列越靠前,默认为 0。(一般用来改变元素的顺序)
flex-grow:定义项目的放大比例,默认为 0,即如果存在剩余空间,也不放大。
flex-shrink:定义了项目的缩小比例,默认为 1,即如果空间不足,该项目将缩小。
flex-basis:定义了在分配多余空间之前,项目占据的主轴空间(main size)。它的默认值为 auto,即项目的本来大小。
flex: flex-grow 、 flex-shrink 和 flex-basis 的简写,默认值为 0 1 auto 。后两个属性可选。
align-self:允许单个项目有与其他项目不一样的对齐方式,可覆盖 align-items 属性。默认值为 auto 。
-
flex:1 表示什么
flex属性是flex-grow,flex-shrink和flex-basis的简写,默认值为0 1 auto。
7. ES6的常用语法
1、块级作用域(let、const)
块级作用域,就是有{}括号中可用范围,不像之前var定义的变量都是函数作用域。
let定义的变量可以改变值,const定义的都是静态变量,不可以修改的。但是像数组,只对数据进行push操作的话,也可以定义成const的。
2、模版字符串
反引号(``),变量(${})
3、解构赋值
从数组和对象中提取值 ,对变量进行赋值 ,这被叫作解构赋值。
// 数组解构
let [a,b,c]=[1,2,3];
console.log(c);
// 对象解构
let {name:myname,age}={name:'lily',age:12};
console.log(myname,age)
// 字符串解构
let str = 'qaz';
let [a,b,c]=str;
console.log(a,b,c);
4、箭头函数
箭头函数作为普通函数的一个补充,将this指向了函数体之外最近一层的this,而不是向普通JS一样将this指向window变量。
function fn(){
console.log('real',this) //{a:100}
var arr = [1,2,3]
//普通JS
arr.map(function (item){
console.log('js',this) //window
return item + 1
})
//箭头函数
arr.map(item => {
console.log('es6',this) //{a:100}
return item + 1
})
}
fn.call({a:100}) //将{a:100}设置为this
5、函数默认参数
function(a, b=0){ //如果b为空,默认b等于0
}
6. 剩余参数&扩展运算符
语法都是…arr。不同在于,剩余参数是将一个不定数量的参数表示为一个数组。扩展运算符是将数组(对象)转为用逗号分隔的参数序列。
//剩余参数是把参数转成数组
function func(arg1, ...args){
// arg1 == 1, args == [2,3,4]
}
func(1,2,3,4);
const sum = (...args) =>{
let total = 0;
arrgs.foreach(item => total += item);
return total;
}
//扩展运算符,合并数组
let arr1 = [1,3,5];
let arr2 = [2,4,6];
let arr3 = [...arr1, ...arr2]; // arr3 == [1,3,5,2,4,6];
// 或者
arr1.push(...arr2); // arr1 = [1,3,5,2,4,6];
剩余参数是把参数转成数组,扩展运算符是把数组转成非数组。
7、对象新增方法,Object.assign()
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);
console.log(target); // Object { a: 1, b: 4, c: 5 }
console.log(returnedTarget); // Object { a: 1, b: 4, c: 5 }
8、Set和Map数据结构
Set类似于数组,但是成员的值都是唯一的,没有重复的值。
const s = new Set([1,2,3,4,5,5]); // 会剔除重复的值,实际上 s=={1,2,3,4,5}
s.size; // 5, 数据结构的大小
add(value):添加某个值,返回Set结构本身
delete(value):删除某个值,返回一个布尔值,表示删除是否成功
has(value):返回一个布尔值,表该值是否为Set的成员
clear(value):清除所有成员,没有返回值
s.add(6).add(7);++
s.delete(7); //true
s.delete(8); //false
s.has(6); //true
s.has(7); //false
s.clear() // s.size === 0
s.forEach( value => console.log(value)); //遍历s数据结构的值
Map是类似Object的一种键值对集合
9、最后其他es6常用的语法
es6模块化 中 import和export 用法 (前端工程化以及如何通过Node.js中babel来编译es6模块化代码)
es6的 promise对象 (JavaScript进阶之Ajax的问题和什么是promise)
asyn 和 await 函数 (fetch和axios接口调用方式的用法)
8.for...in 和 for...of的区别
for...in 只能获得对象的键名 循环主要是为了遍历对象而生,不适用于遍历数组
for...of 遍历获得键值 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象
9. js数据类型
基本数据类型 :
Number,String,Boolean,Undefined,Null,BigInt,
Symbol(Symbol表示一个独一无二的值,用于创建对象属性的唯一标识符)
引用数据类型 :Object,Array,Function,Date,RegExp(正则)
null 与 undefined 区别
undefined 是表示一个未定义或未初始化的值,常用于声明但未赋值的变量,或者访问不存在的属性。
null 是一个被赋予的值,用于表示变量被故意赋值为空。
在判断变量是否为空时,使用严格相等运算符(===),因为 undefined 和 null 在非严格相等运算符(==)下会相等。
判断一个元素是数组还是对象
null,数组,对象 返回的都是object类型,对于函数类型 返回的则是function
数组是属于Object类型的,也就是引用类型,所以不能用typeof判断其具体类型
const arr = []
1.isArray
Array.isArray(arr) // true
2.instanceof (用来检测 constructor.prototype 是否存在于参数 arr 的原型链上。)
console.log(arr instanceof Array) // true
3.Object.prototype.toString.call()
Object.prototype.toString.call(arr); // "[object Array]"
4.利用构造函数来判断他的原型是否为Array
console.log(arr.constructor === Array)
5.通过 Object.getPrototypeOf()来判断是否为数组类型
console.log(Object.getPrototypeOf(arr)===Array.prototype)
6.通过 isPrototypeOf() 方法来判断是否为数组类型
console.log(Array.prototype.isPrototypeOf(arr))
10.数组的reduce方法
accumulator 累计器
currentValue 当前值
currentIndex 当前索引
array 数组
空数组调用reduce()方法时,如果没有提供初始值参数,则会抛出一个TypeError错误。
解决这个问题,可以提供一个初始值参数作为reduce()的第二个参数。
1.无初始值
[0, 1, 2, 3, 4].reduce(function(accumulator, currentValue, currentIndex, array){
return accumulator + currentValue;
});
callback 被调用四次,每次调用的参数和返回值如下表:
callback accumulator currentValue currentIndex array return value
first call 0 1 1 [0, 1, 2, 3, 4] 1
second call 1 2 2 [0, 1, 2, 3, 4] 3
third call 3 3 3 [0, 1, 2, 3, 4] 6
fourth call 6 4 4 [0, 1, 2, 3, 4] 10
2.添加初始值
// 提供初始值为 10
[0, 1, 2, 3, 4].reduce((accumulator, currentValue, currentIndex, array) => { return accumulator + currentValue; }, 10 );
callback 被调用四次,每次调用的参数和返回值如下表:
callback accumulator currentValue currentIndex array return value
first call 0 1 1 [0, 1, 2, 3, 4] 1
second call 1 2 2 [0, 1, 2, 3, 4] 3
third call 3 3 3 [0, 1, 2, 3, 4] 6
fourth call 6 4 4 [0, 1, 2, 3, 4] 10
用途:求和
var total = [ 0, 1, 2, 3 ].reduce(
( acc, cur ) => acc + cur,
0
);
// total 6
累加对象里的值
let sum = [{x: 1}, {x:2}, {x:3}].reduce(
(accumulator, currentValue) => accumulator + currentValue.x
,0
);
console.log(sum) // logs 6
二维数组变一维
var flattened = [[0, 1], [2, 3], [4, 5]].reduce(
( acc, cur ) => acc.concat(cur),
[]
);
// [0, 1, 2, 3, 4, 5]
const names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];
计算数组中每个元素出现的次数
let countedNames = names.reduce(function (allNames, name) {
if (name in allNames) {
allNames[name]++;
}
else {
allNames[name] = 1;
}
return allNames;
}, {});
// countedNames is:
// { 'Alice': 2, 'Bob': 1, 'Tiff': 1, 'Bruce': 1 }
按属性对object分类
var people = [
{ name: 'Alice', age: 21 },
{ name: 'Max', age: 20 },
{ name: 'Jane', age: 20 }
];
function groupBy(objectArray, property) {
return objectArray.reduce(function (acc, obj) {
var key = obj[property]
if(!acc[key]){
acc[key] = []
}
acc[key].push(obj)
return acc
}, {});
}
var groupedPeople = groupBy(people, 'age');
// groupedPeople is:
// {
// 20: [
// { name: 'Max', age: 20 },
// { name: 'Jane', age: 20 }
// ],
// 21: [{ name: 'Alice', age: 21 }]
// }
11.forEach
在foreach中增加数组元素,不会导致循环增加,循环次数还是原来数组的长度。
var arr=[1,2,3];
arr.forEach((item)=>{
if(item==2){
arr.push(7);
arr.push(8);
}
console.log(item); // 1,2,3
});
console.log(arr.length); // 5
和增加不同的是,中数组中减少元素却会减少循环次数,并且删除的元素后面的元素会被“跳过”
var arr=[1,2,3];
arr.forEach((item)=>{
if(item==2){
arr.splice(1,1);
}
console.log(item);//1,2
});
console.log(arr.length); //2
for循环中退出循环有3种方式:return(终止)、break(退出整个循环)、continue(退出当次循环)。
forEach()本身无法跳出循环,必须遍历所有的数据才能结束。
forEach()只能识别上面三种退出循环中的return,其它都识别不了,且return在forEach()中相当于continue。
forEach()可以通过try{}catch(){}结合throw抛错的方式退出循环:
const forEachArray = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n']
try {
forEachArray.forEach((item) => {
if (item === 'd') {
throw new Error("到d就停止吧");
}
console.log(item)
})
} catch (e) {
console.log(e.message); // 到d就停止吧
}
12.筛选两个数组不同的值
const list1 = [{
id: 1,
name: 'z'
}, {
id: 2,
name: 's'
}]
const list2 = [{
id: 1,
name: 'z'
}, {
id: 2,
name: 't'
}, {
id: 3,
name: 'r'
}]
const filterList = list2.filter(item2 => {
return list1.every(item1 => {
return item2.name !== item1.name
// return item2.id !== item1.id
})
})
console.log(filterList); //[{id: 2,name: 't'}, {id: 3,name: 'r'}]
13.setTimeout
setTimeout(f,0) 作用:把f放到运行队列的最后去执行。
setInterval 有两个缺点:
● 使用 setInterval 时,某些间隔会被跳过;
● 可能多个定时器会连续执行;
setTimeout实现setInterval :
function mySetInterval(fn, wait) {
let timer = null
function interval() {
timer = setTimeout(() => {
fn()
interval()
}, wait);
}
interval()
return {
cancel() {
clearTimeout(timer)
}
}
}
用setInterval实现setTimeout:
function mySetTimeout(fn, wait) {
const timer = setInterval(() => {
fn()
clearInterval(timer)
}, wait);
}
14.改变this指向的方法
(1)、call方法
1、call()方法可以进行普通函数的调用
2、call()方法可以改变this的指向,如果没有参数,this指向window
3、call()方法可以改变this的指向,如果有一个参数,this指向该参数
4、call()方法可以改变this的指向,如果有多个参数,this指向第一个参数,剩下的是个参数列表(构造函数继承的案例)
(2)、apply方法
1、 apply()方法可以进行普通函数的调用
2、apply()方法可以改变this的指向,如果没有参数,this指向window
3、apply()方法可以改变this的指向,如果有一个参数,this指向该参数
4、apply()方法可以改变this的指向,如果有多个参数,第一个参数是null或者window,第二个参数是数组
(3)、bind方法
推荐使用第二种形式,第三种用的相对较少
bind() 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。当目标函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
bind()不能进行函数的调用
可以改变this指向
15.new一个对象的时候会做些什么
(1)创建一个新对象;
(2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象) ;
(3) 执行构造函数中的代码(为这个新对象添加属性) ;
(4) 返回新对象。
16.es6的class和es5的function的关系
ES6中的类只是语法糖,它并没有改变类实现的本质。
ES6定义class中的方法,定义在原型对象上的。与ES5不同的是,这些定义在原型对象的方法是不可枚举的。
ES6类和模块是严格模式下的;
不存在变量提升,保证子类父类的顺序;
es6类继承:子类必须在父类的构造函数中调用super(),这样才有this对象,因为this对象是从父类继承下来的。而要在子类中调用父类的方法,用super关键词可指代父类。
ES5中类继承的关系是相反的,先有子类的this,然后用父类的方法应用在this上。ES6类继承子类的this是从父类继承下来的这个特性,使得在ES6中可以构造原生数据结构的子类,这是ES5无法做到的。
17.es5继承
es5继承:es5继承
18.JS 中函数名后面加与不加括号的区别
函数是一个对象,函数名是指向这个对象的指针。
函数名后面加上括号---->就表示立即调用执行这个函数里面的代码。
使用不带圆括号的函数是------>访问函数的指针,而非调用函数。
函数名后面加括号,就直接执行函数返回值。
例如:
函数不加括号的,都是把函数名称作为函数的指针,用于传参,此时不是得到函数的结果,因为不会运行函数体代码。它只是传递了函数体所在的地址位置,在需要的时候好找到函数体去执行。
EX:document.οnmοusedοwn=fx; fx不会立即执行,document.onmousedown事件发生时会调用函数fx
函数只要加括号,是要调用它进行执行的。此时,函数()实际上等于函数的返回值。当然,有些没有返回值,但已经执行了函数体内的行为
EX:document.οnmοusedοwn=fx(); 只调用fx,和document.onmousedown事件无关
19.为什么JavaScript是单线程的?
因为JavaScript的主要用途是和用户互动和操作DOM的,如果用户添加一个信息然后再去删除,肯定是先生成这个信息才能去进行删除操作的,单线程的操作代步同时时间只能进行一件事,那么有些任务是非常耗时的,会阻塞代码的执行,比如你在看一个打开一个视频的时候,可能视频加载需要时间,但是你依旧可以进行点赞和投币操作的,所以我们把代码又分成了同步代码和异步代码,同步代码是会立即加入JS引擎(js主线程)执行,并且会原地等待,而异步代码是先放入宿主环境中(一般是游览器环境来处理异步任务),不必等待,不阻塞主线程,异步结果将来执行
20.什么是宏任务?什么是微任务? 事件循环?
代码分为同步代码和异步代码,异步代码又分为宏任务和微任务
宏任务包括:script(整体代码)、seTimeout、setInterval、setImmediate、I/O(Ajax的请求和数据传给后台都是宏任务)等
微任务:promise.then() catch() (promise本身是同步的,异步的是他的then和catch)process.nextTick()(node)、async/await、object.observe等等
什么是js的事件循环,同时也解释为什么会有事件循环:
1.JS是单线程,防止代码阻塞,我们把代码(任务):同步和异步
2.同步代码给js引擎执行,异步代码交给宿主环境
3.同步代码放入执行栈中,异步代码等待时机成熟送入任务队列排队
4.执行栈执行完毕,会去任务队列看是否有异步任务,有就送到执行栈执
行,反复循环查看执行,这个过程是事件循环(eventloop)
21.跨域问题
http:(协议)//www.example.com(域名):8080(端口号)/role/list(虚拟目录地址)?name=123&password=123456(参数部分)#people(锚部分)
DNS域名解析:将域名解析成对应了IP地址
比如www.bilibili.com你可能一会就记住了但是如果是www.192.168.31.1.com辨识度就非常低也不容易区分
什么情况下会产生跨域?
当一个请求url的协议、域名、端口三者之间的任意一个与当前页面url不同即为跨域。
解决跨域:
- 设置浏览器禁止检查--disabled-web-security --user-data-dir,设置成功,跨域请求可被正常发送出去。
- jsonp:浏览器限制了页面直接向非同源的服务器发送ajax请求,但是并没有限制页面向非同源的服务器请求JS脚本,就像我们可以使用标签请求任意域上图片一样。(实际上是在<html>的<head>中动态添加了一条<script>标签。)(需要后端)
eg:
$.ajax({
url:"https://www.imooc.com/index/getstarlist",
type: "GET",
dataType:"jsonp"
})
jsonp弊端:
jsonp需要服务端配合修改,如果调用的接口不是我们自己的服务器上,jsonp就无能无力了;jsonp只能发送get请求;jsonp发送的时script请求,并不是xhr请求,因此xhr的很多优势都不能使用(例如:异步,事件等) -
cors:(跨域资源共享)是一种允许当前域的资源被其他域的脚本请求访问的机制 (需要后端)
请求方法为:
简单请求:
HEAD、GET、POST中的一种。
HTTP请求头中字段不超过:Accept、Accept-Language、Content-Language、Last-Event-ID
Content-Type字段值为application/x-www-form-urlencoded、multipart/form-data、text/plain中的一种。非简单请求:
请求方法为put、delete.
发送JSON格式的ajax请求。
http中带自定义请求头。
对于简单请求:
浏览器发现是跨域请求,就会自动在请求头中加上Origin字段,代表请求来自哪个域(协议+主机名+端口号)。服务器在收到请求后,根据请求头中Origin字段值来判断是否允许跨域请求通过。具体实现方法是:在响应头Access-Control-Allow-Origin字段中设置指定的域名,表示允许这些域名的跨域请求。如果请求头中Origin字段的域名包含在这些域名中,则可以实现跨域请求(当然有时候还需要结合其他字段来判断),否则不通过。
(Access-Control-Allow-Credentials字段代表服务器允许cookie可以包含在请求中,一起发送给服务器,值为布尔类型。如果要把cookie一起发送到服务器,还需要在请求中打开withCredentials属性。注意:如果要发送cookie,Access-Control-Allow-Origin的值不能为“*”,只能是具体的域名。)
对于非简单请求:
非简单请求在发送http请求时,会预先发送一次“预检”(OPTIONS)请求。预检请求会事先询问服务器,当前域名是否在服务器允许的范围内,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复后,浏览器才会真正发出http请求,否则就会报错。
CORS相比较JSONP的优势:
CORS支持所有类型的http请求,jsonp只支持get请求。JSONP的优势在于支持老式浏览器,以及向不支持CORS的网站请求数据。 -
使用代理服务器:nginx/webpack中做相应配置
服务端请求服务端会跨域吗? 不会
webpack或者vite中可以使用proxy解决跨域,它解决跨域的原理是什么? 代理服务器
为什么跨域的时候有时候请求会发两次? 非简单请求
那为什么非要有这个预检请求呢?
浏览器限制跨域请求一般有两种方式:
1. 浏览器限制发起跨域请求
2. 跨域请求可以正常发起,但是返回的结果被浏览器拦截了
一般浏览器都是第二种方式限制跨域请求,那就是说请求已到达服务器,并有可能对数据库里的数据进行了操作,但是返回的结果被浏览器拦截了,那么我们就获取不到返回结果,这是一次失败的请求,但是可能对数据库里的数据产生了影响。
为了防止这种情况的发生,规范要求,对这种可能对服务器数据产生副作用的HTTP请求方法,浏览器必须先使用OPTIONS方法发起一个预检请求,从而获知服务器是否允许该跨域请求:如果允许,就发送带数据的真实请求;如果不允许,则阻止发送带数据的真实请求。
22.浏览器多标签页tab之间的通信
方法一: 使用localStorage实现通信
1、同域数据共享,跨域数据无法共享;
2、持久化数据存储
3、提供storage事件监听localStorage变化
方法二:使用cookie
cookie特点:
1、同域数据共享,跨域数据无法共享
2、存储空间较小,4kb
3、请求时会自动携带cookie
方法三:使用websocket
websocket特点:
1、保持连接状态,而http协议是无状态连接即请求完毕会关闭连接
2、全双工通信,客户端和服务端平等对待,可相互通信
3、建立在TCP协议上
4、没有同源策略,可以跨域
方法四:使用SharedWorker:SharedWorker (一种在多个浏览器上下文之间共享脚本执行的机制)
它可以在不同的标签页之间进行通信。可以创建一个SharedWorker,然后在各个标签页中连接到该SharedWorker,使它们能够共享数据和通信。
23. 说说的vue的响应式原理
vue2
vue2的响应式原理主要使用的是Object.defineProperty( ),里面需要传入三个参数,分别是:
【响应源数据的对象,源数据中的需要读写的属性,相对应的对象方法(包含了get和set方法)】
Vue2实现响应式的原理核心之一是利用Object.defineProperty()函数,他的主要作用就是数据的劫持/代理,既然要实现响应式,那么就需要有触发者和响应者,数据是触发者,那么Object.defineProperty()就是监视数据的变化,主要通过set和get方法,在我们访问数据的时候会触发get方法,例如obj.a,当我们修改数据的时候就会触发set方法,例如obj.a=6,这样我们就能知道数据在变化了,但是由于set修改后的值无法及时被get捕捉到,我们就需要让get的return值返回一个变量,而且是一个需要保留的变量,下次访问还希望存在,但同时不希望声明成全局变量污染环境,于是用到了闭包,我们将Object.defineProperty()函数封装成defineReactive()函数方法,我们在函数内全局声明var tep 将他作为get的返回值,这样数据就实时更新了,到此我们就及时捕捉到数据的变化了,换而言之就是数据能够作为触发者了
总结就是:在get函数的地方通过watcher收集依赖存储到dep列表中,然后set函数更新的时候dep循环通知依赖的watcher,然后watcher根据回调去dom做视图的更新
vue3
vue3的响应式原理主要依靠的是ES6新增的 Proxy 以及相配合的 Reflect,需要在Proxy的实例对象中传入两个参数
【源数据对象,处理对象的方法【get,set,deleteProperty…等】
差距: 不需要在单独的想vue2之中那样需要特意去指定监控某个对象的变化
24.proxy能够监听到对象中的对象的引用吗?
Proxy可以监听到对象中的对象的引用。
当使用Proxy包装一个对象时,可以为该对象的任何属性创建一个拦截器,包括属性值为对象的情况。
const obj = {
nestedObj: { foo: 'bar' }
};
const handler = {
set(target, property, value) {
console.log(`Setting property '${property}' to '${value}'`);
target[property] = value;
return true;
}
};
const proxyObj = new Proxy(obj, handler);
proxyObj.nestedObj.foo = 'baz'; // 输出: Setting property 'foo' to 'baz'
我们通过Proxy创建了一个代理对象proxyObj,它包装了原始的obj。然后,我们对proxyObj中的nestedObj.foo进行赋值操作,这会触发set拦截器,并打印相应的信息。
25.如何让proxy去监听基本数据类型?
Proxy无法直接监听基本数据类型,Proxy只能在对象级别上进行操作,而不是基本数据类型。
如果要监听基本数据类型的更改,最好使用其他方式,例如通过触发事件或调用回调函数来通知更改。可以创建一个自定义的数据包装器,将基本数据类型包装在对象中,并在该对象上实现监听逻辑。
function ValueWrapper(value) {
this.value = value;
this.onChange = null;
}
ValueWrapper.prototype.setValue = function (newValue) {
this.value = newValue;
if (typeof this.onChange === 'function') {
this.onChange(this.value);
}
}
const wrapper = new ValueWrapper('Hello');
wrapper.onChange = newValue => {
console.log(`Value changed: ${newValue}`);
}
wrapper.setValue('Hi');
// 输出:Value changed:Hi
26.v-if、v-for优先级
vue2中v-for优先级更高,
vue2的优化方案
1.可以用计算属性替换
2.外层嵌套template
标签,页面渲染不生成DOM节点,在template
标签上使用v-if
,在内层使用v-for
vue3中v-if优先级更高
27.v-if、v-show
v-if和v-show的区别?
v-if:适合在切换不频繁的场景中使用,因为它在渲染DOM时会频繁地创建或销毁元素,影响性能。
v-show:适合在切换频繁的场景中使用,因为它只是改变元素的CSS属性值,无需频繁创建或销毁元素,性能更好。
在Vue2和Vue3中,v-show的优先级高于v-if,因为v-show只需要根据条件设置元素的样式,而不需要频繁地创建/销毁DOM,因此它的优先级更高。
它两在对页面产生重绘重排上面有什么不同?
v-show:引起重排
v-if:引起重绘和重排
分别为一个组件设置v-if和v-show,值由false变成true,或者由true变成false,组件的生命周期会怎么执行?
- v-if值为false时,在该位置创建一个注释节点,用来标识元素在页面中的位置。在值发生改变的时候,通过diff,新旧组件进行patch,创建DOM节点,从而动态显示隐藏。
- v-show值为false时,通过设置元素的样式,
display:none
来控制元素是否展示。 -
当v-if指令附属于普通元素时,v-if指令状态变化会使得父组件的dom发生变化,父组件将会更新视图,所以会触发父组件的beforeUpdate和updated钩子函数。
当v-if指令令附属于组件时,v-if指令状态变化对父组件的影响和上一条一致,但是对于本身组件的生命周期的影响是不一样的。
v-if从false切换到true时,会触发beforeCreate,created,beforeMount,mounted钩子。
v-if从true切换到false时,会触发beforeDestroy和destroyed钩子函数。
28.scoped样式击穿
一个style标签拥有scoped属性时,它的CSS样式就只能作用于当前的组件,这样就可以使得组件之间的样式不互相污染。
原理
scope的本质是分别给 HTML 标签和 CSS 选择器添加 data-v-xxx,来达到样式隔离的效果。
样式穿透问题
使用 scoped 后,父组件的样式将不会渗透到子组件中。
但是父组件可以修改子组件根节点样式。
解决办法
vue2 >>> :>>> 会被编译成当前父组件的date-v-xxx
vue3 ::v-deep(.class)
<style>
.a >>> .b >>> span{
color:red;
}
</style>
// 渲染结果,当有多个时只有最左边的>>>生效
.a[data-v-xxx] .b >>> span{
color:red;
}
29.created,mounted
created和mounted的区别
created:实例创建
mounted:DOM挂载完毕
能在这两个声明周期里面通过this拿到method,data,$refs这些吗?
created:能拿到method,data
mounted:能拿到$refs
30.vue的祖孙组件的通信方案
1.父组件props向子组件传值 子组件$emit触发父组件自定义事件并传参数
2.eventBus事件总线
1.创建事件中心管理组件之间的通信
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
2.在某个兄弟组件中引入事件中心,$emit发送事件
3.某个兄弟组件中引入事件中心,$on接受事件
3.依赖注入(父子、祖孙组件之间的通信)
provide / inject
是Vue提供的两个钩子,和data
、methods
是同级的。并且provide
的书写形式和data
一样。
provide
钩子用来发送数据或方法inject
钩子用来接收数据或方法
// 依赖注入所提供的属性是非响应式的
//父组件中:
provide() {
return {
num: this.num
app: this
};
}
data() {
return {
num: 1
};
}
//子组件中:
//inject: ['num']
inject: ['app']
console.log(this.app.num)
//如此可获得父组件的所有属性
4.$refs
$refs进行通讯的弊端:
1.数据流向不清晰,破坏VUE中单向数据流,容易导致状态管理的混乱和难以维护
2.父子组件紧密耦合,可能导致一个组件发生变化另一个组件也无法正常工作
3.组件复用困难,$refs将父子组件绑定在一起,不能实现真正的组件复用
//子组件中:
export default {
data () {
return {
name: 'JavaScript'
}
},
methods: {
sayHello () {
console.log('hello')
}
}
}
//父组件中
<template>
<child ref="child"></component-a>
</template>
<script>
import child from './child.vue'
export default {
components: { child },
mounted () {
console.log(this.$refs.child.name); // JavaScript
this.$refs.child.sayHello(); // hello
}
}
</script>
5. $parent/$children
- 使用
$parent
可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法) - 使用
$children
可以让组件访问子组件的实例,但是,$children
并不能保证顺序,并且访问的数据也不是响应式的。 - 可以使用
$root
来访问根组件的实例 $children
的值是数组,而$parent
是个对象- 在根组件
#app
上拿$parent
得到的是new Vue()
的实例,在这实例上再拿$parent
得到的是undefined
,而在最底层的子组件拿$children
是个空数组
6.$attrs / $listeners(跨代通信)
//APP.vue
<template>
<div id="app">
//此处监听了两个事件,可以在B组件或者C组件中直接触发
<child1 :p-child1="child1" :p-child2="child2" @test1="onTest1" @test2="onTest2"></child1>
</div>
</template>
<script>
import Child1 from './Child1.vue';
export default {
components: { Child1 },
methods: {
onTest1() {
console.log('test1 running');
},
onTest2() {
console.log('test2 running');
}
}
};
</script>
//Child1.vue
<template>
<div class="child-1">
<p>props: {{pChild1}}</p>
<p>$attrs: {{$attrs}}</p>
<child2 v-bind="$attrs" v-on="$listeners"></child2>
</div>
</template>
<script>
import Child2 from './Child2.vue';
export default {
props: ['pChild1'],
components: { Child2 },
inheritAttrs: false,
mounted() {
this.$emit('test1'); // 触发APP.vue中的test1方法
}
};
</script>
//Child2.vue
<template>
<div class="child-2">
<p>props: {{pChild2}}</p>
<p>$attrs: {{$attrs}}</p>
</div>
</template>
<script>
export default {
props: ['pChild2'],
inheritAttrs: false,
mounted() {
this.$emit('test2');// 触发APP.vue中的test2方法
}
};
</script>
31. composition api和公共逻辑、公共组件有何区别?为什么vue3中组件能支持多个根节点?
vue3 Composition Api 组合式API 提高代码逻辑的可复用性,把跟一个功能相关的东西放在一个地方,组合式API的写法见不到this
的使用,减少了this
指向不明的情况
vue2 Options Api 选项式API 上手简单,但是一个功能往往需要在不同的vue配置项中定义属性和方法,在vue2
中是通过Mixins
重用逻辑代码,容易发生命名冲突且关系不清
vue2: vue2流程 模板编译成rander函数 执行rander函数得到虚拟dom 虚拟dom 通过patch函数转换成真实的dom ; vdom 是一个单根树形结构,所以patch在遍历的时候是从根节点开始遍历,patch函数要求它必须是一个单个的数据结构
vue3 : 在vue3中引入fragment概念,在vue3组件中出现多个根节点,则会自动创建fragment节点把组件中的根节点作为自己的children,在patch时如果发现fragment则会开始遍历children
32. 一个vue单文件组件的构建过程
vue-cli搭建,npm run build
33. vuex 状态管理模式
Vuex 实现了一个单向数据流,在全局拥有一个 State 存放数据,当组件要更改 State 中的数据时,必须通过 Mutation 提交修改信息, Mutation 同时提供了订阅者模式供外部插件调用获取 State 数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走 Action ,但 Action 也是无法直接修改 State 的,还是需要通过Mutation 来修改State的数据。最后,根据 State 的变化,渲染到视图上。
34. 虚拟dom , 虚拟dom渲染到页面的时候,框架做了什么动作
虚拟DOM是对DOM的抽象,是一个用js对象来表示真实DOM结构叫法。
为什么要用虚拟DOM?
1.保证性能下限,在不进行手动优化的情况下,提供过得去的性能,首次渲染大量DOM时,慢
页面渲染的流程:解析HTML -> 生成DOM -> 生成 CSSOM -> Layout (布局)-> Paint(绘制) -> Composite(组合)
2. 跨平台:可以很方便的跨平台操作,比如服务端渲染、uniapp等
真实DOM和虚拟DOM:
- 真实DOM∶ 生成HTML字符串+重建所有的DOM元素
- 虚拟DOM∶ 生成vNode+ DOMDiff+必要的dom更新
35.页面从输入URL到展示发生了什么?
1.浏览器的地址栏输入URL并按下回车。
2、浏览器查找当前URL是否存在缓存,并比较缓存是否过期。
3、DNS解析URL对应的IP。
4、根据IP建立TCP连接(三次握手)。
5、HTTP发起请求。
6、服务器处理请求,浏览器接收HTTP响应。
7、渲染页面,构建DOM树。(参考上题页面渲染)
8、关闭TCP连接(四次挥手)。
36. 工程化?webpack?webpack的rule,loader的执行流是怎么样的?有自己写过loader、plugin吗
工程化
1.模块化(js的模块化,css的模块化,其他资源的模块化)
2.组件化(可复用的UI结构、样式、行为)
3.规范化(目录结构划分合理,编码规范化,接口规范化,文档规范化,版本控制规范化)
4.自动化(自动化构建,自动化部署,自动化测试)
loader:加载器
一个loader只做一件事,多个loader的执行顺序是从右到左、从下到上。
loader
是针对module
级别的,即针对每个待打包的文件进行转换处理,并最终输出打包后的文件。
module: {
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'] //链式调用loader
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader' //单一loader
}
]
}
常见loader
- babel-loader: 用于将ES6、ES7等高级语法转换为ES5语法,以便于浏览器兼容。
- css-loader: 用于处理CSS文件中的import、url等导入语法。
- file-loader: 用于将文件以特定格式打包处理,支持将文件重命名、分离等多种操作。
- url-loader: 类似于file-loader, 但将小于指定大小的文件转换为base64格式,以减少请求量。
- image-loader: 用于处理图片文件,支持压缩、解析等多种功能。
- json-loader: 用于处理JSON格式的文件,支持import导入语法。
- xml-loader: 用于处理XML格式的文件。
- csv-loader: 用于处理CSV格式的文件。
plugin插件:用于扩展webpack的功能。
常用plugin:
- html-webpack-plugin : 生成html文件,而且会自动引入所依赖的打包好的js代码
- uglifyjs-webpack-plugin : 用来缩小(压缩优化)js文件
- mini-css-extract-plugin :取代style-loader的作用且打包的时候打包到css文件中
- optimize-css-assets-webpack-plugin :用于优化css资源的插件。
webpack的module、bundle、chunk分别指的是什么
module是Webpack处理的单个文件,代表了应用程序的组成部分。
bundle是由Webpack生成的最终输出文件,它包含了所有模块的代码和资源。
chunk是逻辑上的代码块,表示一组相互依赖的模块。它可以根据需要进行拆分和加载。
37. 用过node吗?怎么用的?
-
JavaScript有ECMAScript BOM DOM组成
-
node.js 由 ECMAScript 和 Node模块API 组成
-
node是
js
的后端运行环境。 -
浏览器是
js
的前端运行环境。 -
node环境中没有bom和dom(文档对象的顶级)的概念
38.项目优化:tree-sharking
tree-sharking是什么 : es6 推出了tree shaking机制,tree shaking就是当我们在项目中引入其他模块时,他会自动将我们用不到的代码,或者永远不会执行的代码摇掉,在Uglify阶段查出,不打包到bundle中。
Tree Shaking 只支持 ESM 的引入方式,不支持 Common JS 的引入方式。
- ESM: export + import
- Common JS: module.exports + require
项目中如何配置tree-shaking?
// 开发环境配置tree-sharking
// webpack.config.js
module.exports = {
// ...
mode: 'development',
optimization: {
usedExports: true,
}
};
//生产环境配置tree-sharking
// webpack.config.js 生产环境下只需要把mode配置成‘production’即可
module.exports = {
// ...
mode: 'production',
};
//根据环境的不同进行配置以后,还需要在 package.json 中,添加字段:**sideEffects: false,告诉 Webpack 哪些代码可以处理
但是!!!!!
对于那些直接引入到 js 文件的文件,例如全局的 css,它们并不会被转换成一个 CSS 模块。
// main.js
import "./styles/reset.css"
例如这样,在打包后,打开页面,就会发现样式并没有应用上,原因在于:上面我们将sideEffects 设置为 false后,所有的文件都会被 Tree Shaking,通过 import 这样的形式引入的 CSS 就会被当作无用代码处理掉。
为了解决这个问题,可以在 loader 的规则配置中,添加 sideEffects: true,告诉 Webpack 这些文件不要Tree Shaking。
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
sideEffects: true
}
]
},
};
39.一次渲染10万条数据:
setTimeout
先将10万条数据10个一堆分成一万堆(相当于转化成二维数组),然后对一万堆数据进行for循环,循环中在setTimeout钟解构赋值,setTimeout间隔时间设置为20*i 一次拿20条
requestAnimationFrame
先将10万条数据10个一堆分成一万堆,然后写个方法利用requestAnimationFrame的刷新频率,在requestAnimationFrame中取出一项拼接下一项,递归调用这个函数
触底加载数据
先将10万条数据10个一堆分成一万堆(相当于转化成二维数组),加载的时候,把二维数组的第一项取出来,拼接到要展示的表格数据中去,拼接展示以后,二维数组的第一项的数据要删除;触底加载相当于把二维数组的每一项取出来用,用完return停止即可
表格渲染 (用到插件VXETable)
虚拟列表实现
首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除
40.分片上传大文件 断点续传
首先是跟后端约定好,每个分片是多大。
但是我们要如何选择一个合适的分片呢?因此我们要考虑如下几个事情:
1.分片越小,那么请求肯定越多,开销就越大。因此不能设置太小。
2.分片越大,灵活度就少了。
然后要判断文件大小,如果文件还没有一个分片大,那就直接走单文件直接上传的逻辑。否则就走分片上传的逻辑。我们约定的大小是5mb
首先将文件md5加密,获得加密的md5值,然后切片,(字节流)slice方法来切割的。切完片过后呢,开始走上传,这个时候我们做的秒传功能就体现出来了。在第一个分片里带上我们文件的md5值,后端判断,这个文件是否已经上传过,如果上传过,就直接返回一个标识符,就不用继续上传 ,直接秒传成功
假如没有,然后开始上传,上传使用的是并行上传。这里需要判断是并行上传还是串行上传。如果是串行上传的话,就对那个分片数组进行for循环,用async/await进行控制。如果是并行上传,就使用promise.allSettled来控制,这个api可以接收一个promise数组,然后并行执行里面的promise,然后返回一个结果数组,这个数组里面的每一项正好对应了那个promise数组里面的每一项promise的结果。
全部上传完成过后呢,会调用一个接口,在这个接口里后端会返回给我,他有哪些分片没有接收到,在我传给他的第一个分片中,已经告诉了他这个文件一共多少片,然后在上传每一片的时候,会带一个这一片是第几片的参数,也就是index,所以他能知道有哪些分片他没接收到。
如果真的有分片没有接收到。就得走续传的逻辑,这个时候我再重新上传,但是这次的重新上传,就只会上传上一次上传失败的那些分片,而不是全部重新上传。这次上传完过后,再去请求那个最后的接口,让后端告诉我他接收完了吗。如果接收完了,文件上传就结束了。如果没接收完。还是继续