一个堕落者的自我救赎之路
此系列博客文章进度
✅js篇
🔲html、css篇
🔲DOM、BOM篇
🔲轮子篇
🔲打包工具篇
🔲数据结构篇
🔲前端必刷小算法篇
🔲HTTP篇
🔲性能优化篇
🔲常见设计模式篇
JS篇
1. 面试官:来谈一下typeof
和instanceof
,原理也要说清楚喔
you:
先来说typeof吧,对于它来说。用它判断一些基本类型还是没有问题的(除了null),如
typeof 1 // 'number'
typeof '1' // 'string'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof undefined // 'undefined'
typeof(null)//'object'
typeof 对于复杂类型来说,除了函数的其他都会判为object
,如:
typeof []//'object'
typeof {}//'object'
typeof function(){}//'function'
即对于简单数据类型来说(除了null)typeof都能看出它们的原形(现出原形?)。对于复杂数据类型它就比较无能为力了
这个时候就得看instanceof
了。instanceof
表示判断当前实例是否属于当前构造函数(会向上找)
举个栗子
function Demo(){}
const demo=new Demo()
demo instanceof Demo//true
demo instanceof Object//true
为了更好的理解,来讲一下instanceof
的原理吧
instanceof
之所以可以正确的判断对象的类型,是因为
知道了它的原理,接下来我们就可以实现一个它了。其实也就是判断实例对象的原型链上是否有这个构造函数的原型对象
function myInstanceof(targetObj, targetConstructor) {
const currentPrototype = targetConstructor.prototype
const objPrototype = targetObj.__proto__
while (1) {
if (objPrototype == null || objPrototype == undefined) {
return false
}
if (objPrototype == currentPrototype) {
return true
}
objPrototype = objPrototype.__proto__
}
}
想到了这里你有没有想到另一个与之相似的方法呢,👉小白曾在原型与继承博客中总结过。没错它就是isPrototypeOf
这个API是用于判断一个对象是否在另一个对象的原型链中
举个栗子
//构造函数的原型对象,存在于fn对象的原型链中
function fn() {}
Function.prototype.isPrototypeOf(fn)//true
2. 面试官:“==”与“===”的区别
you:对于==
来说,如果比较的双方数据类型不同,则会先进行一次数据类型的转换。即可以认为它没有===
那个小子严格,在比较两个数据的时候它会先检测类型。类型一致直接比较大小,否则就是自动帮你进行数据类型的转换。
===
和简单,就是比较数据双方的值
再问:==
比较,那类型不同时的数据转换有什么规则吗?
这个是肯定有的。
- 首先如果比较数据双方是
null
和undefind
。会返回true - 如果比较数据双方式
number
和string
类型,会先将string
类型转为number
举个栗子:
1=='1' //true
//等价于
1===Number('1') //true
- 如果比较数据双方有类型是
boolean
,那么会先将boolean
的数据类型转为number
举个栗子:
1 == true//ture
'1' == true//ture先被转换为number的1,然后就成了string与number的比较,故'1'又被转换为number的1 。1===1,返ture
//即等价于
Number(true) === Number('1')
- 再如果其中一方是
object
,另一方是string
、number
。则会先把object
转为基本类型
举个栗子:
const obj={}
//obj=='1'
//即等价于String(obj)=>'[object Object]'
//即obj=='1'就是'1' === '[object Object]'
3. 面试官:谈一下let、const、var的区别
you:
点1 :首先对于var
来说var具有变量提升的特点,那什么是变量提升呢?
其实如果熟悉js的执行上下文,便能很容易嗯嗯理解。js引擎给一个上下文中给变量声明的时候,开始是先给他们赋值成undefind的。后面再给他们赋成真实数据
这样做会发生什么呢?
显而易见,这样会把变量直接提升到其当前作用域的顶部。说的白话一点也就是说在当前作用域下,你使用var
声明的变量无论写的顺序是在哪里,它都会被提升到顶部。
栗子说话
console.log(a);
var a=1//undefind
//如果是下面这样,则直接报错
console.log(a);
let a=1
点2:使用var
在全局下声明的变量会被自动挂载到window
上,let、const是不会的
点3:我们知道使用function
声明的函数也是具有“提升的特点的”即函数提升,同时函数提升优先于变量提升
点4:let和const的区别在于,使用const声明的变量其值不能再进行改变
4. 面试官:谈一下js中的类型转化
you:在js中类型转化是只有三种情况的,分别是
- 转换为字符串
- 转换为数字
- 转换为布尔
详细说下吧
对于转为字符串,基本数据类型转为字符串很好理解直接转为字符串即可。可是复杂数据类型嘞
直接来一道小白曾经面试过的题目,问
({}+{}).length//30
这道题还是比较简单的,这里我只是为了演示一下数据转换基本类型和复杂类型的区别
还是简单说明一下吧
首先要知道基本上除了两个number或者存在布尔,别的类型是没有加法运算的,也即这里的+号符的作用是用于拼接。既然要进行拼接其两边的数据就要想string方向转换了
即等价于
String({}).length+String({}).length
===
('[object Object]'+'[object Object]' ) .length
===
30
要让小白出个题的话,嘿嘿 ,小白会这样搞
const obj01 = {
name: 'zhang'
}
const obj02 = {
name: {
first: 'tom'
}
}
console.log(obj01.name + obj02.name)
回归正题
一个对象类型的数据在转换类型的时候,会调用其内置的[[ToPrimitive]]
函数,这个函数有两个参数toPrimitive(input,preferedType)
。input
为输入值,preferedType
为期望转换的类型(字字符串,数字等)
如果preferedType
为string,该函数会执行一下步骤
如果input是原始值,则会直接返回这个值
否则,调input.toString()。如果转换后的结果是原始值则直接返回
否则,调input.valueOf()。如果转换后的结果是原始值则直接返回
否则,抛错
如果转换的类型是number,则2、3的顺序颠倒
举个栗子
let a = {
[Symbol.toPrimitive]() {
return 2
}
}
console.log(a + 1)
下面再举两个对基础考察更为严格的题目
问:[]+[]
会输出什么?
此时的preferedType
会被默认为number,虽然会先行调用valueOf(),但是对于数组、函数、对象来说此方法是直接返回本身的。也即[].valueOf()
的返回值仍然是[]
。那么就在调用toString得到一个空字符串
即最后结果[]+[]
=一个空字符串
又问:[]+{}
会输出什么呢?
'[object object]'
+一个空字符串
='[object object]'
再问:{}+[]
了
还是和上面一样吗?显然面试官不会这么傻同一问题问两遍,我们先去浏览器中去跑一下
这是为啥呢?
首先我们要知道{}除了可以表示一个对象还可以被当做一个空的代码块
在[]+{}中,[]一开始就被解析成了一个数组,故后面的+号符就被认为是一个加法运算符,{}就被解析成一个对象了
而{}+[],开始{}就是被解析为一个空的代码块了。这里的运算也即成了{//空代码块}
+[]
也即就成了一个+[]
的转换了
即[]先调用valueOf,再调toString 。正号运算符后面得跟一个number型的吧。故最后在转成number型
{}+[]就等价于
+[]
更进一步
+Number([].valueOf().toString())
后面再遇到相关问题,我们就能明白了吧
{}+0//0
{}+9//9
{}+true//1
{}+'1'//1
现在是不是感觉就{}+{}
是异类了呢,哈哈
5. 面试官:说一下new的原理及实现
you:在new的时候其实,它是为我们做了四件事情的
- 首先创建一个新的对象
- 链接到原型
- 绑定this
- 返回这个新的对象
手动实现一下吧:
function myNew(targetClass, ...args) {
let obj = Object.create(targetClass.prototype)
targetClass.apply(obj, args)
return obj
}
function Dog(name){
this.name=name
}
Dog.prototype.run=function(){
console.log(`${this.name}在run`)
}
const dog = myNew(Dog, 'qiuqiu')
dog.run()
6. 面试官:再讲一下深拷贝与浅拷贝
you:首先我们知道基本类型数据放在栈内存,堆类型的数据放在堆内存
如这样一点代码
let a=118
let obj={}
在内部是这样表现的
如果分别修改一下它们的值呢?
let a=20
let obj={
name:'gxb'
}
这说明了什么,这样描述吧,有一个叫a和叫b的盒子,a里面开始存的是18,修改的时候把18扔出去再换成20(对于不熟悉的人可以这样理解,其实是像图中那样a又指向了一个新的值,18因为没有指针引用便会当成垃圾处理了。至于js的垃圾回收机制下面会有喔)
下面便开始说一下浅拷贝了,什么是浅拷贝呢。就是拷贝的对象中依然有和源对象具有共同的引用对象。一图万法
正如obj和objCopy对象内部中的test属性,引用还是同一个对象。这种情况就是浅拷贝。那么深靠谱就是完整复制,两个对象之间没有任何关联了
三种实现浅拷贝的方法
方法1:循环
const targetObj = {
name: {
first: 'g',
last: 'xb'
},
age: 18
}
const obj = {}
for (let k in targetObj) {
obj[k] = targetObj[k]
}
方法二: Object.assign()
语法:Object.assign(target, ...sources)
const targetObj = {
name: {
first: 'g',
last: 'xb'
},
age: 18
}
let obj = {}
obj = Object.assign(obj, targetObj)
console.log(obj)
方法三:使用扩展运算符
const targetObj = {
name: {
first: 'g',
last: 'xb'
},
age: 18
}
const obj = {...targetObj }
console.log(obj)
实现深拷贝
let targetObj = {
name: {
first: 'g',
last: 'xb'
},
age: 18
}
function copy(object) {
let obj = object instanceof Array ? [] : {}
for (const [k, v] of Object.entries(object)) {
obj[k] = typeof v == 'object' ? copy(v) : v
}
return obj
}
let obj = copy(targetObj)
console.log(obj)
7. 面试官:说一下数组去重与扁平化
数组去重
方法一、最简单的方式:利用set
const arr = [1, 1, 2, 3, 3, 4]
const s = new Set(arr)
console.log([...s])//1,2,3,4
方式二、循环处理(这就使用各种数组的API了)。我看到网上的什么去重方法大总结有是十种之多,小白读了一下感觉蛮可悲的,就是一种思路换不同的API去写。小白感觉没什么意思
//如:
const arr = [1, 1, 2, 3, 3, 4]
const newArr = []
for (const v of arr) {
if (!newArr.includes(v)) {
newArr.push(v)
}
}
console.log(newArr)
扁平化
方法一:使用 flat() ,参数是数组的深度
const arr = [1, [2, 3],
[4, [5]]
]
const newArr = arr.flat(3)
console.log(newArr)//[1,2,3,4,5]
方法二:通用思路(递归处理)
首先先来看小白看到的一段使用迭代器生成搞的,据说蛮是天才级的扁平化方法,我们来看一下吧
function* delayering(arr) {
if (Array.isArray(arr)) {
for (let i = 0; i < arr.length; i++) {
yield* delayering(arr[i])
}
} else {
yield arr
}
}
let arr1 = [1, 2, ['a', 'b', ['中', '文', [1, 2, 3, [11, 21, 31]]]], 3]
for (const x of delayering(arr1)) {
console.log(x);
}
这神奇吗,小白一点都么的感觉,不也是递归吗...
像这样简单改造一下
let arr = [1, 2, ['a', 'b', ['中', '文', [1, 2, 3, [11, 21, 31]]]], 3]
let newArr = []
function delayering(arr) {
if (arr instanceof Array) {
for (const v of arr) {
delayering(v)
}
} else {
newArr.push(arr)
}
}
delayering(arr)
console.log(newArr)
这本质上不都是一个样吗。。。小白么得感觉使用那种好处究竟在哪,可能小白属实太菜了吧(忽略我递归使用了外部变量啊,我只是简单的写了一下。。。太懒了我)
方法三:利用 Array.some 和展开运算符
要说递归是从内部解决,那么这里就是从外部解决,先来看一下完整代码
function delayering(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
let arr = [1, 2, [4, [5, [6]]]]
console.log(iterTree2(arr))//[1,2,3,4,5,6]
递归理解起来是比较简单的,这里是比递归难理解一点
其实一步一步写下来,你也会发现这是非常简单的
首先会判断数组arr的元素是是否还有数组,有就使用展开符展开,有就展开
首先第一次展开
...arr//1,2,[4, [5, [6]]]
用空数组连接一下就是[1,2,4,[5,[6]]]
还有数组,又展开
这时的...arr就是1,2,4,5,[6]
连接后就是[1,2,4,5,6]
over,是不是更加简单呢?
写到最后:
期待我们的下一次的邂逅