在这一章节中我们继续来了解 JS 的一些常考和容易混乱的基础知识点。
== vs ===
涉及面试题:== 和 === 有什么区别?
于是查阅相关文档,关于”=\=” 和 “=\==”的总结: “=\==”叫做严格运算符,”==”叫做相等运算符。
严格运算符的运算规则如下:
1、如果类型不同,就[不相等]
2、如果两个都是数值,并且是同一个值,那么[相等];如果其中至少一个是NaN,那么[不相等]。(判断一个值是否是NaN,只能用isNaN()来判断)
3、如果两个都是字符串,每个位置的字符都一样,那么[相等];否则[不相等]。
4、如果两个值都是true,或者都是false,那么[相等]。
5、如果两个值都引用同一个对象或函数,那么[相等];否则[不相等]。
6、如果两个值都是null,或者都是undefined,那么[相等]。
相等运算符的运算规则如下:
1、如果两个值类型相同,进行 === 比较。
2、如果两个值类型不同,他们可能相等。根据下面规则进行类型转换再比较:
(1)如果一个是null、一个是undefined,那么[相等]。
(2)如果任一值是字符串,另一个值是数值,在比较相等性之前先将字符串转换为数值;即是调用Number()函数。
(3)如果任一值时布尔值,则在比较相等性之前先将其转换为数值,即是调用Number()函数。
(4)如果一个是对象,另一个是数值或字符串,把对象转换成基础类型的值再比较。对象转换成基础类型,利用它的toString或者valueOf方法。 js核心内置类,会尝试valueOf先于toString;例外的是Date,Date利用的是toString转换。
分析 [ ] == ![ ]这道题:
(1)看见 ![ ]这个是要对空数组转成布尔类型结果得到![ ] = false,(2)发现此时符合第三条,如果任一值是布尔类型是需要将其转换为数值,即Number(false),结果为 Number(false) = 0。(3)此时得到 [ ] == 0比较,此时符合第四条
即 [ ].toString();结果为[].toString() = ” ”;(4)此时得到 ” ” == 0,发现符合第二条即Number(“”);
结果为Number(” ”) = 0;(5)此时得到 0 == 0 两个同时为数值类型比较所以结果为true;
You Don’t Know JS的作者关于”=\=”和”=\==”,在第一本书的第二章是这样说的:许多开发者认为 === 的行为更加容易预测,所以他们总是主张使用 === 而劝人们远离 =\=,我认为这种看法是非常短视的。如果你花点时间来搞清楚它是如何工作的话,=\= 绝对是一个能够帮助你程序的强大工具。
==仁者见仁智者见智==
闭包
涉及面试题:什么是闭包?
闭包的定义其实很简单:函数 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 。
但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题。
当然你可能想自己来实现一个深拷贝,但是其实实现一个深拷贝是很困难的,需要我们考虑好多种边界情况,比如原型链如何处理、DOM 如何处理等等,所以这里我们实现的深拷贝只是简易版,并且我其实更推荐使用 lodash 的深拷贝函数。
1 | function deepClone(obj) { |
原型
涉及面试题:如何理解原型?如何理解原型链?
当我们创建一个对象时 let obj = { age: 25 },我们可以发现能使用很多种函数,但是我们明明没有定义过它们,对于这种情况你是否有过疑惑?
当我们在浏览器中打印 obj 时你会发现,在 obj 上居然还有一个 proto 属性,那么看来之前的疑问就和这个属性有关系了。
其实每个 JS 对象都有 proto 属性,这个属性指向了原型。这个属性在现在来说已经不推荐直接去使用它了,这只是浏览器在早期为了让我们访问到内部属性 [[prototype]] 来实现的一个东西。
讲到这里好像还是没有弄明白什么是原型,接下来让我们再看看 proto 里面有什么吧。
看到这里你应该明白了,原型也是一个对象,并且这个对象中包含了很多函数,所以我们可以得出一个结论:对于 obj 来说,可以通过 proto 找到一个原型对象,在该对象中定义了很多函数让我们来使用。
在上面的图中我们还可以发现一个 constructor 属性,也就是构造函数
打开 constructor 属性我们又可以发现其中还有一个 prototype 属性,并且这个属性对应的值和先前我们在 proto 中看到的一模一样。所以我们又可以得出一个结论:原型的 constructor 属性指向构造函数,构造函数又通过 prototype 属性指回原型,但是并不是所有函数都具有这个属性,Function.prototype.bind() 就没有这个属性。
其实原型就是那么简单,接下来我们再来看一张图,相信这张图能让你彻底明白原型和原型链
看完这张图,其实原型链就是多个对象通过 proto 的方式连接了起来。为什么 obj 可以访问到 valueOf 函数,就是因为 obj 通过原型链找到了 valueOf 函数。
对于这一小节的知识点,总结起来就是以下几点:
- Object 是所有对象的爸爸,所有对象都可以通过 proto 找到它
- Function 是所有函数的爸爸,所有函数都可以通过 proto 找到它
- 函数的 prototype 是一个对象
- 对象的 proto 属性指向原型, proto 将对象和原型连接起来组成了原型链