admin管理员组文章数量:1608019
长文噩梦预警!
如果你
- 想进大型互联网公司
- 本文掌握程度90%~100%
- 想进中大企业
- 掌握程度70%~85%
- 想进小企业
- 掌握程度45%~80%
- 想家里蹲
- 掌握程度:undefined
咳咳,如果你是大神的话当我什么都没说哈,小弟祝您发大财。
接下来的题我会根据重点程度使用来标记,越多标明越重点,满星是5颗星
ok,你准备好了吗?咱们开始吧!
本文章是根据2022年的面试题走向对《身为三本的我就是凭借这些前端面试题拿到百度京东offer的,前端面试题2021及答案》的做了一些增添删除。
JS
数据类型
面试官:JS的数据类型都有哪些
答:
数据类型分为基本数据类型和引用数据类型;
基本数据类型有:
- Number
- String
- Boolean
- null
- Undefined
- Symbol
- BigInt
引用数据类型统称为Object类型,细分的话有:
- 统称为Object类型
- 细分的话:
- Object
- Array
- Function
- Date
- RegExp
- 细分的话:
基本数据类型的数据直接存储在栈中;而引用数据类型的数据存储在堆中,在栈中保存数据的引用地址,这个引用地址指向的是对应的数据,以便快速查找到堆内存中的对象。
顺便提一句,栈内存是自动分配内存的。而堆内存是动态分配内存的,不会自动释放。所以每次使用完对象的时候都要把它设置为null,从而减少无用内存的消耗
为什么0.1+0.2>0.3
答:
因为在JS底层中,每个变量是以二进制表示,固定长度为64位,其中第1位是符号位,再往后11位是指数为,最后52表示的是尾数位,而0.1和0.2转为二进制的时候是无限循环小数,所以JS就会进行截取,截取以后0.1和0.2就不是他们本身了,要比原来大那么一丢丢,所以0.1+0.2就>0.3了
如何解决这个问题,使0.1+0.2等于0.3?
答:
先给他们放大倍数,随后在除以相应倍数
const a = 0.1;
const b = 0.2;
console.log(a + b === 0.3) // false
console.log((a * 1000 + b * 1000) / 1000 === 0.3) // true
数据类型的判断方式
答:
1.typeof
- 缺点:
typeof null
的值为Object
,无法分辨是null
还是Object
2.instanceof
- 缺点:只能判断某对象是否存在于目标对象得的原型链上
3.constructor
4.Object.prototype.toString.call()
-
一种最好的基本类型检测方式
Object.prototype.toString.call()
;它可以区分 null 、 string 、boolean 、 number 、 undefined 、 array 、 function 、 object 、 date 、 math 数据类型。
-
缺点:不能细分为谁谁的实例
// -----------------------------------------typeof typeof undefined // 'undefined' typeof '10' // 'String' typeof 10 // 'Number' typeof false // 'Boolean' typeof Symbol() // 'Symbol' typeof Function // ‘function' typeof null // ‘Object’ typeof [] // 'Object' typeof {} // 'Object' // -----------------------------------------instanceof function Foo() { } var f1 = new Foo(); var d = new Number(1) console.log(f1 instanceof Foo);// true console.log(d instanceof Number); //true console.log(123 instanceof Number); //false -->不能判断字面量的基本数据类型 // -----------------------------------------constructor var d = new Number(1) var e = 1 function fn() { console.log("ming"); } var date = new Date(); var arr = [1, 2, 3]; var reg = /[hbc]at/gi; console.log(e.constructor);//? Number() { [native code] } console.log(e.constructor.name);//Number console.log(fn.constructor.name) // Function console.log(date.constructor.name)// Date console.log(arr.constructor.name) // Array console.log(reg.constructor.name) // RegExp //-----------------------------------------Object.prototype.toString.call() console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]" console.log(Object.prototype.toString.call(null)); // "[object Null]" console.log(Object.prototype.toString.call(123)); // "[object Number]" console.log(Object.prototype.toString.call("abc")); // "[object String]" console.log(Object.prototype.toString.call(true)); // "[object Boolean]" function fn() { console.log("ming"); } var date = new Date(); var arr = [1, 2, 3]; var reg = /[hbc]at/gi; console.log(Object.prototype.toString.call(fn));// "[object Function]" console.log(Object.prototype.toString.call(date));// "[object Date]" console.log(Object.prototype.toString.call(arr)); // "[object Array]" console.log(Object.prototype.toString.call(reg));// "[object RegExp]"
为什么要用Object.prototype.toString.call()
,为什么不用 Array.prototype.toString.call()
答:
因为只有Object.prototype.toString.call()
返回的是统一格式,而且 Array.prototype.toString.call()
的部分类型无法检验。
function fn() {
console.log("ming");
}
var date = new Date();
var arr = [1, 2, 3];
var reg = /[hbc]at/gi;
console.log(Array.prototype.toString.call(undefined)); // 报错
console.log(Array.prototype.toString.call(null)); // 报错
console.log(Array.prototype.toString.call(123)); // "[object Number]"
console.log(Array.prototype.toString.call("abc")); // "[object String]"
console.log(Array.prototype.toString.call(true)); // "[object Boolean]"
console.log(Array.prototype.toString.call(fn)); // "[object Function]"
console.log(Array.prototype.toString.call(date)); // "[object Date]"
console.log(Array.prototype.toString.call(arr)); // "1,2,3"
console.log(Array.prototype.toString.call(reg));// "[object RegExp]"
instanceof原理
-
instanceof原理实际上就是查找目标对象的原型链
function myInstance(L, R) {//L代表instanceof左边,R代表右边 var RP = R.prototype var LP = L.__proto__ while (true) { if(LP == null) { return false } if(LP == RP) { return true } LP = LP.__proto__ } } console.log(myInstance({},Object));
面试官:为什么typeof null 是Object
答:
因为在JavaScript中,不同的对象都是使用二进制存储的,如果二进制前三位都是0的话,系统会判断为是Object类型,而null的二进制全是0,自然也就判断为Object
这个bug是初版本的JavaScript中留下的,扩展一下其他五种标识位:
-
000 对象
-
1 整型
-
010 双精度类型
-
100字符串
-
110布尔类型
面试官:==
和===
有什么区别
答:
===
是严格意义上的相等,会比较两边的数据类型和值大小
- 数据类型不同返回false
- 数据类型相同,但值大小不同,返回false
==
是非严格意义上的相等,
-
两边类型相同,比较大小
-
两边类型不同,根据下方表格,再进一步进行比较。
- Null == Undefined ->true
- String == Number ->先将String转为Number,在比较大小
- Boolean == Number ->现将Boolean转为Number,在进行比较
- Object == String,Number,Symbol -> Object 转化为原始类型
面试官:NaN === NaN
返回什么?
返回 false
,NaN
永远不等于NaN
,判断是否为NaN
用一个函数 isNaN
来判断;
isNaN
传入的如果是其他数据类型,那么现将它使用Number()
转为数字类型在进行判断
面试官:手写call、apply、bind
答:
- call和apply实现思路主要是:
- 判断是否是函数调用,若非函数调用抛异常
- 通过新对象(context)来调用函数
- 给context创建一个
fn
设置为需要调用的函数 - 结束调用完之后删除
fn
- 给context创建一个
- bind实现思路
- 判断是否是函数调用,若非函数调用抛异常
- 返回函数
- 判断函数的调用方式,是否是被new出来的
- new出来的话返回空对象,但是实例的
__proto__
指向_this
的prototype
- new出来的话返回空对象,但是实例的
- 判断函数的调用方式,是否是被new出来的
- 完成函数柯里化
Array.prototype.slice.call()
call:
Function.prototype.myCall = function (context) {
// 先判断调用myCall是不是一个函数
// 这里的this就是调用myCall的
if (typeof this !== 'function') {
throw new TypeError("Not a Function")
}
// 不传参数默认为window
context = context || window
// 保存this
context.fn = this
// 保存参数
let args = Array.from(arguments).slice(1) //Array.from 把伪数组对象转为数组
// 调用函数
let result = context.fn(...args)
delete context.fn
return result
}
apply
Function.prototype.myApply = function (context) {
// 判断this是不是函数
if (typeof this !== "function") {
throw new TypeError("Not a Function")
}
let result
// 默认是window
context = context || window
// 保存this
context.fn = this
// 是否传参
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
bind
Function.prototype.myBind = function(context){
// 判断是否是一个函数
if(typeof this !== "function") {
throw new TypeError("Not a Function")
}
// 保存调用bind的函数
const _this = this
// 保存参数
const args = Array.prototype.slice.call(arguments,1)
// 返回一个函数
return function F () {
// 判断是不是new出来的
if(this instanceof F) {
// 如果是new出来的
// 返回一个空对象,且使创建出来的实例的__proto__指向_this的prototype,且完成函数柯里化
return new _this(...args,...arguments)
}else{
// 如果不是new出来的改变this指向,且完成函数柯里化
return _this.apply(context,args.concat(...arguments))
}
}
}
面试官:字面量创建对象和new创建对象有什么区别,new内部都实现了什么,手写一个new
答:
字面量:
- 字面量创建对象更简单,方便阅读
- 不需要作用域解析,速度更快
new内部:
- 创建一个新对象
- 使新对象的
__proto__
指向原函数的prototype
- 改变this指向(指向新的obj)并执行该函数,执行结果保存起来作为result
- 判断执行函数的结果是不是null或Undefined,如果是则返回之前的新对象,如果不是则返回result
手写new
// 手写一个new
function myNew(fn, ...args) {
// 创建一个空对象
let obj = {}
// 使空对象的隐式原型指向原函数的显式原型
obj.__proto__ = fn.prototype
// this指向obj
let result = fn.apply(obj, args)
// 返回
return result instanceof Object ? result : obj
}
执行栈和执行上下文
面试官:什么是作用域,什么是作用域链?
答:
- 规定变量和函数的可使用范围称作作用域
- 每个函数都有一个作用域链,查找变量或者函数时,需要从局部作用域到全局作用域依次查找,这些作用域的集合称作作用域链。
面试官:什么是执行栈,什么是执行上下文?
答:
执行上下文分为:
- 全局执行上下文
- 创建一个全局的window对象,并规定this指向window,执行js的时候就压入栈底,关闭浏览器的时候才弹出
- 函数执行上下文
- 每次函数调用时,都会新创建一个函数执行上下文
- 执行上下文分为创建阶段和执行阶段
- 创建阶段:函数环境会创建变量对象:arguments对象(并赋值)、函数声明(并赋值)、变量声明(不赋值),函数表达式声明(不赋值);会确定this指向;会确定作用域
- 执行阶段:变量赋值、函数表达式赋值,使变量对象编程活跃对象
- eval执行上下文
执行栈:
- 首先栈特点:先进后出
- 当进入一个执行环境,就会创建出它的执行上下文,然后进行压栈,当程序执行完成时,它的执行上下文就会被销毁,进行弹栈。
- 栈底永远是全局环境的执行上下文,栈顶永远是正在执行函数的执行上下文
- 只有浏览器关闭的时候全局执行上下文才会弹出
闭包
很多人都吃不透js闭包,这里推荐一篇文章:彻底理解js中的闭包
面试官:什么是闭包?闭包的作用?闭包的应用?
答:
函数执行,形成私有的执行上下文,使内部私有变量不受外界干扰,起到保护和保存的作用
作用:
- 保护
- 避免命名冲突
- 保存
- 解决循环绑定引发的索引问题
- 变量不会销毁
- 可以使用函数内部的变量,使变量不会被垃圾回收机制回收
应用:
- 设计模式中的单例模式
- for循环中的保留i的操作
- 防抖和节流
- 函数柯里化
缺点
- 会出现内存泄漏的问题
原型和原型链
面试官:什么是原型?什么是原型链?如何理解
答:
原型: 原型分为隐式原型和显式原型,每个对象都有一个隐式原型,它指向自己的构造函数的显式原型。每个构造方法都有一个显式原型。
-
__proto__
是隐式原型;prototype
是显式原型 -
所有实例的
__proto__
都指向他们构造函数的prototype
-
所有的
prototype
都是对象,自然它的__proto__
指向的是Object()
的prototype
-
所有的构造函数的隐式原型指向的都是
Function()
的显示原型 -
Object的隐式原型是null
原型链: 多个__proto__
组成的集合成为原型链(概念类似于作用域链)
instanceof
就是判断某对象是否位于某构造方法的原型链上。
继承
面试官:说一说 JS 中的常用的继承方式有哪些?以及各个继承方式的优缺点。
答:
原型继承、组合继承、寄生组合继承、ES6的extend
原型继承
// ----------------------方法一:原型继承
// 原型继承
// 把父类的实例作为子类的原型
// 缺点:子类的实例共享了父类构造函数的引用属性 不能传参
var person = {
friends: ["a", "b", "c", "d"]
}
var p1 = Object.create(person)
p1.friends.push("aaa")//缺点:子类的实例共享了父类构造函数的引用属性
console.log(p1);
console.log(person);//缺点:子类的实例共享了父类构造函数的引用属性
组合继承
// ----------------------方法二:组合继承
// 在子函数中运行父函数,但是要利用call把this改变一下,
// 再在子函数的prototype里面new Father() ,使Father的原型中的方法也得到继承,最后改变Son的原型中的constructor
// 缺点:调用了两次父类的构造函数,造成了不必要的消耗,父类方法可以复用
// 优点可传参,不共享父类引用属性
function Father(name) {
this.name = name
this.hobby = ["篮球", "足球", "乒乓球"]
}
Father.prototype.getName = function () {
console.log(this.name);
}
function Son(name, age) {
Father.call(this, name)
this.age = age
}
Son.prototype = new Father()
Son.prototype.constructor = Son
var s = new Son("ming", 20)
console.log(s);
寄生组合继承
// ----------------------方法三:寄生组合继承
function Father(name) {
this.name = name
this.hobby = ["篮球", "足球", "乒乓球"]
}
Father.prototype.getName = function () {
console.log(this.name);
}
function Son(name, age) {
Father.call(this, name)
this.age = age
}
Son.prototype = Object.create(Father.prototype)
Son.prototype.constructor = Son
var s2 = new Son("ming", 18)
console.log(s2);
extend
// ----------------------方法四:ES6的extend(寄生组合继承的语法糖)
// 子类只要继承父类,可以不写 constructor ,一旦写了,则在 constructor 中的第一句话
// 必须是 super 。
class Son3 extends Father { // Son.prototype.__proto__ = Father.prototype
constructor(y) {
super(200) // super(200) => Father.call(this,200)
this.y = y
}
}
内存泄露、垃圾回收机制
面试官:什么是内存泄漏
答:
内存泄露是指不再用的内存没有被及时释放出来,导致该段内存无法被使用就是内存泄漏
面试官:为什么会导致的内存泄漏
答:
内存泄漏指我们无法在通过js访问某个对象,而垃圾回收机制却认为该对象还在被引用,因此垃圾回收机制不会释放该对象,导致该块内存永远无法释放,积少成多,系统会越来越卡以至于崩溃
面试官:垃圾回收机制都有哪些策略?
答:
- 标记清除法
- 垃圾回收机制获取根并标记他们,然后访问并标记所有来自它们的引用,然后在访问这些对象并标记它们的引用…如此递进结束后若发现有没有标记的(不可达的)进行删除,进入执行环境的不能进行删除
- 引用计数法
- 当声明一个变量并给该变量赋值一个引用类型的值时候,该值的计数+1,当该值赋值给另一个变量的时候,该计数+1,当该值被其他值取代的时候,该计数-1,当计数变为0的时候,说明无法访问该值了,垃圾回收机制清除该对象
- 缺点: 当两个对象循环引用的时候,引用计数无计可施。如果循环引用多次执行的话,会造成崩溃等问题。所以后来被标记清除法取代。
深拷贝和浅拷贝
手写浅拷贝深拷贝
// ----------------------------------------------浅拷贝
// 只是把对象的属性和属性值拷贝到另一个对象中
var obj1 = {
a: {
a1: { a2: 1 },
a10: { a11: 123, a111: { a1111: 123123 } }
},
b: 123,
c: "123"
}
// 方式1
function shallowClone1(o) {
let obj = {}
for (let i in o) {
obj[i] = o[i]
}
return obj
}
// 方式2
var shallowObj2 = { ...obj1 }
// 方式3
var shallowObj3 = Object.assign({}, obj1)
let shallowObj = shallowClone1(obj1);
shallowObj.a.a1 = 999
shallowObj.b = true
console.log(obj1); //第一层的没有被改变,一层以下就被改变了
// ----------------------------------------------深拷贝
// 简易版
function deepClone(o) {
let obj = {}
for (var i in o) {
// if(o.hasOwnProperty(i)){
if (typeof o[i] === "object") {
obj[i] = deepClone(o[i])
} else {
obj[i] = o[i]
}
// }
}
return obj
}
var myObj = {
a: {
a1: { a2: 1 },
a10: { a11: 123, a111: { a1111: 123123 } }
},
b: 123,
c: "123"
}
var deepObj1 = deepClone(myObj)
deepObj1.a.a1 = 999
deepObj1.b = false
console.log(myObj);
// 简易版存在的问题:参数没有做检验,传入的可能是 Array、null、regExp、Date
function deepClone2(o) {
if (Object.prototype.toString.call(o) === "[object Object]") { //检测是否为对象
let obj = {}
for (var i in o) {
if (o.hasOwnProperty(i)) {
if (typeof o[i] === "object") {
obj[i] = deepClone(o[i])
} else {
obj[i] = o[i]
}
}
}
return obj
} else {
return o
}
}
function isObject(o) {
return Object.prototype.toString.call(o) === "[object Object]" || Object.prototype.toString.call(o) === "[object Array]"
}
// 继续升级,没有考虑到数组,以及ES6中的map、set、weakset、weakmap
function deepClone3(o) {
if (isObject(o)) {//检测是否为对象或者数组
let obj = Array.isArray(o) ? [] : {}
for (let i in o) {
if (isObject(o[i])) {
obj[i] = deepClone(o[i])
} else {
obj[i] = o[i]
}
}
return obj
} else {
return o
}
}
// 有可能碰到循环引用问题 var a = {}; a.a = a; clone(a);//会造成一个死循环
// 循环检测
// 继续升级
function deepClone4(o, hash = new map()) {
if (!isObject(o)) return o//检测是否为对象或者数组
if (hash.has(o)) return hash.get(o)
let obj = Array.isArray(o) ? [] : {}
hash.set(o, obj)
for (let i in o) {
if (isObject(o[i])) {
obj[i] = deepClone4(o[i], hash)
} else {
obj[i] = o[i]
}
}
return obj
}
// 递归易出现爆栈问题
// 将递归改为循环,就不会出现爆栈问题了
var a1 = { a: 1, b: 2, c: { c1: 3, c2: { c21: 4, c22: 5 } }, d: 'asd' };
var b1 = { b: { c: { d: 1 } } }
function cloneLoop(x) {
const root = {};
// 栈
const loopList = [ //->[]->[{parent:{a:1,b:2},key:c,data:{ c1: 3, c2: { c21: 4, c22: 5 } }}]
{
parent: root,
key: undefined,
data: x,
}
];
while (loopList.length) {
// 深度优先
const node = loopList.pop();
const parent = node.parent; //{} //{a:1,b:2}
const key = node.key; //undefined //c
const data = node.data; //{ a: 1, b: 2, c: { c1: 3, c2: { c21: 4, c22: 5 } }, d: 'asd' } //{ c1: 3, c2: { c21: 4, c22: 5 } }}
// 初始化赋值目标,key 为 undefined 则拷贝到父元素,否则拷贝到子元素
let res = parent; //{}->{a:1,b:2,d:'asd'} //{a:1,b:2}->{}
if (typeof key !== 'undefined') {
res = parent[key] = {};
}
for (let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
// 下一次循环
loopList.push({
parent: res,
key: k,
data: data[k],
})
} else {
res[k] = data[k];
}
}
}
}
return root
}
function deepClone5(o) {
let result = {}
let loopList = [
{
parent: result,
key: undefined,
data: o
}
]
while (loopList.length) {
let node = loopList.pop()
let { parent, key, data } = node
let anoPar = parent
if (typeof key !== 'undefined') {
anoPar = parent[key] = {}
}
for (let i in data) {
if (typeof data[i] === 'object') {
loopList.push({
parent: anoPar,
key: i,
data: data[i]
})
} else {
anoPar[i] = data[i]
}
}
}
return result
}
let cloneA1 = deepClone5(a1)
cloneA1.c.c2.c22 = 5555555
console.log(a1);
console.log(cloneA1);
// ------------------------------------------JSON.stringify()实现深拷贝
function cloneJson(o) {
return JSON.parse(JSON.stringify(o))
}
// let obj = { a: { c: 1 }, b: {} };
// obj.b = obj;
// console.log(JSON.parse(JSON.stringify(obj))) // 报错 // Converting circular structure to JSON
深拷贝能使用hash递归的方式写出来就可以了
不过技多不压身,推荐还是看一看使用while实现深拷贝方法
单线程,同步异步
面试官:为什么JS是单线程的?
**答:**因为JS里面有可视的Dom,如果是多线程的话,这个线程正在删除DOM节点,另一个线程正在编辑Dom节点,导致浏览器不知道该听谁的
面试官:说说 Promise 的原理?你是如何理解 Promise 的?
- 做到简易版的promise理解,以及会写race和all函数就可以
答:
class MyPromise2 {
constructor(executor) {
// 规定状态
this.state = "pending"
// 保存 `resolve(res)` 的res值
this.value = undefined
// 保存 `reject(err)` 的err值
this.reason = undefined
// 成功存放的数组
this.successCB = []
// 失败存放的数组
版权声明:本文标题:想进互联网大公司?那这些题你总得会吧?前端面试题2022及答案前端面试题2022及答案 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.elefans.com/dianzi/1728532596a1162290.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论