内容主要涉及JavaScript:
运算符比较、闭包、深浅拷贝、原型
。
JavaScript闭包、原型、深浅拷贝
- 1、== 和 === 的差异
- 2、JavaScript闭包
- 3、JavaScript深浅拷贝
- 3.1、浅拷贝
- 3.2、深拷贝
- 4、JavaScript原型
1、== 和 === 的差异
对于==
而言,若对比双方的类型不一样的话,则存在类型转换,这也就用到了。
JavaScript类型转换:包括
显式转换和隐式转换
。
当需要对比两个变量a
和b
是否相同,需要如下流程:
- 首先判定两者类型是否相同,相同则比较大小。
- 类型不同的话,则进行类型参数转换。
- 判定是否对比
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
综述,如下流程图:
2、JavaScript闭包
首先给出闭包的正确定义:
设定一个函数能访问外部的变量,那么就形成了一个闭包,而且不是一定要返回一个函数。
给出下述代码:
let a = 1
// 产生闭包
function fn() {
console.log(a);
}
function fn1() {
let a = 1
// 产生闭包
return () => {
console.log(a);
}
}
const fn2 = fn1()
fn2()
接下来阐述数据存放的正确规则:局部、占用空间确定的数据
,一般存放于栈中,否则就在堆中;
上图能看到一个内部的对象scopes
,这个对象就是一般所说的作用域链。
根据作用域链寻找的顺序,包含了闭包、全局对象。因此我们能够通过闭包访问本次销毁的变量,所以原始数据一般存放在栈上。
另外最开始对于闭包的定位是:假如一个函数能够访问外部的变量,则形成了闭包。给出如下代码:
let a = 1
var b = 2
// 形成闭包
function fn() {
console.log(a, b);
}
可以得出结论,若是var
则直接被挂载到global
上,若是其他的关键字声明则被挂载到Script
上面,虽然这些数据同样还是存在于scopes
,但是全局变量在内存中是存放在静态区域的,因为全局变量无需参与到GC。
最后总结一下原始类型存储位置:局部变量被存储在栈上,全局变量存储在静态区域上,其它都存储在堆上。
3、JavaScript深浅拷贝
3.1、浅拷贝
首先可以通过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
3.2、深拷贝
深拷贝通常可以利用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支持的数据类型:
若需要拷贝的对象含有内置类型并且不包含函数,则可以利用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
}
不过上述代码存在缺陷,递归可能存在栈溢出,这是因为执行栈的大小有限,可以通过遍历的方式改写递归,例如JavaScript实现层序遍历BFS,也可以利用数组模拟栈解决栈溢出的问题。
4、JavaScript原型
当创建一个对象时,let obj = { age: 25 }
,我们可以发现能使用很多种函数,但是没有定义过他们,如下图:
在浏览器中打印obj
,会发现存在一个 ,这个属性目前不推荐使用,这只是浏览器早起为了访问内部属性__proto__
的属性,这是因为每一个JavaScript对象都有一个__proto__
的属性,这个属性指向了原型prototype
来实现的东西;
原型也是一个对象,并且该对象包含了很多的函数,所以推出结论:
对于
obj
而言,可以通过__proto__
找到一个原型对象,再该对象中定义了很多的函数让我们使用。
同时还可以发现一个constructor
的属性,也就是构造函数:
打开constructor
属性可以发现其中还有一个prototype属性,并且该属性和之前的__prototype__
中的一模一样,因此给出如下的结论:
原型的
constructor
属性指向了构造函数,构造函数通过prototype
来指向原型,不过并不是所有的函数都具有这个属性,Function.prototype.bind() 就没有这个属性。
给出如下的图来总结记忆:
原型链就是多个对象通过__proto__
方式连接起来了,总结具体如下:
object
是所有对象的父亲,所有对象都可以通过__proto__
找到它。Funciton
是所有函数的父亲,所有函数都可以通过__proto__
找到它。- 函数的
prototype
是一个对象。 - 对象的
__proto__
属性指向了原型,__proto__
把对象和原型连接起来组成了原型链。