艺术喵 2 年前端面试心路历程(字节跳动、YY、虎牙、BIGO)| 掘金技术征文
2021前端面试经常被问到的题(附答案)
1、h5和css3新特性
h5新特性
- 语义化标签:header、footer、section、nav、aside、article
- 增强型表单:input的type多了color、date、email、number、tel、url、week
- 新增表单属性:placehoder、autofocus、min 和 max
- 音频视频:audio、video
- canvas
- 地理定位、拖拽api
- 新事件:onresize、onscroll、onerror
- WebSocket:单个 TCP 连接上进行全双工通讯的协议
css新特性
- 新增选择器:
:last-child /* 选择元素最后一个孩子 /
:first-child / 选择元素第一个孩子 /
:nth-child(n) / 按照第几个孩子给它设置样式 /
:nth-child(even) / 按照偶数 /
:nth-child(odd) / 按照奇数 /
:disabled / 选择每个禁用的E元素 /
:checked / 选择每个被选中的E元素 /
::selection / 选择被用户选取的元素部分 */- background-size、background-origin、border-radius、box-shadow / text-shadow、border-image、
- 文本效果:text-shadow、@font-face 自定义字体
- 2D/3D 转换、动画、过渡
- 多列布局:column-count、column-gap、column-rule
2、浏览器由那部分组成
用户界面、浏览器引擎(渲染引擎、js引擎)、网络、UI后端 、数据存储
3、说说DOM、BOM
javascript 有三部分构成,ECMAScript,DOM和BOM
DOM(文档对象模型): 处理网页内容的方法和接口
BOM (浏览器对象模型): 访问BOM(Browser Object Model)对象来访问、控制、修改客户端(浏览器); BOM的核心是Window,而Window对象又具有双重角色,它既是通过js访问浏览器窗口的一个接口,又是一个Global(全局)对象;
Window对象包含属性:document、location、navigator、screen、history、frames
4、Promise 对象方法
then()、catch()、all()、race():将多个 Promise 实例,包装成一个新的 Promise 实例 resolve ()、reject()
1、判断js类型的方式
typeof: 能检测出的数据类型有 number、string、boolean、undefined、object、function 缺点不能细分对象、数组,并且null返回'object'
instanceof:([1,2,3] instanceof Array) 判断是否是某个类的实例,所以左侧要是一个对象 缺点:不能判断基本数据类型
constructor: ([1,2,3] .constructor==Array) constructor是prototype对象上的属性,指向构造函数。根据实例对象寻找属性的顺序,若实例对象上没有实例属性或方法时,就去原型链上寻找,因此,实例对象也是能使用constructor属性的。 缺点:不能判断null、undefined,constructor 所指向的的构造函数 可以被修改的
Object.prototype.toString.call(): Object.prototype.toString对任何变量都会返回这样一个字符串"[object class]",class 就是 JS 内置对象 构造函数的名字。 call是用来改变调用函数作用域的。
2、Es5和es6分别几种声明变量方式区别是什么
let、const声明变量没有提升,并且const、let声明变量并不会同步window
3、闭包的概念?优缺点?解决方案
函数执行形成一个全新的执行上下文,进栈执行后所创建的一些变量被外部所引用,不能出栈销毁,就形成了闭包。
作用:延长了局部变量的生命周期,保护全不被污染
缺点:函数不能被出栈销毁,频繁使用会造成内存泄漏问题。
解决方法:手动销毁 fn=null
4、数组去重
let ary = [1,2,1,5,4,6,6,1,2,8,3]
let arr = new Set(ary)
ary = [...arr]
```
```js
let ary = [1,2,1,5,4,6,6,1,2,8,3]
let newAry = []
for(let i = 0;i < ary.length;i++){
if(newAry.includes(ary[i])){
continue
}
newAry.push(ary[i])
}
5、Dom事件有哪些阶段?谈谈对事件代理的理解
Dom事件阶段:捕获、目标、冒泡
事件代理:事件不直接绑定到某个元素上,而是绑定到该元素的父元素上,通过事件触发冒泡,在判断是否是目标元素从而执行相应的逻辑。
优化:代码简单,节省内存
6、讲讲Js事件队列
队列结构特点:
先进先出,只允许在队列的最开始删除(出),只允许在队列的末尾添加(进);
特殊情况:优先队列
一个队列具有:1、队列容器2、进去队列的方法3、出队列的方法4、可以查看队列的方法5、查看队列的内容方法
// 自己实现一个普通队列
function Queue(){
// 创建一个队列容器
this.container = []
}
Queue.prototype = {
construntor:Queue,
// 进入队列 element进入队列的元素
enter:function(element){
this.container .push(element)
},
// 移除队列
leave:function(){
if(this.container .length === 0 ) return;
this.container .shift()
},
// 查看队列长度
size:function(){
return this.container .length
},
// 查看队列的内容
value:function(){
// 深度克隆是为了保证外面接收到值后更改不会影响内容容器的值
return JSON.parse(JSON.stringfy(this.container ))
}
}
let qe = new Queue()
优先队列:可以在插入的时候指定优先级
// 实现一个优先级队列
function Queue(){
// 创建一个队列容器
this.container = []
}
Queue.prototype = {
construntor:Queue,
// 进入队列 priority 优先级,默认都是0,数值越大,优先级越高
enter:function(element,priority = 0){
let obj = {
value : element,
priority :priority
}
if(priority === 0){
// 不指定优先级(默认的优先级),直接存储到末尾即可
this.container .push(element)
return
}
// 指定优先级,我们需要从最后一项一次来比较
// 遇到比自己大的直接插到这项末尾
let flag = false
for(let i = this.container.length - 1;i>=0;i--){
let item = this.container[i]
if(item.priority >= priority ){
// 插入到比较项的后面
this.container.splice(i + 1,0,obj)
flag = true
return
}
}
// 优先级没有比我大的,我就是最大的,直接插入到容器的最开始位置即可
!flag ? this.container.unshift(obj) : null
},
// 移除队列
leave:function(){
if(this.container .length === 0 ) return;
this.container .shift()
},
// 查看队列长度
size:function(){
return this.container .length
},
// 查看队列的内容
value:function(){
// 深度克隆是为了保证外面接收到值后更改不会影响内容容器的值
return JSON.parse(JSON.stringfy(this.container ))
}
}
let qe = new Queue()
事件队列和事件循环
js是单线程的只有等主线程执行完毕,才会去执行异步的任务。
异步任务放在任务队列中,任务队列分为微任务和宏任务 先执行微任务在执行宏任务
微任务:promise、async、await
宏任务:定时器
事件循环:主线程执行完代码后去任务队列中查找可执行异步任务,先找微任务找到执行,在查找执行,微任务没有后再找宏任务,查找执行;这一套机制称之微事件循环机制。
7、说说async 和 await
ES7 引入了 async/await,这是 JavaScript 异步编程的一个比较大的改进。我们可以像写同步代码一些编写异步代码,避免了回调地狱,同时也代码也比 Promise 更易于阅读。async 就是异步的意思,在函数的定义前加上 async 关键字,表示这是一个异步函数,意味着该函数的执行不会阻塞后面代码的执行。await 就是等待的意思,即等待请求或者资源。await 后面可以接任何普通表达式,但一般会在 await 后面放一个返回 promise 对象的表达式。
注意 :await 关键字只能放到 async 函数里面。
8、es6的class和构造函数的区别
es5中没有类使用构造函数来模拟类。不同点:类的内部所有定义的方法,都是不可枚举的;类的构造函数,不使用new是没法调用的,会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行;Class不存在变量提升(hoist),这一点与ES5完全不同;
// 使用构造函数实现类
function Animal(){
// 首先判断 不是通过new来调用的 判断依据 this是不是构函数的实例
if(!(this instanceof Animal)){
throw new Error('NOT NEW')
}
this.name = {name:'zf'}; //实例上的属性
this.age = 10; // 实例上的属性
}
Animal.prototype.say =function(){ // 公共属性
console.log('say')
}
let a1 = new Animal();
let a2 = new Animal();
9、实现new方法
创建一个空的对象;
绑定this指向,执行构造函数;
链接到原型;
确保返回的是对象;
new关键字的大致实现原理
function mockNew(A){
// 创建一个空的对象;
let obj = {}
// 绑定this指向,执行构造函数;
let returnVal = A.call(obj);
// 如果一个类返回了一个引用空间 那么实例将这个空间
if((typeof returnVal === 'object' && returnVal !== null) || typeof returnVal === 'function'){
return returnVal;
}
// 链接到原型;
obj.__proto__ = A.prototype
return obj
}
10、实现继承
原型继承:(继承方法)
function Parent(){
this.name = '名字'
this.age = 18
}
Parent.protptype = sun(){
console.log('唱歌')
}
function Son(){
}
// 子类的实例指向父类的实例,子类实例在调用方法的时候可以通过__proto__向上查找
Son.prototype = new Son()
借用call继承:(继承属性)
function Son(){
// 借用call来执行构造函数,传入this
Parent.call(this);
}
组合继承:(原型继承和借用call继承一起使用)
11、实现promise
12、实现一个call函数
13、实现一个apply函数
14、实现一个bind函数
15、浅拷贝、深拷贝的实现
浅拷贝:
for···in只循环第一层:
function simpleCopy(obj1) {
var obj2 = Array.isArray(obj1) ? [] : {};
for (let i in obj1) {
obj2[i] = obj1[i];
}
return obj2;
}
Object.assign方法
... 扩展运算符
深拷贝:
1、通过JSON对象来实现深拷贝
let newObj = { ...school, ...my };
newObj = JSON.parse(JSON.stringify(newObj));
缺点: 无法实现对对象中方法的深拷贝,无法拷贝属性值为undefined的、正则对象的...
2、lodash函数库实现深拷贝
let result = _.cloneDeep(test)
3、通过jQuery的extend方法实现深拷贝
var array = [1,2,3,4];
var newArray = $.extend(true,[],array); // true为深拷贝,false为浅拷贝
4、手动实现深拷贝
const deepClone = (value ,hash = new WeakMap) => {
if(value == null) return value; // 排除掉null 和undefine 的情况
if(typeof value !== 'object') return value; // 这里包含了函数类型
if(value instanceof RegExp) return new RegExp(value);
if(value instanceof Date) return new Date(value);
// .....
let instance = new value.constructor; // 根据当前属性构造一个新的实例 attay||object
if(hash.has(value)){ // 先去hash中查看一下是否存在过 ,如果存在就把以前拷贝的返回去
return hash.get(value); // 返回已经拷贝的结果
}
hash.set(value,instance); // 没放过就放进去
// 拷贝的人可能是一个对象 或者是一个数组 (循环) for in
for(let key in value){ // 一层
if(value.hasOwnProperty(key)){ // 将hash 继续向下传递 保证这次拷贝能拿到以前拷贝的结果
instance[key] = deepClone(value[key],hash); // 递归--产生的就是一个新的拷贝后的结果
}// 过滤掉原型链上的属性
}
return instance
};
16、实现一个防抖函数
n秒内函数只会执行一次,如果n秒内事件再次被触发,则重新计算时间,事件触发时间会无限延长。
// 防抖函数返回值为一个匿名函数,其中设置定时器来执行某函数
function debounce(fn,delay){
var timer = null
return (...args) {
clearTimeout(timer)
timer = window.setTimeout(()=>{
fn.apply(this,args)
},delay)
}
}
17、实现一个节流函数
高频事件在规定时间内只会执行一次,执行一次后,只有大于设定的执行周期后才会执行第二次
function throttle(fn,delay){
var timer = null
let flag = true // 是否在单位时间内执行的判断依据
return (...args) {
if(!flag) return
clearTimeout(timer)
flag = flase
timer = window.setTimeout(()=>{
fn.apply(this,args)
flag = true
},delay)
}
}
18、Instanceof的原理
19、柯里化函数的实现
20、Object.create的基本实现原理
// Object.create()使用指定的原型对象及其属性去创建一个新的对象,并使新对象的__proto_指向这个原型对象 ,想对于Object.setPrototypeOf唯一有些不同的是他在中间加了一层;
function create(parentProto){
function Fn(){}
Fn.prototype = parentProto;
let fn = new Fn();
fn.constructor = Tiger
return fn;
}
21、实现一个基本的Event Bus
// 组件通信,一个触发与监听的过程
class EventEmitter {
constructor () {
// 存储事件
this.events = this.events || new Map()
}
// 监听事件
addListener (type, fn) {
if (!this.events.get(type)) {
this.events.set(type, fn)
}
}
// 触发事件
emit (type) {
let handle = this.events.get(type)
handle.apply(this, [...arguments].slice(1))
}
}
// 测试
let emitter = new EventEmitter()
// 监听事件
emitter.addListener('ages', age => {
console.log(age)
})
// 触发事件
emitter.emit('ages', 18) // 18
22、实现一个双向数据绑定(思路:劫持数据、监听input值变化并修改)
<input type="text" id="input_1">
<span id="span_1"></span>
复制代码
var obj = {};
Object.defineProperty(obj, 'test', {
set: (newVal)=>{
document.getElementById('input_1').value = newVal;
document.getElementById('span_1').innerHTML = newVal;
}
});
document.addEventListener('keyup', (e)=>{
obj.test = e.target.value;
})
23、实现一个简单路由
// hash路由
class Route{
constructor(){
// 路由存储对象
this.routes = {}
// 当前hash
this.currentHash = ''
// 绑定this,避免监听时this指向改变
this.freshRoute = this.freshRoute.bind(this)
// 监听
window.addEventListener('load', this.freshRoute, false)
window.addEventListener('hashchange', this.freshRoute, false)
}
// 存储
storeRoute (path, cb) {
this.routes[path] = cb || function () {}
}
// 更新
freshRoute () {
this.currentHash = location.hash.slice(1) || '/'
this.routes[this.currentHash]()
}
}
24、实现懒加载
25、手写ajax
window.onload = function(){
var oBtn = document.getElementById("btn1");
oBtn.onclick = function(){
//1、创建ajax对象
var xhr = null;
try{
xhr = new XMLHttpRequest();
}catch(error){
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
//2、等待数据响应
//必须在调用open()方法之前指定onreadystatechange事件处理程序才能确保跨域浏览器兼容性 //问题
//只要readyState属性的值有变化,就会触发readystatechange事件
xhr.onreadystatechange = function(){
if(xhr.readyState == 4){
//判断本次下载的状态码都是多少 304表示请求的资源没有被修改
if((xhr.status >= 200 && xhr.status<300)||xhr.status ==304){
alert(xhr.responseText);
}else{
alert("Error:" + xhr.status);
}
}
}
//3、调用open
xhr.open("get", "1.get.php?username=yyy&age=19&password=123abc", true);
//4、调用send
xhr.send();
}
}
26、如何理解mvvm原理?
mvvm 双向绑定,采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() 来劫持各个属性的 setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
27、V-model实现原理?
model只不过是一个语法糖而已,真正的实现靠的还是
v-bind:绑定响应式数据
触发oninput事件并传递数据
28、Vue双向数据绑定原理(响应式数据的原理)
核心点: Object.defineProperty
默认 Vue 在初始化数据时,会给 data 中的属性使用 Object.defineProperty 重新定义所有属
性(只会劫持已经存在的属性)多层对象通过递归实现劫持,当页面取到对应属性时,会进行依赖收集(收集当前组件的watcher) 如果属性发生变化会通知相关依赖进行更新操作。
29、描述一下vue从初始化页面–修改数据–更新页面ui的过程
默认 Vue 在初始化数据时,会给 data 中的属性使用 Object.defineProperty 重新定义所有属
性(只会劫持已经存在的属性)多层对象通过递归实现劫持,当页面取到对应属性时,会进行依赖收集(收集当前组件的watcher) 如果属性发生变化会通知相关依赖进行更新操作。
30、虚拟dom实现原理
虚拟DOM本质上是JavaScript对象,是对真实DOM的抽象状态变更时,记录新树和旧树的差异最后把差异更新到真正的dom中
31、Vue中key值的作用
当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。key 的作用主要是为了高效的更新虚拟DOM。
32、Vue的生命周期
beforeCreated、created、beforeMounted、mounted、brforeUpdated、updated、beforeDestroy、destoryed
33、Vue的组件通信方式
自定义属性\props 自定义事件\$emit 事件总线 $parent/$children $attrs/$listeners provide/inject依赖注入
34、Watch、methods和computed的区别
watch 为了监听某个响应数据的变化。computed 是自动监听依赖值的变化,从而动态返回内容,主要目的是简化模板内的复杂运算。所以区别来源于用法,只是需要动态值,那就用 computed ;需要知道值的改变后执行业务逻辑,才用watch。
methods是一个方法,它可以接受参数,而computed 不能,computed 是可以缓存的,methods 不会。computed 可以依赖其他 computed,甚至是其他组件的 data。
35、Vue中怎么重置data
使用Object.assign(),vm.$data可以获取当前状态下的data,vm.$options.data(this)可以获取到组件初始化状态下的data。
Object.assign(this.$data, this.$options.data(this))
36、组件中写name有什么作用
组件递归是用、keep-alive有用、调试用
37、vue-router有哪些钩子函数
beforeEach、afterEach、beforeEnter、beforeRouteEnter
38、Route和router的区别和router的区别
Route:路由信息对象,path、query、name等路由信息
router:路由实例对象,包含路由调转的方法
39、Vue的nextTick的原理是什么?
nextTick的回调是dom更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,可以获取更新后的dom。原理:就是异步方法(promise/mutationObserver/setImmediate/setTimeout)
补充回答:vue多次更新数据,最终会把watcher存放到一个自定义队列中,采用异步的方法(异步方法会在同步代码执行完毕在执行达到延迟更新)进行批量更新。内部调用的是nextTick实现延迟更新,用户自定义的nextTick回调会被延迟到更新完后调用,从而获取到更新后的dom
40、Vuex有哪些属性
state、 mutation (改变state的状态不能书写异步代码)、action、Getter、modules
41、Vue首屏加载优化
CDN优化、路由懒加载、组价按需引入
42、Vue-cli替我们做了哪些工作?
43、对前端性能优化有什么方案