停止如此那样的JSON.parse(JSON.stringify(oldObj))

浅克隆
浅克隆之所以被称为浅克隆,是因为对象只会被克隆最外部的一层,至于更深层的对象,则依然是通过引用指向同一块堆内存.
1 | // 浅克隆函数 |
我们可以看到,很明显虽然oldObj.c.h
被克隆了,但是它还与oldObj.c.h
相等,这表明他们依然指向同一段堆内存,这就造成了如果对newObj.c.h
进行修改,也会影响oldObj.c.h
,这就不是一版好的克隆.
1 | newObj.c.h.i = 'change'; |
我们改变了newObj.c.h.i
的值,oldObj.c.h.i
也被改变了,这就是浅克隆的问题所在.
当然有一个新的api Object.assign()
也可以实现浅复制,但是效果跟上面没有差别,所以我们不再细说了.
深克隆
JSON.parse方法
前几年微博上流传着一个传说中最便捷实现深克隆的方法, JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,这两个方法结合起来就能产生一个便捷的深克隆.
1 | const newObj = JSON.parse(JSON.stringify(oldObj)); |
我们依然用上一节的例子进行测试
1 | const oldObj = { |
确实,这个方法虽然可以解决绝大部分是使用场景,但是却有很多坑.
1.他无法实现对函数 、RegExp等特殊对象的克隆
2.会抛弃对象的constructor,所有的构造函数会指向Object
3.对象有循环引用,会报错
主要的坑就是以上几点,我们一一测试下.
1 | // 构造函数 |
我们可以看到在对函数、正则对象、稀疏数组等对象克隆时会发生意外,构造函数指向也会发生错误。
对于RegExp修饰符
i - 修饰符是用来执行不区分大小写的匹配。
g - 修饰符是用于执行全文的搜索(而不是在找到第一个就停止查找,而是找到所有的匹配)。
其他错误
而且构造函数指向也会发生错误。
1 | const oldObj = {}; |
对象的循环引用会抛出错误.
构造一个深克隆函数
我们知道要想实现一个靠谱的深克隆方法,上一节提到的序列/反序列是不可能了,而通常教程里提到的方法也是不靠谱的,他们存在的问题跟上一届序列反序列操作中凸显的问题是一致的.
1 | function isArray(arr) { |
补充一下面试过程捣鼓出来的深克隆
1 | const mapTag = '[object Map]' |
科普(本文涉及到的陌生知识点)
为什么用 Object.prototype.toString.call() ?
Object.prototype.toString.call(obj) 可以用来检测传入参数的数据类型,最常见的就是用来检测数组。你是否有被问过怎样判断一个数据是否是数组类型?
这样的问题,如果你回答的是typeof
,那就得抓把紧多学学了。
这里我就不介绍所有可以判断数组类型的方法了,我只说Object.prototype.toString.call()
这一种方法,因为它让人觉得有点奇怪。
使用 Object.prototype.toString.call() 判断数组类型
使用方法很简单,举个例子:
1 | var arr = [1, 2, 3] |
当我第一次知道可以这样判断数组类型时,我就觉得很奇怪,为什么要用这么奇怪的方法呢?当时的我并没有深究,只是记住了这个方法,但回过头来再仔细想想,这样的方法是有它的道理的。这段语法的意思是Object
这个对象的原型链上的toString()
方法执行在arr
这个上下文中,这里让人疑惑的就是为什么要用Object
上的toString()
方法,因为数组本身也有toString()
。要了解其原理,我们可能要先了解一下toString()
这个方法。
toString()
从字面上的意思就可以看出它可以将数据转换为字符串,但将各类型的数据转换为字符串的方式又不一样,如下:
1 | var num = 123 |
以上是各数据类型使用toString()
方法转换为字符串的结果,除了null
和undefined
不能转换以外,其他数据都有自己的方式变为字符串。
其实toString
是对象上的方法,每一个对象上都有这个方法,那就意味着数字
、字符串
和布尔值
这些基本数据类型不能使用toString()
方法,但上例中的基本数据类型却是可以使用,这要归功于javascript中的包装类,即Number
、String
和Boolean
。原始值不能有属性和方法,当要使用toString()
方法时,会先将原始值包装成对象再使用。
所以现在可以知道,上例中使用到的toString()
方法分别属于Number
、String
、Boolean
、Array
、Object
和Function
这些类。我们又知道在JavaScript中,所有类都继承自Object
,既然是继承,那么toString()
方法理应也被继承了,但看上例中的结果,显然toString()
并没有被继承,不然所有的输出结果应该都类似于'[object Object]'
这样。
其实各数据类型使用toString()
后的结果表现不一的原因在于:所有类在继承Object的时候,改写了toString()方法。原始Object
上的toString()
方法是可以输出数据类型的,如上例中的'[object Object]'
这个结果,所以当我们想要判断数据类型时,必须使用Object
上的toString()
方法。
验证:上面解释了为什么要使用Object
上的toString()
方法来判断数据类型,是因为其他类上的toString()
方法被改写了。为了加深理解,我们可以举个例子验证一下:
1 | // 定义一个数组 |
当我们删除了Array
自己的toString()
方法后,再次使用时会向上查找这个方法,即Object
的toString()
,这是原型链的知识。
//第一个Object表示这是一个复杂数据类型Object,然后细分出来就是第二个,比如Object、Number、Array、Function……
hasOwnProperty
1 | // Poisoning Object.prototype |
判断一个属性是定义在对象本身而不是继承自原型链,我们需要使用从 Object.prototype
继承而来的 hasOwnProperty
方法。hasOwnProperty
方法是 Javascript
中唯一一个处理对象属性而不会往上遍历原型链的。
typeof返回几种
JS中typeof返回结果有七种如下:
number (数字)
1 | typeof(10); |
boolean(布尔)
string(字符串)
object(对象)
1 | 对象,数组,null返回object |
function(函数)
1 | typeof(Array); |
undefined(未定义)
1 | typeof(undefined); |
symbol
1 | typeof Symbol() // ES6提供的新的类型 |
注意类型的话分为基本类型跟复杂类型
基本类型:
undefined
null
number
string
Boolean
Symbol
通过 Symbol 方法创建值的时候不用使用 new 操作符,原因是通过 new 实例化的结果是一个 object 对象,而不是原始类型的 symbol
1
2
3
4
5// 即使是传入相同的参数,生成的 symbol 值也是不相等的,因为 Symbol 本来就是独一无二的意思
const foo = Symbol('foo');
const bar = Symbol('foo');
console.log(foo === bar); // false
复杂类型: Object
Object.prototype.toString.call()与Object.prototype.constructor
1 | var number = 1; // [object Number] |
返回创建实例对象的 Object
构造函数的引用。注意,此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串。对原始类型来说,如1
,true
和"test"
,该值只可读。
所有对象都会从它的原型上继承一个 constructor
属性:
1 | var o = {}; |
基本数据类型和复杂数据类型区别
内存的分配不同
基本数据类型存储于栈中;
复杂数据类型存储在堆中,栈中存放指向堆中的地址。
值的结构不同
基本数据类型值指的是简单的数据段。
复杂的数据类型值是由一个或多个值构成的对象。
复制变量时不同
基本数据类型拷贝时,拷贝的是值,并且原变量和拷贝后的变量是相互独立的,这两个变量可以参与任何操作而不会相互影响;
复杂数据类型拷贝时,也就是所说的引用类型的值拷贝时,副本拷贝的是指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上引用的是同一个对象,因此,改变其中一个变量,就会影响另一个变量。这里关系到深克隆与浅克隆
传递参数时不同
道理和拷贝一样。
在向参数传递基本类型的值时,被传递的值会被赋值给一个arguments对象中的一个元素(即局部变量);
在向参数传递引用类型的值时,会把对应值的地址复制给一个局部变量,因此局部变量的变化会反映到函数的外部。