About
随着学习的深入,有时候不满足仅仅看书看教程,喜欢在一些细节上钻研,今天花时间研究了下JavaScript
的new
命令与对象深拷贝,研究所得记录如下,如果有不正确的地方还请大佬指正,谢谢。
一、new命令
之前团队leader问我new
命令的工作原理,当时我就懵了,下来花时间看了阮一峰大神的ES5
教程,终于弄懂了这个问题,现将感悟记录如下
1. new命令工作原理
当调用构造函数时使用new
命令,new
命令会做如下操作:
- 创建一个空对象,作为将要返回的对象实例。
- 将这个空对象的原型
__proto__
,指向构造函数的prototype
属性。 - 将这个空对象赋值给函数内部的
this
关键字。 - 开始执行构造函数内部的代码。
2. 使用new命令应该注意的地方
- 本来
new
命令会产生一个新的对象,并让构造函数操作这个新的对象,并返回这个新的对象,所以构造函数中不需要使用return
语句,但是如果构造函数中有return
语句,并且return
的是个对象,那么执行了构造函数就会返回return
后面的对象而非新产生的对象,如果return
后面是一个语句,那么new
会忽略它而返回新的对象。 - 如果调用构造函数时忘记使用
new
命令,那么构造函数就会对执行构造函数时的执行上下文对象进行操作。所以使用new
是必要的。
3. 如何避免忘记使用new命令
我们可以通过使用严格模式来避免犯错,但是一般情况下,我们也可以通过在声明构造函数时加入一些容错机制来避免忘记使用new
造成的不良后果。
function Constructor(param) {
if ( new.target === Constructor){ // 判断出使用了new命令,那么执行构造函数
this.name = dog
this.age = 24
} else { // 如果没有使用new命令,那么重新调用构造函数并且使用new命令
return new Constructor(param)
}
}
当然也可以判断if (this instanceOf Constructor)
。
二、深拷贝
众所周知JavaScript
中对复杂数据类型是采用引用的方式,即变量内存储的是数据的内存地址而非真正的数据,所以一旦修改了数据,所有引用该数据的变量都会受影响。
浅拷贝的意思就是只拷贝第一层数据,如果该数据还有引用型数据,那么它仍旧不是完全独立的,当其引用的数据被修改,它仍旧会受到影响,比如:
a = [1,2,3,[4,5,6]]
b = [...a] // 对a进行浅拷贝 b = [1,2,3,[4,5,6]]
b[0] = 100
console.log(a[0]) // 1 说明b数组的第一层已经脱离了a数组
b[3][0] = 100
console.log(a[3][0]) // 100 说明b数组的第二层仍未脱离a数组,表示这并非真正的拷贝
1. 深拷贝需要注意的问题
从博客上看到其他作者写的一些深拷贝的方法,一般都会有一些不完善的地方,比如:
1. 1 无法拷贝对象的构造函数的prototype上的属性
一般情况下,我们需要深拷贝对象时都是希望能够完全复制一个对象,所以无论是原型属性还是实例属性我们需要完全复制,例如:
function Person(){}
Person.prototype.name = 'bing'
man = new Person() // {}
console.log(man.name) // 'bing'
上面的代码中我们把一个属性定义到构造函数的原型上,我们通过实例能够访问到该属性,但是如果我们不拷贝该属性,那么这两个对象就不是完全一样的了。例如:
function deepCopy(obj) {
var result = Array.isArray(obj) ? [] : {}
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object' && obj[key]!==null) {
result[key] = deepCopy(obj[key]); //递归复制
} else {
result[key] = obj[key];
}
}
}
return result;
}
b = deepCopy(man)
console.log(b.name) // undefined
1.2 如果对象的某一属性是对象本身
如果对象的某一属性是对象本身,那么会造成无限循环,例如:
b = {
'name': 'bing'
}
b.self = b
c = deepCopy(b) // Uncaught RangeError: Maximum call stack size exceeded
1.3 注意某些特殊对象
我们还应该注意一些特殊的对象,比如:RegExp
和Date
a = new Regexp(/a/i)
b = deepCopy(a)
console.log(a) // /a/i
console.log(b) // /(?:)/
1.4 注意某些不可枚举的属性
我们知道使用for...in
是会跳过不可枚举的属性的,这样会导致如果某些不可枚举的属性不能被拷贝致使目标对象和源对象不一致
const a = new Object()
Object.defineProperty(a, 'name', {value: 'bing', enumerable: false})
a.age = 23
for (let i in a) { console.log(`${i}: ${a[i]}`)} // age: 23
通过上述代码可以发现属性name
没有被遍历到,所以我们需要使用Object.getOwnPropertyNames()
方法来获取全部的key
1.5 注意symbol属性
同样,使用for...in
也无法遍历symbol
属性,所以我们需要使用Object.getOwnPropertySymbols()
来获取全部的symbol
属性
const a = new Object()
a.age = 23
sym = Symbol('foo')
a[sym] = 123
for (let i in a) { console.log(`${i}: ${a[i]}`)} // age: 23
1.6 注意拷贝属性描述对象
假设源对象中某一属性是禁止配置的,或者是不可枚举的,如果我们只拷贝了属性的value
而没有拷贝其属性描述对象,那么目标对象中该属性的属性描述对象会变成一个默认的对象,从而导致目标对象与源对象不同。
const a = new Object()
Object.defineProperty(a, 'name', {value: 'bing', enumerable: false})
const b = new Object()
b.name = a.name
Object.getOwnPropertyDescriptor(a,'name')
// {value: "bing", writable: false, enumerable: false, configurable: false}
Object.getOwnPropertyDescriptor(b,'name')
// {value: "bing", writable: true, enumerable: true, configurable: true}
通过上面的代码我们可以发现通过赋值的办法,name
的属性描述对象并没有被拷贝,所以这里应该采用Object.defineProperty()
方法。
1.7 注意传入的参数不是对象类型
如果传入的不是对象这种复杂数据类型,我们应该直接返回,因为简单数据类型并非引用型。
三、较为完善的深拷贝算法
function deepCopy(obj) {
if (obj instanceof Date) { return new Date(obj) } // 解决特殊对象的拷贝
if (obj instanceof RegExp) { return new RegExp(obj)}
let result = new obj.constructor() // 解决无法拷贝原型
for (let key of Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj))) {
if (obj[key] !== obj) { // 解决无限循环
if (typeof obj[key] === 'object' && obj[key] !== null) {
result[key] = deepCopy(obj[key]); //递归复制
} else {
Object.defineProperty(result, key, Object.getOwnPropertyDescriptor(obj, key))
}
}
}
return result;
}
另外,如果我们非常了解被拷贝的对象,并且知道该对象中无特殊Unicode
码点,函数,以及正则表达式,我们也可以通过该方法:
function deepCopy (obj) {
return JSON.parse(JSON.stringify(obj))
}
结束语
本文可能会有一些不严谨的地方,因为作者本人才疏学浅,还请各位大佬批评指正,谢谢。