文章目录
- 前言
- 一、javascript相关
-
-
-
- 1.js的八个基本数据类型(高频)(京东)
- 2.this的五种指向方式
- 3.Function的call,apply和bind方法的区别?
- 4.Promise,手写promise
- 5.闭包
- 6.原型与原型链,作用域链与原型链的区别?
- 7.继承与继承的五种实现方式
- 8.深拷贝与浅拷贝的区别?(高频)
- 9.事件委托
- 10.var,const和let对比(高频)
- 11.宏任务与微任务
- 12.eventloop,事件循环(超高频)
- 13.new的过程,手动实现一个new方法
- 14.0.1+0.2!=0.3的原因
- 15.什么是防抖和节流?有什么区别?怎么实现
- 16.“==”和“= = =”的区别?
- 17.null和undefined的区别?
- 18.内存泄露和内存溢出
- 19.垃圾回收机制
- 20.怎么判断数组array和对象(京东)
- 21.js的三个面向对象的属性
- 22.堆和栈的区别?
- 23.函数的四种写法?
-
-
- 二、浏览器
- 三、css相关
- 四、HTML相关
- 五、ES6相关
- 六、vue相关
- 七、react相关
- 八、计算机网络
- 九、操作系统
- 十、手撕代码
- 十一、其他问题
- 总结
前言
记录一下在面试过程中遇到的以及搜罗到的web前端开发岗位的面试题,以八股为目录,部分题目会记录公司出处,先记录题目,答案之后补充,欢迎各位小伙伴评论区补充。
以下是本篇文章正文内容
一、javascript相关
1.js的八个基本数据类型(高频)(京东)
- 7个基本数据类型:数字number,字符串string,boolean,null,undefined,es6新增的symbol,es10新增的bigint
- 1个引用数据类型:object
- typeof返回的数据类型有:number,string,boolean,undefined,symbol,object,function
2.this的五种指向方式
1、默认绑定
2、隐式绑定
3、显式绑定
4、new绑定
5、箭头函数的绑定
1、默认绑定
非严格模式下,this指向window
;严格模式下(‘use strict’),this指向undefined
。
2、隐式绑定
函数调用的时候,前面存在调用对象,那么this就会隐式地绑定再这个对象上;如果前面存在多个对象,this指向距离自己最近的那个。
function fun(){
console.log(this.name)
}
let obj = {
name:'行星飞行',
func:fun,
}
let ob = {
name:'听风是风',
o:obj
}
ob.o.func() //行星飞行 this隐式指向obj
注意:存在隐式丢失的情况
var name = '行星飞行';
let obj = {
name: '听风是风',
fn: function () {
console.log(this.name);
}
};
obj.fn();// 听风是风
let fn1 = obj.fn;
fn1(); //行星飞行 仅仅调用了fn,此时this指向了window
3、显示绑定
通过call
、apply
和bind
的方法改变this的行为,函数调用使得函数处在一个被动的状态,而这三个函数使得函数从被动变为主动,函数可以主动选择自己的上下文。
var name = '行星飞行';
let obj = {
name: '听风是风'
}
function fun(){
console.log(this.name);
}
fun() //行星飞行
fun.call(obj) //听风是风
fun.apply(obj) //听风是风
fun.bind(obj)() //听风是风
fun.call(undefined) //行星飞行
fun.call(null)//行星飞行
如果参数是null或者undefined,那么this指向全局对象window
4、new绑定
function func(){
this.name = '听风是风'
}
let echo = new func()
console.log(echo.name) // '听风是风'
实例化之后this绑定到实例化之后的对象上。
5、箭头函数的绑定
外层函数的this指向谁,箭头函数的this就跟着指向谁
function fun() {
return () => {
console.log(this.name);
};
}
let obj = {
name: '听风是风'
}
fun.call(obj)() //'听风是风'
6、优先级
显示绑定>隐式绑定>默认绑定
new绑定>隐式绑定>默认绑定
3.Function的call,apply和bind方法的区别?
call,apply和bind三个方法都是为了显式地绑定this。区别如下:
- call和apply除了传参的方式不同之外,其余都相同。
fn.call(this, arg1, arg2, …);参数以序列的方式
fn.apply(this, [arg1, arg2, …]);参数以数组的方式 - apply和call绑定之后仍然可以使用apply、bind和call更换绑定,但是bind绑定之后就只能使用bind更换绑定对象。
- call和apply是立即调用,而bind是返回一个函数,还要在后面加一个()函数才能执行。
4.Promise,手写promise
什么是promise?
promise是异步编程的一种解决方案,主要用于异步计算,可以将异步操作队列化,按照期望的顺序执行,返回符合预期的结果
认识promise
promise的三个状态:pending(初始状态)、fulfilled(操作成功)和 rejected(操作失败)
resolve作用:promise的状态从pending变为fulfilled
rejecte作用:promise的状态从pending变为rejected
//简单用法
new Promise((resolve,reject)=>{
//resolve('成功') //执行.then
//reject('error123') //跳过.then执行.catch
throw new Error('error123') //跳过.then执行.catch
})
.then((val)=>{
//成功执行的回调函数,promise状态变为fulfilled
console.log(val)
})
.catch((err)=>{
//失败执行的回调函数,promise状态变为rejected
console.log(err) //输出Error: error123
})
.finally(()=>{
//无论成功与否都会执行的回调函数
//finally不接受任何参数,这意味着无法知道前面的promise状态到底是fulfilled还是rejected
})
promise的API?
const p = Promise.all([p1,p2,p3])
//1:p1,p2,p3的状态都变为fulfilled,p的状态变为fulfilled,且p1,p2,p3的返回值组成一个数组返回给p的回调函数
//2:p1,p2,p3有一个返回rejected,p的状态就会变为rejected,此时第一个被rejected的返回值传递给p的回调函数
const p = Promise.race([p1,p2,p3])
//只要p1,p2,p3中有一个实例率先改变状态,p的状态就会跟着改变,且将状态返回给p的回调函数
Promise.allSettled()
//该方法,用来确定一组异步操作是否都结束了(不管成功或失败)。
Promise.any()
//1:只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;
//2:如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。
关于异步解决方案
1、js传统的异步方式:回调函数
setTimeout(()=>{
console.log("等3秒")
setTimeout(()=>{
console.log("等3秒")
setTimeout(()=>{
console.log("等3秒")
},3000)
},3000)
},3000)
回调很容易嵌套,造成回调地狱
,且可读性比较差
2、更进一步的解决方案:Promise
优点:只需要.then.catch的链式调用即可,避免了层层嵌套的回调地狱,提高了可读性,且能够方便地捕获异常
缺点:一旦开始执行中途无法取消;如果不设置.catch函数,promise内部的错误不会反映到外部
3、新的异步解决方案:es6的异步方式:async、await
优点:使得异步操作更加简单明了,可读性更高,避免了繁琐的链式调用的写法
缺点:async、await的异步编程方式不能捕获异常,只能借助于try…catch…
示例:
function getTime(n) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("消耗" + n + "ms");
resolve(n + 1000);
}, n);
});
}
// promise链式写法
function doThis() {
const time1 = 1000;
getTime(time1)
.then((time2) => getTime(time2))
.then((time3) => getTime(time3))
.then((retult) => {
console.log("retult is ", retult);
});
}
// async函数写法
async function doThis() {
const time1 = 1000;
const time2 = await getTime(time1);
const time3 = await getTime(time2);
const result = await getTime(time3);
console.log(`result is, ${
result}`);
}
doThis();
如上图所示,async搭配await的用法可以让代码看起来像同步代码一样简洁。
async…await用法说明:
async函数会返回一个promise对象,可以调用.then和.catch等函数
await必须在async函数内部使用,当一个await后面的语句还未执行完成的时候必须等待执行完成才能向下执行
任何一个await后面的promise变为rejected状态的时候,整个async函数都会中断执行
async function doThis() {
const time1 = 1000;
const time2 = await getTime(time1);
const fail = await Promise.reject('失败了');
const result = await getTime(time2);
console.log(`result is, ${
result}`);
}
doThis().then(val=>console.log(val,'then'));
doThis().catch(err=>console.log(err,'catch'))
//失败了 catch
没有继发关系的两个await函数,可以让他们同时触发,这样可以省去不少时间
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
5.闭包
6.原型与原型链,作用域链与原型链的区别?
作用域链与原型链的区别?
作用域链:当访问一个变量的时候,解释器会首先在当前作用域查找标识符,如果没有找到就去父作用域找,
作用域顶端是全局对象`window`,如果在window都没有找到该变量就会报错。
原型链:当在对象上访问某属性的时候,首先会在当前对象查找,如果没有就会顺着原型链向上查找,
原型链顶端是`null`,如果全程都没有找到则返回一个undefined,而不是报错。
原型与原型链
-
原型
在js中万物皆是对象,对象又分两种:普通对象
(Object)和函数对象
(Function)。任何对象都具有隐式原型属性(_proto_)
,只有函数对象具有显式原型属性(prototype)
在js中,每当定义一个函数数据类型的时候,都会天生自带一个prototype
属性,这个属性就是函数的显式原型属性,同时也是实例的隐式原型属性。所有同一个构造函数下创建的实例都可以访问这个原型对象,所以我们可以将对象中共有的内容统一设置到原型对象中。
创建新的实例person的时候,都会创建一个新的内存空间,通过this定义的方法会存在内存空间里面,而通过prototype定义的方法会存在对象的原型中,实例化对象的时候,都只是在实例对象中复制了一个指向该方法的指针,以减少内存开销。 -
原型链
如上图所示,蓝色链条就是原型链,寻找实例化对象person的属性的时候就是沿着该原型链查找。假设在person对象身上寻找name属性,首先在person对象本身查找,没找到,在person对象的隐式原型属性_proto_中查找,如若没有,继续向原型的原型Object.prototype查找,还找不到就只能返回undefined,因为Object.prototype的原型是null(原型链的尽头)。可以看出,整个查找过程都是顺着__proto__属性,一步一步往上查找,形成了像链条一样的结构,这个结构,就是原型链
。
7.继承与继承的五种实现方式
1:原型链继承
2:构造函数继承
3:组合继承
4:寄生组合继承
5:class
//父类
function Parent(name,age){
//属性
this.name = name
this.pet = ['dog','cat']
//实例方法
this.getage = function() {
return age
}
}
//原型属性
Parent.prototype.hobby = ['sing','run']
原型链继承
将父类的实例作为子类的原型
function Child(name,age){
}
Child.prototype = new Parent('tom',17)
var child1 = new Child('child1',15)
child1.pet.push('fish')
const child2 = new Child('child2',14)
console.log(child1.name,'child1.name') //tom child1.name
console.log(child1.pet) //['dog', 'cat', 'fish']
console.log(child2.pet) //['dog', 'cat', 'fish']
优点:
- 简单易实现
- 父类新增原型方法/原型属性,子类都能访问到
缺点:
- 无法实现多继承
- 创建子类实例的时候,无法向父类构造函数穿参,因此child1.name输出的是tom而不是child1
- 当属性是引用数据类型的时候是浅拷贝的,即当子类实例改变该属性的时候,会影响到其他实例的属性。
构造函数继承
通过call改变父类中this的指向,让父类中的属性和方法绑定中在子类上
function Child(name,age){
Parent.call(this,name,age)
}
var child1 = new Child('child1',17)
child1.pet.push('fish')
var child2 = new Child('child2',14)
console.log(child1.name) //child1
console.log(child1.hobby) //undefined
console.log(child1.pet) //['dog', 'cat', 'fish']
console.log(child2.pet) //['dog', 'cat']
优点:
- 解决了原型链继承中引用类型的属性共享的问题
- 解决了原型链继承中无法向父类传参的问题
- 可以实现多继承,可以通过call多个父类
缺点:
- 只能继承父类在构造函数里声明的属性和方法,不能继承父级原型链上的属性和方法;所有方法都需要定义在构造函数中,每次都需要重新创建,导致内存开销大。
组合继承
组合继承即原型链继承加上构造函数继承,这样就可以在构造函数的基础上解决了父类原型属性和方法无法继承的问题
function Child(name,age){
Parent.call(this,name,age)
}
Child.prototype = new Parent()
child1.pet.push('fish')
var child2 = new Child('child2',14)
child1.hobby.push("swim");
console.log(child1.hobby); // ['sing', 'run', 'swim']
console.log(child2.hobby); // ['sing', 'run', 'swim']
优点:在构造函数继承的基础上解决了父类原型链上的属性和方法无法继承的问题
缺点:
- 原型链上的属性如果是引用数据类型,则仍然是浅拷贝
- 调用了两次父类构造函数,一次是在构造函数内部,一次是在创建子类原型的时候,生成了两份实例,导致在子类原型上创建了多余不必要的属性(子类实例将子类原型上的那份屏蔽了)
寄生组合继承
是最理想的继承方式,就是在组合继承的基础上,解决了父类构造函数调用两次导致子类原型上存在多余属性的问题
//创建一个拥有指定原型的对象
function creatObject(o){
function f(){
};
f.prototype = o;
return new f();
}
//复制了一个父类的原型对象并赋值给子类的原型
function inherit(child,parent){
var prototype = creatObject(parent.prototype)
prototype.constructor = child;
child.prototype = prototype
}
function Child(name,age){
Parent.call(this,name,age)
}
inherit(child,parent)
var child1 = new Child()
优点:堪称完美,解决了以上三种继承方式所有存在的问题
缺点:实现较为复杂
class
class 是ES6的语法 直接class 创建一个类,使用extends来继承。
class Father {
constructor() {
this.name = "bob";
}
getname() {
return this.name;
}
}
class Child extends Father {
constructor() {
super();
}
}
var child = new Child();
console.log(child.name); //bob
console.log(child.getname()); //bob
最方便快捷的继承方式,但是仅支持ES6及以上版本,所以要考虑兼容性问题
8.深拷贝与浅拷贝的区别?(高频)
-
什么是深拷贝和浅拷贝?
基本数据类型的拷贝操作:基本数类型的名字和value值都存在栈内存中,当进行复制的时候,就开辟新的内存进行存储。
引用数据类型的拷贝操作:引用数据类型名字存在栈中,value值存在堆中,但是栈中会提供一个地址指向堆中的value值。
深浅拷贝就是针对引用数据类型而言的。
浅拷贝:浅拷贝指的是只复制栈内存中的地址,当你对数据进行改变的时候改变的是堆内存中的变量,所以改变其中的一个值,另一个值会跟着改变,例如b=a,当你改变b的值时,a的值也会变化。
深拷贝:深拷贝指的是复制之后会开辟新的堆内存进行value值的存储,所以复制之后的值的改变不会影响到被复制的变量的值。 -
为什么要讨论深浅拷贝?
在一个团队中,多人开发一个项目时,当无法得知某个数据的用处时,随意使用浅拷贝操作数据会带来无法估量的影响。 -
浅拷贝和深拷贝操作举例。
浅拷贝:“传址操作”
var a = [1,2,3,4]
var b = a
b.splice(1,1)
console.log(a)//[1,3,4]
console.log(b)//[1,3,4]
深拷贝的几种方法:“传值操作”
//方法1:只是深拷贝第一层,如果该数组存在下一层,则仍然是浅拷贝
var s = [1,2,3,4,5]
var s1 = s.slice(1,3)
s1[1] = 34
console.log(s)//[1,2,3,4,5]
console.log(s1)//[2,34]
//方法2:
var s = [1,2,3,4,5]
var s1 = [7,8,9]
console.log(s.concat(s1))//[1,2,3,4,5,7,8,9]
console.log(s)//[1,2,3,4,5]
//方法3:遍历数组的操作
var copy = function(obj){
let objclone = Array.isArray(obj)?[]:{
};
for(key in obj){
if(typeof obj[key]==="object"){
objclone[key] = copy(obj[key])
}else{
objclone[key] = cobj[key]
}
}
return objclone;
}
var c = [1,2,3,[4,5,6]]
var d = c
d[0] = 11
d[3][0] = 50
console.log(c)//[1,2,3,[4,5,6]]
console.log(d)//[11,2,3,[50,5,6]]
9.事件委托
又称为事件代理,即把原本绑定在子元素中的响应事件委托给父元素,让父元素担任监听的任务
事件冒泡
捕获阶段:从window对象传到目标节点,称为’捕获阶段‘
目标阶段:在目标节点上触发
冒泡阶段:从目标节点传回window对象
事件代理即利用事件冒泡的机制把里层的响应事件绑定到外层
事件委托
- 大量节省内存占用,减少了注册事件
- 当子元素增多的时候,无需再次绑定触发事件
- 实现:
<body>
<ul id="uul">
<li>去百度</li>
<li>在控制台写</li>
<li>弹出</li>
</ul>
</body>
<script>
var dom = document.getElementById("uul");
dom.addEventListener("click",function(event){
console.log('当前点击了'+event.target.innerText)
})
// 另一种监听方式
dom.onclick = function(event){
console.log('当前点击了'+event.target.innerText)
}