专栏名称: 大转转FE
定期分享一些团队对前端的想法与沉淀
目录
相关文章推荐
51好读  ›  专栏  ›  大转转FE

你做的拷贝是真的深拷贝吗

大转转FE  · 公众号  ·  · 2017-09-01 17:14

正文

背景

上周组长给看了一下校招生的一道笔试题,题目是实现一个深拷贝的函数,实现方式各有不同,但是大多数还是只实现成了浅拷贝,作为一个刚入职的小校招生,也没有多少高深莫测的技术沉淀,就拿这个小小的点作一个总结吧,毕竟再牛逼的技术还是要足够扎实的基础才能理解与运用。

标准的工科生一枚,不善文笔,有什么不对的地方还需要大佬们指出。

数据类型

为什么要作深浅拷贝呢?什么样的数据类型存在深浅拷贝的问题?这还得从数据类型说起。在js中,变量的类型主要分为 基本数据类型 (string、number、boolean、null、undefined)和 引用数据类型 (比如Array、Date、RegExp、Function),基本类型保存在 内存中,它们都是简单的数据段,大小固定,而引用类型可能是由多个值构成的对象,大小不固定,保存在 内存中。

基本类型是按值访问的,可以操作保存在变量中的实际的值,而引用类型的值是保存在内存中的对象,js不能直接操作直接访问内存中的位置,也就是不能直接操作对象的内存空间,在操作对象时,实际上操作的是对象的引用,所以,引用类型的值是按引用访问的。

当从一个变量赋值另一个变量的引用类型的值的时候,复制的副本实际上是一个指针,指向储存在堆中的同一个对象,因此改变其中一个变量就会影响另一个变量。

   var person_1 = {

  1.   name: 'max',

  2.   age: 22

   };

   var person_2 = person_1;

  1. person_2.name = 'caesar';

  2. console.log(person_1, person_2);    //{name: "caesar", age: 22}

   //{name: "caesar", age: 22}

在这里,我们只是简单的复制了一次引用类型的地址而已,因为两者引用的是同一个地址,对新变量操作后,之前的变量也就跟着变了,但是往往我们并不想这样。

Good!有这些就够了,现在我们进入正题,所以,基本类型不存在深浅拷贝的问题,我们要拷贝的类型肯定就是引用类型了,比如Array和Object。

浅拷贝

好,这个时候我想拷贝一个数组,我大脑里快速的闪现了几个原生的数组copy方法,以及我飞快写出的一个循环赋值法。

  • Array.prototype.slice

  • Array.prototype.concat

  • ES6 copyWithin()

  • function copyArr(arr) { let res = [] for (let i = 0; i < arr.length; i++) { res.push(arr[i]) } return res }

但是它们是深拷贝吗?我小试了一下。

我只改变了新数组里面对象的一个属性,为什么之前的数组里的对象也跟着变了!!

要知道为什么,还得看看实现它们的原理,于是我搜了一下v8源码(就不分析其他详情功能啦- -,我们只看关键),举个栗子: Array . prototype . slice :

ArraySliceFallback 主函数

  1. function ArraySliceFallback(start, end) {

  2.  CHECK_OBJECT_COERCIBLE(this, "Array.prototype.slice");

  3.  ...

  4.    //最终返回结果的值,可以从这里看到创建了一个新的真正的数组

  5.  var result = ArraySpeciesCreate(array, MaxSimple(end_i - start_i, 0));

  6.  if (end_i < start_i) return result;

  7.    //走这里的逻辑大概处理的数组长度相对较大,但是拷贝的数与数组的总体大小相比,处理元素的数量相对较小,但是这和我们的目的无关,不细究。

  8.  if (UseSparseVariant(array, len, IS_ARRAY(array), end_i - start_i)) {

  9.    %NormalizeElements(array);

  10.    if (IS_ARRAY(result)) %NormalizeElements(result);

  11.    //看这里看这里,处理值的地方

  12.    SparseSlice(array, start_i, end_i - start_i, len, result);

  13.  } else {

  14.      //还有这里这里

  15.    SimpleSlice(array, start_i, end_i - start_i, len, result);

  16.  }

  17.  result. length = end_i - start_i;

  18.  return result;

  19. }

slice方法的主函数。前面对start,end输入的一些判断处理就忽略了,主要看两个分支处理 SparseSlice SimpleSlice

SparseSlice

  1. function SparseSlice(array, start_i, del_count, len, deleted_elements) {

  2.   var indices = %GetArrayKeys(array, start_i + del_count);

  3.  if (IS_NUMBER(indices)) {

  4.    var limit = indices;

  5.    //循环

  6.    for (var i = start_i; i < limit; ++i ) {

  7.      var current = array[i];

  8.    //赋值

  9.      if (!IS_UNDEFINED(current) || i in array) {

  10.        %CreateDataProperty(deleted_elements, i - start_i, current);

  11.      }

  12.     }

  13.  } else {

  14.    var length = indices.length;

  15.    //循环

  16.    for (var k = 0; k < length; ++k) {

  17.       var key = indices[k];

  18.      if (key >= start_i) {

  19.        var current = array[key];

  20.        //赋值

  21.        if (!IS_UNDEFINED(current) || key in array) {

  22.           %CreateDataProperty(deleted_elements, key - start_i, current);

  23.        }

  24.      }

  25.    }

  26.  }

  27. }

可以看到该函数两个分支都只是循环了数组,然后进行直接的赋值操作。

SimpleSlice

  1. function SimpleSlice(array, start_i, del_count, len, deleted_elements) {

  2.    //直接循环

  3.  for (var i = 0; i < del_count; i++) {

  4.    var index = start_i + i;

  5.    if (index in array) {

  6.    //赋值

  7.      var current = array[index];

  8.      %CreateDataProperty(deleted_elements, i, current);

  9.    }

  10.  }

  11. }

所以slice方法并没有对循环的每一项是基本类型还是引用类型作区分,并针对引用元素对其内部属性再作判断,所以,Array.prototype.slice是浅拷贝无疑。后面2个原生方法就不拿出来看了,其实也类似,感兴趣和不信的小伙伴可以点击这个链接去看看(https://github.com/v8/v8/blob/master/src/js/array.js)



小结

我们可以用 = 直接复制基本类型,复制引用类型时,循环遍历对象,对每个属性使用 = 完成复制,所以以上的这些拷贝都只是复制了第一层的属性,这就是浅拷贝,虽然我们确实得到了一个新的与复制源独立的对象,但是其内部包含的引用属性值仍然指向同一个地址(也就是我们只复制了地址)。

假如数组中保存的对象的某个属性还是一个对象呢?(然后一层一层这样下去呢?虽然肯定没人设计这么深的数据结构),但是为了解决这个问题,就有了深拷贝的实现方式:对属性中所有引用类型的值,一直遍历到基本类型为止,要实现深拷贝,也能想到用 递归 了。

深拷贝

所以深拷贝并不是简单的复制引用,而是在堆中重新分配内存,并且把源对象实例的所有属性都新建复制,以保证复制的对象的引用不指向任何原有对象上或其属性内的任何对象,复制后的对象与原来的对象是完全隔离的。

其他类库的深拷贝实现(jquery)

我们先来看看jquery是怎么实现深拷贝的。jquery是使用extend函数来实现对象的深拷贝。

  1. jQuery.extend = jQuery.fn.extend = function() {

  2.    ...

  3.     //前面都不重要,我们直接看拷贝的核心代码

  4.    for ( ; i < length; i++ ) {

  5.       // 只处理非空参数

  6.       if ( (options = arguments[ i ]) != null ) {

  7.           for ( name in options ) { //遍历源对象的属性名

  8.              src = target[ name ]; //获取目标对象上,属性名对应的属性

  9.              copy = options[ name ]; //获取源对象上,属性名对应的属性

  10.              // jq这里做的比较好,为了避免深度遍历时死循环,jq不会覆盖目标对象的同名属性,也就是避免对象环的问题(对象的某个属性值是对象本身)

  11.               if ( target === copy ) {

  12.                  continue;

  13.              }

  14.              // 深度拷贝且值是普通象或数组,则递归,也就是说,jquery对{}或者new Object创建的对象以及数组作了递归的深拷贝处理

  15.              if ( deep && copy && ( jQuery.isPlainObject (copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {

  16.                  // 如果copy是数组

  17.                  if ( copyIsArray ) {

  18.                     copyIsArray = false;

  19.                     // clone为src的修正值

  20.                     clone = src && jQuery.isArray(src) ? src : [];

  21.                  // 如果copy的是对象

  22.                  } else {

  23.                     // clone为src的修正值

  24.                     clone = src && jQuery.isPlainObject(src) ? src : {};

  25.                  }

  26.                  // 递归调用jQuery.extend

  27.                  target[ name ] = jQuery.extend( deep, clone, copy );

  28.              // 否则,如果不是纯对象或者数组jquery最后会直接把源对象的属性,赋给源对象(浅拷贝)

  29.              } else if ( copy !== undefined ) {

  30.                  target







请到「今天看啥」查看全文