小感在前

React学习与开发过程要经历一个相当长的准备阶段,此前看阮一峰老师的文章中,他就特别提到这一点。但是,由于React框架或者说是一种优秀的前端架构实在太诱人,所以,掌握这项技术所涉及的技术栈过程中经历的任何苦恼都是值得的。在还没有全面掌握这项技术的不断探索过程中,如果包括后端的一部分在内(例如Node.js和Express等,还有一些数据库及API知识),至少要有几十部分,仅限于核心的算起来也有十多项吧。
作为一个基础部分,ES6势在必学。此前,我曾得意于自己已经掌握了一定程度的JS知识,但是在匆读阮老师的《ES6标准入门》一书后才感觉自己落后许多了。同时,在阅读本书的过程中,也发现:这本书或者整个ES6,的确是为着眼于将来的“一统前后端”的大项目作准备的,因此,重复地专门学习这个语言是没有必要的。这是一本案头书,需要什么时再读什么更合适。
本文罗列的就是在本人在学习Redux过程中在遭遇到JS的深浅拷贝过程中总结的相关内容。太匆忙与粗略,仅供同学们参考。

JavaScript 中变量的赋值

结论:JavaScript中变量的赋值分为「传值」与「传址」。

基本数据类型的赋值,就是「传值」;而引用类型变量赋值,实际上是「传址」。
基本数据类型变量的赋值、比较,只是值的赋值和比较,也即栈内存中的数据的拷贝和比较,参见如下代码:

var num1 = 123;
var num2 = 123;
var num3 = num1;
num1 === num2; // true
num1 === num3; // true
num1 = 456;
num1 === num2; // false
num1 === num3; // false

引用数据类型变量的赋值、比较,只是存于栈内存中的堆内存地址的拷贝、比较,参加如下代码:

var arr1 = [1, 2, 3];
var arr2 = [1, 2, 3];
var arr3 = arr1;
arr1 === arr2; // false
arr1 === arr3; // true
arr1 = [1, 2, 3];
arr1 === arr2; // false
arr1 === arr3; // false

JavaScript 中变量的拷贝

JavaScript中的拷贝区分为「浅拷贝」与「深拷贝」。

浅拷贝

浅拷贝只会将对象的各个属性进行依次复制,并不会进行递归复制,也就是说只会进行赋值目标对象的第一层属性。

对于目标对象第一层为基本数据类型的数据,就是直接赋值,即「传值」;而对于目标对象第一层为引用数据类型的数据,就是直接赋存于栈内存中的堆内存地址,即「传址」。

深拷贝

深拷贝不同于浅拷贝,它不但拷贝目标对象的第一层属性,而且还递归拷贝目标对象的所有属性。

一般来说,在JavaScript中考虑复合类型的深层复制的时候,往往就是指对于 Date 、Object 与 Array 这三个复合类型的处理。我们能想到的最常用的方法就是先创建一个空的新对象,然后递归遍历旧对象,直到发现基础类型的子节点才赋予到新对象对应的位置。

不过这种方法会存在一个问题,就是 JavaScript 中存在着神奇的原型机制,并且这个原型会在遍历的时候出现,然后需要考虑原型应不应该被赋予给新对象。那么在遍历的过程中,我们可以考虑使用 hasOwnProperty 方法来判断是否过滤掉那些继承自原型链上的属性。

Object.assign

Object.assign 方法可以把 任意多个的源对象所拥有的自身可枚举属性 拷贝给目标对象,然后返回目标对象。
注意:

对于访问器属性,该方法会执行那个访问器属性的 getter 函数,然后把得到的值拷贝给目标对象,如果你想拷贝访问器属性本身,请使用 Object.getOwnPropertyDescriptor() 和 Object.defineProperties() 方法;
字符串类型和 symbol 类型的属性都会被拷贝;
在属性拷贝过程中可能会产生异常,比如目标对象的某个只读属性和源对象的某个属性同名,这时该方法会抛出一个 TypeError 异常,拷贝过程中断,已经拷贝成功的属性不会受到影响,还未拷贝的属性将不会再被拷贝;
该方法会跳过那些值为 null 或 undefined 的源对象;

利用 JSON 进行忽略原型链的深拷贝

var dest = JSON.parse(JSON.stringify(target));

同样的它也有缺点:
该方法会忽略掉值为 undefined 的属性以及函数表达式,但不会忽略值为 null 的属性。

借助于Lodash 的 merge 方法

在Redux开发中的一个应用是借助于Lodash 的 merge 方法可以实现深拷贝,达到管理范式化数据的目的,示例代码如下:

import merge from "lodash/object/merge";

function commentsById(state = {}, action) {
    switch(action.type) {
        default : {
           if(action.entities && action.entities.comments) {
               return merge({}, state, action.entities.comments.byId);
           }
           return state;
        }
    }
}

存在大量深拷贝需求时借助immutable库

实际上,即使我们知道了如何在各种情况下进行深拷贝,我们也仍然面临一些问题: 深拷贝实际上是很消耗性能的。(我们可能只是希望改变新数组里的其中一个元素的时候不影响原数组,但却被迫要把整个原数组都拷贝一遍,这不是一种浪费吗?)所以,当你的项目里有大量深拷贝需求的时候,性能就可能形成了一个制约的瓶颈了。

immutable的作用是通过immutable引入的一套API,实现:

1.在改变新的数组(对象)的时候,不改变原数组(对象)
2.在大量深拷贝操作中显著地减少性能消耗

参考代码:
const { Map } = require('immutable')
const map1 = Map({ a: 1, b: 2, c: 3 })
const map2 = map1.set('b', 50)
map1.get('b') // 2
map2.get('b') // 50

尽量保持数据设计的扁平化与科学设计Reducer

在React+Redux开发中,经常要遇到不可变数据的更新问题。复杂情形中,往往需要复制嵌套数据的所有层级。但不幸的是,正确地使用不变的更新去深度嵌套状态的过程很容易变得冗长难读。 更新 ate.first.second[someId].fourth 的示例(http://www.redux.org.cn/docs/recipes/reducers/ImmutableUpdatePatterns.html)大概如下所示

function updateVeryNestedField(state, action) {
    return {
        ....state,
        first : {
            ...state.first,
            second : {
                ...state.first.second,
                [action.someId] : {
                    ...state.first.second[action.someId],
                    fourth : action.someValue
                }
            }
        }
    }
}

显然,每一层嵌套使得阅读更加困难,并给了更多犯错的机会。这是其中一个原因,鼓励你保持状态扁平,尽可能构建小巧而灵活的Reducer。

引用

1,https://juejin.im/post/5acc7e606fb9a028c67609f7
2,https://zhuanlan.zhihu.com/p/28508795
3,http://www.zsoltnagy.eu/cloning-objects-in-javascript/
4,http://www.cnblogs.com/penghuwan/p/7359026.html
5,https://blog.csdn.net/github_38524608/article/details/78812761
6,https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Spread_syntax
7,http://www.redux.org.cn/docs/recipes/reducers/UpdatingNormalizedData.html