• JavaScript 引用数据类型


    1. 问题描述

    今天在写一个代码题时候, 有一个BUG 导致自己停滞好久, 该BUG 可以描述为如下代码:
    PS: 由于原题是算法题, 为了叙述方便以及展示重点考虑, 这里只复现BUG, 不提供原场景.

    const log = console.log.bind(console)
    
    let obj = {}
    let list = [1, 2, 3]
    
    obj.array_1 = list
    obj.array_2 = list
    obj.array_1.push(4)
    
    log(obj)
    // { 
    // array_1: [ 1, 2, 3, 4 ], 
    // array_2: [ 1, 2, 3, 4 ] 
    // }
    

    场景说明:
    在第8行代码: obj.array_1.push(4) 中, 目的是将obj.array_1 值改变, 而obj.array_2 值期望是不要改变, 但是结果事与愿违, obj.array_1, obj.array_2 的值均发生了改变.

    2. 原因分析

    要理解上面代码, 我们先来了解一下JavaScript 的数据类型, JavaScript 的数据类型可分为:

    1. 基本数据类型: Undefined, Null, Number, String, Boolean
    2. 引用数据类型: Object(array, object, set 等数据类型)

    JavaScript 对于数据类型值操作的方式如下:

    1. 基本数据类型: 按值访问, 因此可以操作保存在变量中的实际值;
    2. 引用数据类型: 引用类型的值是保存在内存中的对象, 和其他语言不通, JavaScript 不允许直接访问内存中的位置, 也就是说不能直接操作对象的内存空间, 我们在操作对象时实际上是操作对象的引用而不是实际的对象, 为此 引用类型的值是按引用访问的.

    简而言之, 当一个变量 a 是引用数据类型时, a 保存的是一个地址, 我们操作 a 来达到改变地址值的目的; 当变量 b 和 a 指向同一个地址时, 他们的操作会互相影响;

    在上面的例子中, obj.array_1 和 obj.array_2 指向的是同一个引用数据类型 list, 因此它们之间的操作会互相影响, 如果要消除这种影响, 我们将obj.array_1 和 obj.array_2 指向不同的引用数据类型即可

    const log = console.log.bind(console)
    
    let obj = {}
    let list = [1, 2, 3]
    
    obj.array_1 = list
    // 通过Array.prototype.slice() 方法完成数组的拷贝
    obj.array_2 = list.slice()
    obj.array_1.push(4)
    
    log(obj)
    // {
    // array_1: [ 1, 2, 3, 4 ],
    // array_2: [ 1, 2, 3 ]
    // }
    
    log(typeof 'here')
    
    

    3. React 中的引用数据类型

    在React 中, 引用数据也具有这种性质(因为React 本身支持JavaScript, JSX 也是JavaScript 语法的升级), 如下面例子:

    import React from "react";
    
    const log = console.log.bind(console)
    
    class ReactObjectAdress extends React.Component{
        constructor(props) {
            super(props)
            this.state = {
                arrayOne: [],
                arrayTwo: []
            }
        }
    
        componentDidMount() {
            let array = [1, 2, 3, 4]
            this.setState({
                arrayOne: array,
                arrayTwo: array
            })
        }
    
        onClickEvent = () => {
            let array = this.state.arrayOne
            array.push(5)
            this.setState({
                arrayOne: array
            })
        }
    
        render() {
            let { arrayOne, arrayTwo } = this.state
            log('arrayOne: ', arrayOne)
            log('arrayTwo: ', arrayTwo)
            return (
                <>
                    <button onClick={this.onClickEvent}>click Me!</button>
                </>
            )
        }
    }
    
    export default ReactObjectAdress
    

    描述:
    当我们点击 'click Me!' 按钮触发 onClickEvent 实践时, 可以在控制台看到 this.state.arrayOne 和 this.state.arrayTwo 这两个数组发生了改变: 数组被添加了一个新数据 5

    4. 业务场景

    根据上面React 框架的例子, 我们来思考这样一个业务场景:

    现在需要在ReactObjectAdress 组件中添加两个组件: (array 表示在componentDidMount 中的值, 可以理解出初始数据)

    1. 组件ShowArray, 用于展示数组 array
    2. 组件UpdateArray, 用于更新数组 array, 且更新需要向后端确认

    根据上面的场景, 我们可能会写如下的代码:

    import React from "react";
    
    const log = console.log.bind(console)
    
    class ReactObjectAdress extends React.Component{
        constructor(props) {
            super(props)
            this.state = {
                arrayOne: [],
                arrayTwo: []
            }
        }
    
        componentDidMount() {
            let array = [1, 2, 3, 4]
            this.setState({
                arrayOne: array,
                arrayTwo: array
            })
        }
    
        onClickEvent = () => {
            let array = this.state.arrayOne
            array.push(5)
            this.setState({
                arrayOne: array
            })
        }
    
        render() {
            let { arrayOne, arrayTwo } = this.state
            // log('arrayOne: ', arrayOne)
            // log('arrayTwo: ', arrayTwo)
            return (
                <>
                    <button onClick={this.onClickEvent}>click Me!</button>
                    <ShowArray array={arrayOne} />
                    <UpdateArray array={arrayTwo} />
                </>
            )
        }
    }
    
    export default ReactObjectAdress
    

    描述:
    上面代码中, ShowArray UpdateArray 的参数分别为arrayOne, arrayTwo, 这样会存在一个问题:
    arrayOne, arrayTwo 指向的是同一个地址, 如果我们在UpdateArray 中操作this.props.array 参数, 导致的结果就是arrayOne, arrayTwo 这两个数会马上改变; ShowArray, UpdateArray这两个组件会立即更新显示!

    但正确的逻辑应该是先等到UpdateArray 和后端确认之后, ShowArray 组件才完成更新, 因此这种场景下: 显示的数据和更改的数据不应该使用同一个地址存储, 上面的代码需要进行如下的变更:

    componentDidMount() {
        let array = [1, 2, 3, 4]
        this.setState({
            arrayOne: array,
            arrayTwo: array.slice()
        })
    }
    

    5. 参考资料

    1. JavaScript 高级程序设计第4章 - 变量、作用域和内存问题(第三版)
  • 相关阅读:
    【转载】Unity 合理安排增量更新(热更新)
    COCOS2D 释放资源的最佳时机
    【转载】利用Unity自带的合图切割功能将合图切割成子图
    用GL画出人物的移动路径
    使用行为树(Behavior Tree)实现游戏AI
    C#学习笔记
    题目:给定一数组 例如:a = [1,2,3,5,2,1] 现用户提供一个数字 请返回用户所提供的数字的所有下标
    算法: 归并排序
    题目:给定两个有序数组,对其进行合并
    数据结构 顺序表实现优先队列 回顾练习
  • 原文地址:https://www.cnblogs.com/oulae/p/12995944.html
Copyright © 2020-2023  润新知