金三银四 2021年前端面试笔记
又到了找工作的黄金时间,3-4月份,跳槽是每个人的职业生涯中都要经历的过程,笔者最近也是复习了一波,整理了一下面试中关于javascript和vue的一些问题。看到本文的你如果感觉对你有帮助,不如素质三连,码字不易,感谢您的支持!
JavaScript
数据类型
介绍一下js中的数据类型以及值是如何存储的
JavaScript中一共有8种数据类型,其中基本数据类型有:Null、Undefined、Boolean、String、Number、Bigint、Symbol。
还有一种数据类型object:里面包含function、Array、Date等。基本数据类型保存在栈区中,占据空间小、大小固定。
引用数据类型保存在栈区和堆区,占据空间大,且大小不固定。引用数据类型在栈区中存放了指针,指针指向堆区的实际地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
js中数据类型判断
说一说在js中判断数据类型的方法
typeof
- typeof可以判断原始数据类型,除了null之外:
console.log(typeof 2) //number console.log(typeof 'hello') //string console.log(typeof null) //object console.log(typeof true)//boolean console.log(typeof undefined) //undefined console.log(typeof []) //object
- 因为因为特殊值null被认为是一个对空对象的引用
instanceof
- instanceof可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的prototype:
<figcaption style="line-height: inherit; margin: 0px; padding: 0px; margin-top: 10px; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;">wWAKt1.png</figcaption>
console.log([] instanceof Array) //true console.log({} instanceof Object)//true console.log(function(){} instanceof Function)//true console.log(1 instanceof Number)//false
constructor
- constructor主要是利用原型上的prototype.constructor指向实例的构造函数来进行判断的
console.log((1).constructor === Number) //true console.log('1'.constructor===String)//true console.log((function(){}).constructor===Function) //true console.log([].constructor===Array) //true console.log(({}).constructor===Object) //true
- 但constructor有个特点就是:如果我创建了一个对象,我们再去修改它的原型,就变得不那么可靠:
function Func(){} Func.prototype=new Array(); const f=new Func() console.log(f.constructor===Function)//false
Object.prototype.toString.call
- 使用 Object 对象的原型方法toString,返回值是[object 类型]字符串,该方法基本上能判断所有的数据类型.
var toString = Object.prototype.toString; console.log(toString.call(2)) //[object Number] console.log(toString.call(true)) //[object Boolean] console.log(toString.call(function(){})) //[object Function]
作用域和作用域链
谈一谈你对作用域、作用域链的理解。
作用域:作用域就是定义变量的区域,它有一套访问变量的规则,根据这套规则来管理浏览器引擎如何在当前作用域和嵌套作用域中中根据变量(标识符)进行变量查找。
作用域链:作用域链保证对执行环境有权访问所以变量和函数的有序访问,通过作用域链,我们可以访问到外层环境中的变量和函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找
this
谈一谈你对this的理解,以及在各种环境下的this
在浏览器里,在全局范围内this指向window对象
在函数中,this永远指向最后调用他的那个对象(箭头函数除外)。
在构造函数中,this指向new出来的新对象。
call、apply、bind中的this被强绑定在指定的那个对象上。
箭头函数this为父作用域的this,不是调用时的this。
原型,原型链
谈一谈JavaScript中原型,原型链,有什么特点
- 在js中,我们可以通过构造函数来创建一个对象,每个构造函数都会有个prototype属性,这个属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是Object.prototype。
function Func(name){ this.name=name }; let tom=new Func('TOM'); console.log(tom) console.log(tom.__proto__===Func.prototype) //true console.log(tom.__proto__.constructor==Func) //true
- 再上一张图,更好理解
<figcaption style="line-height: inherit; margin: 0px; padding: 0px; margin-top: 10px; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;">w2zC4g.png</figcaption>
- 原型对象的作用:
function Func(name){ this.name=name; this.say=function(){ console.log(this.name) } } let m=new Func('tom'); let n=new Func('tom') console.log(m) console.log(n) console.log(m.say===n.say) //false
- 每次进行new,都会开辟新的区域,这样很显然不好,所以我们可以吧共有的方法放在原型对象上,这样就避免了内存浪费:
function Func(name){ this.name=name; } Func.prototype.say=function(){ console.log(this.name) } let m=new Func('tom'); let n=new Func('tom') console.log(m) console.log(n) console.log(m.say===n.say) //true
闭包
谈一谈对闭包的理解,以及使用场景
闭包是指有权访问另一个函数作用域内的变量的函数。闭包最常见的就是在函数中创建函数,创建的函数就可以访问到当前函数的局部变量。
过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
function func(){ let n=0; function add(){ n++; console.log(n) } return add } let a=func(); a() //1 a()//2
事件模型
什么是事件?都有哪几种事件?
事件是指用户操作页面时候发生的交互或者网页本身的一些操作,浏览器一共有三种事件模型:
DOM0级模型:这种模型不会传播,没有事件流的概念,但现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以通过js属性来指定监听函数。
IE事件模型:在这种事件模型中,一次事件一共有两个过程,事件处理阶段和事件冒泡阶段,事件处理阶段首先会执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从冒泡到document,一次检查经过的节点是否绑定了事件监听函数,会按顺序依次进行。
DOM2级事件模型:在该事件模型中,一次事件一共有三个过程,第一个过程就是事件捕获阶段,捕获指的是事件从document一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面的两个阶段和IE事件模型基本一样。这样的事件模型,事件绑定的函数就是addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行。
//DMO0 element.onclick=function(){} //DOM2 element.addEventListener('click',function(){},false) //DOM3 增加了鼠标事件,键盘事件 element.addEventListener('keyup',function(){},false)
DOM事件的捕获流程:window--->document--->html--->body--->逐渐传递
DOM事件冒泡过程:目标元素--->父元素--->body--->html--->document--->window
Event对象的常见应用
event.preventDefault() //阻止默认行为 比阻止点击a标签转跳
event.stopPropagation() //阻止事件冒泡
event.stoplmmediatePropagation() //同时注册两个事件,决定事件优先级
event.currentTarget() //事件代理,把子元素的事件委托给父元素
event.target() //当前被点击的元素
异步编程
谈一谈js中的异步编程方案,它为了解决什么?
- 我们之前写代码,可能会出现函数嵌套函数,如果多个嵌套,结构就会很乱,也不容易维护,于是便有了异步编程的概念。
Promise
Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise对象的状态不受外界影响,它有三种状态,
pending
(进行中)、fulfilled
(已成功)、rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。它的状态可以从pending变为fulfilled,或者从pending变为rejected。
then方法
- Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。
const testPormise=new Promise((resovle,reject)=>{ console.log("hi,Pormise"); let test=true; if(test){ resovle('成功~') }else{ reject("失败了") } }); testPormise.then((res)=>{ console.log(res) }).catch((erro)=>{ console.log(erro) })
rejected
- 只有执行了rejected这样就可以在then中捕获到,然后执行失败情况下的回调:
let p = new Promise((resolve, reject) => { //做一些异步操作 setTimeout(function(){ var num = Math.ceil(Math.random()*10); //生成1-10的随机数 if(num<=3){ resolve(num); } else{ reject('数字太大了'); } }, 2000); }); p.then((data) => { console.log('resolved',data); },(err) => { console.log('rejected',err); } );
catch
- catch和then用法一样,用来指定reject的回调:
p.then((data) => { console.log('resolved',data); }).catch((err) => { console.log('rejected',err); });
all
- all接收一个数组参数,里面的值最终都算返回Promise对象,谁执行的慢,以谁为准执行回调:
let Promise1 = new Promise(function(resolve, reject){}) let Promise2 = new Promise(function(resolve, reject){}) let Promise3 = new Promise(function(resolve, reject){}) let p = Promise.all([Promise1, Promise2, Promise3]) p.then(funciton(){ // 三个都成功则成功 }, function(){ // 只要有失败,则失败 })
race
- 谁跑的快,以谁为准执行回调,常见应用场景为设置请求超时时间,在请求超时后执行相应的回调。
Event Loop
说一说js中事件执行机制是怎么样的?
“JavaScript 是单线程、异步、非阻塞、解释型脚本语言”。JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,Event Loop 的方案应用而生。
在js中,任务进入执行栈,先判断任务类型,如果是同步任务,直接进入到主线程执行。如果是异步任务,会把任务放到异步队列,等同步任务执行完以后,事件触发线程会从消息队列中取出刚才加入队列的函数,如果有,就一条一条的去执行。
console.log(1) setTimeout(() => { console.log(2) }, 1000); console.log(3) //1 3 2
微任务
- js中,setTimeout属于宏任务,像Promise为微任务,
console.log(1) setTimeout(() => { console.log(2) }, 1000); let test=new Pormise((resolve)=>{ console.log(3); resolve(); }) .then(=>console.log(4)) console.log(5) 1.3 5 4.2
- 首先会输出1,然后遇到setTimeout,注册任务接着又遇到Pormise,首先先输出3,然后注册任务,接着会输出5,这时候执行栈没有可执行的,然后会从队列中取,这时候会先取出微任务进行执行,进入到then,输出4,这时候执行栈又为空,这时候继续从队列中取出一条任务,这时候会输出2。
继承
如何实现继承?怎么样能完美继承?
- 构造函数继承,借助构造函数通过call apply改变指向实现继承,但这种继承方式有一个缺点:继承不了父类原型对象上的属性,只能继承构造函数内的属性。
function Parent1(){ this.name='Parent1' }; Parent1.prototype.say=function(){ console.log(this.name) } function Child1(){ Parent1.call(this);//apply this.type='Child1' } let n=new Child1(); console.log(n.say)//undefined
- 原型链实现继承,缺点:实例出来的是共用的。
function Parent(){ this.name='Parent1'; this.arr=[1,2,3,4,5] }; Parent.prototype.say=function(){ console.log(this.name) } function Child(){ this.type='Child1' } Child.prototype=new Parent(); var s1=new Child(); var s2=new Child(); s1.arr.push(6) console.log(s1.arr) //[1,2,3,4,5,6] console.log(s2.arr) //[1,2,3,4,5,6] //他们俩是公用的 console.log(s1.__proto__===s2.__proto__) //true
- 组合继承(借鉴上面两个的优点)
function Parent(){ this.name='tom'; }; Parent.prototype.say=function(){ console.log(this.name) }; function Child(){ Parent.call(this) this.age=18; }; Child.prototype=Object.create(Parent.prototype); Child.prototype.constructor=Child;
Vue
vue生命周期
Vue有几个生命周期?哪个生命周期可以获取到真实DOM?修改data里面的数据,会触发什么生命周期?组件data为什么是一个函数?
- 简单来说,vue的生命周期可以归为3类,创建阶段、运行阶段、销毁阶段。
创建阶段
beforeCreate:实例刚在内存中创建出来,还没有初始化 data和 methods,只包含一些自带额生命周期函数。
created:实例已经在内存中创建完成,此时data和methods已经创建完成。
beforeMount:此时已经编译模版,但没有渲染到页面中。
mounted:渲染模版,创建阶段到此结束。这时候可以操作dom。
运行阶段
beforeUpdate:界面中的数据还是旧的,但是data数据已经更新,页面中和data还没有同步。修改data数据就会触发这个函数。
updated:页面重新渲染完毕,页面中的数据和data保持一致。修改data数据就会触发这个函数。
销毁阶段
beforeDestroy:执行该方法的时候,Vue的生命周期已经进入销毁阶段,但是实例上的各种数据还出于可用状态。
destroyed:组件已经全部销毁,Vue实例已经被销毁,Vue中的任何数据都不可用
vue组件通信
vue组件如何通信?有几种方式?
-
在vue中组件通讯可以分为父子组件通讯和非父子组件通信。
父子组件通信: props;
listeners
跨级通信: eventBus;Vuex;provide / inject 、
兄弟组件通信: eventBus ;vuex
下面演示几种常用的使用方法:
props / $emit
- 父组件通过props的方式向子组件传递数据,而通过$emit子组件可以向父组件通信。
父组件向子组件传值(props)
prop 只可以从上一级组件传递到下一级组件(父子组件),即所谓的单向数据流。而且 prop 只读,不可被修改,所有修改都会失效并警告。
<!-- section父组件 --> <template> <div class="section"> <com-article :articles="articleList"></com-article> </div> </template> <script> import comArticle from './test/article.vue' export default { name: 'HelloWorld', components: { comArticle }, data() { return { articleList: ['红楼梦', '西游记', '三国演义'] } } } </script> // 子组件 article.vue <template> <div> <span v-for="(item, index) in articles" :key="index">{{item}}</span> </div> </template> <script> export default { props: ['articles'] } </script>
子组件向父组件传值($emit)
$emit绑定一个自定义事件, 当这个语句被执行时, 就会将参数arg传递给父组件,父组件通过v-on监听并接收参数。
<!-- // 父组件中 --> <template> <div class="section"> <com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article> <p>{{currentIndex}}</p> </div> </template> <script> import comArticle from './test/article.vue' export default { name: 'HelloWorld', components: { comArticle }, data() { return { currentIndex: -1, articleList: ['红楼梦', '西游记', '三国演义'] } }, methods: { onEmitIndex(idx) { this.currentIndex = idx } } } </script> <!-- 子组件 --> <template> <div> <div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div> </div> </template> <script> export default { props: ['articles'], methods: { emitIndex(index) { this.$emit('onEmitIndex', index) } } } </script>
parent
通过
children就可以访问组件的实例,拿到实例代表什么?代表可以访问此组件的所有方法和data。如在#app上拿parent得到的是undefined,而在最底层的子组件拿parent和children 的值是数组,而children已经被移除。
<template> <div class="hello_world"> <div>{{msg}}</div> <!-- child --> child <com-a></com-a> <button @click="changeA">点击改变子组件值</button> </div> </template> <script> import ComA from './child' export default { name: 'HelloWorld', components: { ComA }, data() { return { msg: 'Welcome' } }, methods: { changeA() { // 获取到子组件A console.log(this.$children) this.$children[0].messageA = 'this is new value' } } } </script> <!-- 子组件中 --> <template> <div class="com_a"> <span>{{messageA}}</span> <p>获取父组件的值为: {{parentVal}}</p> </div> </template> <script> export default { data() { return { messageA: 'this is old' } }, computed:{ parentVal(){ return this.$parent.msg; } } } </script>
ref/refs
ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据
<!-- //子组件 --> <template> <div >child</div> </template> <script> export default { data() { return { name: 'this is child' } }, methods: { sayHello(){ return 'say hello' } }, } </script> <!-- 父组件 --> <template> <div > <com-a ref="child"></com-a> </div> </template> <script> import ComA from './child' export default { components: { ComA }, data() { return { msg: 'Welcome' } }, mounted() { const child = this.$refs.child; console.log(child.name) //this is child console.log(child.sayHello()) //say hello }, } </script>
eventBus
eventBus 又称为事件总线,在vue中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件, 所以组件都可以通知其他组件。eventBus也有不方便之处, 当项目较大,就容易造成难以维护的灾难。
vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化.
Vuex 解决了多个视图依赖于同一状态和来自不同视图的行为需要变更同一状态的问题,将开发者的精力聚焦于数据的更新而不是数据在组件之间的传递上.
双向绑定的原理
vue2是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。vue3中则采用Proxy,它可以监听到数组内的数据变化。
为什么 Vue 组件中 data 必须是一个函数?
如果 data 是一个对象,当复用组件时,因为 data 都会指向同一个引用类型地址,其中一个组件的 data 一旦发生修改,则其他重用的组件中的 data 也会被一并修改。
如果 data 是一个返回对象的函数,因为每次重用组件时返回的都是一个新对象,引用地址不同,便不会出现如上问题。
Vue 中 computed 和 watch 有什么区别
计算属性 computed:
(1)支持缓存,只有依赖数据发生变化时,才会重新进行计算函数;
(2)计算属性内不支持异步操作;
(3)计算属性的函数中都有一个 get(默认具有,获取计算属性)和 set(手动添加,设置计算属性)方法;
(4)计算属性是自动监听依赖值的变化,从而动态返回内容。侦听属性 watch:
(1) 不支持缓存,只要数据发生变化,就会执行侦听函数;
(2) 侦听属性内支持异步操作;
(3) 侦听属性的值可以是一个对象,接收 handler 回调,deep,immediate 三个属性;
(3) 监听是一个过程,在监听的值变化时,可以触发一个回调,并做一些其他事情。
结尾
更多前端学习文章,请点击前端进阶班,欢迎关注!记得素质三连!