logo头像

流莹离|拼命往前,仗剑天涯

常见面试题

前端如何进行seo优化

  • 合理的title、description、keywords:搜索对着三项的权重逐个减小,title值强调重点即可;description把页面内容高度概括,不可过分堆砌关键词;keywords列举出重要关键词。

  • 语义化的HTML代码,符合W3C规范:语义化代码让搜索引擎容易理解网页,重要内容HTML代码放在最前:搜索引擎抓取HTML顺序是从上到下,保证重要内容一定会被抓取

  • 重要内容不要用js输出:爬虫不会执行js获取内容

  • 少用iframe:搜索引擎不会抓取iframe中的内容

  • 非装饰性图片必须加alt

  • 提高网站速度:网站速度是搜索引擎排序的一个重要指标

一次性插入1000个div,如何优化插入的性能

  • 使用Fragment,碎片收集
1
2
var fragment = document.createDocumentFragment();
fragment.appendChild(elem);

说说你对 SPA 单页面的理解,它的优缺点分别是什么?

SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。

一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  • 基于上面一点,SPA 相对对服务器压力小;
  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

缺点:

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

computed 和 watch 的区别和运用的场景?

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

运用场景:

  • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
  • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

直接给一个数组项赋值,Vue 能检测到变化吗?

由于 JavaScript 的限制,Vue 不能检测到以下数组的变动:

  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength
1
2
3
4
5
6
7
8
9
10
// 为了解决第一个问题,Vue 提供了以下操作方法:
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
// 复制代码为了解决第二个问题,Vue 提供了以下操作方法:
// Array.prototype.splice
vm.items.splice(newLength)

Vue 的父组件和子组件生命周期钩子函数执行顺序?

  • 加载渲染过程
    父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
  • 子组件更新过程
    父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
  • 父组件更新过程
    父 beforeUpdate -> 父 updated
  • 销毁过程
    父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

在哪个生命周期内调用异步请求?

可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。但是本人推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

能更快获取到服务端数据,减少页面 loading 时间;
ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

父组件可以监听到子组件的生命周期吗?

需要手动触发$emit

1
2
3
4
5
6
7
<!--// Parent.vue-->
<Child @mounted="doSomething"/>

<!--// Child.vue-->
mounted() {
this.$emit("mounted");
}

更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,

1
2
3
4
5
6
7
8
9
10
11
<!--//  Parent.vue-->
<Child @hook:mounted="doSomething" ></Child>

doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},

<!--// Child.vue-->
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
}

谈谈你对 keep-alive 的了解?

keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:

  • 一般结合路由和动态组件一起使用,用于缓存组件;
  • 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
  • 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

组件中 data 为什么是一个函数?

为什么组件中的 data 必须是一个函数,然后 return 一个对象,而 new Vue 实例里,data 可以直接是一个对象?

因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。

虚拟 DOM 实现原理?

虚拟 DOM 的实现原理主要包括以下 3 部分:

用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
diff 算法 — 比较两棵虚拟 DOM 树的差异;
pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。

你有对 Vue 项目进行哪些优化?

(1)代码层面的优化

v-if 和 v-show 区分使用场景
computed 和 watch 区分使用场景
v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
长列表性能优化
事件的销毁
图片资源懒加载
路由懒加载
第三方插件的按需引入
优化无限列表性能
服务端渲染 SSR or 预渲染

(2)Webpack 层面的优化

Webpack 对图片进行压缩
减少 ES6 转为 ES5 的冗余代码
提取公共代码
模板预编译
提取组件的 CSS
优化 SourceMap
构建结果输出分析
Vue 项目的编译优化

js的运行机制

js是单线程,执行代码的时候,只有一个主线程来处理所有的任务。一个线程可以保证程序执行的一致性。

单线程的缺点就是当有一个任务阻塞的时候,就无法继续执行下去,为了解决这种问题,就出现了异步任务,异步任务就是执行这个任务时,无法立刻返回,驻现场就会把这个任务挂起,然后继续往下执行

js执行代码时会将不同变量存于内存中的不同位置,堆和栈,堆是对象,栈放基础类型变量以及对象的指针。

当我们调用js一个方法的时候,js会生成一个与这个方法对应的执行环境,又叫执行上下文,js是单线程,同一时间只能执行一个方法,于是这些方法就会放入一个栈中排队,这个栈叫做执行栈。

当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。。这个过程反复进行,直到执行栈中的代码全部执行完毕。这个过程也叫做event loop(事件循环)

事件队列:微任务和宏任务,执行顺序先微后宏

微任务 promise
宏任务 setTimeout setIntervel

js垃圾回收机制

  • 有一组基本的固有可达值,由于显而易见的原因无法删除。例如:

    • 本地函数的局部变量和参数
    • 当前嵌套调用链上的其他函数的变量和参数
    • 全局变量
    • 还有一些其他的,内部的

这些值称为根。

  • 如果引用或引用链可以从根访问任何其他值,则认为该值是可访问的。

例如,如果局部变量中有对象,并且该对象具有引用另一个对象的属性,则该对象被视为可达性, 它引用的那些也是可以访问的,

可达性 和 引用链

不可达,引用链丢失,就回收对象。

null undefined NaN “” 的区别

null

  • 是一种特殊的object
  • 空值,空指针,指针未指向任何内存空间(指向不存在的东西)
  • 可以与undefined相等 == ,但 === 时不相等
  • 可以等于自己,返回true
  • 转化为数值是0,布尔值是false,字符串是’null’
  • 与false比较或严格比较,输出false

undefined

  • 对应类型undefined
  • 全局作用域的一个变量
  • 空值,未定义,使用了一个并未声明的变量时,或者使用了已经声明但还没有赋值的变量时,又或者使用了一个并不存在的对象属性时,返回这个值
  • 可以等于自己,返回true
  • 转为数值是NaN,布尔值是false,字符串是’undefined’
  • 与false比较或严格比较,输出false

NaN

  • 一种特殊的Number,表示非数字值,实际上也是一个数字(typeof NaN = “number”)
  • NaN不等于自己(可以用isNaN()检测)
  • 转为数值是NaN,布尔值是false,字符串是’NaN’
  • 与false比较或严格比较,输出false

“”

  • 字符串类型,不等于空格
  • 空字符串,长度为0
  • 转为数值是0,布尔值是false,字符串是””
  • 与false比较时输出true,严格比较时,输出false

完整的登陆流程

登陆页面,点击登录按钮时触发的事件

  • 客户端请求后台接口
  • 后台验证通过,保存用户的登陆状态,并写入cookie中
  • 用户进入需要登录权限的页面时,判断该状态,确认用户是否需要重新登录
  • 如cookie过期,跳转至登录页重新认证

new操作符具体干了什么

new的实现原理是什么?

1
let func = new Func()
  • 创建一个空对象obj
1
let obj = new Object()
  • 链接到原型

让obj对象的proto指向构造函数Func的原型
此时便建立了 obj 对象的原型链:obj->Func.prototype->Object.prototype->null

1
obj.__proto__ = Func.prototype
  • 绑定this值

让Func中的this指向obj对象

1
Func.call(obj)
  • 返回obj对象
1
func = obj

数组去重方法

  • 双重for循环

  • filter + indexOf

1
2
3
arr.filter((item, index)=> {
return arr.indexOf(item) === index
})
  • ES6 new Set()
1
2
3
function distinct(arr) {
return Array.from(new Set([...arr]))
}
  • for…of + Object
1
2
3
4
5
6
7
8
9
10
11
12
function distinct(arr) {
let result = []
let obj = {}
for (let i of arr) {
if (!obj[i]) {
result.push(i)
obj[i] = 1
}
}

return result
}

回调函数与回调地狱

在某一个方法的参数里放一个函数,这个函数就称之为回调函。

回调地狱,就是回调函数里又调用方法,方法的参数里又放函数。

回调地狱的根本问题:

  • 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,影响后面的代码执行。

  • 嵌套函数一多,就很难定位和处理错误。