== VS ===
涉及面试题:== 和 === 有什么区别?
对于 ==
来说,如果对比双方的类型不一样的话,就会进行类型转换
假如我们需要对比x
和y
是否相同,就会进行如下判断流程:
- 首先会判断两者类型是否相同。相同的话就是比较大小了
- 类型不相同的话,那么就会进行类型转换
- 会先判断是否在对比
null
和undefined
,是的话就会返回true
- 判断两者类型是否为
string
和number
,是的话就会将字符串转换为number
- 判断其中一方是否为
boolean
,是的话就会把boolean
转为number
再进行判断 - 判断其中一方是否为
object
且另一方为string
、number
或者symbol
,是的话就会把object
转为原始类型再进行判断
思考题:看完了上面的步骤,对于[] == ![] 你能否写出正确答案呢?
1 | [] == ![] // ? |
如果你觉得记忆步骤太麻烦的话,我还提供了流程图供大家使用:
对于 ===
来说就简单多了,就是判断两者类型和值是否相同
闭包
涉及面试题:什么事闭包?
闭包的定义其实很简单:函数 A 内部有一个函数 B ,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。
1 | function A() { |
很多人对于闭包的解释可能是函数嵌套了函数,然后返回一个函数。其实这个解释是不完整的,就比如我上面的例子就可以反驳这个观点。
在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。
经典面试题,循环中使用闭包解决
var
定义函数的问题
1 | for (var i = 1; i <= 5; i++) { |
首先因为setTimeout
是个异步函数,所以会先把循环全部执行完毕,这时候i
就是6了,所以会输出一堆6。
解决办法有三种,第一种是使用闭包的方式
1 | for (var i = 1; i <= 5; i++) { |
在上述代码中,我们首先使用了立即执行函数将 i
传入函数内部,这个时候值就被固定在了参数j
上面不会改变,当下次执行timer
这个闭包的时候,就可以使用外部函数的变量j
,从而达到目的。
第二种就是使用setTimeout
的第三个参数,这个参数会被当成timer
函数的参数传入。
1 | for (var i = 1; i <= 5; i++) { |
第三种就是使用let
定义i
来解决问题了,这个也是最为推荐的方式
1 | for (let i = 1; i <= 5; i++) { |
深浅拷贝
涉及面试题:什么是浅拷贝?如何实现浅拷贝?什么是深拷贝?如何实现深拷贝?
在上一章节中,我们了解了对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况,通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个情况。
1 | let a = { |
浅拷贝
首先可以通过Object.assign
来解决这个问题,很多人认为这个函数是用来深拷贝的。其实并不是,Object.assign
只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。
1 | let a = { |
另外我们可以通过展开运算符...
来实现浅拷贝
1 | let a = { |
通常浅拷贝就能解决大部分问题了,但是当我们遇到了如下情况就可能需要使用到深拷贝了
1 | let a = { |
浅拷贝只解决了第一层的问题,如果接下去的值中海油对象的话,那么就又回到了最开始的话题了,两者享有相同的地址。要解决这个问题,我们就得使用深拷贝了。
深拷贝
这个问题通常可以通过JSON.parse(JSON.stringify(object))
来解决
1 | let a = { |
该方法也是有局限性的:
- 会忽略
undefined
- 会忽略
symbol
- 不能序列化函数
- 不能解决循环引用的对象
1 | let obj = { |
如果你有这么一个循环引用对象,你会发现并不能通过该方法实现深拷贝
在遇到函数、undefined
或者symbol
的时候,该对象也不能正常的序列化
1 | let a = { |
你会发现在上述情况中,该方法会忽略掉函数和undefined
但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题。
如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用MessageChannel
1 | function structuralClone(obj) { |
当然你可能想自己来实现一个深拷贝,但是其实实现一个深拷贝是很困难的,需要我们考虑好多种边界情况,比如原型链如何处理、DOM如何处理等等,所以这里我们实现的深拷贝只是简易版,并且我其实更推荐使用 lodash 的深拷贝函数。
1 | function deepClone(obj) { |
原型
涉及面试题:如何理解原型?如何理解原型链?
当我们创建一个对象时let obj = { age: 25 }
,我们可以发现能使用很多种函数,但是我们明明没有定义过它们,对于这种情况你是否有过疑惑?
当我们在浏览器中打印obj
时你会发现,在obj
上居然还有一个__proto__
属性,那么看来之前的疑问就和这个属性有关系了。
其实每个 JS 对象都有__proto__
属性,这个属性指向了原型。这个属性在现在来说已经不推荐直接去使用它了,这只是浏览器在早期为了让我们访问到内部属性 [[protorype]]
来实现的一个东西。
讲到这里好像还是没有弄明白什么是原型,接下来让我们再看看__proto__
里面有什么吧。
看到这里你应该明白了,原型也是一个对象,并且这个对象汇总包含了很多函数,所以我们可以得出一个结论:对于obj
来说,可以通过__proto__
找到一个原型对象,在该对象中定义了很多函数让我们使用。
在上面的图中我们还可以发现一个constructor
属性,也就是构造函数
打开constructor
属性我们又可以发现其中还有一个prototype
属性,并且这个属性对应的值和先前在__proto__
中看到的一模一样。所以我们又可以得出一个结论:原型的constructor
属性指向构造函数,构造函数又通过prototype
属性指回原型,但是并不是所有函数都具有这个属性,Function.prototype.bind()
就没有这个属性。
其实原型就是那么简单,接下来我们再来看一张图,相信这张图能让你彻底明白原型和原型链
看完这张图,我再来解释下设么是原型链吧。其实原型链就是对个对象通过__proto__
的方式连接了起来。为什么obj
可以访问到valueOf
函数呢,就是因为obj
通过原型链找到了valueOf
函数。
对于这一小节的知识点,总结起来就是以下几点:
Object
是所有对象的爸爸,所有对象都可以通过__proto__
找到它Function
是所有函数的爸爸,所有函数都可以通过__proto__
找到它- 函数的
prototype
是一个对象 - 对象的
__proto__
属性指向原型,__proto__
将对象和原型连接起来组成了原型链