Javascript中的原始值与引用值

498 阅读4分钟

原始值

最简单的数据,即Javascript基础数据类型中的任意一种

保存原始值的变量是 **按值** 访问的,因为我们操作的就是存储在变量中的实际值

原始值不能有属性,尽管尝试给原始值添加属性不会报错
```
let name = 'echo';
name.age = 12;
console.log(name.age); // undefined
```
如果使用`new`关键字初始化原始类型的变量,则js会创建一个Object类型的实例,但其行为类似原始值
```
let name = new String('echo');
name.age = 12;
console.log(name.age); // 26
console.log(typeof name); // object
```

引用值

多个值构成的对象(Object)

引用值是保存在内存中的对象,JS不允许直接访问内存位置,因此在操作对象时,实际上操作的是该对象的引用(指针),即 保存引用值的变量是 按引用 访问的

基本引用类型

Date、RegExp、原始值包装类型、单例内置对象

集合引用类型

Object、Array、定型数组、Map、WeakMap、Set、WeakSet、迭代与扩展

原始值 VS 引用值 ——> 复制 ——> 浅拷贝&深拷贝

原始值: 通过变量把一个原始值赋值给另一个变量时,原始值会被复制到新变量的位置,这两个变量互相独立

引用值: 而复制引用值时,复制的只是当前引用值的指针,该指针指向堆内存中实际的对象。此时修改其中一个变量会影响另外一个。也就是所谓的浅复制。而在实际开发中,有时需要复制一份完全独立的对象数据(即深拷贝)。可实现的方法有:

  • 使用js工具库的现成方法,比如loadsh_.cloneDeep(val)

  • 递归拷贝,即递归遍历对象,复制每一个基础类型值

    function deepClone(val) {
        const baseType = ['string', 'number', 'boolean', 'undefined', 'symbol'];
        if(baseType.includes(typeof val) || val === null || val.constructor === RegExp) {
            return val;
        }
        if(Array.isArray(val)) {
            return val.map(item => deepClone(item));
        }
        const result = {};
        Object.entries(val).forEach(item => {
            const [name, value] = item;
            result[name] = deepClone(value);
        })
        return result;
    }
    const obj = {
        name: 'echo',
        age: 12,
        like: [
            {name: 'red', level: 1},
            {name: 'green', level: 2}
        ]
    };
    const a = deepClone(obj);
    console.log(a);
    

    截图.PNG

  • 利用JSON.stringify() + JSON.parse(),缺点:只能正确处理可以转成JSON格式的对象(比如undefined, function, RegExp等类型是无法被正确处理的)

  • 利用Object.assign({}, val),缺点:只能深拷贝val(源对象)的第一层数据,深层数据则是浅拷贝

原始值 VS 引用值 ——> 函数传参

ECMAScript中所有函数的参数都是 按值传递 的。这意味着函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样。原始值比较好理解,以下只对引用值举例说明

function setName(obj) {
    obj.name = 'echo';
}
let person = new Object();
setName(person);
console.log(person.name); // 'echo'

以上代码说明,即使对象是按值传进函数的,obj也会通过引用访问对象。此时当函数内部给obj设置了name属性时,函数外部的对象也发生了变化。因为obj指向的对象保存在全局作用域的堆内存上。

function setName(obj) {
    obj.name = 'echo';
    obj = new Object();
    obj.name = 'foo';
}
let person = new Object();
setName(person);
console.log(person.name); // 'echo'
以上例子用来证明对象是按值传递的,当变量`obj`被设置为一个新对象,并设置`name`属性为`foo`时,如果`person`是按引用传递的,那么`person`应该自动将指针改为指向`name``foo`的对象,然而结果并不是,这表明函数中参数的值改变之后,原始的引用仍然没变。当`obj`在函数内部被重写时,它变成了一个指向本地对象的指针,而那个本地对象在函数执行结束时就被销毁了

原始值 VS 引用值 ——> 类型判断

判断数据类型的方法有:

  • typeof

    typeof val; // 如果val为null,typeof会返回'object'
    

    对原始值很有用,对引用值用处不大(因为我们通常不关心一个值是不是对象,而是想知道它是什么类型的对象)

    使用typeof操作符检测函数时也会返回function

    Safari5之前和Chrome7之前,由于实现细节的原因,typeof对于正则表达式的检测,也会返回function

  • instanceof

    obj instanceof Object; // obj 是 Object 吗?
    obj instanceof Array;  // obj 是 Array 吗?
    obj instanceof RegExp; // obj 是 RegExp 吗?
    

    通过原型链检测

    按照定义,所有引用值都是Object的实例,因此通过instanceof操作符检测任何引用值和Object构造函数都会返回true。类似的,使用instanceof检测原始值,始终返回false。因为原始值不是对象

  • constructor

    val.constructor === Object;
    val.constructor === Array;
    

    null, undefined没有constructor方法,因此不能使用constructor检测这两个值。

    需注意,constructor的指向有可能被修改

  • Object.prototype.toString.call

    const val = 'echo';
    Object.prototype.toString.call(val); // [object String]