基础面试题汇总(更新中)
目录
H5
H5新特性有哪些
H5的新特性大概是有这样几个,
最常见的就是我们这个语义化标签,然后是这个localstorage和sessionstorage。
然后音视频标签,矢量图标svg和这个canvas画布
除此之外,比较少提到的还有这一个web worker
谈谈对语义化的理解
我对语义化的理解其实就是用正确的标签去做正确的事。
实际的使用其实没有什么特别的要点,他毕竟就只是一个标签。
但是实际的项目中用这一个语义化标签对页面的展示是有很大的作用的。
比较经典的一些应用场景,就比如说我们页面的样式丢失了,使用了这个语音化标签呢,在这种情况下也能够保持整体页面的结构的清晰和完整。
同时在跨平台的网页渲染中,比如像这个移动阅读器或者是盲人阅读器,使用语音化的标签可以更好的辅助这些终端设备对网页进行解析。
还有在这一个语音化的网页中。爬虫可以更有效地爬取这个网页信息。可以有效的提升页面的SEO,索引擎优化。获得更好的这一个搜索引擎排名。
而且在后期维护中,语义化标签的带来的代码可读性可以对维护提供一定的帮助。
谈谈H5的存储方式
H5的储存方式通常就是local storage和session story这两种。
除此以外,还有一些就是离线缓存,webSQL,和索引型数据库;但这三个我就不是特别的了解。
通常在使用中和这一个web storage去比较的存储方式的话,就是这个浏览器本身的这一个cookie。
像这个local storage和这个session storage的区别就主要是他们两个的生命周期。local storage就是本地缓存嘛,他的这个生命周期会一直持续到你手动对他的这个数据进行一个删除。而这个session storage就是会话缓存嘛,通常他的生命周期就是在这个会话窗口关闭的时候就结束了,就不需要去手动的进行一个删除操作。而这个cookie token session这些,就属于是浏览器自带的这个存储方式。与H5的web storage的存储方式相比呢,他们的存储空间更小,读取性能也稍微差一些,但是他们拥有更好的兼容性。毕竟这一个webstorage是在h5的时候才提出的新特性嘛。所以IE8之前的版本就不是太支持。而且sessionStorage和localStorage不会把数据发给服务器,仅在本地保存,而cookie可以在浏览器与服务器之间来回传递
- cookie,session,token:
在我看来,cookie session是存储机制,而token是一段特殊的数据。
Cookie是存储在客户端,也就是我们的浏览器,Session是存储在服务端,也就是后台服务器。Token是服务端验证了我们发送的这个登录请求信息的正确之后返回的一个身份令牌,通常会存储在这个cookie中。
这三个的应用场景不完全相同,不能相互取代
谈谈对webworker的理解
webworker的实质就是开启了一个新的子线程,用来处理数据量比较大的数据,或者是比较复杂的业务逻辑逻辑。
主要目的还是提升这个页面的整体性能。避免这种复杂的js操作去阻塞它这个呃主线程,也是我们常说这个UI线程的加载嘛。这样用户感觉就会觉得这个页面加载速度非常的快。同时它在子线程中是不能进行这个DOM操作,也就没有违背这个js单线程的设计理念,避免这一个复杂的DOM操作可能产生的同步问题。比较常用的两个api的话,是两个message ,通过postmessage去发送消息,onmessage接受这个消息,来实现这个子线程和主线程之间的交互。
CSS
谈谈CSS3的新特性
CSS3的新特性太多了,主要的比如像这个阴影,过度,形变,动画,颜色函数,盒子模型,弹性布局,一些新的伪类选择器,我经常用的就这些吧。
还有一些像什么多重背景啊,web字体呀,就了解的比较少了
如何将一个元素水平垂直居中
大体上有两种实现思路吧。
第一大种就是通过这个绝对定位去实现,比如说使用绝对定位之后,将他的这个top和left设置50%,然后去使用这个Transform的translate,让他去自动去偏移50%。或者也可以用margin去手动偏离50%嘛。
也可以在使用这个绝对定位之后,通过margin对它的上下左右分别置0。
第二种思路就是用这个布局去实现。
像用这个flex布局的Justify-content和align-items去实现
清除浮动
清除浮动,我们要先搞清楚清除浮动的目的嘛。
他实际上就是为了避免这个元素浮动之后,他父辈的这个高度丢失的问题。
这样我们就拥有了一个最粗暴的思路,我们就直接对他父辈设置一个固定高度嘛,他就不会丢失这个高度了。但是这样的话就效率非常低下。
那就还有一个比较简单的思路嘛,用这个clear both,去使他的内部不受到外部的元素影响。但这个时候他同时也会使这个margin失效。
所以最常用的思路了,实际上是将浮动的元素和普通元素进行一个隔离,也就是在其间添加一个块级元素,也就是所谓的隔墙法;在实际应用中,我们就可以对一个类设置它的伪类after,然后给这个伪类设置块级样式,固定高度,不可见,来实现隔墙法的封装。在我们需要清除浮动的时候去调用这个类,来清除浮动。
还有这一个overflow hidden也是可以实现清除浮动的效果的。
谈谈样式的优先级问题
CSS样式的优先级可以分为五大类。
第一类!important,无论以何种方式引入,何种选择器,它的优先级都是最高的。
第二类引入方式,行内样式的优先级要高于嵌入和外链,若嵌入和外链使用的选择器相同就看它们在页面中插入的顺序,后面插入的覆盖前面的。
第三类选择器,选择器的优先级为id选择器 >类选择器 > 标签选择器 > 通配符选择器。
第四类继承样式,是所有样式中优先级比较低的。
第五类浏览器默认样式,优先级最低。
盒子模型box-sizing
通常我们将div标签视作一个标准的盒子,由内容,内边距,边框,外边距组成
而box-sizing则是用于修改盒子模型,一般使用两个属性
border-box:惊奇盒子模型,使得盒子标签的width属性=内容+内边距+边框
content-box:标准盒子模型,使得盒子标签width属性=内容
Js
谈谈Js的数据类型
JavaScript的基础数据类型,有number,string,Null,Undefined,布尔值,在Es6之后还添加了这个Symbol,和这个bigint;
引用数据类型有数组,函数,对象,自定义类等;
但是在不同的数据类型间使用运算符的时候,不同类型间又会发生隐式转换来处理这类操作
有几条相对明确的规则:
1. String(字符串类型)与任何数据之间相加运算都会转化为字符串类型,
eg> console.log('name'+true) // 输出: nametrue
2. 除string以外的数据类型 , 与number数字类型间进行相加运算的时候,都会转换为数字类型,
返回的结果通常为两类, 一种是有明确的返回值的,这种会直接返回计算后的具体结果; 另一种是数字类型与undefined进行计算,会返回NaN,表示返回了一个数字类型的值,但是不清楚具体的大小
2.1 null(空数据类型) , boolean(布尔值) 与 number(数字类型) 间的运算 ,空值与布尔值会隐式转换为数字类型后再进行运算 ,甚至布尔值之间的运算也是通过隐式转换成数字后再进行的 , true对应1 , false对应0 ,null对应0
eg>
let a = true
let b = false
console.log(a+1); // 2
console.log(b-1); // -1
console.log(a+b); // 1
3.减号运算,这种情况比较特殊,字符串的转换优先级变低了,会被优先转化成数字类型 ,并且没有具体大小
eg>
console.log('qwe'-null); //NaN
console.log('qwe'-'qwe'); //NaN
console.log('qwe'-'1'); //NaN
console.log('asd' + 'qwe'- null); //NaN
console.log('asd' + 'qwe' * null); //asdNaN
console.log('qwe'- null +'asd'); //NaNasd
按照上面的运行结果进行总结, 我们大概可以得出一个结论:
在加法运算中,字符串的转化优先级最高,
在减法运算中,数字的转化优先级最高,
在混合运算中,按照算数优先级化简至二元运算后,再按照上面的标准进行判定
4.逻辑运算符对变量种类的影响
var a = 666< 0 || typeof(666+'') //string
var a = 666< 0 || typeof(''-666) //number
var a = 666< 0 && typeof(''-666) //false
null与undefined
js的作者在设计js的时候参考了java,所以先创建了null,同时就出现了两个问题
1. null像在java中一样,被当做是一个对象,而作者觉得,一个表示`无`的值最好不要是一个对象
2. null在发生数据不匹配的时候,会自动装换类型,隐式变换为0
为了弥补这些缺陷,才在后来又设计了undefined
JavaScript的最初版本是这样区分的:
null是一个表示"无"的对象(空对象指针),转为数值时为O ;
undefined是一个表示"无"的原始值,转为数值时为NaN.
Js的作用域问题
1.除了函数外,js是没有块级作用域。(es6之前)
2.作用域链:内部可以访问外部的变量,但是外部不能访问内部的变量。
注意:如果内部有,优先查找到内部,如果内部没有就查找外部的。
3.注意声明变量是用var还是没有写(window .) //实例见下方`面试实例3`
4.注意:var有变量提升的机制【变量悬挂声明】,在定义前输出定义的变量会输出 undefined ,而let和const在这种情况下会直接报错 //实例见下方`面试实例2`
5.同名选取优先级: 声明变量 > 声明普通函数 > 参数 >变量提升 //实例见下方`面试实例3`
//代码1
function f1(){
var a = b =10
}
f1()
console.log(b) //10
console.log(a) //报错
//因为实际上在f1中执行的代码 是这样的:
{
var a = window.b = 10
}
未声明变量相当于全局变量
无论是使用let 还是 const去声明a ,实际上都没有对b进行声明,所以结果都与var一致
代码题
//面试实例1: 请按输出顺序写出函数a()的输出
function a(){
var b = 1;
function c(){
console.log(b);
var b = 2
console.log(b)
}
c()
console.log(b)
}
a()
//undefined 2 1
//面试实例2 :请按输出顺序写出函数的输出
var name = 'a';
(function () {
if (typeof name == 'undefined') {
var name = 'b ';
console.log('111' + name);
} else {
console.log('222' + name)
}
})()
//111b
这里蛮有意思的,虽然没有执行var name = 'b',但是仍然进行了变量提升
就像是下面的代码
console.log(a); //undefined
if(false){
var a=1,
}
console.log(a); //undefined
//面试实例3 :请按输出顺序写出函数的输出
function fun(a) {
var a = 10;
function a() { }
console.log(a);
}
fun(100);
//10
闭包
JavaScript里面有一句非常经典的话,就是说函数即闭包嘛。
或者说,闭包是一个函数加上创建函数作用域的链接,并关闭了函数的自由变量
在函数作用域内呢,你可以访问到函数作用域外的变量。但在这个函数作用域外就访问不到函数作用域内的变量。
就实现了这样子一个避免全局变量污染。
但是闭包的变量会贮存在内存中,造成内存损耗; 可以通过在使用后设置该变量为空来解决这个问题
//内存泄漏问题是在低版本的IE浏览器中发生的,并不是一个常驻的问题
代码题
//1.判断下列代码的输出
var a = 10;
var a = 100;
function f1() {
var a = 1;
function f2() {
var a = 0;
return function () {
console.log(a++)
}
}
var f3 = f2()
f3()
f3()
}
f1()
输出结果: 0 1
结果分析:
function f1() {
var a = 10;
function f2() {
var a = 0;
function f3() {
console.log(a)
}
f3()
}
f2()
}
f1()
将上题命名为A,本段代码命名为B
实际运行后,B输出0,说明闭包中,对同名变量的获取是优先取最近的外部变量,这也是在实际开发中闭包的重要用法
此时回来看A,f3 = f2(),实际上就是获取了f2返回的方法,此时这个方法访问到的外部变量是f2中的a,所以,f3中的a的初值等于0,并且由于f2只执行过一次,且在f3中没有对其进行初始化等类似的操作,所以,a++会输出a再自增加,结果累加,最终输出 0 1;
//2.判断下列输出
function f1() {
var a = 1;
return function () {
console.log(a++)
}
}
var a = f1();
a();
a();
var b = f1();
b();
b();
b();
var c = f1();
c();
a();
输出结果: 1 2 1 2 3 1 3
结果分析:
一个闭包中对器变量的修改不会影响到其他闭包中的变量;
所以这里进行了三次闭包后形成了三个不同的作用域,变量在三个作用域中分别累积;
//3.判断控制台输出内容
function fun(a,b){
console.log(a,b);
return {
fun:function(c){
return fun(c,a)
}
}
}
const x = fun(0);
x.fun(1);
x.fun(2);
x.fun(3);
let y = fun(0).fun(1).fun(2).fun(3).fun(4)
输出结果:
0 undefined
1 0
2 0
3 0
0 undefined
1 0
2 1
3 2
4 3
结果分析:
首先,进行fun()的指向的分析,按从上到下的顺序简称三个fun为f1,f2,f3;
f1简单判断可知是全局函数,f2是f1返回对象上的属性,f3是f2的返回值,但是f3由于闭包的关系,向上回溯原型链,找到f1,所以,f3和f1一样指向全局作用域;
在赋值的时候进行了一次函数的调用,x = fun(0), 所以x={fun:function(c){return fun(c,0)}},同时输出`0,underfind`,后续的x.fun(c)都是调用的这个x,所以输出的第二个数都是0;
而在y的链式执行中,每次返回的fun(),都会将上一次输入的形参c转变为本次执行的形参a,所以才会不断改变第二个值的输出;
Js对象问题
js对象注意点:
1.对象是通过new操作符构建出来的,所以对象之间不想等;
2.对象注意:引用类型;
3.对象的key都是字符串类型;
4.对象如何找属性|方法:先在对象本身找=>构造函数中找=>对象原型中找=>构造函数原型中找=>对象上一层原型查找
//面试题
var b = {
key: 'a'
}
var c = {
key: 'c'
}
a[b] = '123';
a[c] = '456';
console.log(a[b]); //456
因为在实际运行中,b和c都是object,会被解析为对象类型但没有具体值,而对象的key都会被解析成字符串类型,所以两者实际上被解析成同一字符串进入a中,可以用加上下列代码来验证这个结论
for(var k in a){
console.log( typeof k)
} //[object Object]
原型链与原型
Js在设计之初,参考java设计了new,但是new创建的对象没法直接的共享方法与属性,所以引入了prototype这个属性来解决这个问题
就是我们要谈论这个原型原型链嘛,我们就不得不来谈一下这个构造函数。
就我们都知道,就是当我们在说过着函数去创建这个对象的时候,有一个__proto__的属性,这个属性呢它就是指向了我们的这一个构造函数,而这个构造函数上又有一个属性就是这个prototype,也就是我们说的这一个实例对象的原型。
这个原生概念呢,其实很像就是我们所说的模板嘛,这个实力对象的属性方法就是继承自这一个模板。所以我们就说这一个构造函数就是实例对象的原型对象。
我们用一个对象上的__proto__去向上回溯他的原型对象,同时不断的去调用这个属性,再向上回溯,直到最后返回null,而这一个原型链形容的就是这样的关系;
对象查找属性或者方法的顺序:
先在对象本身查找-->构造函数中查找--〉对象的原型-->构造函数的原型中
function f1(){
console.log(123);
return 'aaa'
}
console.log(f1) //function f1(){console.log(123);return 'aaa'}
console.log(f1()) //123 aaa
console.log(new f1())//123 f1{}
//面试题
function Foo() {
getName = function () {
console.log(1)
}
return this
}
Foo.getName = function () { console.log(2) }
Foo.prototype.getName = function () { console.log(3) }
var getName = function () { console.log(4) }
function getName() { console.log(5) }
Foo.getName(); //2
getName(); //4
Foo().getName(); //1
getName(); //1
new Foo().getName(); //3
这道题首先执行Foo.getName() , 上方有定义, 对应输出2 , getName 优先寻找变量声明式函数 ,对应输出4,
Foo().getName() 首先执行Foo() ,重新对getName进行定义 ,再输出 ,输出1
getName() , 刚刚重定义完 , 现在还是输出1
new Foo().getName ,通过new与构造函数进行 ,首先根据构造函数生成新的对象 , 因为原型上的getName被重写了 ,所以新的对象上getname的输出为3
Js判断变量是不是数组
1. isArray()
2. instanceof
3. 原型prototype ,通过转化字符串后查询字段来判断
4. isPrototypeOf()
5. constructor
var str = '你好';
var arr = [1, 2, 3];
console.log(Array.isArray(arr)); //true , 严格判断
console.log(arr instanceof Array); //true , 并不那么严格 , 是根据原型去判断的 ,将Array换成Object也会返回true
console.log(Object.prototype.toString.call(arr)); //[object Array]
console.log(Object.prototype.toString.call(arr).indexOf('Array') != -1); //true
console.log(arr.constructor.toString().indexOf('Array') != -1 )
Js的垃圾回收机制(GC算法)
我们现在最常见的这个gc算法呢就是
标记清除法,当一个变量进入这个执行环境的时候,这一个系统会给他标记'进入环境'记号。然后离开时会给他标记一个'离开环境'的记号。
当这个垃圾回收机制运行的时候呢,它会对所有的变量进行一个标记。然后再清除掉那些进入了环境的变量上的标记。最后把仍留有标记的这些变量的统一的去进行一个回收。
然后以前还有一个引用计数的算法,每次使用到这个变量时,就给它添加一个计数。然后回收机制运行的时候呢,会回收计数为0的变量,也就是表示这个变量没有被引用。但是那种方式有可能会有一些记号没有被及时清除,所以造成一个呃内存泄露的问题。
==
与===
的区别
== 只比较值
=== 除了比较值,还要比较数据类型
但是==在进行前比较之前需要对数据类型进行隐式转换,
1. null==undefined ,返回true ,这也说明一件事,null与undefined并没有进行隐式转换,所以 null == 0 ,会返回false
2. string与number ,优先将string转换为number ,值相等时 ,返回true
3. boolean与number ,优先将boolean转换为number ,值相等时 ,返回true
4. 引用数据类型与基础数据类型的比较 ,会将引用数据类型转换为对应的数据类型后再比较
5. 引用数据类型之间的比较, 引用数据类型的比较实际上是比较物理存储的地址,所以即便值相同,也不会返回true
隐式转换是通过 Object.prototype.valueOf() 实现的
所以在实际的开发场景中,建议尽量使用 === 进行比较
宏任务与微任务
js的执行流程:
同步事件 => [事件循环(异步操作)]
[事件循环] =>{ 微任务 =>宏任务 }
微任务:promise.then() 等
宏任务:setTImeout() 等
在事件循环中,永远是先执行完所有的微任务后再执行宏任务
在我看来,可以将Promise这种,执行结果有不同状态的事件称为微任务
而宏任务则是无论何时都...那如果Promise阻塞了一段时间,是不是settimeOut的等待时间等于我们设定的时间加上延缓的时间呢?
跨域问题的前端解决方案
防抖节流
防抖的话,它其实就相当于是说避免一个函数在短时间内被多次触发,比较经典的一个应用场景,就是在这一个用户去进行用户名设定的时候,查看这个用户名是否已经被占用,一般会在用户停止输入一两秒之内向后台去发送请求,查看是否占用了用户名。在这一两秒之内呢,用户如果继续进行了这个输入,这个计时就会被重置。像这个鼠标的滑动事件和这个页面的滚动事件,也经常会有这一个防抖的需求。
而节流的话就相当于是在这个事件被触发的情况下。避免再一次触发这个事件。比较经典的就像是那个呃我们这一个验证码,手机验证码,你发短信之后,它会有一个再次请求发送验证码的这一个按钮,它会进入一个倒计时。这就相当于是一个节流。
重绘回流
重绘回流就是在我们改变样式改变的时候,DOM进行的操作。
重绘就是指修改页面样式时,页面的布局没有改动,此时DOM就会只去修改页面的样式而不改变页面布局
而回流是样式的修改影响到了页面布局,此时页面整体会去重新渲染
对深浅拷贝的理解,实现深浅拷贝的思路
前拷贝实际上就是拷贝了一个索引,而这索引指向是原来的这一个物理存储地址。所以在浅拷贝出来的这个对象中去修改数据,被拷贝的对象也会受到影响。反过来也是一样的,修改这一个备拷贝的对象,这个拷贝出来的对象也会受到影响。而深拷贝是将这个被拷贝的对象中的数据进行复制,然后存储到一个新的物理地址中。这样两个对象就相互不会受到对方影响。
浅拷贝的实质就是索引的复制,所以从原理上说,将拷贝的对象的属性索引按顺序的复制到新的对象上就可以了,最基础的实现就是
let oldObj = {nub:'123';}
let newObj = oldObj
除此以外,像是 Object.assign() ,扩展运算符也可以实现一定程度上的浅拷贝
深拷贝的实现方式:
1. 使用JSON.parse()与JSON.stringify()
2. 深拷贝实际上就是 newObj[key] = oldObj[key]不断重复的过程
因为上面的代码只能实现一层的深拷贝,后续仍然是浅拷贝,所以需要进行递归,只有在拷贝对象是基本数据类型是才进行拷贝,
否则将整个拷贝对象传入递归函数中,直至每层都被分解为基本数据类型,也就是obj.hasOwnProperty(key)为true时,进行拷贝
具体实现方法:
合并对象的方法
1. Object.assign(a,b) :可以将a,b合并为一个新的对象并返回,不改变原对象
2. [...a,...b] :可以将a,b合并为一个新的对象并返回,不改变原对象
3. 手写一个:
let a = {x:1,name:'xiaoming'}
let b = { y:2 , age:'18'}
function f1(arr1,arr2){
for(key in arr2){
arr1[key] = arr2[key]
}
return arr1
}
console.log(f1(a,b))
页面性能优化的策略
1.数据懒加载
2.路由懒加载
3.封装组件,提高代码的复用性复用
4.防抖和节流
5.webWorker异步加载
6.减少重绘回流的操作,例如使用transform书写动画效果,在for循环结束后再去操作dom等
7.图片压缩、合并(精灵图)、使用字体图标代替小图片、使用base64、图片懒加载
8.使用CDN网络托管
9.利用缓存来缓存文件
10.减少闭包,递归优化,使用高效的算法
11.webpack优化:去除无用代码treeShaking、组件按需加载、使用chunck、模板预编译等
常见的遍历方法和相关问题
多种获取key的方法
1.Object.keys(),可以遍历自身可以枚举属性
2.for-in 遍历可枚举属性
3.Object.hasOwnProperty(),配合for-in使用,用于判断是否存在自有属性
4.getOwnPropertyNames() 返回可枚举属性和不可枚举属性,不包括prototype属性
typeof和instanceof的区别
主要用于判断基本数据类型,返回值有number、string、boolean、function、undefined、object 六个。
但是除了对象以外的两个引用类型和null都会返回object,
所以会用instanceof来判断应用类型是否为对象,instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上,所以可以用来判断引用数据类型的具体种类,但是基本数据类型上__proto__返回null,所以没有办法用来判断基本数据类型;
除此以外的还有constructor可以用来判断数据类型
数组去重的方法
1. new Set() ,会返回一个对象,需要转化回一个数组
var arr1 = [1, 2, 3, 2, 4, 1];
console.log(Array.from(new Set(arr1)));
console.log([...new Set(arr1)]);
2. 通过indexOf,设置空数组,遍历原数组,新数组中有则进行下一轮,没有则加入
3. 通过sort排序后,设置空数组,比较arr[i]与arr[i+1],不同的加入新数组
new操作符
1.创建了一个空的对象
2.将空对象的原型,指向于构造函数的原型
3.将空对象作为构造函数的上下文(改变this指向)
4.对构造函数有返回值的处理判断
手写new函数:
function create(fn, ...args) {
//1.创建了一个空的对象
var obj = {}
var obj = Object.create({})
//2.将空对象的原型,指向于构造函数的原型
Object.setPrototype0f(obj, fn.prototype);
//3,将空对象作为构造函数的上下文(改变this指向)
var result = fn.apply(obj,args);
//4。对构造函数有返回值的处理判断
return result instanceof Object ? result : obj
}
js的继承方式
1. es6通过extends来继承
2. 原型链继承
3. 借用构造函数,通过call去修改构造函数的指向
call,apply,bing的区别
共同点:都可以改变函数体内的this指向
1. apply,call:改变指向后会立即执行一次函数;bind:不会立即执行,它返回的其实是一个函数
2. 参数不同: apply第二个参数是数组。call和bind有多个参数需要挨个写。
事件捕获与事件冒泡的区别是什么,如何阻止事件冒泡
1、事件捕获
捕获型事件(event capturing):事件从最不精确的对象(document 对象)开始触发,然后到最精确(也可以在窗口级别捕获事件,不过必须由开发人员特别指定),换句话说事件触发是从顶级父容器到最后一个子容器的一个过程。
2、事件冒泡(事件的默认状态)
冒泡型事件:事件按照从最特定的事件目标到最不特定的事件目标(document对象)的顺序触发,这个与捕获阶段正好相反,从离用用户最近的子容器到最远的顶级容
通过添加时间监听实现事件的捕获
element.addEventListener(event, function, useCapture);
阻止事件冒泡的方法:
1.event.stopPropagation();
事件处理过程中,阻止了事件冒泡,但不会阻击默认行为(执行超链接的跳转)
2.return false;
事件处理过程中,阻止了事件冒泡,也阻止了默认行为(不执行超链接的跳转)
还有一种与冒泡有关的:
3.event.preventDefault();
事件处理过程中,不阻击事件冒泡,但阻击默认行为(它只执行所有弹框,却没有执行超链接跳转)
4.在vue中使用.stop修饰符
<a v-on:click.stop="doThis"></a>
5.在小程序中使用catch:tap
如何阻止事件的默认行为
event.preventDefault();
事件处理过程中,不阻击事件冒泡,但阻击默认行为(它只执行所有弹框,却没有执行超链接跳转)
Es6
谈谈Es6的新特性
简单的将es6的新特性分类,我们可以将其分为两大类,新增的模块和对原有模块的封装或优化
新增的模块有,模板字符串,let和const关键字,symbol数据类型,set和map两种数据结构,箭头函数
对原有模块的封装或优化的部分有,promise对象,for-of,解构赋值
Promise 有哪些特点?
Promise本身就是为了解决回调地狱问题,并且在es6中被标准化了
最为显著的特征自然是它不受外界因素的影响,Promise对象的状态取决于异步事件的状态;
Promise的状态是单向变动的,只能够由等待态进入到另外的两种状态,同时,Promise对象一旦转变,那么在任何时候去请求这个对象返回的结果都是一样的;
Promise也同样有一些缺点,比如说一个Promise处于Pending时无法查询到它究竟进行到了哪一步,还有Promise一旦开始便无法取消
箭头函数的特点
箭头函数根本没有自己的this ,箭头函数内部的this指向声明这段代码的作用域。以此箭头函数有着一些特殊的衍生特性,比如不能用箭头函数作为构造函数配合new关键字,又或者可以使用箭头函数解决setTimeOut等方法丢失this的问题
1.箭头函数都是匿名函数
2.箭头函数没有自己的this指向
箭头函数在全局作用域声明,所以它捕获全局作用域中的this,this指向window对象。
箭头函数的 this 永远指向其上下文的 this ,任何方法都改变不了其指向,如 call() , bind() , apply()。
3.箭头函数不能作为构造函数使用
4.箭头函数不绑定arguments,取而代之用rest参数…解决
将数组数据转化为树结构
实现思路:通过寻找parentID找到对应的父辈,再通过(parent.child[]||(parent.child=[])).push(item),
即有子类数组的情况添加,没有子类数组的情况创建并添加
//实例
let data = [
{ id: 0, parentId: null, name: '生物' },
{ id: 1, parentId: 0, name: '动物' },
{ id: 2, parentId: 0, name: '植物' },
{ id: 3, parentId: 0, name: '微生物' },
{ id: 4, parentId: 1, name: '哺乳动物' },
{ id: 5, parentId: 1, name: '卵生动物' },
{ id: 6, parentId: 2, name: '种子植物' },
{ id: 7, parentId: 2, name: '蕨类植物' },
{ id: 8, parentId: 4, name: '大象' },
{ id: 9, parentId: 4, name: '海豚' },
{ id: 10, parentId: 4, name: '猩猩' },
{ id: 11, parentId: 5, name: '蟒蛇' },
{ id: 12, parentId: 5, name: '麻雀' }
]
function transTree(data) {
let result = []
// let map = {}
if (!Array.isArray(data)) {//验证data是不是数组类型
return []
}
// data.forEach(item => {//建立每个数组元素id和该对象的关系
// map[item.id] = item //这里可以理解为浅拷贝,共享引用
// })
data.forEach(item => {
// let parent = map[item.parentId]
let parent = data[item.parentId] //找到data中每一项item的爸爸
if (parent) {//说明元素有爸爸,把元素放在爸爸的children下面
(parent.children || (parent.children = [])).push(item)
} else {//说明元素没有爸爸,是根节点,把节点push到最终结果中
result.push(item) //item是对象的引用
}
})
return result //数组里的对象和data是共享的
}
console.log(JSON.stringify(transTree(data)))
Vue
谈谈对MVC与MVVM的理解
这俩都是软件设计模式
MVC就是Model-view-Controller,比较经典的应用像是java的三层架构嘛,View-Server-Dao
MVVM则是Model-view-viewModel
MVVM与MVC的最大区别是:实现了View和Model的自动同步,也就是当Model的数据改变时,我们不用再自己手动操作Dom元素,来改变View的显示,而是改变数据后该数据对应View层显示会自动改变。
MVVM并不是用VM完全取代了C,ViewModel存在目的在于抽离Controller中展示的业务逻辑,而不是替代Controller,其它视图操作业务等还是应该放在Controller中实现
VueRouter路由的机制
常见的路由模式就是hash和history两种
这两种模式最显眼的区别就是#,hash有,history没有
在一些app中,可能对url的格式有严格要求,携带# 的url可能无法正常使用,就需要用到history模式
但是使用history时,url格式是没有# 的,会被浏览器当做后端链接去请求数据,所以刷新的时候会找不到页面资源,产生404。
前端的解决方法:在出现资源未找到的情况下重定向到网站根目录上的index.html
VueRouter路由守卫
vue-router一共给我们提供了三大类钩子函数来实现路由守卫:
1. 全局钩子函数(beforeEach、afterEach)
2. 路由独享的钩子函数(beforeEnter)
3. 组件内钩子函数(beforeRouterEnter, beforeRouterUpdate, beforeRouterLeave)
通常来讲,全局守卫是最为常用的,最常见的使用场景就是登录的跳转,通过判断登录令牌的有无来确定是否进行跳转
VueRouter路由重定向
通常重定向有两种应用场景吧
第一种就是我们在判断这个用户到底有没有登录的情况,比方说在用户跳转到他的信息展示页面之前,判断是否已经存在这个token,Token是否有效,在这个token不存在或者是无效的情况下,我们就需要进行路由的重定向,让他跳转到登录页面去,并且通知他您需要重新登陆。这种情况下的路由重定向实际上是通过beforeEach,也就是全局路由守卫去实现的。
而另一种的话就是在我们那个使用这个history模式,刷新会造成一个404问题,这种情况下我们就需要在router.js 文件里面去配置路由并配置redirect,去重定向到网站根目录上的index中。
Vue生命周期
通常来说,我喜欢把这生命周期分为三组。
第一组就是我们在页面渲染完成之前,一对Create一对Mounte。
在阶段就是正式开始,我们这个生命周期,此时还没有做数据代理,没有获取到这个data上的数据,然后到我们这个created完成了初始化,就可以从Vue中去读取到实例的具体的数据。
然后到这个Before mount阶段,进行虚拟DOM的加载,此时页面尚未挂载完成,不能去操作DOM; Mounted阶段,虚拟DOM转化为真实DOM,这个时间就可以获取并操作这个DOM对象了。
然后第二组就是我们这个update。这个阶段就就是我们用户和页面发生交互的阶段;在before update中去生成虚拟DOM,并且和原本DOM进行比较。Updated阶段就已经完成了我们这一个DOM的更新。
第三组的话就是我们这个Destroy,通常我们会在这一个BeforeDestroy中去做一些消费前的准备,像是发布订阅的这些事件的解绑这些工作,而在这个Destroyed阶段,这个vue的生命周期就已经宣告正式结束了,这个vue的实例对象已经不复存在了。
Vue组件间传值
1.父子传值。
2.事件总线
3.vueX
4.缓存传值
主要的应该就是上面这四种吧。
其他的还有像这种ref他在某些情况下也是可以去获取这个组件中的数据。
数据持久化
说白了,数据持久话就是将这个数据存储到本地嘛。
根据需要场景去选择这个cookie local storage和这一个session storage。
但你可以去这个呃插件嘛,比较经典的像是vuex的persistedstate,原理上是一样的;
Vue异步渲染原理
vue在异步渲染的时候,会将所有引起页面变化的部分统一的存放到这个异步渲染的API中。直到页面的同步代码执行完之后才会去进行这个异步操作。然后异步回调执行的时候,将所有的同步代码中需要渲染变化的部分合并,只执行一次操作。
主要的目的的话还是去节省这个机器资源,提升性能和用户体验。比较像节流吧。
v-if和v-for的优先级
当vue处理指令时,v-for比v-if具有更高的优先级,通过v-if移动到容器元素,不会再重复遍历列表中的每个值,取而代之的是,我们只检查它一次,且不会再v-if为否的时候运行v-for。
但是在vue3中,v-if具有比v-for更高的优先级
1.永远不要把 v-if 和 v-for 同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)
2.如果避免出现这种情况,则在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环
Vue双向数据绑定原理(响应式原理)
Vue.js 一个核心思想是数据驱动
数据变化更新视图(model => view)
是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()或Proxy()来给各个属性添加setter,getter并劫持监听,在数据变动时发布消息给订阅者,触发相应的监听回调,也就是通知watcher重新渲染页面
视图变化更新数据(view => model) 利用事件监听(例如input、change事件等),拿到页面上输入的最新值赋值给data
监听属性与计算属性的异同
他们两个的核心都是这个setter和getter。
计算属性呢他在进行这个页面初始化时,会获取他的这个参数,然后去进行计算,并且将他的这个计算结果呢缓存下来,在页面渲染的时候呢就直接去调用这个缓存。然后在这一个呃参数发生变化的时候,通过getter去监听这种变化,然后重新的开始计算,并返回将缓存的结果更新,之后通知页面重新渲染;
而这个监听呢,他就只是单纯的计算,而不去进行这一个数据的缓存。所以在变化次数较多的情况下,使用监听听比使用计算属性更加的节省机器性能。
反过来,使用计算属性也可以通过缓存来减少此属性变化较少时,页面重渲染的性能开销
Vue中key的作用
key实际上相当于是vue在进行这个update的时候的标志物。
虚拟DOM和真实DOM进行对比的时候,会去比较这个key,如果存在这个key,那就去比较这个节点的是否产生变化。有变化就重新渲染这个节点,没有变化的时候就直接去使用原来的这个节点。如果没有这个key就去新增上这个节点。
可以说他就是vue实现按需渲染的关键。
事件总线
简单的说吧,就是除了这一个父子传值以外,其他的这个跨组件传值的核心的思路,就是将需要共享的数据放在一个所有组件都能访问得到的地方。
事件总线实际上是把这一个vue对象,我暂且称它为bus,当做是容器,将它作为属性挂载到了vue的原型上,然后通过这个emit去发送,on去进行接收。就形成了我们说的这个发布订阅模式
Vuex
VueX有五种属性,分别是 State、 Getter、Mutation 、Action、 Module
State相当于data,用来存放数据
Getter相对应计算属性,
Mutaion和Action相当于方法(method),Mutation进行同步操作。Action进行异步操作再将异步结果提交给mutation。同时因为view本身的设计理念是避免直接去改动数据,vuex延续了这个思路,不能直接修改state中的数据,而是要通过Mutation去修改。
Module的话就相当于组件,为了避免这主要vuex文件过于臃肿,你可以通过module去引入其他的vueX文件。
vuex可以提升代码的可维护性,增加耦合,同步组建间的状态。
Vuex与事件总线的区别
本质上最大的区别:bus利用事件抛发的原理进行传递数据而vuex通过数据劫持,并且复制一份相同的_data来进行数据管理.
Vue样式隔离的原理
通过设置scoped属性
cli会对其包裹的选择器对应的dom添加唯一标识,根据标识来进行样式的添加,所以能够实现样式的隔离
Vue样式穿透
在修改第三方组件样式的同时不想取消scoped的样式隔离时使用
:deep(选择器)
/deep/ 选择器
::v-deep(选择器)
外层>>>选择器 (不能在sass和less中被解析,因此用的较少)
vue中的data为什么是一个函数
组件中的data写成函数,数据是以函数的返回值形式定义的,每次复用组件的时候,都会创建一个新的私有空间,这样的话各个组件实例都有各自的实例,如果写成对象形式,就容易造成数据污染,使得所有组件实例之间共用一份data
keep-alive
这是一个vue自带的组件
功能:缓存不活动的组件的状态,避免多次重复渲染降低性能:
常用的知识点:
1. include - 字符串或者正则表达式,名称匹配的组件会被缓存
2. exclude - 字符串或者正则表达式,名称匹配的组件不会会被缓存
3. max - 数字, 最多可以缓存多少组实例
4. 结合router , 缓存部分页面 `$router.metakeepAlice`
小程序
1.封装小程序的请求事件
/*
1.配置通用url
2.设置接收请求和发送请求的遮罩信息
3.通过promise进行封装
*/
import { BASE_URL } from "./url.js"
export const request = (params) => {
wx.showLoading({
title: '正在加载中',
mask: true,
})
return new Promise(function (resolve, reiect) {
wx.request({
url: BASE_URL + params.url, success: (res) => {
resolve(res)
},
fail: (err) => { reject(err); },
complete: (res) => { wx.hideLoading(); }
})
})
}
2.小程序有哪些参数传值的方法
//1.通过data属性去传递
console.log(e.currentTarget.dataset);
//2.通过id传值
console.log(e.currentTarget.id);
//3.通过globalData传递参数
const app = getApp();
console.log(app.globalData)
app.globalData.user = '修改了user'
//4.通过页面传值
wx.navigateTo({
url:'/pages/logos/logo?type=1'
})
//在这种方式中传递的数据,可以在新页面onLoad的时候初始化
//5.通过storage 传递参数
wx.setStorageSync("local","我是从storage中来的")
3.wxss与css相比,扩展的特性
- 响应式长度单位
rpx
- 样式导入(小程序不支持通配符*)
- 目前支持的选择器有:类,id,标签,并集,nth-child(n), ::after , ::befor
4.小程序如何确定用户的唯一性
通过unionId来确认用户的唯一性
微信小程序中的用户ID(openid与unionid)
openid
表示用户在当前应用中的唯一标示,比如小程序,微信公众号等,这些都算是一个应用,如果你有多个应用,同一个用户的openld可能并不相同
unionid
借用微信官方文档中的话来说,如果开发者拥有多个移动应用、网站应用、和公众帐号(包括小程序),可通过UnionIlD来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,unionid是相同的
5.如何在微信小程序中实现下拉刷新
- 首先在全局config中的window配置
enablePullDownRefresh
- 在Page中定义
onPullDownRefresh
钩子函数,到达下拉刷新条件后,该钩子函数执行,发起请求方法- 请求返回后,调用
wx.stopPullDownRefresh
停止下拉刷新
6.小程序的生命周期
应用的生命周期
属性 | 类型 | 默认值 | 必填 | 说明 |
---|---|---|---|---|
onLaunch | function | 否 | 监听小程序的初始化 | |
onShow | function | 否 | 监听小程序启动或者切换前台 | |
onHide | function | 否 | 监听小程序切换后台 | |
onError | function | 否 | 错误监听函数 | |
onPageNotFound | function | 否 | 页面不存在监听函数 |
页面的生命周期
属性 | 类型 | 说明 |
---|---|---|
data | Object | 页面的初始数据 |
options | Object | 页面的组件选项,同 Component 构造器 中的 options ,需要基础库版本 2.10.1 |
behaviors | String Array | 类似于 mixins 和traits的组件间代码复用机制,参见 behaviors,需要基础库版本 2.9.2 |
onLoad | function | 生命周期回调—监听页面加载 |
onShow | function | 生命周期回调—监听页面显示 |
onReady | function | 生命周期回调—监听页面初次渲染完成 |
onHide | function | 生命周期回调—监听页面隐藏 |
onUnload | function | 生命周期回调—监听页面卸载 |
onPullDownRefresh | function | 监听用户下拉动作 |
onReachBottom | function | 页面上拉触底事件的处理函数 |
onShareAppMessage | function | 用户点击右上角转发 |
onShareTimeline | function | 用户点击右上角转发到朋友圈 |
onAddToFavorites | function | 用户点击右上角收藏 |
onPageScroll | function | 页面滚动触发事件的处理函数 |
onResize | function | 页面尺寸改变时触发,详见 响应显示区域变化 |
onTabItemTap | function | 当前是 tab 页时,点击 tab 时触发 |
onSaveExitState | function | 页面销毁前保留状态回调 |
7.简述一下navigateTo ,redirectTo ,switchTab ,navigateBack ,reLaunch的区别
- wx.navigateTo():保留当前页面,跳转到应用内的某个页面。但是不能跳到
tabbar
页面 - wx.redirectTo():关闭当前页面,跳转到应用内的某个页面。但是不允许跳转到
tabbar
页面 - wx.switchTab():跳转到TabBar页面,并关闭其他所有非tabBar页面
- wx.navigateBack()关闭当前页面,返回上一页面或多级页面。可通过
getcurrentPages ()
获取当前的页面栈,决定需要返回几层 - wx.reLaunch(),关闭所有页面,打开到应用内的某个页面
8.wx小程序常用的api
1. wx.uploadFile({obj})文件上传
2. wx.downloadFile({obj})文件下载
3. wx.chooselmage({obj})选择图片
4. wx.previewImage({obj})预览图片
5. wx.setStorageSync('名',值) 存入本地存储
6. wx.getStorageSync('名',值) 读取本地存储
7. wx.checkSession() 检测token是否过期
8. wx.getUserProfile()
规定:wx.getUserProfile() API必须在事件中使用。也就是可以做个单击事件来使用它,一般是用户同意获取信息
自定义 tabBar
分包
算法基础
递归基础
递归的核心是将问题重复的部分通过函数封装起来,让他自己调用自己,知道满足停止条件
解题思路: 找出循环逻辑,终止条件,考虑初始条件
//1. 通过递归累加50~100
// 循环逻辑:{ n + (n+1) + (n+2) + }, 终止条件 :{ n 不属于[50,100] }, 初始条件: { n == 50 }
let f1 = function (n) {
if (n >= 50 && n <= 100) {
return n + f1(n+1)
}else{
return 0
}
}
console.log(f1(50)) //3825
工程化问题
移动端如何适配
首先,无论哪种方案都需要统一标准视口
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
rem
rem方案的核心概念是,根据页面的font-size,来生成对应大小的节点,也就是我们生成的节点大小是一个相对大小,会因为font-size的大小自动改变
通常使用这个方案的时候,可以使用插件cssrem,这会使开发方便的多
手动实现:
function remSet() {
let a = document.documentElement.style.fontSize = screen.width / 10 + 'px'
console.log(a)
}
remSet()
window.onresize = remSet
响应式布局
当页面满足一定条件时,显示对应的样式
@media screen and (max-width:750px){
//当视窗宽度小于750px时使用这个样式
}
除此以外还有百分百和flex布局等方案
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构