5. JavaScript 数据类型

237 阅读9分钟

基本类型与引用类型

JavaScript 中的数据类型分为基本类型和应用类型,其中基本类型有:

  • Boolean

  • String

  • Number

  • Null

  • Undefined

  • Symbol(ES6新增,表示独一无二的值)

引用类型:

  • Object(对象,数组,函数)

这些类型在内存中存放的方式也不同:

JavaScript 把基本类型直接存放在栈内存中,而引用数据类型则放在堆内存中,栈内存中存放的是引用类型在堆内存中的地址。

81223748

所以我们在引用类型赋值的时候,如果修改其中一个,另一个的值也会变,因为我们赋值时给予的其实是它的内存地址。

const 的不可变也是针对的栈内存,所以我们用 const 定义了一个对象后依然能够修改对象的内容,只是不能重新赋值。

// 基本类型
var a = 10
var b = a
b = 20
console.log(a)  // 10
console.log(b)  // 20
// 引用类型
var a = {x: 10, y: 20}
var b = a
b.x = 100
b.y = 200
console.log(a)  // {x: 100, y: 200}
console.log(b)  // {x: 100, y: 200}

数据类型判断

typeof

它会返回一个表示数据类型的字符串,表达式为 typeof A ,返回结果包括:number、Boolean、string、symbol、object、undefined、function 这 7 中

缺点:它无法判断 array 和 object,因为它统一返回的 object

typeof Symbol(); // symbol 有效
typeof ''; // string 有效
typeof 1; // number 有效
typeof true; //boolean 有效
typeof undefined; //undefined 有效
typeof new Function(); // function 有效
typeof null; //object 无效
typeof [] ; //object 无效
typeof new Date(); //object 无效
typeof new RegExp(); //object 无效

instanceof

这个方法是基于原型链进行的判断,它可以判断 A 是不是 B 的实例,表达式为 A instanceof B ,返回结果包括:true 和 false(返回 Boolean )

这个方法多用于测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

缺点:它无法检测 null 和 undefined,我们多在判断继承中使用它。

[] instanceof Array; //true
{} instanceof Object;//true
new Date() instanceof Date;//true
new RegExp() instanceof RegExp//true
null instanceof Null//报错
undefined instanceof undefined//报错

constructor

它和 instanceof 类似,但它还可以检测基本数据类型。不过 constructor 不稳定,因为类对原型进行重写时,可能会覆盖之前的 constructor ,这样检测的结果就不准确。

Object.prototype.toString.call()

利用对象的类属性进行的判断:对象的类属性是一个字符串,用来表示对象的类型信息。

只有调用对象的 toString() 方法可以获得对象的类,而很多对象继承的 toString() 方法重写了,为了调用正确的 toString() 版本,必须间接地调用 Function.call() 方法。

这是最准确也是用得最多的类型判断方法(我大多数时候就在用它),它的检测结果很细致:

Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ;    // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]

通常会截取出内容:

Object.prototype.toString.call('').slice(8, -1)  // String

类型比较

//true的只有
console.log("" == 0);
console.log("" == []); 
console.log(0 == []); 
console.log(undefined == null);
//undefined 和 null 只有互相比较才会是 true
console.log(undefined == null); //true *********************
//object 与任何的比较都是 false

详细:

console.log("" == 0); //true *********************
console.log("" === 0); //false
console.log("" == []); //true *********************
console.log("" === []); //false
console.log("" == {}); //false
console.log("" === {}); //false
console.log(0 == []); //true *********************
console.log(0 === []); //false
console.log(0 == {}); //false
console.log(0 === {}); //false
console.log([] == {}); //false
console.log([] === {}); //false
console.log(null == 0); //false
console.log(null === 0); //false
console.log(null == ""); //false
console.log(null === ""); //false
console.log(null == []); //false
console.log(null === []); //false
console.log(null == {}); //false
console.log(null === {}); //false
console.log(undefined == 0); //false
console.log(undefined === 0); //false
console.log(undefined == ""); //false
console.log(undefined === ""); //false
console.log(undefined == []); //false
console.log(undefined === []); //false
console.log(undefined == {}); //false
console.log(undefined === {}); //false
console.log(undefined == null); //true *********************
console.log(undefined === null); //false

深拷贝与浅拷贝

深拷贝与浅拷贝是针对引用类型的,因为前面讲类型的时候有提到,引用类型是把数据存放在堆内存中,而栈内存 放它的地址,而我们一般赋值或拷贝都是拷贝的栈内存中的数据,这就造成了我们修改其中一项,会修改堆内存中的数据,而被赋值或被拷贝的对象按照地址找过去的值是被修改过的。

这种我们就称之为 浅拷贝,而深拷贝就是修改其中一项,另一项它不会改变。

浅拷贝 的原理是拷贝地址。

深拷贝的原理就是在拷贝的时候,直接在堆内存中新建一个新的相同的数据,然后把新数据的地址给被拷贝的对象,这样我们修改其中任何一项,都不会对另一项造成影响。

81223749

81223750

总结

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创建一个一模一样的对象,新对象跟原对象不共享内存,修改新对象就不会修改到原对象。

赋值与浅拷贝

赋值与浅拷贝结果类似,但它们是不同的。

赋值:当我们把一个对象赋值给一个新的变量时:赋的其实是这个对象在栈内存中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间里的内容,因此,两个对象是联动的。

浅拷贝:浅拷贝是按位拷贝对象,它会创建一个对象,这个对象有着原始对象属性值的一份精确拷贝。如果对象是基本数据类型,拷贝的就是基本数据类型的值;如果对象是引用数据类型,拷贝的就是内存地址,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

默认构造函数是对对象进行的浅拷贝,即只复制对象空间而不复制对象资源。

// 对象赋值
 var obj1 = {
    'name' : 'zhangsan',
    'age' :  '18',
    'language' : [1,[2,3],[4,5]],
};

var obj2 = obj1;
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)

81223751

// 浅拷贝
 var obj1 = {
    'name' : 'zhangsan',
    'age' :  '18',
    'language' : [1,[2,3],[4,5]],
};

 var obj3 = shallowCopy(obj1);
 obj3.name = "lisi";
 obj3.language[1] = ["二","三"];

 function shallowCopy(src) {
    var dst = {};
    for (var prop in src) {
        if (src.hasOwnProperty(prop)) {
            dst[prop] = src[prop];
        }
    }
    return dst;
}
console.log('obj1',obj1)
console.log('obj3',obj3)

81223752

总结

赋值是直接给予地址,浅拷贝要对赋值的对象深处进行判断,如果是基本类型,就创建新对象并给进行浅拷贝的对象,如果是引用类型就给地址给浅拷贝的对象。

浅拷贝的实现方式

Object.assign()

这个方法可以把任意多个源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

但是要注意的是,Object.assign 只有当 object 是一层的时候,会进行深拷贝,其他的都是浅拷贝。

// 深拷贝(一层)
let obj = {
    username: 'kobe'
};
let obj2 = Object.assign({},obj);
obj2.username = 'wade';
console.log(obj);//{username: "kobe"}
// 浅拷贝(多层)
var obj = {
    a: {
        a: "kobe",
        b: 39
    }
};
var initalObj = Object.assign({}, obj);
initalObj.a.a = "wade";
console.log(obj.a.a); //wade

ES6 解构赋值

objTmp={
    ...obj  
}

性质: 如果对象的属性值为简单类型(string, number 等),通过以上两种方式得到的新对象为深拷贝;如果属性值为对象或其它引用类型,那对于这个对象而言其实是浅拷贝的,拷贝后仍指向同一块内存区域

Array.prototype.concat()

这个方法我们通常用来合并数组,但是要注意这个方法是浅拷贝,修改了新对象会改到原对象:

let arr = [1, 3, { username: 'kobe' }];
let arr2 = arr.concat();    
arr2[2].username = 'wade';
console.log(arr);

81223753

我们看到,改了新对象后,原对象对应的值也发生了改变。

Array.prototype.slice()

同样的这个也是我们常用的方法,我们用它来返回想要的数组,但要注意的是它也是浅拷贝。

let arr = [1, 3, {
    username: ' kobe'
    }];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr);

81223754

同样,我们修改了我们得到的新数组,原数组也发生了改变。

我们常用的这两种方法(concat、slice)都是返回新数组,而返回的数组就是通过浅拷贝得到的。

深拷贝的实现方式

JSON.parse(JSON.stringify())

JSON.parse() 这个方法在前端用得非常多,作用的将字符串解析成对象。

而 JSON.stringify() 会将对象传化成 JSON 字符串。

把两个方法一起用,就产生了新的对象,实现了深拷贝。

let arr = [1, 3, {
    username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan'; 
console.log(arr, arr4)

81223755

可以看到这样两次传化后,新对象与原对象除了内容相同已经没有什么不同了,所以这里新对象修改后原对象没有发生改变。

缺点:这个方法有个缺点就是它不能处理函数,原因是因为 JSON.stringify()这个方法不接受函数。

JQ 的 extend 方法

$.extend( [deep ], target, object1 [, objectN ] ) deep:如果设为true,则递归合并即深拷贝。 target:待修改对象。 object1:待合并到第一个对象的对象。 objectN:待合并到第一个对象的对象。

let a = [1,2,3],
let b = $.extend(true,[],a);
a[0]=1;
console.log(a); // [0,2,3]
console.log(b); // [1,2,3]

lodash 的方法 cloneDeep

在 JQ 很少用的今天,前端多用函数库 lodash,这个库中就有提供深拷贝的方法:_.cloneDeep

import _ from 'lodash'
let obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
let obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
// false

附上对应 lodash 文档:www.lodashjs.com/docs/latest…

最后一种最麻烦也是最基本的方法:手写递归法

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝

我们把对象、数组一直递归遍历到只有基础类型后,再进行复制,这样就手动完成了深拷贝。

//定义检测数据类型的功能函数
function checkedType(target) {
    return Object.prototype.toString.call(target).slice(8, -1)
}

//实现深度克隆---对象/数组
function clone(target) {
    //判断拷贝的数据类型
    //初始化变量result 成为最终克隆的数据
    let result, targetType = checkedType(target)
    if (targetType === 'Object') {
        result = {}
    } else if (targetType === 'Array') {
        result = []
    } else {
        return target
    }

    //遍历目标数据
    // for...in 遍历数组会遍历到数组原型上的属性和方法,更适合遍历对象
    for (let i in target) {
        // 这里使用 hasOwnProperty 来判断是否是自有方法
        if(target.hasOwnProperty(i)) {
            //判断目标结构里的每一值是否存在对象/数组
            if (checkedType(target[i]) === 'Object' ||
                checkedType(target[i]) === 'Array') { //对象/数组里嵌套了对象/数组
                //继续遍历获取到value值
                result[i] = clone(target[i])
            } else { //获取到value值是基本的数据类型或者是函数。
                result[i] = target[i];
            }
        }
    }
    return result
}

参考