JS 知识基础 (下)

== VS ===

Q:涉及面试题:== 和 === 有什么区别?

  • ==:如果对比双方的类型不一样的话,就会进行类型转换
  • ===:判断两者类型和值是否相同。

例子🌰:

假如我们需要对比x和y是否相同,就会进行如下判断流程:

[图片上传失败...(image-de4c8f-1618542354617)]

  • 首先会判断两者类型是否相同。相同的话就是比大小;类型不相同的话,那么就会进行类型转换

  • 会先判断 是否 在 对比null和undefined,是的话就会返回true;也就是 null == undefined 输出 true

  • 判断两者类型是否为string 和 number,是的话,就会将 字符串 转换为 number

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">1 == '1' ↓ 1 == 1</pre>

  • 判断其中一方是否为boolean,是的话就会把boolean转为number,再进行判断

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">'1' == true ↓ '1' == 1 ↓ 1 == 1 //输出 true '2' == true ↓ '2' == 1 ↓ 2 == 1 //输出 false </pre>

  • 判断其中一方是否为object且另一方为string、number或者symbol,是的话就会把object转为原始类型再进行判断

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">'1' == { name: 'yck' } ↓ '1' == '[object Object]'</pre>

思考题:看完了上面的步骤,对于 [] == ![] 你是否能正确写出答案呢?

过程如下:

  • ![] 就是 !true 也就是 false
  • [] == false
  • [] == 0
  • '' == 0
  • 0 == 0

更多的内容可以参考标准文档

闭包

Q: 什么是闭包?

闭包的定义:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包

闭包存在的意义:就是让我们可以间接访问函数内部的变量。

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">function A() { let a = 1 window.B = function () { console.log(a) } } A() B() // 1</pre>

很多人对于闭包的解释可能是函数嵌套了函数,然后返回一个函数。其实这个解释是不完整的,就比如我上面这个例子就可以反驳这个观点。

经典面试题,循环中使用闭包解决 var 定义函数的问题

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">for (var i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000) } // 6 6 6 6 6 6</pre>

首先因为setTimeout是个异步函数,所以会先把循环全部执行完毕,由因为是var声明的,所以这时候i就是 6 了,所以会输出一堆 6。

解决办法有三种

第一种是使用闭包的方式 - 立即执行函数

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">for (var i = 1; i <= 5; i++) { ;(function(j) { setTimeout(function timer() { console.log(j) }, j * 1000) })(i) } </pre>

在上述代码中,我们首先使用了立即执行函数将i传入函数内部,这个时候值就被固定在了参数j上面不会改变,当下次执行timer这个闭包的时候,就可以使用外部函数的变量j,从而达到目的。

第二种就是使用setTimeout的第三个参数,这个参数会被当成timer函数的参数传入

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">for (var i = 1; i <= 5; i++) { setTimeout( function timer(j) { console.log(j) }, i * 1000, i ) }</pre>

第三种就是使用let定义i了来解决问题了,这个也是最为推荐的方式

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000) }</pre>

深浅拷贝

Q:什么是浅拷贝?如何实现浅拷贝?什么是深拷贝?如何实现深拷贝?

对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况。

通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个情况。注意:浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到最开始的话题了,两者享有相同的地址。要解决这个问题,我们就得使用深拷贝了

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">let a = { age: 1 } let b = a a.age = 2 console.log(b.age) // 2</pre>

浅拷贝

  • Object.assign: let b = Object.assign({}, a)
  • ... 展开运算符:let b = { ...a }

首先可以通过 Object.assign 来解决这个问题,很多人认为这个函数是用来深拷贝的。其实并不是

Object.assign只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">let a = { age: 1 } let b = Object.assign({}, a) a.age = 2 console.log(b.age) // 1</pre>

另外我们还可以通过展开运算符...来实现浅拷贝

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">let a = { age: 1 } let b = { ...a } a.age = 2 console.log(b.age) // 1</pre>

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就可能需要使用到深拷贝了

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">let a = { age: 1, jobs: { first: 'FE' } } let b = { ...a } a.jobs.first = 'native' console.log(b.jobs.first) // native</pre>

浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到最开始的话题了,两者享有相同的地址。要解决这个问题,我们就得使用深拷贝了。

深拷贝

这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">let a = { age: 1, jobs: { first: 'FE' } } let b = JSON.parse(JSON.stringify(a)) a.jobs.first = 'native' console.log(b.jobs.first) // FE</pre>

但是该方法也是有局限性的:

  • 会忽略undefined
  • 会忽略symbol
  • 不能序列化函数
  • 不能解决循环引用的对象

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">let obj = { a: 1, b: { c: 2, d: 3, }, } obj.c = obj.b obj.e = obj.a obj.b.c = obj.c obj.b.d = obj.b obj.b.e = obj.b.c let newObj = JSON.parse(JSON.stringify(obj)) console.log(newObj)</pre>

如果你有这么一个循环引用对象,你会发现并不能通过该方法实现深拷贝

[图片上传失败...(image-d235ca-1618542354616)]

在遇到函数、undefined****或者****symbol的时候,该对象也不能正常的序列化

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">let a = { age: undefined, sex: Symbol('male'), jobs: function() {}, name: 'yck' } let b = JSON.parse(JSON.stringify(a)) console.log(b) // {name: "yck"} </pre>

你会发现在上述情况中,该方法会忽略掉函数和undefined和symbol。

但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题。

如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用MessageChannel

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">function structuralClone(obj) { return new Promise(resolve => { const { port1, port2 } = new MessageChannel() port2.onmessage = ev => resolve(ev.data) port1.postMessage(obj) }) } var obj = { a: 1, b: { c: 2 } } obj.b.d = obj.b // 注意该方法是异步的 // 可以处理 undefined 和循环引用对象 const test = async () => { const clone = await structuralClone(obj) console.log(clone) } test()</pre>

当然你可能想自己来实现一个深拷贝,但是其实实现一个深拷贝是很困难的,需要我们考虑好多种边界情况,比如原型链如何处理、DOM 如何处理等等,所以这里我们实现的深拷贝只是简易版,并且我其实更推荐使用lodash 的深拷贝函数

自己实现一个深拷贝

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">function deepClone(obj) { function isObject(o) { return (typeof o === 'object' || typeof o === 'function') && o !== null } if (!isObject(obj)) { throw new Error('非对象') } let isArray = Array.isArray(obj) let newObj = isArray ? [...obj] : { ...obj } Reflect.ownKeys(newObj).forEach(key => { newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key] }) return newObj } let obj = { a: [1, 2, 3], b: { c: 2, d: 3 } } let newObj = deepClone(obj) newObj.b.c = 1 console.log(obj.b.c) // 2</pre>

原型

Q:如何理解原型?如何理解原型链?

当我们创建一个对象时let obj = { age: 25 },我们可以发现能使用很多种函数,但是我们明明没有定义过它们,对于这种情况你是否有过疑惑?

[图片上传失败...(image-b77be5-1618542354616)]

当我们在浏览器中打印obj时你会发现,在obj上居然还有一个proto属性,那么看来之前的疑问就和这个属性有关系了。

其实每个 JS 对象都有proto属性,这个属性指向了原型。这个属性在现在来说已经不推荐直接去使用它了,这只是浏览器在早期为了让我们访问到内部属性[[prototype]]来实现的一个东西。

讲到这里好像还是没有弄明白什么是原型,接下来让我们再看看proto里面有什么吧。

[图片上传失败...(image-1ea8e0-1618542354616)]

看到这里你应该明白了,原型也是一个对象,并且这个对象中包含了很多函数,所以我们可以得出一个结论:对于obj来说,可以通过proto找到一个原型对象,在该对象中定义了很多函数让我们来使用。

在上面的图中我们还可以发现一个constructor属性,也就是构造函数

[图片上传失败...(image-41d937-1618542354616)]

打开constructor属性我们又可以发现其中还有一个prototype属性,并且这个属性对应的值和先前我们在proto中看到的一模一样。

所以我们又可以得出一个结论:原型的constructor属性指向构造函数构造函数又通过prototype属性指回原型,但是并不是所有函数都具有这个属性,Function.prototype.bind()就没有这个属性。

其实原型就是那么简单,接下来我们再来看一张图,相信这张图能让你彻底明白原型和原型链

[图片上传失败...(image-84da7-1618542354616)]

其实原型链就是多个对象通过proto的方式连接了起来。

为什么obj可以访问到valueOf函数,就是因为obj通过原型链找到了valueOf函数。

总结起来就是以下几点:

  • Object是所有对象的爸爸,所有对象都可以通过proto找到它
  • Function是所有函数的爸爸,所有函数都可以通过proto找到它
  • 函数的prototype是一个对象
  • 对象的proto属性指向原型,proto将对象和原型连接起来组成了原型链
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容