Vue踩坑:父子组件传值问题

在用Vue的父子组件传值时遇到一个冷门的问题,子组件改变值后父组件的值也随之改变了,特此记录下原因和解决方式。
再系统梳理下JavaScript的深拷贝与浅拷贝相关知识点。

1. 问题描述

  父组件传值给子组件,子组件改变传过来的值后,父组件的值也会跟着改变。
  这个问题比较冷门,平时如果对组件通信使用得比较简单,一般不会遇到。

2. 原因剖析

  ● 核心:双向绑定
    父子组件传值的时候涉及双向绑定,当传值为 object 类型时,传值之后数据源会被改变。
  ● 深拷贝与浅拷贝
    下文详细讲。

3. 解决方案

  我目前采用的解决办法是:
  传值的时候不要直接传数据源,而是通过拷贝或者定义新变量等方式传值。

  简单处理就 JSON.parse(JSON.stringify(obj)),但是这种简单粗暴的方法有其局限性。当值为 undefinedfunctionsymbol 会在转换过程中被忽略。所以,对象值有这三种的话用这种方法会导致属性丢失。
  剩下的就是自写深拷贝的工具函数,或者直接借助第三方的库函数,下面展开讲。

4. 深拷贝和浅拷贝

  JavaScript中的浅拷贝与深拷贝,只是针对复杂数据类型(ObjectArray)的复制问题。浅拷贝与深拷贝都可以实现在已有对象上再生出一份的作用。但是对象的实例是存储在堆内存中然后通过一个引用值去操作对象,由此拷贝的时候就存在两种情况了:拷贝引用拷贝实例,这也是浅拷贝和深拷贝的区别。
  下图为JavaScript复杂数据类型的浅拷贝示意图:
javascript浅拷贝

  ● 浅拷贝
    浅拷贝是拷贝引用,拷贝后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响。
    值得注意的是:Object.assgin() 是浅拷贝,它只能深拷贝第一层,深层的还是浅拷贝。因为 Object.assign() 拷贝的是属性值。假如源对象的属性值是一个对象的引用,那么它也只指向那个引用。(摘选自MDN)
    MDN讲述 assign 的时候,就有一个典型的例子,这里是文章链接

    下面列举第一类浅拷贝 - 拷贝原对象的引用

/**
 * 对象的浅拷贝
 */
var obj1 = {
  name:'winyuan',
  age: 22
}
var obj2 = obj1;
obj2['job'] = 'coder';
console.log(obj1); //Object {name: "winyuan", age: 22, job: "coder"}
console.log(obj2); //Object {name: "winyuan", age: 0, job: "coder"}


/* ------------------------- 华丽的分割线 ------------------------- */


/**
 * 数组的浅拷贝
 */
var arr1 = [1, 2, 3, '4'];
var arr2 = arr1;
arr2[1] = "test"; 
console.log(arr1); // [1, "test", 3, "4"]
console.log(arr2); // [1, "test", 3, "4"]

    接下来看第二类浅拷贝 - 源对象拷贝实例,其属性对象拷贝引用
    这种情况,外层源对象是拷贝实例,如果其属性元素为复杂数据类型(ObjectArray)时,内层元素拷贝引用。
    对源对象直接操作,不影响另外一个对象,但是对其属性操作时候,会改变另外一个对象的属性的值。

/**
 * 对象的浅拷贝
 * jQuery的 $.extend(a,b) 或 $.extend({},a,b)
 */
var obj1 = {
  name:'winyuan',
  age: 22,
  social: {
    blog: 'www.wenyuanblog.com'
  },
  skills: ['js', 'html', 'css', 'python']
}
var obj2 = $.extend({},obj1);
console.log(obj1 === obj2) // 输出false,说明外层数组拷贝的是实例
console.log(obj1.social === obj2.social) // 输出true,说明对于Object类型的属性是拷贝引用
console.log(obj1.skills === obj2.skills) // 输出true,说明对于Array类型的属性是拷贝引用


/**
 * 对象的浅拷贝
 * ES6的 Object.assign() 和 对象扩展运算符...
 */
var obj1 = {
  name:'winyuan',
  age: 22,
  social: {
    blog: 'www.wenyuanblog.com'
  },
  skills: ['js', 'html', 'css', 'python']
}
var obj2 = Object.assign({},obj1);
console.log(obj1 === obj2) // 输出false,说明外层数组拷贝的是实例
console.log(obj1.social === obj2.social) // 输出true,说明对于Object类型的属性是拷贝引用
console.log(obj1.skills === obj2.skills) // 输出true,说明对于Array类型的属性是拷贝引用
var obj3 = {...obj1};
console.log(obj1 === obj3) // 输出false,说明外层数组拷贝的是实例
console.log(obj1.skills === obj3.skills) // 输出true,说明对于Array类型的属性是拷贝引用
console.log(obj1.skills === obj3.skills) // 输出true,说明对于Array类型的属性是拷贝引用


/* ------------------------- 华丽的分割线 ------------------------- */


/**
 * 数组的浅拷贝
 * Array.prototype.slice()
 */
var arr1 = [{name: "winyuan"}, {name: "Evan You"}];
var arr2 = arr1.slice(0);
console.log(arr1 === arr2); // 输出false,说明外层数组拷贝的是实例
console.log(arr1[0] === arr2[0]); // 输出true,说明其元素拷贝的是引用


/**
 * 数组的浅拷贝
 * Array.prototype.concat()
 */
var arr1 = [{name: "winyuan"}, {name: "Evan You"}];
var arr2 = arr1.concat();
console.log(arr1 === arr2); // 输出false,说明外层数组拷贝的是实例
console.log(arr1[0] === arr2[0]); // 输出true,说明其元素拷贝的是引用


/**
 * 数组的浅拷贝
 * ES6的 Object.assign() 和 对象扩展运算符...
 * 由于数组是特殊的对象,所以ES6中的这种方式也可以用于数组
 */
var arr1 = [{name: "winyuan"}, {name: "Evan You"}];
var arr2 = Object.assign([],arr1)
var arr3 = { ...arr1 };
console.log(arr1 === arr2); // 输出false,说明外层数组拷贝的是实例
console.log(arr1 === arr3); // 输出false,说明外层数组拷贝的是实例
console.log(arr1[0] === arr2[0]); // 输出true,说明其元素拷贝的是引用
console.log(arr1[0] === arr3[0]); // 输出true,说明其元素拷贝的是引用

  ● 深拷贝
    在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。
    下面列举一些深拷贝的例子:

/**
 * 对象的深拷贝
 * JSON.stringify()和JSON.parse()
 * 这种深拷贝最简单,但有其局限性,上文已经提到过了
 */
var obj1 = {
  name:'winyuan',
  age: 22,
  social: {
    blog: 'www.wenyuanblog.com'
  },
  skills: ['js', 'html', 'css', 'python']
}
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1 === obj2) // 输出false,说明外层数组拷贝的是实例
console.log(obj1.social === obj2.social) // 输出false,说明对于Object类型的属性也是拷贝实例
console.log(obj1.skills === obj2.skills) // 输出false,说明对于Array类型的属性也是拷贝实例


/**
 * 对象的深拷贝
 * jQuery的 $.extend(true,a,b)
 */
var obj1 = {
  name:'winyuan',
  age: 22,
  social: {
    blog: 'www.wenyuanblog.com'
  },
  skills: ['js', 'html', 'css', 'python']
}
var obj2 = $.extend(true,obj1);
console.log(obj1 === obj2) // 输出false,说明外层数组拷贝的是实例
console.log(obj1.social === obj2.social) // 输出false,说明对于Object类型的属性也是拷贝实例
console.log(obj1.skills === obj2.skills) // 输出false,说明对于Array类型的属性也是拷贝实例


/**
 * 对象的深拷贝
 * 也可以自己写一个函数实现,用递归+判断,注意别进入死循环就好
 * 这里不举例了,以前我整理过一篇常用工具类函数的博客,里面包含了深拷贝函数
 */


/**
 * 对象的深拷贝
 * lodash的_.cloneDeep
 */
var obj1 = {
  name:'winyuan',
  age: 22,
  social: {
    blog: 'www.wenyuanblog.com'
  },
  skills: ['js', 'html', 'css', 'python']
}
var obj2 = _.cloneDeep(obj1);
console.log(obj1 === obj2) // 输出false,说明外层数组拷贝的是实例
console.log(obj1.social === obj2.social) // 输出false,说明对于Object类型的属性也是拷贝实例
console.log(obj1.skills === obj2.skills) // 输出false,说明对于Array类型的属性也是拷贝实例

  以上就是JavaScript中的浅拷贝与深拷贝的知识点,以代码的形式记录下来,方便回顾。


  目录