JavaScript
js有哪些数据类型
- 基本类型:string,number,undefined,null, boolean, symbol
- 引用类型:object, array, function, set, map
什么是变量提升
用var声明的变量,可以在变量声明前就先进行赋值操作。原因是在代码执行前,js解释器会进行预编译,在预编译阶段,会找到当前作用域所有已经声明的变量和函数,并且提前给他们开辟好内存空间,这样,在代码执行阶段,就能从作用域里找到已经提前声明过的变量和函数。最后重点是如果变量名和函数名相同,那么一定是函数(后提升)覆盖变量(先提升)。
什么是原型和原型链
在js中,每个函数上都有 prototype 属性,指的就是原型。而每个对象上面都有一个_proto_属性,指向了创建该对象的构造函数上的原型对象(prototype),这就是原型链。原型链作用:当对象在自己身上找不到指定的属性时,通过原型链,就可以一层层的往上查找属性,直到找到属性或者指向null为止。
什么是作用域和作用域链
作用域指的就是一个变量可以使用的范围,主要分为全局作用域和函数作用域以及块级作用域。而作用域链指的就是一个查找变量的过程:如果在当前作用域找不到指定变量,就会向父作用域里找,直到找到全局作用域上全局作用域就是js中最外层的作用域,函数作用域是js通过函数创建的一个独立作用域,函数可以嵌套,所以作用域也可以嵌套
什么是闭包
闭包指的就是在函数里定义了一个函数,内层函数能够访问并使用外层函数作用域中定义的变量,让这个变量不会被销毁 优点:闭包因为长期驻扎在内存中。可以重复使用变量,不会造成变量污染。缺点:闭包会使外层函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,可能会导致内存泄露。解决方法是在退出函数之前,将不使用的变量全部删除。
怎样添加、移除、移动、复制、创建和查找节点
- 创建新节点
createElement() - 添加、移除、替换、插⼊
appendChild() //添加
removeChild() //移除
replaceChild() //替换
insertBefore() //插⼊ - 查找
getElementsByTagName() //通过标签名称
getElementsByName() //通过元素的Name属性的值
getElementById() //通过元素Id,唯⼀性
querySelector() //通过选择器
如何准确判断变量类型
- 通过
Object.prototype.toString.call(xx)可以准确判断所有变量类型,包括基础类型和引用类型。可以获得类似 [object Type] 的字符串。 - 通过
instanceof可以准确判断引用类型,实现机制是检测构造函数的prototype属性是否出现在实例对象的原型链上。例如
var obj = {name:'张三'};
obj instanceof Object // true
var arr = [];
arr instanceof Array // true
var map = new Map();
map instanceof Map // true
- 通过
typeof不能准确判断所有变量类型,对于基础类型,除了 null 都可以显示正确的类型,对于引用类型,除了函数都会显示 object。因此只能在判断基础类型时使用。引用类型完全无法使用。
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b 没有声明,但是还会显示 undefined
typeof [] // 'object'
typeof {} // 'object'
typeof new Map() // 'object'
typeof new Set() // 'object'
typeof console.log // 'function'
//对于 null 来说,虽然它是基本类型,但是会显示object ,这是⼀个存在很久了的 Bug
typeof null // 'object'
== 和 ===之间区别
都是用来比较两个值是否相等,区别是如果两个值不是同一类型,严格相等运算符(===)直接返回false,而相等运算符(==)会将尝试将它们转换成同一个类型,再进行比较。因此大部分情况都应该用严格相等运算符。少部分情况可以用相等运算符,例如,在开发中,对于后端返回的code码, 可以通过 ==去判断。
if括号内的类型强制转换
实际上会调用Boolean()方法进行限制转换,分为以下几种情况
- 所有Object类都会转化为true,如对象,数组
- Undefined 转化为false
- Null 转化为false
- Booleans 转化为boolean的值
- Numbers +0,-0或NaN转化为false,其他全部转化为true
- String 空字符串为false,其他全部为true
也就是说,只有'', Undefine, Null, NAN, 0,这五种才会为false
基本数据类型和引⽤类型在存储上的差别
基本类型存储在栈上,引⽤类型存储在堆上。具体来说,基本类型的变量存储在栈上,直接指向具体的值,而引用类型的变量也存储在栈上,但指向的是一个地址,通过这个地址找到存在堆上的值。
This的指向有几种
在全局范围内的 this 指向 window对象。在函数中的this,谁调用这个函数,this就指向谁。在构造函数中的this 指向 new 出来的那个新的对象在箭头函数中的this 指向父作用域的this
new操作符具体⼲了什么呢
创建⼀个空对象,并且 this 变量引⽤该对象,同时还继承了该函数的原型 属性和⽅法被加⼊到 this 引⽤的对象中 新创建的对象由 this 所引⽤,并且最后隐式的返回 this
null,undefined 的区别
undefined 表示未定义,例如变量被声明了,但没有赋值时,就等于 undefined,null 表示⼀个对象被定义了,但被人为定义为“空值”
es6有哪些新特性
对象和数值可以解构赋值,新增了模板字符串,字符串拼接更加容易,新增了Set,Map数据类型,新增了Promise,async,await等异步处理方式,对象和数值还新增了许多新方法,比如Array.isArray(),Array.portotype.reduce(),Object.assign(),Objcet.create()等,还新增了箭头函数,简化函数使用,还可以使用let和const声明变量,还有es6的模块
数组去重实现方式
Set集合去重、利⽤Map数据结构去重、双重for循环去重、filter方法去重
// Set集合去重
function unique (arr) {
return Array.from(new Set(arr))
}
// 利⽤Map数据结构去重
function arrayNonRepeatfy(arr) {
let map = new Map();
let array = new Array(); // 数组⽤于返回结果
for (let i = 0; i < arr.length; i++) {
if(map.has(arr[i])) { // 如果有该key值
map.set(arr[i], true); }
else {
map.set(arr[i], false); // 如果没有该key值
array.push(arr[i]);
}
}
return array ;
}
// filter方法去重
function unique(arr) {
return arr.filter(function(item, index, arr) {
//当前元素,在原始数组中的第⼀个索引==当前索引值,否则返回当前元素
return arr.indexOf(item) === index;
});
}
浅拷贝和深拷贝区别以及实现方式
区别:浅拷贝拷贝的是地址 深拷贝拷贝的是值
- 浅拷贝实现方式:
es6的扩展运算符(...),Object.assign函数,Array.prototype.concat函数,Array.prototype.slice函数。
//es6的扩展运算符
const source = {
name: 'nordon',
info: {
age: 18
}
};
const obj = {...source};
// Object.assign函数
const source = {
name: 'nordon',
info: {
age: 18
}
};
const obj = {};
Object.assign(obj, source);
// Array.prototype.concat函数
const arr = [1, 2, {name: 'nordon'}];
const newArr = arr.concat();
// Array.prototype.slice函数
const arr = [1, 2, {name: 'nordon'}];
const newArr = arr.slice();
- 深拷贝实现方式:
JSON.parse(JSON.stringify(object)),递归遍历。
使用JSON这种方式的缺点是会忽略 undefined和symbol, 以及不能序列化函数
// JSON.parse(JSON.stringify(object))
let a = {
age: 1,
jobs: {
first: 'FE'
}
};
let b = JSON.parse(JSON.stringify(a));
a.jobs.first = 'native';
console.log(b.jobs.first);// FE
// 递归遍历
var obj = {
name: '张三',
age: 18,
property: {
vehicle: '奔驰',
houseNumber: 2,
},
};
var arr = [1, 1, 34, 45, '李四', true, { name: '里面' }];
function deepClone(object = {}) {
var newObj = Array.isArray(object) ? [] : {};
for (let key in object) {
if (object.hasOwnProperty(key)) {
var temple =
typeof object[key] === 'object'
? deepClone(object[key])
: object[key];
newObj[key] = temple;
}
}
return newObj;
}
console.log(deepClone(obj));
console.log(deepClone(arr));
bind、call、apply 的区别
bind,call,apply 都可以改变 this的指向,只是传参的⽅式不同。bind,call可以接收⼀个参数列表, apply只接受⼀个参数数组。此外,bind方法会返回一个函数,执行函数后才能改变this指向。
let a = { value: 1 };
function getValue(name, age) {
console.log(name);
console.log(age);
console.log(this.value)
}
let bindVal = getValue.bind(a, 'yck', '24');
bindVal();
getValue.call(a, 'yck', '24') ;
getValue.apply(a, ['yck', '24']);
如何实现一个call
实现思路:
Function.prototype.myCall = function (context = window, ...argument) {
// 1.获取要执行的函数
let fn = this;
// 2.在传入的context上挂载要执行的函数
context.fn = fn;
// 3.执行
let result = context.fn(...argument);
// 4.删除
delete context.fn;
// 5.返回结果
return result;
}
如何实现一个bind
实现思路:
Function.prototype.myBind = function (context = window, ...argument) {
// 1.获取要执行的函数
let fn = this;
// 2.利用闭包返回一个函数,保存执行逻辑
return () => {
// 3.在传入的context上挂载要执行的函数
context.fn = fn;
// 4.执行
let result = context.fn(...argument);
// 5.删除
delete context.fn;
//6.返回结果
return result
// 3,4,5,6等价于 fn.call(context, ...argument);
}
}
如何实现一个apply
实现思路:
Function.prototype.myApply = function (context = window, argumentArray) {
// 1.获取要执行的函数
let fn = this;
// 2.在传入的context上挂载要执行的函数
context.fn = fn;
// 3.执行
let result = context.fn(...argumentArray);
// 4.删除
delete context.fn;
// 5.返回结果
return result;
}
如何实现防抖,节流
防抖的主要用处是减少一个函数的频繁频率。避免在短时间内多次调用,主要用在js的事件监听中。比如按钮的多次点击,可以使用防抖来避免函数被多次执行,节流就是在防抖的基础上保证了函数在一定时间内一定会执行一次,避免了防抖函数调用频率过高导致代码一直无法得到执行的问题。比如页面滚动事件,如果在滚动事件中有复杂的计算操作,就要使用节流来减少代码的执行频率,避免页面卡顿。
都是利用闭包的原理实现,返回一个函数
// 防抖
function debounce (fn, wait) {
let timer;
return function(...reset) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, reset)
}, wait)
}
}
// 节流
function throttle(fn,wait,mustRun) {
let timer;
let startDate = new Date();
return function (...rest) {
clearTimeout(timer);
let endDate = new Date();
if(endDate - startDate >= mustRun) {
fn.apply(this, rest);
startDate = endDate;
}
timer = setTimout(()=> {
fn.apply(this, rest);
}, wait)
}
// 使用
var myOperation2 = throttle(function (e) {
console.log(e);
},500,1000);
window.addEventListener('scroll', myOperation1);
进阶版防抖,彻底避免按钮多次点击导致的ajax请求被多次发送
/*
*防抖原理:
*用超时器,每次触发时清除上一个定时器,这样就只能在规定的时间段触发
*/
// 异步模式,用于按钮中有异步操作,必须等待异步操作有结果才能再次请求,如api请求
function debounce(func, { wait = 16, isAsync = false }) {
let timeout;
let asyncStatus = 0;
const debounceContext = {
isAsync: isAsync,
asyncStart() {
if(!debounceContext.isAsync) return;
asyncStatus = 1;
},
asyncEnd() {
if(!debounceContext.isAsync) return;
asyncStatus = 0;
},
};
return function (...rest) {
clearTimeout(timeout);
if (asyncStatus === 1) return;
timeout = setTimeout(() => {
debounceContext.asyncStart();
rest.push(debounceContext);
func.apply(this, rest);
}, wait);
};
}
// 使用
var myOperation1 = debounce(async function (e, {asyncEnd}) {
const result = await axios.get('https://api.paugram.com/wallpaper/');
asyncEnd();
console.log('已发送',result);
}, {wait:16, isAsync:true});
window.addEventListener('scroll', myOperation1);
js里如何实现一个继承
可以通过借用构造函数和原型链实现继承关系,借用构造函数指得是子类通过call函数改变this指向,调用父类的构造函数完成实例属性的赋值,然后再让子类的原型指向父类的原型,通过原型链调用父类的方法。
最佳实践:寄生组合继承模式
function People(name,sex,phone){
this.name=name
this.sex=sex
this.phone=phone
}
function ChinesePeople(name,sex,phone,national){
// 关键点
People.call(this, name, sex, phone)
this.national=national
}
// 关键点
ChinesePeople.prototype= Object.create(People.prototype)
ChinesePeople.prototype.constructor = ChinesePeople
let sonobj2 = new Son("lisi", 34, "打篮球", "中国");
如何实现⼀个 Promise
Promise 是 ES6 新增的语法,解决了回调地狱的问题。可以把 Promise 看成⼀个状态机。初始是 pending 状态,可以通过函数 resolve 和 rejected,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化。
如何获取到 pomise 多个then 之后的值?
在 .then() 链式调用中,返回的值会传递到下一个 .then()。因此在then方法设置返回值就可以
const promise = new Promise((resolve) => resolve(1));
promise
.then((value1) => {
console.log('First then:', value1); // 输出: 1
return value1 + 1; // 返回 2
})
.then((value2) => {
console.log('Second then:', value2); // 输出: 2
return value2 + 1; // 返回 3
})
.then((value3) => {
console.log('Third then:', value3); // 输出: 3
});
// 简单实现
function myPromise(excuteFn) {
this.status = 'pending';
this.value = '';
let resolve = (value) => {
this.status = 'resolved'
this.value = value;
}
let rejiect = (value) => {
this.status = 'rejected';
this.value = value;
}
excuteFn(resolve,rejiect)
}
myPromise.prototype.myThen(resolvedFn,rejectedFn) {
if(this.status == 'resolved') {
resolvedFn(this.value)
}else if(this.status == 'rejiect') {
rejectedFn(this.value)
}
}
什么是事件代理(事件委托)
一种绑定事件的常用技巧,不直接在子元素绑定事件,而是委托给父元素去绑定事件。利用冒泡机制去处理子元素的响应。
详细介绍:是 js中常用的绑定事件的常用技巧。顾名思义,“事件代理”即是把原本需要在子元素绑定的事件委托给父元素,让父元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡。使用事件代理的好处是可以提高性能,大量节省内存占用,减少事件注册,比如在table上代理所有的click事件就非常棒
es6新增了那些东西
新增了let、const变量声明方式,新增了新的数据类型,set、map、symbol、新增了class、新增了模块化、新增了promise、async、await等异步处理方式、新增了箭头函数、新增了模版字符串、新增了解构赋值、新增了扩展运算符、新增了一些新的对象和数组方法,
CommonJS模块和es6模块的区别
CommonJS支持动态导⼊,可以在if语句里动态判断是否导入模块,而es6模块不支持。CommonJS采用的是同步导入,es6模块采用的是异步导入。CommonJS导出时都是拷⻉的值,就算导出的值变了,导⼊的值也不会改变,es6是引用拷贝,导⼊导出的值都指向同⼀个内存地址,会同时变化。
CommonJS采用同步导入,es6模块采用异步导入的原因:因为CommonJS模块⽤于服务端,⽂件都在本地,同步导⼊即使卡住主线程影响也不⼤,而es6模块⽤于浏览器,导入模块都不在本地,而是去服务器上下载⽂件,如果也采⽤同步导⼊会对页面渲染有很⼤影响
浏览器
cookie和localSrorage、sessioSrorage的区别
cookie可以由服务器和浏览器生成,存储空间比较小(4k),每次发送http请求都会携带在 请求头中(header)中,因此不适合存储大量数据。localSrorageh和sessioSrorage可以在浏览器设置,用于本地数据存储,存储空间比较大(5M),适合存储大量数据,他们之前的区别是localSrorage的数据除非手动清理,否则一直存在,而sessioSrorage的数据在页面关闭后就会被清除。
tips: cookie属性有哪些?
| 属性 | 作用 |
|---|---|
| value | 如果⽤于保存⽤户登录态,应该将该值加密,不能使⽤明⽂的⽤户标识 |
| http-only | 不能通过 JS 访问 Cookie ,减少 XSS 攻击 |
| secure | 只能在协议为 HTTPS 的请求中携带 |
| same-site | 规定浏览器不能在跨域请求中携带 Cookie ,减少 CSRF 攻击 |
什么叫跨域
跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,从一个网页去请求服务器的资源时,协议、域名、端口号,只要有一个不同,就会造成跨域。
跨域有几种解决方式
CORS(跨域资源共享,主流)、node中间件代理(webpack-dev-server代理,开发常用)、nginx反向代理,jsonp(最早的方案,过时)。
- CORS: 服务端设置Access-Control-Allow-Origin就可以开启CORS。
- node中间件代理: 一般由前端配置,webpack项目启动就是走的node服务器。
proxy: {
'/downloadApi': { // 文件代理
target: 'http://192.168.16.206:7095', // 代理转发地址(最终地址)
ws: true, // 代理websocketl
/**
覆写http请求头host属性,指向最终的服务器地址(target),否则将会指向代理服务器,
这可以确保后端获取host返回的是正确的地址,而不是代理服务器地址
**/
changeOrigin: true,
secure: false, // 如果是https接口,需要配置这个参数,避免验证
pathRewrite: {
'^/downloadApi': '' // 路径重写 将/downloadApi替换为空串
}
},
},
- nginx反向代理:客户端请求nginx服务器,在nginx.conf配置文件中配置server监听客户端的请求,然后把location匹配的路径代理到真实的服务器,服务器处理请求后返回数据,nginx再把数据给客户端返回。
- jsonp:
有一些网页的标签具有跨域能力,例如:img、link、iframe、script。jsonp的原理就是利用了script标签不受浏览器同源策略的限制,且只支持get请求。具体的实现就是在客户端用一个script标签,然后把请求后端的接口拼接一个回调函数名称作为参数传给后端,并且赋值给script标签的src属性,当后端接收到客户端的请求时,会解析得到回调函数名称,然后把数据和回调函数名称拼接成函数调用的形式返回,客户端解析后会调用定义好的回调函数,然后在回调函数中就可以获取到后端返回的数据了。
<!-- 客户端 -->
<script>
function callbackFunction(data) {
alert(data)
}
</script>
<script type="text/javascript"
src="http://www.baidu.com/getResource?jsoncallback=callbackFunction" />
// 服务端
app.get('/getResource',(req, res) => {
const fnName = req.query.jsoncallback;
const result = fnName + '({book:"书"})'; // 等价于 callbackFunction({{book:"书"}})
res.send(result);
})
浏览器缓存
浏览器缓存分为两种:强缓存和协商缓存。强缓存可以通过Expires,Cache-Control两个属性实现,通过指定过期时间来控制缓存,如果没有过期就使用本地缓存,过期了就向服务器发起请求,进行协商缓存,由服务器判断资源是否更新。协商缓存可以通过过两组属性实现,分别是Etag,Last-Modified,如果Etag/Last-Modified变化了就返回200状态,表示资源更新了,同时返回新资源给浏览器,没有就返回304状态吗,表示资源没有更新,返回空内容,浏览器会直接读取本地缓存。
- 强缓存: Expires缺点:浏览器时间不一定是准确的,会导致缓存时间不正确。假设现在时间为2023/3/16日9点,缓存三个小时,服务器返回的expires为2023/3/16日12点,但是浏览器的时间不准,往后了一天,为2023/3/17日9点,这就会导致浏览器导致认为缓存失效了,但实际上缓存根本没过期。Cache-control的max-age属性就能很好的避免这个问题,直接让浏览器自己来算过期时间。
- 协商缓存:last-modified缺点:只能监听到秒级的文件修改,毫秒级的文件修改无法发现。Etag则是通过hash算法生成文件摘要,只要文件修改了任何内容,摘要就会不一致,避免了前面的问题。
Cache-control属性值
| 属性 | 含义 |
|---|---|
| max-age | 资源过期时间 |
| public | 允许代理服务器缓存资源 |
| s-maxage | 代理服务器的资源过期时间 |
| private | 不允许代理服务器缓存资源,只有浏览器可以缓存 |
| immutable | 就算过期了也不用协商,资源就是不变的 |
| max-stale | 不新鲜时间,过期了一段时间的话,资源也能用 |
| stale-while-revalidate | 在验证(协商)期间,返回过期的资源 |
| stale-if-error | 验证(协商)出错的话,返回过期的资源 |
| must-revalidate | 不允许过期了还用过期资源,必须等协商结束 |
| no-store | 禁止缓存和协商,直接获取最新资源 |
| no-cache | 禁止强缓存,允许协商缓存。 |
最佳做法:前端项目,index.html文件的Cache-control设置为no-store或no-cache,表示不进行强缓存,不进行缓存或者协商缓存,静态资源如图片,音频,视频,第三方依赖等Cache-control的max-age设置为一年。经常更改的业务文件(js,vue)等Cache-control的max-age设置为一年或者不进行任何设置,webpack在打包时对于内容变动的业务文件,会自动变更文件名上的的hash值,一旦⽂件名变动就会⽴刻下载新的⽂件。
说说事件触发的流程(事件模型)
先从document往事件触发处传播,遇到注册的捕获事件会就会触发,被称为捕获阶段,传播到事件触发处时就会触发注册的事件,最后再从事件触发处往document传播,遇到注册的冒泡事件会触发,称为冒泡阶段
e.preventDefault()。 用于阻止特定事件的默认行为,如 a 标签的跳转等
e.stopPropagation()。用于阻⽌事件冒泡,事件不再被分派到祖先节点。
什么是事件循环(event loop)
事件循环是指js在处理异步代码时使用的一种机制,它可以让js以单线程模式执行异步任务,。异步任务分为宏任务和微任务,在执行js代码的过程中,也就是一个事件循环中,首先会执行宏任务,然后再去执行同步代码,再执行同步代码的过程中把遇到的异步任务按类别加入宏任务队列和微任务队列,同步代码执行完后,最后再检查微任务队列里是否有微任务,有的话把队列里的微任务全部执行完,最后进行页面渲染,开始下一轮循环.
- 微任务:promise.then、MutationObserver、process.nextTick,
- 宏任务: setTimeout、setInterval、 、 script(整体代码)、I/O 操作、UI渲染等
tips: 众所周知 JS 是⻔⾮阻塞单线程语⾔,因为在最初 JS 就是为了和浏览器交 互⽽诞⽣的。如果 JS 是⻔多线程的语⾔话,我们在多个线程中处理 DOM 就可能会发⽣问题(⼀个线程中新加节点,另⼀个线程中删除节点)。
浏览器内核有哪些
- Trident(IE)
- Gecko(火狐)
- Blink(Chrome、Opera)
- Webkit(Safari)。
浏览器绘制页面流程
解析 HTML 文件,生成 DOM 树:当浏览器接收到 HTML 文件时,它会使用 HTML 解析器将文本内容解析为 DOM 树。在解析过程中,解析器会根据 HTML 标签的语法规则,将 HTML 文件中的每个元素转换为 DOM 树中的一个节点,同时将元素的属性值作为节点的属性保存。解析 CSS 文件,生成 CSS 树:当浏览器解析 HTML 文件时,如果遇到 或 标签,则会加载和解析相应的 CSS 文件,生成 CSS 树。在解析过程中,浏览器会根据 CSS 的选择器规则,将每个元素匹配到相应的 CSS 规则中,生成对应的 CSS 树。将 DOM 树和 CSS 树合并,生成渲染树:将 DOM 树和 CSS 树合并后,生成渲染树。在渲染树中,每个节点都对应着一个可见的元素,并包含了元素的样式信息和布局信息。计算布局:浏览器使用布局引擎对渲染树进行布局计算,计算出每个元素在页面中的位置和大小。绘制渲染树:浏览器使用绘制引擎将渲染树转换成屏幕上的像素,渲染出最终的页面。
DOM树怎么生成的
首先读取html资源文件,对读取的的字符串(标签)做词法检查,生成token,标识这是一个什么标签,是开始标签还是结束标签,根据这些信息生成对应的节点对象,按照 HTML 标签的层次关系,通过递归操作最终完成一个dom节点树。
cssOM树怎么生成的
首先读取css资源文件,对读取的的字符串(选择器)做词法检查,生成token,标识这是一个什么选择器,选择器权重是多少,根据这些信息生成对应的css规则对象。生成完后,就要去生成cssOM树。首先去遍历 html(DOM)树中的每个节点,并匹配对应的 CSS 规则,生成 CSSOM 树。当匹配节点和规则时,还会根据选择器权重计算出节点的最终样式,包括继承的样式和自己的样式,将这些值存储在 CSSOM 树的对应节点中。
简要回答:就是依赖于 html(dom)节点树去生成的,在遍历节点树的过程中,匹配解析到的css选择器规则,根据选择器权重计算节点样式。最后等html节点树遍历完一遍,cssOM节点树也就生成了
从输入 URL 到页面加载完成,发生了什么?
浏览器根据请求的 URL 交给 DNS 域名解析,找到对应的IP地址 ,向服务器发起请求。服务器收到请求后,返回对应的html文件。浏览器对返回的html文件进行解析和布局计算。最后将解析到的结果渲染到⻚⾯。
什么是重绘(Repaint)和重排(Reflow)
重绘指的是网页中元素的样式发生变化时,重新绘制该元素所在的区域,⽐如改变背景颜色就会导致重绘,而重排指的网页布局发生变化时,重新计算网页中所有元素的位置和大小,并重新绘制整个页面,比如修改网页中元素的大小或者位置就会发生重绘
重排的成本比重绘高得多,因为它涉及到页面中所有元素的重新计算和绘制,而重绘只涉及到部分元素的绘制。因此,为了提高网页的性能,应尽可能避免频繁的重排和重绘操作,可以采取一些优化措施,比如使用CSS3的transform属性来代替top/left等属性来移动元素,这样可以避免重排;同时,避免使用table布局和使用多层嵌套的DOM结构也可以减少重排的次数。
HTML、CSS
WEB标准以及W3C标准是什么?
标签闭合、标签⼩写、不乱嵌套、使⽤外链 css 和 js 、结构⾏为表现的分离
垂直/水平居中有哪些实现方式
一般都是采用felx布局和和绝对定位去实现
/* felx布局 */
#center {
display: flex;
justify-content: center;
align-items: center;
}
/* 绝对定位-宽高已知 */
#center {
width: 200px;
height: 200px;
position: absolute;
left: 50%;
top: 50%;
margin-left: -100px;
margin-top: -100px;
}
/* 绝对定位-宽高已知方案 */
#center {
width: 200px;
height: 200px;
position: absolute;
left: 50%;
top: 50%;
margin-left: -100px;
margin-top: -100px;
}
#center {
width: 200px;
height: 200px;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
/* 绝对定位-宽高未知方案 */
#center {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
BFC 是什么?如何创造一个 BFC?
BFC 叫做块级格式化上下文,是一个独立的作用范围,在这个范围内与外界有不同的渲染机制,决定块级元素如何布局,经常被用来清除浮动,防止margin发生重叠,阻止元素被浮动的元素覆盖。
BFC 具有以下特点:
- BFC 内部的元素会按照一定的规则进行布局。当一个元素生成了 BFC,它的子元素会按照一定的规则在 BFC 内部进行布局,而不会受到外部元素的影响。
- BFC 区域不会与浮动元素重叠。当一个元素生成了 BFC,它的区域会与浮动元素产生边缘重叠,但是不会与浮动元素重叠。
- BFC 可以包含浮动元素。当一个元素生成了 BFC,它可以包含浮动元素,而不会使浮动元素溢出到外部区域。
- BFC 可以阻止元素被浮动元素覆盖。当一个元素生成了 BFC,它可以阻止浮动元素覆盖它的内容区域。
生成 BFC 的方式包括以下几种:
- float 属性不为 none。
- position 属性不为 static 或者 relative。
- display 属性为 table-cell、table-caption、inline-block 中的任意一个。
- overflow 属性不为 visible。
伪元素清除浮动
.clearfix:after{
content:"";//设置内容为空
height:0;//高度为0
line-height:0;//行高为0
display:block;//将文本转为块级元素
visibility:hidden;//将元素隐藏
clear:both//清除浮动
}
.clearfix{
zoom:1;为了兼容IE
}
⾏内元素有哪些?块级元素有哪些
- ⾏内元素有: a b span img input select strong
- 块级元素有: div p ul ol li dl dt dd h1 h2 h3 h4…
区别就是:⾏内元素不可以设置宽⾼,不独占⼀⾏ 块级元素可以设置宽⾼,独占⼀⾏
CSS不同选择器的权重
- !important 规则最重要,⼤于其它规则
- ⾏内样式规则,加 1000
- id选择器,加 100
- 类、属性选择器或者伪类选择器,加 10
- 标签选择器,加1
如果权值⼀样,则按照样式规则的先后顺序来应⽤,顺序靠后的覆盖靠前的规则
文本溢出隐藏
.ellipsis {
overflow: hidden; /* 超出部分隐藏 */
white-space: nowrap; /* 禁止文本换行 */
text-overflow: ellipsis; /* 添加省略号 */
}
CSS的盒⼦模型
盒模型有两种分别是iE盒模型和标准盒模型,他们之间的区别就是IE盒模型的content部分把 border和 padding计算了进去;
display: none; 与 visibility: hidden; 的区别
它们都能让元素不可⻅,但是display:none ;会让元素完全从dom树中消失,渲染的时候不占据任何空间,visibility: hidden ;只是视觉上消失,不会从dom树中消失,继续占据空间。
Babel
什么是Babel
Babel是一个js编译器,可以将最新的js代码转换为以前的版本,比如将ES6的代码转换为ES5代码,通过babel可以直接在项目里使用最新的js语法,而不用担心浏览器兼容性。
Babel的原理
本质上就是一个编译器,分为解析-转换-生成三步。先把源代码转为 AST(抽象语法树) ,然后去遍历 AST,进行转换和修改 ,最后把修改后的AST重新生成为JavaScript代码。Babel支持很多的插件和预设,可以根据项目的需求来选择需要的转换规则。例如,可以使用babel-preset-env预设来根据目标浏览器或环境自动选择需要的转换规则,也可以使用babel-plugin-transform-runtime插件来减少重复代码和减小打包后的文件体积。
Babel内置功能
插件类型
语法插件(syntax plugin):告诉解析器(babylon)要解析什么语法。转换插件(transform plugin):用来定义对 AST 转换的逻辑,在转换阶段,会在遍历ast的过程中调用这些转换插件,实现转换。
预设(preset)
预设指得就是插件的集合,但是它可以动态确定包含的插件,比如@babel/preset-env就是根据目标浏览器的版本来确定使用的插件。
帮助函数(helper)
helper是用于babel插件逻辑复用的一些工具函数,分为两种
- 一种是注入到 AST 的运行时用的全局函数(注入runtime代码的helper)
- 一种是操作 AST 的工具函数,比如变量提升这种通用逻辑(简化AST操作的helper)
垫片(polyfill)
polyfill用来实现低版本浏览器不支持的js方法的代码,Babel默认只转换js语法,而不转换js方法,比如es6最新的数组方法find(),对象方法Object.assign(),所以对于低版本浏览器需要使用polyfill来支持最新的js api。polyfill有corejs和regenerator两个库,实现了最新的js api方法
运行时(runtime)
babel runtime 里面放运行时加载的模块,会被打包工具打包到产物中,下面放着各种需要在 runtime使用的函数,包括三部分:regenerator、corejs、helper。
- corejs 这就是新的 api 的 polyfill,分为 2 和 3 两个版本,3 才实现了实例方法的polyfill
- regenerator 是 facebook 实现的 aync 的 runtime 库,babel 使用 regenerator-runtime来支持实现 async await 的支持。
- helper 是 babel 做语法转换时用到的函数,比如 _typeof、_extends 等
使用 @babel/preset-env引入polyfill和@babel/plugin-transform-runtime引入polyfill、helper之间的区别
用preset-env引入polyfil、helper,其中polyfil是全局引入的,helper是每个模块都会定义一遍,因此会有污染全局变量和重复注入helper函数导致打包后的代码体积变大的问题,但是好处就是polyfil是按需引入的。而使用@babel/plugin-transform-runtime引入polyfil、helper,对于polyfil、helper都是模块化引入,好处就是避免了污染全局变量和打包后代码体积变大的问题,坏处就是polyfil不是按需引入的,刚好相反。
最佳实践: 开发项目用@babel/preset-env就行,不用担心污染全局变量的问题,开发js库则推荐使用@babel/plugin-transform-runtime。防止染全局变量。
@babel/preset-env 配置
"presets": [
["@babel/preset-env", {
"targets": "> 0.25%, not dead",
"useBuiltIns": "usage", // or "entry" or "false"
"corejs": 3
}]
]
配置下 corejs 和 useBuiltIns。
- corejs 就是 babel 7 所用的 polyfill,需要指定下版本,corejs 3 才支持实例方法(比如 Array.prototype.fill )的 polyfill。
- useBuiltIns 就是使用polyfill (corejs)的方式,是在入口处全部引入(entry),还是每个文件引入用到的(usage),或者不引入(false)。
前端构建工具相关
什么是前端工程化?
前端工程化指的是通过项目规范和构建工具自动化处理各种前端开发流程,提高项目开发效率。比如项目的模块化,代码检测,代码压缩,代码混淆,代码打包等等
什么是前端构建工具?为什么需要它们
前端构建工具是用于自动化处理各种前端开发流程的工具,可以对项目源代码进行一定的处理,如代码打包、压缩、混淆、代码检查等,开发阶段还提供了一个本地的开发服务器,可以直接在本地运行项目,支持热重载,提高开发效率
Vite为什么比webpack要快
-
快速的冷启动:Vite 使用了一种称为「按需编译」的模式,它仅在启动时编译正在编辑的文件,而不是像 Webpack 那样要编译整个项目。这意味着在启动开发服务器时,Vite 的冷启动速度更快,因为它只需编译少量的文件。
-
通过 ES 模块进行原生导入:Vite 基于原生 ES 模块的导入方式,而不是像 Webpack 那样需要将所有模块打包成一个或多个捆绑包。这使得 Vite 不需要进行大量的代码分析和重新构建工作,并且可以更快地处理模块的导入过程。
-
高效的 HMR(热模块替换)机制:Vite 在开发过程中使用了高效的 HMR 机制。它通过直接将更新的模块推送到浏览器,而无需重新刷新整个页面来实时更新应用程序。这样可以节省重新加载的时间,提高开发体验。
-
优化的构建过程:在生产构建方面,Vite 通过使用 Rollup 进行优化,将每个模块作为单独的文件进行输出。这样可以提高浏览器的缓存命中率,并减少构建后的文件大小。Vite 也支持代码分割和按需加载,以进一步优化性能。
总结:vite使用按需编译的方式,可以直接启动一个开发服务器,然后在浏览器请求时按需加载和编译对应的模块。然后对于项目的依赖(node_modules),还会进行预构建(相当于缓存),不用每次启动服务都重新构建一遍依赖,而webpack必须先打包(编译)构建完整个项目才能启动开发服务器,所以对应大型项目webpack的启动速度很慢
什么是webpack
webpacks是一个模块化的打包工具,能够以模块化的方式管理项目,用于将多个JavaScript模块打包成一个或多个输出文件。在webpack中,所有类型的文件(包括js文件和非js文件)都被视为一个模块,webpack通过加载和解析这些模块,将其合并打包成浏览器兼容的Web资源文件 。
loader是什么
loader主要用来处理非js文件,因为webpack只能解析和转换js文件,对于非js文件,比如图片、视频、css文件等,webpack将使用对应的loader去解析资源并转换为js模块,这样webpack就能接着处理了。
Plugin是什么
Plugin的功能更强大,主要目的就是用来解决loader无法实现的事情,比如打包优化和代码压缩等。
Webpack的基本功能有哪些?
代码转换:TypeScript编译成JavaScript、SCSS编译成CSS等等
文件优化:压缩JavaScript、CSS、html代码,压缩合并图片等
代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载
模块合并:在采用模块化的项目有很多模块和文件,需要构建功能把模块分类合并成一个文件
热更新:监听本地源代码的变化,自动构建,刷新浏览器
vue
什么是MVVM
分为3部分,m代表模型,指的是应用程序中的数据和业务逻辑,v代表视图,指的是真实可见的用户界面,vm代表视图模型,用于模型和视图的之间的通信,提供双向绑定,可以把模型层数据的修改同步到视图层,同样的也可以把视图层的修改同步到模型层
好处是:低耦合、关注点分离,开发者只需要专注业务逻辑和数据的开发,设计人员可以专注于页面的设计,无需关注两种之间如何通信,vue就代表vm层
什么是MVC
mvc是后端最常用的软件架构,m代表模型层,指的是应用程序中的数据和业务逻辑。 v代表视图,指的是真实可见的用户界面。c代表控制层,用于处理用户的输入,从视图中读取数据,并向模型发送数据。也可以从模型中获取数据并更新视图。
MVVM和MVC的区别
区别在于MVVM的vm和MVC的c,虽然都是负责数据模型的更新和视图的刷新,但vm抽离了视图展示的逻辑,实现了视图和模型层的自动同步,当模型层的数据改变时,我们不用再自己手动操作Dom元素,来改变视图的显示,而是改变数据后对应的视图显示会自动改变。因此开发者不用再手动操作dom改变视图,而是交给vm层来做。
为什么data是一个函数
为了避免组件复用时共用同一份data,如果使用对象定义一个data,因为对象是一个引用类型,就会导致组件的所有实例引用同一个data,导致数据共用,而把data定义成函数里返回一个对象的形式,可以保证组件每次实例化只用执行这个函数就可以得到新的data对象,让各个组件实例都能维护各自的数据。
Vue组件通讯有哪些方式
props和$emit,父组件向子组件传递数据是通过props传递的,子组件传递给父组件是通过$emit触发事件来做到的。(父子通信)$parent 和 $children,获取当前组件的父组件和当前组件的子组件。(父子通信)$refs获取子组件实例。(父子通信)。provide/inject。(跨组件通信)$attrs和$listeners。(跨组件通信)- $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。通常配 合inheritAttrs 选项一起使用。
- $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它 可以通过 v-on="$listeners" 传入内部组件
vuex状态管理(父子通信、跨组件通信、相邻通信)
Vue的生命周期方法有哪些,一般在哪一步发送请求
-
beforeCreate在实例初始化之后,数据观测(data observe)和 event/watcher 事件配置之前被调用。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。 -
created实例已经创建完成之后被调用。在这一步,实例已经完成以下的配置:数据观测(data observe ),属性和方法的运算,watch/event 事件回调。这里没有 nextTick 来访问 DOM。 -
beforeMount在挂载开始之前被调用:相关的 render 函数首次被调用。 -
mounted在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom节点。 -
beforeUpdate数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁 (patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。(数据修改页面未修改) -
updated发生在更新完成之后,当前阶段组件 Dom 已经完成更新。要注意的是避免在此期间更新数据,因为这个可能导致无限循环的更新,该钩子在服务器渲染期间不被调用。 -
beforeDestroy实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行 善后收尾工作,比如清除定时器。 -
destroyedVue实例销毁后调用。调用后,Vue实例指示的东西都会解绑定,所有的事件监听器会被移除,左右的子实例也会被销毁,该钩子在服务器端渲染不被调用。 -
activatedkeep-alive专属,组件被激活时调用 -
deactivatedkeep-alive 专属,组件被销毁时调用
异步请求在哪一步发起?
可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data已经创建,可以将服务器端返回的数据进行赋值。
如果异步请求不需要依赖DOM推荐加载created钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:能更快获取到服务端数据,减少页面loading时间;如果依赖DOM元素:需要在mounted里面进行请求。
v-if 和 v-show 的区别
v-if 通过操作dom节点的新增与删除来控制是否展示, v-show是通过控制css样式的display等于block或none来控制是否展示。v-if组件会被销毁,v-show组件不会被销毁,如果需要组件状态被保存,要用v-show
v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景。v-show 适用于需要非常频繁切换条件的场景。
说说 vue 内置指令
v-once- 定义它的元素或组件只渲染一次,包括元素或组件的所有节点,首次渲染后,不再随数据的变化重新渲染,将被视为静态内容。v-cloak- 这个指令保持在元素上直到关联实例结束编译 -- 解决初始化慢到页面闪动的最佳实践。v-bind- 绑定属性,动态更新HTML元素上的属性。例如 v-bind:class。v-on- 用于监听DOM事件。例如 v-on:click v-on:keyupv-html- 赋值就是变量的innerHTML -- 注意防止xss攻击v-text- 更新元素的textContentv-model- 1、在普通标签。变成value和input的语法糖,并且会处理拼音输入法的问题。2、再组件上。也是处理value和input语法糖。v-if / v-else / v-else-if。可以配合template使用;在render函数里面就是三元表达式。v-show- 使用指令来实现 -- 最终会通过display来进行显示隐藏v-for- 循环指令编译出来的结果是 -L 代表渲染列表。优先级比v-if高最好不要一起使用,尽量使用计算属性去解决。注意增加唯一key值,不要使用index作为key。v-pre- 跳过这个元素以及子元素的编译过程,以此来加快整个项目的编译速度。
怎样理解 Vue 的单项数据流
数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样可以防止从子组件意外改变父组件的状态,从而导致应用的数据流向难以理解。
computed 和 watch 的区别和运用的场景
区别: computed和watch都是响应式的,用来监视数据的变化。computed用来计算一个新的属性值,具有缓存的功能,只有依赖的响应式数据发生变化时,才会重新计算,否则返回上一次内容。watch没有缓存,只要数据变化,就会执行侦听函数,computed不支持异步操作,watch支持异步操作
运用的场景:
-
计算属性一般用于多个响应式数据改变从而影响一个数据的情况,比如两个数值相加得到总数,就可以用计算属性得到结果并展示在界面上,还有根据多个条件进行过滤得到最终结果也可以使用计算属性,比如展示性别为男,部门为开发部,姓陈的人员列表。
-
而watch一般用于一个响应式数据改变从而执行相应操作的情况,适用于需要在数据变化时执行异步操作,或需要执行较为复杂的操作的场景,比如发送ajax请求。
Vue2深度监听(watch的deep:true)造成新旧值相同
vue2中 v-if 和 v-for 为什么不建议一起使用
v-for和v-if不要在同一标签中使用,因为解析时先解析v-for再解析v-if(每次渲染都会先循环再进行条件判断)。会浪费性能方面的浪费。
如果要避免出现这种情况,则在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环
vue3 会先执行v-if 在执行v-for
<template v-if="isShow">
<p v-for="item in items">
</template>
数组哪些方法会触发Vue监听,哪些不会触发监听
会修改数组本身的方法会触发监听(push,pop, unshift,shift, splice,sort,reverse),不修改数组本身的方法不会(forEach,filter,map, slice,join,reduce,some,every)。
Vue 2.0 响应式数据的原理(常问)
是采用数据劫持和观察者模式实现的,首先,Vue组件在初始化时(beforeCreate调用之后,created调用之前),会对data对象进行响应式处理,也就是遍历data对象里的所有属性,并使用Object.defineProperty函数对每个属性进行劫持,添加get和setter函数,其中geetter函数被调用会进行依赖收集,收集所有依赖于这个属性的观察者,也就是watcher实例,而setter函数被调用则会进行通知,通知所有依赖于这个属性的观察者(watcher实例),观察者(watcher实例)收到通知后就会进行更新操作,更新方式有三种,做多的就是更新render函数,去重新渲染页面。依赖搜集发生在三个地方,对应三种更新方式,第一种是remder函数,也就是vue的模版,第二种是计算属性,第三种是监听器watch,在这三个地方使用响应式数据就会触发getter函数进行依赖收集,通知就很简单了,只要响应书数据发生变化,就会触发setter函数,进行通知。
百度百科介绍的观察者模式
- Dep: 叫做依赖,指的是我们定义的一个个响应式属性,在观察者模式中为被观察者;
- watcher: 叫做观察者,每个组件实例有且只有一个观察者,可以用来通知对应组件实例更新。
- nofify: 叫做通知,指的是通知watcher,依赖更新了,让watcher去更新视图。
- 依赖收集: 依赖收集指的把Dep和watcher之间进行关联,广义上理解应该是
watcher去收集自己所依赖的数据属性;所以叫做依赖收集,但实际上根据观察者模式定义来说,应该是被观察者(dep)去搜集观察者(watcher)。,源码里就是这么实现的
代码实现:
// 响应式处理
function observation(data) {
if (typeof data !== 'object' || data === null) {
throw new Error('data属性不是对象');
}
if (Array.isArray(data)) {
Object.setPrototypeOf(data, reactiveArray);
}
for (const key in data) {
if (typeof data[key] === 'object') {
observation(data[key]);
}
defineReactive(data, key, data[key]);
}
}
// 定义响应式
function defineReactive(data, key, value) {
// 定义一个 Dep 对象
const dep = new Dep();
if (typeof data[key] === 'object') {
data[key]._dep = dep
}
Object.defineProperty(data, key, {
set: function (newVal) {
if (newVal !== value) {
if (typeof newVal === 'object') {
observation(newVal);
}
value = newVal;
// 感知更新、通知 watcher
dep.notify();
}
},
get: function () {
// 收集依赖、关联到 watcher
dep.depend();
return value;
},
});
}
class Dep {
constructor () {
// 存储 Watcher 实例的数组
this.subs = []
}
// 将 watcher 实例添加到 subs 中(这个方法在 Watcher 类的实现里会用到)
addSub (sub) {
this.subs.push(sub)
}
// 收集依赖
depend() {
// Dep.target 实际上就是当前组件的Watcher实例,
if (Dep.target) {
// 把当前的 dep 实例关联到组件对应的 watcher 上去
Dep.target.addDep(this)
}
}
// 通知 watcher 对象发生更新
notify () {
const subs = this.subs.slice()
// 这里 subs 的元素是 watcher 实例,逐个调用 watcher 实例的 update 方法
for (let i = 0, l = subs.length; i < 1; i++) {
subs[i].update()
}
}
}
class Watcher {
constructor() {
// Dep 的 target 属性是有赋值过程的^_^,它是组件对应的 watcher 对象
Dep.target = this;
}
addDep (dep) {
// 把当前的 watcher 推入 dep 实例的 watcher 队列(subs)里去
dep.addSub(this)
}
update() {
// 更新视图
console.log('视图更新了');
}
}
//监听来自数组方法的添加行为
let ArrayProto = Array.prototype;
let reactiveArray = Object.create(ArrayProto);
['push', 'pop', 'unshift', 'shift'].forEach(function (item) {
reactiveArray[item] = function () {
console.log(this)
this._dep.notify();
ArrayProto[item].call(this, ...arguments);
};
});
class Vue {
constructor(data) {
// 组件初始化,进行响应式处理
observation(data);
// 组件挂载,调用Watcher
new Watcher()
}
}
let data = {
name: 'xt',
age: 18,
sex: '男',
deep: {
father: 'xgy',
},
property: [1, 2, 3, 46, 33, 11, 4, 1111, 23345, 552432, 4356, 23423453],
}
// 测试数据
new Vue({
data: data
})
// 测试方法 ,先读取属性触发依赖搜集,在设置属性进行通知,例如data.age; data.age = 21;
Vue如何检测数组变化
vue2.x 中实现检测数组变化的方法,是将数组的常用方法进行了重写 。Vue 将 data 中的 数组进行了原型链重写 ,指向了自己定义的数组原型方法。这样当调用数组 api 时,可以 通知依赖更新 。
有两种情况无法检测到数组的变化 。
- 当利用索引直接设置一个数组项时,
例如vm.items[indexOfItem] = newValue - 当修改数组的长度时,例如 vm.items.length = newLength 不过这两种场景都有对应的解决方案。 利用索引设置数组项的替代方案
/使用该方法进行更新视图
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)
Vue的父子组件生命周期钩子函数执行顺序
加载渲染过程: 父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created-> 子 beforeMount -> 子 mounted -> 父 mounted- 子组件更新过程: 父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
- 父组件更新过程: 父 beforeUpdate -> 父 updated
- 销毁过程: 父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
总结: 父组件先开始 子组件先结束
v-model 双向绑定的原理是什么?
v-model本质上就是value属性 + input事件的语法糖 。比如在一个输入框上绑定v-model,当用户在输入框输入内容时,就会触发 input 事件,在input 事件执行过程中,就会把用户输入的内容赋值给value。因为value属性是一个响应式属性,所以value的改变会导致所有依赖value这个属性的组件更新视图,这就是vue中的双向绑定。
Vue3.x响应式数据原理
Vue3用Proxy替代了Object.defineProperty,去实现响应式数据。因为Proxy可以很完美的监听对象和数组的变化,直接去代理劫持整个对象,弥补了Object.defineProperty的缺点,对于对象类型,Object.defineProperty无法监听对象的新增属性和删除属性操作。对于数组类型, Object.defineProperty无法监控到通过数组下标修改属性的操作,此外直接修改数组长度也无法监控到。而这些问题使用Proxy就能完美解决。Object.defineProperty 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果属性值是对象,还需要深度遍历。 Proxy可以劫持整个对象,并返回一个新的对象。
Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢
判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度代理。
监测数组的时候可能触发多次 get/set,那么如何防止触发多次呢
我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。
Vue3和vue2有什么区别
响应式系统使用Proxy代替object.defineProperty,解决了vue2响应式的缺点。性能更好。使用Composition API替代之前的选项式api,代码组织更加高效。支持在模板中有多个根节点。支持按需加载,只会打包已经使用的api。全面拥抱ts,
说一下虚拟Dom
虚拟dom实际上就是用一个原生的JS对象去描述一个DOM节点。因为在浏览器中操作DOM的代价很高。频繁的操作DOM,会产生一定的性能问题。因此可以使用虚拟dom减少操作dom的代价,使用虚拟dom并不是说不用操作dom了,而是以最小的代价去操作dom,在vue中通过使用diff算法去比较变化前和变化后的虚拟dom树,找到需要更新的dom节点,从而去批量更新dom。相比直接操作dom,进行无差别的全部更新,显然使用diff算法加虚拟dom的方式更加节省性能。
vue2.x 和 vuex3.x 渲染器的 diff 算法分别说一下
Diff 算法用于高效计算虚拟 DOM 树之间的差异(Diff),找到需要更改的dom。
有以下过程
- 同级比较,再比较子节点
- 先判断一方有子节点一方没有子节点的情况(如果新的 children 没有子节点,将旧的子 节点移除)
- 比较都有子节点的情况(核心 diff)
- 递归比较子节点
正常 Diff 两个树的时间复杂度是 O(n^3),但实际情况下我们很少会进行跨层级的移动 DOM,所以 Vue 将 Diff 进行了优化,从O(n^3) -> O(n),只有当新旧 children 都为多个子节点时才需要用核心的 Diff 算法进行同层级比较。 Vue2 的核心 Diff 算法采用了双端比较的算法 ,同时从新旧 children 的两端开始进行比较, 借助 key 值找到可复用的节点,再进行相关操作。相比 React 的 Diff 算法,同样情况 下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。 Vue3.x 借鉴了 ivi 算法和 inferno 算法 在 创建 VNode 时 就确定其类型,以及在 mount/patch 的过程中 采用位运算来判断 一个 VNode 类型,在这个基础之上再配合核心的 Diff 算法,使得性能上较 Vue2.x 有了提 升 。该算法中还运用了动态规划的思想求解最长递归子序列。
nextTick 的作用是什么?他的实现原理是什么
作用 :nextTic用来在DOM更新循环结束之后执行延迟回调,在vue中,响应式数据发生变化,DOM的更新不会马上完成,而是把更新任务放入异步任务中,异步更新DOM 。
实现原理 :nextTick 主要使用了宏任务和微任务 。根据执行环境分别尝试采用Promise,MutationObserver,setTimeout实现异步任务派发。
- Promise:可以将函数延迟到当前函数调用栈最末端
- MutationObserver :是 H5 新加的一个功能,其功能是监听 DOM 节点的变动,在所有 DOM 变动完成后,执行回调函数setImmediate:用于中断长时间运行的操作,并在浏览器完成其他操作(如事件和显 示更新)后立即运行回调函数
- setTimeout 把函数延迟到 DOM 更新之后再使用,原因是宏任务消耗大于微任务,优先使用微任务,最后使用消耗最大的宏任务
keep-alive使用场景和原理
keep-alive 组件是 vue 的内置组件 ,用于缓存我们定义的组件。被keep-alive组件缓存的组件在来回切换时, 不用重新创建和销毁组件实例,而直接使用缓存中的组件实例,好处就是能够避免创建和销毁组件带来的性能开销,同时可以保留组件的状态 。
keep-alive 具有 include 和 exclude 属性,通过它们可以控制哪些组件进入缓存。另外它 还提供了 max 属性,通过它可以设置最大缓存数,当缓存的实例超过该数时,vue 会移除最久没有使用的组件缓存。受keep-alive的影响,其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是activated 和 deactivated,它们分别在组件激活和失活时触发。第一次 activated 触发是在 mounted 之后
在具体的实现上,keep-alive 在内部维护了一个 key 数组和一个缓存对象
// keep-alive 内部的声明周期函数
created () {
this.cache = Object.create(null)
this.keys = []
}
key 数组记录目前缓存的组件 key 值,如果组件没有指定 key 值,则会为其自动生成一个 唯一的 key 值 cache 对象以 key 值为键,vnode 为值,用于缓存组件对应的虚拟 DOM 在 keep-alive 的渲染函数中,其基本逻辑是判断当前渲染的 vnode 是否有对应的缓存, 如果有,从缓存中读取到对应的组件实例;如果没有则将其缓存。当缓存数量超过 max 数值时,keep-alive 会移除掉 key 数组的第一个元素。
Vue.set方法原理
当给对象新增不存在的属性,首先会把新的属性进行响应式处理 然后会通知所有依赖这个对象的watcher实例更新
vueRouter hash模式和history模式的实现原理
使用hash模式刷新页面 ,主要是通过监听hashchange事件,根据hash变化来实现更新页面部分内容的操作,hash值的变化,不会导致浏览器向服务器发出请求。可以很完美的实现页面跳转。
使用history模式刷新页面,主要是通过HTML5发布的pushState和replaceState API,这两个API 可以改变URL,但是不会向服务器发送请求。这样就可以监听url的变化来实现更新页面部分内容的操作。
两种模式的区别:
- hash 模式有“#”,history 模式没有
- 刷新页面时,hash 模式可以正常加载到 hash 值对应的页面,而 history 没有处理的话,会返回404,一般需要后端将所有页面都配置重定向到首页路由
- 在兼容性上,hash 可以支持低版本浏览器和 IE
vue-router路由钩子函数是什么?执行顺序是什么?
钩子函数种类有:全局守卫(beforeEach,afterEach,beforeResolve)、路由守卫(beforeEnter)、组件守卫(beforeRouterLeave, beforeRouterEnter)。
完整的导航解析流程:
-
导航被触发。
-
在失活的组件里调用 beforeRouterLeave 守卫。
-
调用全局的 beforeEach 守卫。
-
在重用的组件调用 beforeRouterUpdate 守卫( 2.2+ )。
-
在路由配置里面 beforeEnter 。
-
解析异步路由组件。
-
在被激活的组件里调用 beforeRouterEnter 。
-
调用全局的 beforeResolve 守卫( 2.5+ )。
-
导航被确认。
-
调用全局的 afterEach 钩子。
-
触发 DOM 更新。
-
调用 beforeRouterEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
vue router 和route 区别
router指的是Vue Router 的实例,用于管理和操作路由。route指的是当前页面的一个路由信息对象,
谈一下对vuex的个人理解
vuex 是什么
vuex 是一个专为 Vue 应用程序开发的状态管理器, 可以用来存储应用的全局数据。数据都存在内存中,供所有组件使用。
为什么需要 vuex
由于组件只维护自身的状态(data),组件创建时或者路由切换时,组件会被初始化,从而导致 data 也随之销毁,因此需要一个存储全局数据的仓库。
使用方法
导入vuex库,然后创建vuex的实例,在 main.js 引入这个实例。并使用vue.use注册。只用来读取的状态集中放在 store 中, 改变状态的方式是提交mutations,这是个同步的事物,异步逻辑应该封装在 action 中。
主要包括以下几个模块:
- State:定义了应用状态的数据结构,可以在这里设置默认的初始化状态。
- Getter:允许组件从Store中获取数据,mapGetters 辅助函数仅仅是将 store 中的
- getter 映射到局部计算属性。
- Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
- Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步请求。
- Module:允许将单一的 Store 拆分更多个 store 且同时保存在单一的状态树中。
Vuex 页面刷新数据丢失怎么解决
可以使用本地储存的方案来保存数据,比如可以存在localStorage或者cookie中。此外也可以使用第三方插件。推荐使用 vuex-persist 插件,它是为 Vuex 持久化储存而生的一个插件。不需要你手动存取 storage ,而是直接将状态保存至 cookie 或者 localStorage 中。
vue中使用了哪些设计模式
- 工厂模式,传入参数即可创建实例 虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode。
- 单例模式,整个程序有且仅有一个实例 vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉。
- 发布-订阅模式。(vue 事件机制)
- 观察者模式。(响应式数据原理)
- 装饰器模式(@装饰器的用法)
- 策略模式,策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案 - 比如选项的合并策略。
你都做过哪些 Vue 的性能优化?
- 响应式数据的对象层级不要过深,否则性能就会差。
- 不需要响应式的数据不要放在 data 中,可以放在外面
- v-if 和 v-show 区分使用场景
- computed 和 watch 区分场景使用
- v-for 遍历必须加 key,key最好是id值,且避免同时使用 v-if
- 图片懒加载
- 路由懒加载
- 第三方组件的按需加载(elementUI)
- 适当采用 keep-alive 缓存组件
网页的性能优化、项目第一次加载太慢优化?
- 进行代码拆分,减少首次加载页面的代码体积(保证只加载首页相关资源,对于二级页面按需加载)。比如vue的路由懒加载、
- 减少代码体积,利用构建工具的Tree Shaking功能,自动除未使用的代码。
- 合理使用缓存、对图片、视频、字体、css资源等文件使用缓存,有效减少http请求
- 还可以对文件进行压缩,在构建阶段对js、css、图片等文件进行压缩,然后在http请求的时候还可以在服务端设置gzip压缩
- 还可以使用cdn提升资源(如框架、库或图片)的加载速度
- 还可以对图片、视频等进行懒加载,只有滚动到当前位置才进行加载
安全
什么是XSS(Cross-Site Scripting)
xss是指跨站脚本攻击,黑客往网页面里插入可执行的恶意脚本代码,当用户浏览该网页时,嵌入页面里的脚本代码会被执行,从而导致用户的信息或和隐私被窃取。xss攻击发生在用户浏览信任的网页时。
主要分为反射型攻击和持久型攻击
-
反射型的攻击指的是
黑客把攻击脚本放在url链接的请求参数上,通过诱导用户点击链接,访问页面,从而执行恶意代码,导致用户信息被窃取。因此不要直接使用 eval, document.write(),innerHtml,v-html等操作执行来自用户输入的任何内容,如果有需求,也要先使用过滤库过滤用户输入的内容。 -
持久性攻击指的是
黑客在网页的文本框里输入恶意脚本,提交到被攻击网站的服务器,这时其他正常用户在访问这个页面时,服务器里的恶意脚本就会被返回给页面,导致恶意脚本被浏览器执行,用户信息被窃取。
防止手段:
- 使用xss过滤库去过滤用户输入的内容。
- cookie中的http-only设置为ture, 防止通过js获取cookie。
- 使用内容安全策略(CSP),这是浏览器级别的防御策略,可以设置一系列的规则去防止恶意脚本被执行。
总结: 一般来说,使用xss过滤库和http-only设置为true,就可以防止绝大多数xss攻击,对于一般的网站已经够用了,而对于安全性要求高的网站,则可以使用csp,防止漏网之鱼。
什么是CSRF(Cross-site request forgery)
CSRF是指跨站请求伪造,黑客利用用户的登录状态发起恶意请求,从而执行非用户本意的操作。利用的就是浏览器会自动携带同域名下的cookie机制 CSRF攻击发生在用户浏览黑客伪造的网页上。
攻击流程
- 登录受信任网站 A ,并在本地生成保存Cookie;
- 在不登出 A 情况下,访问黑客网站 B;
- 黑客网站向信任网站A发起请求,浏览器自动携带a网站生成的cookie。
- 信任网站A的服务器收到请求后,因为cooike是正确的,所以认为请求合法,于是进行黑客所希望的操作。
防止手段:
- 使用jwt认证,而不是cooke,session认证体系,根本上杜绝。
- 设置cooke的some-site属性为lax(chrom80之后默认就是lax),可以防止绝大部分第三方请求,第三方的post请求全部都不会携带cookie,get请求只有
a标签、link标签、类型为get的表单才会携带cookie。具体可以看这篇文章--> Cookie的SameSite属性 - 设置cooke的Secure为ture,限制只能在https协议里携带cookie。
登录后服务器设置一个随机值,叫做token,存在session里,并且返回给页面,以后页面每次发送请求都要携带这个值,不然就是非法请求。其实这种方式就是类似jwt的原理,如果项目登录使用的是cookie体系,就可以使用这种方法。也是可以从根本上杜绝。
计算机网络
Post 和 Get 的区别
- Get请求能缓存, Post 不能
- Post相对Get安全⼀点点, Get 请求参数在URL⾥,Post请求参数在相响应体⾥。
- Get请求参数有限制(url链接长度限制),无法太长。post请求参数没有长度限制。
http常见状态码
说一部分就行
- 200 OK ,表示从客户端发来的请求在服务器端被正确处理。
- 204 No content ,表示请求成功,但响应报⽂不含实体的主体部分,例如预检请求。
- 206 Partial Content ,表示进⾏范围请求,请求文件的一部分。例如文件分片下载。
- 301 moved permanently ,永久性重定向,表示资源已被分配了新的 URL。
- 304 not modified ,表示服务器允许访问资源,但请求未满⾜条件的情况,比如资源在客户端有缓存,就会返回304。
- 400 bad request ,请求报⽂存在语法错误。
- 401 unauthorized ,表示发送的请求需要有通过 HTTP 认证的认证信息,例如token没传。
- 403 forbidden ,表示对请求资源的访问被服务器拒绝,例如用户权限不够。
- 404 not found ,表示在服务器上没有找到请求的资源。
- 500 internal sever error ,表示服务器端在执⾏请求时发⽣了错误。
- 503 service unavailable ,表明服务器暂时处于超负载或正在停机维护,⽆法处理请求。
http常见字段
- http首部
- http请求字段
- http响应字段
- http实体字段
http 和 http2 的区别
-
支持多路复用,同时发送多个请求,可以在多个请求里使用同一个TCP 连接
-
二进制分帧, 数据传递效率更高
-
支持服务端推送 ,主动向客户端推送消息
简单编程题
如何实现 list 数据结构转 tree结构 ?
可以通过递归实现,每次递归时,通过判断列表中parentId等于传进来的parentId来筛选出子节点
function listToTreeRecursive(list, parentId = null) {
return list
.filter(item => item.parentId === parentId)
.map(item => ({
...item,
children: listToTreeRecursive(list, item.id),
}));
}
// 测试
const tree = listToTreeRecursive(list);
console.log(JSON.stringify(tree, null, 2));
通过循环加map结构,先用map存储列表所有数据,id和本身数据的对应关系,然后通过循环编利每条数据,判断当前数据的parentId是否为空,如果为空,直接将当前数据在map中对应的值加入结果数组,如果不为空,则将当前节点加入到父节点在map中对应的值的children数组中
function listToTree(list) {
const map = {}; // 用于存储节点引用
const result = []; // 最终的树形结构
// 初始化每个节点
list.forEach(item => {
map[item.id] = { ...item, children: [] };
});
// 建立父子关系
list.forEach(item => {
const node = map[item.id];
if (item.parentId === null) {
// 根节点直接加入结果
result.push(node);
} else {
// 子节点加入父节点的 children
map[item.parentId].children.push(node);
}
});
return result;
}
// 测试
const tree = listToTree(list);
console.log(JSON.stringify(tree, null, 2));
冒泡排序
// 编写方法,实现冒泡
var arr = [29,45,51,68,72,97];
//外层循环,控制趟数,每一次找到一个最大值
for (var i = 0; i < arr.length - 1; i++) {
// 内层循环,控制比较的次数,并且判断两个数的大小
for (var j = 0; j < arr.length - 1 - i; j++) {
// 白话解释:如果前面的数大,放到后面(当然是从小到大的冒泡排序)
if (arr[j] > arr[j + 1]) {
var temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
console.log(arr);
快速排序
let arr = [7,6,8,1,2,0,4,3,9,5];
function quickSort(arr,left,right){
let l = left;//左下标
let r = right;//右下标
let pivot = arr[Math.floor((l+r)/2)];//中轴值
let tmp =0;//临时变量,作为交换时使用
//while循环的目的是让比pivot值小的放到左边,比pivot值大的放到右边
while(l<r){
//在pivot的左边一直找,直到找到大于等于pivot的值,才退出
while(arr[l] < pivot){
l+=1;
}
//在pivot的右边一直找,直到找到小于等于pivot值,才退出
while(arr[r] > pivot){
r-=1;
}
//如果l >= r说明pivot 左右两边的值,已经按照左边全都是小于等于pivot的值,右边全是大于等于pivot值
if(l >= r){
break;
}
//交换
tmp = arr[l];
arr[l] = arr[r];
arr[r] = tmp;
//如果交换完后,发现arr[l]==pivot值相等,r=r-1
if(arr[l]==pivot){
r-=1;
}
//如果交换完后,发现arr[r]==pivot值相等,l++
if(arr[r]==pivot){
l+=1;
}
//如果l==r,必须l++,r--否则会出现栈溢出
if(l==r){
l+=1;
r-=1;
}
//向左递归
if(left < r){
quickSort(arr,left,r);
}
//向右递归
if(right > l){
quickSort(arr,l,right);
}
}
return arr;
}
console.log(quickSort(arr,0,arr.length-1));
选择排序
let arr = [4, 7, 2, 8, 0, 3]; // 要排序的数组
// 外层循环,从该位置取数据,剩下最后一个数字无需选择排序,因此-1
for (let i = 0; i < arr.length - 1; i++) {
let min = i; // 初始时假设当前最小数据的下标为i,并记录在min中
// 内层循环,找出最小的数字下标
for (let j = min + 1; j < arr.length; j++) {
// 如果记录的最小数字大于当前循环到的数组数字
if (arr[min] > arr[j]) {
min = j; // 将min修改为当前的下标
}
}
// 内层循环结束,此时min记录了剩余数组的最小数字的下标
// 将min下标的数字与i位置的数字交换位置
let temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
}
console.log(arr); // [ 0, 2, 3, 4, 7, 8 ]
插入排序
/**
* 插入排序
*/
function insertSort() {
console.log('--------------------')
console.log('插入排序:开始:' + arr)
let cur, temp
for (var i = 1; i < arr.length; i++) {
cur = i
for (var j = i - 1; j >= 0; j--) {
if (arr[j] > arr[cur]) {
temp = arr[cur]
arr[cur] = arr[j]
arr[j] = temp
cur = j
} else {
break
}
}
console.log('插入排序' + (i) + '趟---' + arr)
}
}
希尔排序
/**
* 希尔排序
*/
function shellSort() {
let arr = deepClone(array)
console.log('--------------------')
console.log('希尔排序:开始:' + arr)
let cur, temp, gap = 1
while (gap < array.length / 3) {
gap = gap * 3 + 1
}
for (gap; gap > 0; gap = Math.floor(gap / 3)) {
console.log('希尔排序:------gap:' + gap + '-----')
for (var i = gap; i < arr.length; i++) {
cur = i
for (var j = i - gap; j >= 0 && arr[j] > arr[cur]; j -= gap) {
temp = arr[cur]
arr[cur] = arr[j]
arr[j] = temp
cur = j
console.log('希尔排序' + (i) + '趟---' + arr)
}
}
}
}
二分查找
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
// 二分查找运用了数组双指南针技巧, nums为升序
let l = 0;
let r = nums.length - 1; // r参与运算
while(l <= r) {
let mid = Math.floor(l + (r - l) / 2);
if(nums[mid] === target) {
return mid;
}else if(nums[mid] > target) {
r = mid - 1;
}else {
l = mid + 1;
}
}
return -1;
};