JS 基础知识点及常考面试题(二)
在这一章节中我们继续来了解 JS 的一些常考和容易混乱的基础知识点。
Excerpt
本篇文章针对于 vue2,目前正在基于 vue3 做一个开源的脚手架,如果有感兴趣的同学,可以来这里贡献代码。 考虑到后续招人和现有人员的技术栈,选择 Vue 作为框架。 公司主要业务是 GIS 和 BIM,通常开发一些中大型的系统,所以 vue-router 和 vuex 都…
更新日志
- 重写闭包及深浅拷贝章节
== vs ===
对于 ==
来说,如果对比双方的类型不一样的话,就会进行类型转换,这也就用到了我们上一章节讲的内容。
当我们需要对比 x
和 y
是否相同时,就会进行如下判断流程:
首先会判断两者类型是否相同。相同的话就是比大小了
类型不相同的话,那么就会进行类型转换
会先判断是否在对比
null
和undefined
,是的话就会返回true
判断两者类型是否为
string
和number
,是的话就会将字符串转换为number
1 == '1' ↓ 1 == 1
判断其中一方是否为
boolean
,是的话就会把boolean
转为number
再进重写进行判断流程'1' == true ↓ '1' == 1 ↓ 1 == 1
判断其中一方是否为
object
且另一方为string
、number
,是的话就会把object
转为原始类型再进行判断,也就是执行x.toString()
及valueOf
,具体内容在上一章节已经聊过'1' == { name: 'yck' } ↓ '1' == '[object Object]'
返回
false
如果你觉得记忆步骤太麻烦的话,我还提供了流程图供大家使用:
以上内容大家也可以参考 标准文档。
最后对于 ===
来说,判断简单多了,就是判断两者类型和值是否相同就行。
常见面试题
==
和===
操作符有什么区别==
操作符的类型转换规则
闭包
很多读者的闭包概念是完全错误的,如果你觉得你理解的闭包概念与我不同,那么请以我的内容为准。
首先闭包正确的定义是:假如一个函数能访问外部的变量,那么就形成了一个闭包,而不是一定要返回一个函数。这个定义很重要,下面的内容需要用到。
let a = 1
// 产生闭包
function fn() {
console.log(a);
}
function fn1() {
let a = 1
// 产生闭包
return () => {
console.log(a);
}
}
const fn2 = fn1()
fn2()
其实到这里为止闭包的概念已经讲完了,没有别的东西了。接下来我们通过 Chrome 的开发工具来进一步了解闭包是如何存储外部变量的。
先来说下数据存放的正确规则是:局部、占用空间确定的数据,一般会存放在栈中,否则就在堆中(也有例外)。 那么接下来我们来验证这个说法:
上图中画红框的位置我们能看到一个内部的对象 [[Scopes]]
,这个对象就是我们常说的作用域链。根据作用域链寻找的顺序,其中包含了闭包、全局对象。据此我们能通过闭包访问到本该销毁的变量,所以我会在上一章节中说原始数据一般存放在栈上。
另外最开始我们对于闭包的定位是:假如一个函数能访问外部的变量,那么就形成了一个闭包,因此接下来我们看看在全局下的表现是怎么样的。
let a = 1
var b = 2
// 形成闭包
function fn() {
console.log(a, b);
}
从上图我们能发现全局下声明的变量,如果是 var 的话就直接被挂到 global
上,如果是其他关键字声明的话就被挂到 Script
上。虽然这些数据同样还是存在 [[Scopes]]
上,但是全局变量在内存中是存放在静态区域的,因为全局变量无需进行垃圾回收。
最后总结一下原始类型存储位置:局部变量被存储在栈上,全局变量存储在静态区域上,其它都存储在堆上。
常见考点
闭包能考的很多,概念和笔试题都会考。
概念题就是考考闭包是什么了。
笔试题的话基本都会结合上异步,比如最常见的:
for (var i = 0; i < 6; i++) {
setTimeout(() => {
console.log(i)
})
}
这道题会问输出什么,有哪几种方式可以得到想要的答案?
深浅拷贝
在上一章节中,我们了解了对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方时其它也都被改变的情况。通常在开发中我们不希望出现这样的问题,此时我们可以使用浅拷贝来解决这个情况。
浅拷贝
首先可以通过 Object.assign
来解决这个问题,这个函数会拷贝所有的属性值到新的对象中。如果属性值是对象的话,拷贝的是地址。
let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1
另外我们还可以通过展开运算符 ...
来实现浅拷贝:
let a = {
age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1
通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了:
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native
深拷贝
深拷贝通常可以通过 JSON.parse(JSON.stringify(object))
来解决,这个方式基本能解决大部分情况。
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
当然了,这个方法是存在局限性的。
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)
如果对象中存在循环引用,你会发现程序会报错:
同时在遇到不支持的数据类型,比如函数、 undefined
或者 symbol
的时候,这些属性都会被忽略:
let a = {
age: undefined,
sex: Symbol('male'),
jobs: function() {},
name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}
你会发现在上述代码中,JSON.stringify(a)
只保留了 name
属性。
以下是 JSON 支持的数据类型:
如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel
:
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()
当然我们也可以自己实现一个深拷贝,同时这也是一个非常高频的手写题:
// 利用 WeakMap 解决循环引用
let map = new WeakMap()
function deepClone(obj) {
if (obj instanceof Object) {
if (map.has(obj)) {
return map.get(obj)
}
let newObj
if (obj instanceof Array) {
newObj = []
} else if (obj instanceof Function) {
newObj = function() {
return obj.apply(this, arguments)
}
} else if (obj instanceof RegExp) {
// 拼接正则
newobj = new RegExp(obj.source, obj.flags)
} else if (obj instanceof Date) {
newobj = new Date(obj)
} else {
newObj = {}
}
// 克隆一份对象出来
let desc = Object.getOwnPropertyDescriptors(obj)
let clone = Object.create(Object.getPrototypeOf(obj), desc)
map.set(obj, clone)
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key])
}
}
return newObj
}
return obj
}
以上代码解决了常见类型的 copy 及循环引用的问题。
当然还是存在一部分缺陷的,比如说递归肯定会存在爆栈的问题,因为执行栈的大小是有限制的,到一定数量栈就会爆掉。当遇到这种问题,我们可以通过遍历的方式来改写递归。也就是如何写层序遍历(BFS)的问题了,只需要通过数组来模拟执行栈就能解决爆栈问题。
但是在面试时能写出上面的代码已经完全够了,剩下的能口述思路基本这道题就能拿到高分了。
常见面试题
- 浅拷贝和深拷贝的区别是什么
JSON.parse(JSON.stringify(a))
存在什么问题- 手写深拷贝函数
原型
当我们创建一个对象时 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__
将对象和原型连接起来组成了原型链
如果你还想深入学习原型这部分的内容,可以阅读我之前写的文章。
常见面试题
- 如何理解原型及原型链
- 如何重写原型,这个在下一章节的继承小节中有解释
- 原型链指向
小结
以上就是全部的常考和容易混乱的基础知识点了,下一章节我们将会学习 ES6 部分的知识。如果大家对于这个章节的内容存在疑问,欢迎在评论区与我互动。