Js深浅拷贝原理

更新日期: 2019-09-13 阅读: 2.8k 标签: 原理

如果拷贝的值是基本数据类型,拷贝的是基本类型的值。如果是引用类型拷贝的是内存地址。浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址。

也就是说:只能保证第一层数据为基本数据类型时,不会随原数据改变。原数据中包含子对象时,随原数据变化。


1.1 浅拷贝的使用场景

1. Object.assign()

该方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

let a = {
  name: 'owenli',
  book: {
    title:'前端进阶之路',
    price: '66'
  }
}
let b = Object.assign({}, a)
console.log('b原始值')
console.log(b)
a.name = '啦啦啦'
a.book.price = '88'
console.log('修改 name 和 price 后')
console.log(a)
console.log(b) // b 中的book.price也是 88。

注意:Object.assign() 不是深拷贝。

2. 展开运算符

let c = {...a}
//...
console.log('展开运算符...')
console.log(c)
console.log(a)

展开运算符的效果和 Object.assign 相同。

另外,slice 、concat 等也是浅拷贝。


二、深拷贝

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

2.1 使用场景

1. JSON.parse(JSON.stringify(object))

let a = {
  name: 'owenli',
  book: {
    title: '啦啦啦',
    price: '44'
  }
}
let b = JSON.parse(JSON.stringify(a)) // 深拷贝
console.log(a)

a.name = '啦啦啦'
a.book.price = '66'

console.log(a)
console.log(b)
// a 和 b 之间互不影响。

注意:undefined、Symbol 和 函数三种情况会直接忽略。不能处理 new Date() 和正则表达式。


三、浅拷贝 Object.assigin 的原理及实现

Object.assign(target, ...sources)

其中 target 是目标对象,sources 是源对象,可以有多个,返回修改后的目标对象 target。

如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后来的源对象的属性将类似地覆盖早先的属性。

3.1 Object.assign 模拟实现

// 第一步判断 Object 是否支持 assign2 
if (typeof Object.assign2 != 'function') {
  // 第二步:不存在assign2, 使用Object.definePerperty 创建并将函数绑定到Object上。
  Object.defineProperty(Object, 'assign2', {
    value: function (target) {
      'use strict' // 在严格模式下对不可写的属性进行修改时会报错。默认情况下静默失败。
      // 第三步:判断参数是否正确,target 不可以为空可以是 {}。
      if (target == null) {
        throw new TypeError('Cannot convert undefined or null to object')
      }
      // 第四步:使用Object() 转为对象,保存为 to,最后返回 to。将原始类型包装成对象。
      var to = Object(target)
      for (var index = 1; index < arguments.length; index++){
        var nextSource = arguments[index]
        if (nextSource != null) {
          // 第五步:for...in遍历出所有可枚举的自有属性。并赋值个新的变量
          for (var nextKey in nextSource) {
            // 可访问性:hasOwnProperty 判断对象 nextSoruce 中是否存在 nextKey 属性。
            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
              // 赋值给 对象 to。
              to[nextKey] = nextSource[nextKey]
            }
          }
        }
      }
      return to
    },
    // enumerable: false, // 不可枚举,原生Object属性是不可以枚举的。默认为 false。
    writable: true,
    configurable: true
  })
}

let a = {
  name: '1111',
  age: 19
}
let b = {
  name: 'owenli',
  book: {
    title: 'lalalal',
    price: 20
  }
}
let c = Object.assign2(a, b)
console.log(c)

console.log(a === c)

3.2 可枚举性

原生的Object中的属性是不可以枚举的。 Object.assign 是否可以被枚举。

Object.getOwnPropertyDescriptor(Object, 'assign')
// {value: ƒ, writable: true, enumerable: false, configurable: true} enumerable:false 说明是不可以被枚举。
Object.propertyIsEnumerable('assign')
// false

直接向 Object 上挂载属性是可以枚举的。

Object.a = function () {
  //...
}
Object.getOwnPropertyDescriptor(Object, 'a')
// {value: ƒ, writable: true, enumerable: true, configurable: true} 
// 直接挂载 a ,是可以枚举的。

所以要想实现 Object.assign 就需要使用 Object.defineProperty,并设置 writable: true, enumerable: false, configurable: true。默认都是 false。

Object.defineProperty(Object, 'b', {
	value: function() {
        console.log('b')
    }
})
Object.getOwnPropertyDescriptor(Object, 'b')
// {value: ƒ, writable: false, enumerable: false, configurable: false}

综上: Object.assign2 实现是需要设置 writable: true, configurable: true 。因为默认是enumerable 是 false。

3.3 参数判断

undefined == null // true。

在使用判断的时候直接用 target == null 。

3.4 原始类型包装成对象

var v1 = 'abc'
var v2 = true
var v3 = 10
var v4 = Symbol('foo')
// 原始类型会被包装,null 和 undefined 会被忽略。
var obj = Object.assign({}, v1, null, v2, undefined, v3, v4)

console.log(obj)

// {0: "a", 1: "b", 2: "c"}

v2, v3, v4 被忽略,原因是自身没有可枚举的属性。

通过 Object.keys() 和 Object.getOwnPropertyNames() 可以查看枚举属性和所有的属性。

// 通过 Object.keys() 测试,是否含有可枚举属性。
console.log(Object.keys(v1)) // ["0", "1", "2"]
console.log(Object.keys(v2)) // []
console.log(Object.keys(v3)) // []

// 获取所有属性 Object.getOwnPropertyNames(),无论是不是枚举类型。

console.log(Object.getOwnPropertyNames(v1)) // ["0", "1", "2", "length"]
console.log(Object.getOwnPropertyNames(v2)) // []

那没 Object.assign 是如何实现:

var a = 'abc'
var b = {
  v1: 'def',
  v2: true, 
  v3: 10,
  v4: Symbol('foot'),
  v5: null,
  v6: undefined
}
var objc = Object.assign(a, b)
console.log(objc) // k可以获取到 v2, v3, v4 .. 的值。

原因:因为 undefined , true 不是作为对象,而是作为对象 b 的属性值,对象 b 是可以枚举的。

console.log(Object.keys(b)) // 可枚举

这里其实又可以看出一个问题来,那就是目标对象是原始类型,会包装成对象,对应上面的代码就是目标对象 a 会被包装成 [String: ‘abc’],那模拟实现时应该如何处理呢?很简单,使用 Object(..) 就可以了。

另外,在严格模式下向不可修改的对象属性赋值会报错。默认情况静默失败。

3.5 存在性

如何在不访问对象属性的情况下判断属性是否存在。

var obj = {
  a: 1
}
// newObj 是关联到 obj 的对象。
var newObj = Object.create(obj)
newObj.b = 10

console.log(newObj)

// in 操作符会检查属性是否在对象及其 `[[Prototype]]`原型链中。
console.log('a' in newObj) // true
console.log('b' in newObj) // true
// 只检测是否在 newObj 中,不检查原型链。
console.log(newObj.hasOwnProperty('a')) // false
console.log(newObj.hasOwnProperty('b')) // true

两种方法的区别:

  1. in 操作符检查 对象 和 原型链。
  2. hasOwnProperty 值检查 对象,不检查原型链。

模拟实现 Object.assign 只需检测对象中属性,不需要检查原型链。

var obj1 = Object.create(null)
obj1.b = 2
// 问题: b 没有连接到prototype上
console.log(obj1.hasOwnProperty('b')) // false
console.log(Object.prototype.hasOwnProperty.call(obj1, 'b')) // true

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/5902

相关推荐

React Native之原理浅析

讲React Native之前,了解JavaScriptCore会有帮助,也是必要的。React Native的核心驱动力就来自于JS Engine. 你写的所有JS和JSX代码都会被JS Engine来执行, 没有JS Engine的参与,你是无法享受ReactJS给原生应用开发带来的便利的

连v-show都不会你还敢说熟悉 Vue 原理?

Vue 作为最主流的前端框架,中文资料齐全、入门简单、生态活跃,可以说是工作中最常用的,如今对 Vue 原理的熟悉基本上是简历的标配了。之前参与了部分 2019 校园招聘的面试工作,发现很多简历上都写了:

Angular ZoneJS 原理

如果你阅读过关于Angular 2变化检测的资料,那么你很可能听说过zone。Zone是一个从Dart中引入的特性并被Angular 2内部用来判断是否应该触发变化检测

js中flat方法的实现原理

Array.prototype.flat()在Array的显示原型下有一个flat方法,可以将多维数组,降维,传的参数是多少就降多少维,自定义flat的步骤1、第一步是类型判断,需要判断当前调用方法的this是否为一个数组,若不是数组则返回undefined

JavaScript 中的函数式编程原理

做了一些研究,我发现了函数式编程概念,如不变性和纯函数。 这些概念使你能够构建无副作用的功能,而函数式编程的一些优点,也使得系统变得更加容易维护。我将通过 JavaScript 中的大量代码示例向您详细介绍函数式编程和一些重要概念。

理解Vue的Watch原理

watch 是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用。在面试时,也是必问知识点,一般会用作和 computed 进行比较。

写一个简单的vue-router来剖析原理

随着前端业务的发展, 我们一般在写一个较为大型的vue项目时候,会使用到vue-router,来根据指定的url或者hash来进行内容的分发,可以达到不像服务端发送请求,就完成页面内容的切换,能够减少像服务器发送的请求

CSS定位之BFC背后的神奇原理

BFC已经是一个耳听熟闻的词语了,网上有许多关于 BFC 的文章,介绍了如何触发 BFC 以及 BFC 的一些用处(如清浮动,防止 margin 重叠等)。BFC直译为\"块级格式化上下文\"。它是一个独立的渲染区域,只有Block-level box参与

前端手写代码原理实现

现在的前端门槛越来越高,不再是只会写写页面那么简单。模块化、自动化、跨端开发等逐渐成为要求,但是这些都需要建立在我们牢固的基础之上。不管框架和模式怎么变,把基础原理打牢才能快速适应市场的变化。下面介绍一些常用的源码实现

关于vue过滤器的原理解析

Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!