== 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__将对象和原型连接起来组成了原型链