javascript基础--数据类型

391 阅读10分钟

7种基础类型+1种引用类型(Object)

    引用数据类型(Object)又分为几种常见的类型:Array - 数组对象、RegExp - 正则对象、Date - 日期对象、Math - 数学函数、Function - 函数对象。

 数据类型

因为各种 JavaScript 的数据类型最后都会在初始化之后放在不同的内存中,因此上面的数据类型大致可以分成两类来进行存储: 

  1.  基础类型存储在栈内存,被引用或拷贝时,会创建一个完全相等的变量; 
  2.  引用类型存储在堆内存,存储的是地址,多个引用指向同一个地址,这里会涉及一个“共享”的概念。

下面我们看看引用数据类型共享的含义

let a = {

  name: 'lee',

  age: 18

}

let b = a;

console.log(a.name);  //第一个console   'lee'

b.name = 'son';

console.log(a.name);  //第二个console    'son'

console.log(b.name);  //第三个console     'son'

这里就体现了引用类型的“共享”的特性,即这两个值都存在同一块内存中共享,一个发生了改变,另外一个也随之跟着变化。

下面我们再看一个例子

let a = {
  name: 'Julia',
  age: 20
}
function change(o) {
  o.age = 24;
  o = {
    name: 'Kath',
    age: 30
  }
  return o;
}
let b = change(a);     // 注意这里没有new,后面new相关会有专门文章讲解
console.log(b.age);    // 第一个console   30
console.log(a.age);    // 第二个console   24

原因在于:函数传参进来的 o,传递的是对象在堆中的内存地址值,通过调用 o.age = 24(第 7 行代码)确实改变了 a 对象的 age 属性;但是第 11 行代码的 return 却又把 o 变成了另一个内存地址,将 {name: "Kath", age: 30} 存入其中,最后返回 b 的值就变成了 {name: "Kath", age: 30}。而如果把第 11 行去掉,那么 b 就会返回 undefined。

数据类型转换

强制类型转换

Number()、parseInt()、parseFloat()、toString()、String()、Boolean()

Number()转数字需要注意

Number(true);        // 1

Number(false);       // 0

Number('0111');      //111

Number(null);        //0

Number('');          //0

Number('1a');        //NaN

Number(-0X11);       //-17

Number('0X11')       //17

Boolean()需要注意

Boolean(0)          //false

Boolean(null)       //false

Boolean(undefined)  //false

Boolean(NaN)        //false

Boolean(1)          //true

Boolean(13)         //true

Boolean('12')       //true

隐式类型转换

(&&、 ||、 !)、运算符 (+、-、*、/)、关系操作符 (>、 <、 <= 、>=)、相等运算符 (==) 或者 if/while 条件的操作,如果遇到两个数据类型不一样的情况,都会出现隐式类型转换。

'==' 的隐式类型转换规则

  • 如果类型相同,无须进行类型转换;

  • 如果其中一个操作值是 null 或者 undefined,那么另一个操作符必须为 null 或者 undefined,才会返回 true,否则都返回 false;

  • 如果其中一个是 Symbol 类型,那么返回 false;

  • 两个操作值如果为 string 和 number 类型,那么就会将字符串转换为 number;

  • 如果一个操作值是 boolean,那么转换成 number;

  • 如果一个操作值为 object 且另一方为 string、number 或者 symbol,就会把 object 转为原始类型再进行判断(调用 object 的 valueOf/toString 方法进行转换)。

这里拓展一下js中‘==‘和’==='的区别

‘==’会先进行数据转换再对比

‘===’类型不同直接返回false,这里注意如果两个值都引用同一个对象或是函数,如果两个值都是null,或是undefined 返回true

'+' 的隐式类型转换规则

加号的作用:数字运算;字符串拼接

  • 如果其中有一个是字符串,另外一个是 undefined、null 或布尔型,则调用 toString() 方法进行字符串拼接;如果是纯对象、数组、正则等,则默认调用对象的转换方法会存在优先级(下一讲会专门介绍),然后再进行拼接。

  • 如果其中有一个是数字,另外一个是 undefined、null、布尔型或数字,则会将其转换成数字进行加法运算,对象的情况还是参考上一条规则。

  • 如果其中一个是字符串、一个是数字,则按照字符串规则进行拼接。

    1 + 2 // 3 常规情况

    '1' + '2' // '12' 常规情况

    // 下面看一下特殊情况

    '1' + undefined // "1undefined" 规则1,undefined转换字符串

    '1' + null // "1null" 规则1,null转换字符串

    '1' + true // "1true" 规则1,true转换字符串

    '1' + 1n // '11' 比较特殊字符串和BigInt相加,BigInt转换为字符串

    1 + undefined // NaN 规则2,undefined转换数字相加NaN

    1 + null // 1 规则2,null转换为0

    1 + true // 2 规则2,true转换为1,二者相加为2

    1 + 1n // 错误 不能把BigInt和Number类型直接混合相加

    '1' + 3 // '13' 规则3,字符串拼接

数据类型检测

常用的数据类型检测有三种方法

1.使用typeof

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'

缺点:使用typeof判断会存在null判断为object的bug;引用类型除了function以外其余会都是object。

2.使用instenceof

有人会使用instenceof利用原型链继承的方式来判断数据类型

let Car = function() {}

let benz = new Car()

benz instanceof Car // true

let car = new String('Mercedes Benz')

car instanceof String // true

let str = 'Covid-19'

str instanceof String // false

s先来模拟一下instanceof的实现

function instanceof(left, right) {

  // 这里先用typeof来判断基础数据类型,如果是,直接返回false

  if(typeof left !== 'object' || left === null) return false;

  // getProtypeOf是Object对象自带的API,能够拿到参数的原型对象

  let proto = Object.getPrototypeOf(left);

  while(true) {                  //循环往下寻找,直到找到相同的原型对象

    if(proto === null) return false;

    if(proto === right.prototype) return true;//找到相同原型对象,返回true

    proto = Object.getPrototypeof(proto);

    }

}

// 验证一下自己实现的myInstanceof是否OK

console.log(instanceof(new Number(123), Number));    // true
console.log(instanceof(123, Number));                // false

缺点:

  1. instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型;

  2. 而 typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了 function 类型以外,其他的也无法判断。

以上两种都不能满足全部类型判断的要求

3.Object.prototype.toString

Object.prototype.toString({})       // "[object Object]"

Object.prototype.toString.call({})  

Object.prototype.toString.call(1)    // "[object Number]"

Object.prototype.toString.call('1')  // "[object String]"

Object.prototype.toString.call(true)  // "[object Boolean]"

Object.prototype.toString.call(function(){})  // "[object Function]"

Object.prototype.toString.call(null)   //"[object Null]"

Object.prototype.toString.call(undefined) //"[object Undefined]"

Object.prototype.toString.call(/123/g)    //"[object RegExp]"

Object.prototype.toString.call(new Date()) //"[object Date]"

Object.prototype.toString.call([])       //"[object Array]"

Object.prototype.toString.call(document)  //"[object HTMLDocument]"

Object.prototype.toString.call(window)   //"[object Window]"

使用这种方法可以有效的检测各种类型,下面将几种方式组合封装一个类型检测函数。

function getType(obj){

  let type  = typeof obj;

  if (type !== "object") {    // 先进行typeof判断,如果是基础数据类型,直接返回

    return type;

  }

  // 对于typeof返回结果是object的,再进行如下的判断,正则返回结果

  return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1');  // 注意正则中间有个空格

}

/* 代码验证,需要注意大小写,哪些是typeof判断,哪些是toString判断?思考下 */

getType([])     // "Array" typeof []是object,因此toString返回

getType('123')  // "string" typeof 直接返回

getType(window) // "Window" toString返回

getType(null)   // "Null"首字母大写,typeof null是object,需toString来判断

getType(undefined)   // "undefined" typeof 直接返回

getType()            // "undefined" typeof 直接返回

getType(function(){}) // "function" typeof能判断,因此首字母小写

getType(/123/g)      //"RegExp" toString返回

深浅拷贝

自己创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象。

在使用中我们需要注意object.assign(),ES6中的拓展运算符,concat(),slice()都是浅拷贝

手动实现一个浅拷贝

const shallowClone = (target) => {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
          cloneTarget[prop] = target[prop];
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}

利用类型判断,针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性,基本就可以手工实现一个浅拷贝的代码了。浅拷贝也仅仅拷贝当前的一层的对象。

深拷贝的原理和实现

浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。

将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。

深拷贝实现:

1.json.stringfy()

let obj1 = { a:1, b:[1,2,3] }
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log(obj2);   //{a:1,b:[1,2,3]} 
obj1.a = 2;
obj1.b.push(4);
console.log(obj1);   //{a:2,b:[1,2,3,4]}
console.log(obj2);   //{a:1,b:[1,2,3]}

缺点:

  • 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;

  • 拷贝 Date 引用类型会变成字符串;

  • 无法拷贝不可枚举的属性;

  • 无法拷贝对象的原型链;  

  • 拷贝 RegExp 引用类型会变成空对象;

  • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;

  • 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。

2.递归实现

let obj1 = {
  a:{
    b:1
  }
}
function deepClone(obj) { 
  let cloneObj = {}
  for(let key in obj) {                 //遍历
    if(typeof obj[key] ==='object') { 
      cloneObj[key] = deepClone(obj[key])  //是对象就再次调用该函数递归
    } else {
      cloneObj[key] = obj[key]  //基本类型的话直接复制值
    }
  }
  return cloneObj
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2);   //  {a:{b:1}}

缺点:

  • 这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型; 
  •  这种方法只是针对普通的引用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝; 
  •  对象的属性里面成环,即循环引用没有解决。

上面两个版本都存在缺陷使得深拷贝不完美,唯一的好处就是便捷,能够针对不复杂的引用类型进行使用。

3.改进后的递归

  • 针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法; 当参数为 Date、RegExp 类型,则直接生成一个新的实例返回;

  •  利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链; 

  •  利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏(你可以关注一下 Map 和 weakMap 的关键区别,这里要用 weakMap),作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值。co

    const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
    
    const deepClone = function (obj, hash = new WeakMap()) {
    
      if (obj.constructor === Date) 
    
      return new Date(obj)       // 日期对象直接返回一个新的日期对象
    
      if (obj.constructor === RegExp)
    
      return new RegExp(obj)     //正则对象直接返回一个新的正则对象
    
      //如果循环引用了就用 weakMap 来解决
    
      if (hash.has(obj)) return hash.get(obj)
    
      let allDesc = Object.getOwnPropertyDescriptors(obj)
    
      //遍历传入参数所有键的特性
    
      let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
    
      //继承原型链
    
      hash.set(obj, cloneObj)
    
      for (let key of Reflect.ownKeys(obj)) { 
    
        cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
    
      }
    
      return cloneObj
    
    }
    
    // 下面是验证代码
    
    let obj = {
    
      num: 0,
    
      str: '',
    
      boolean: true,
    
      unf: undefined,
    
      nul: null,
    
      obj: { name: '我是一个对象', id: 1 },
    
      arr: [0, 1, 2],
    
      func: function () { console.log('我是一个函数') },
    
      date: new Date(0),
    
      reg: new RegExp('/我是一个正则/ig'),
    
      [Symbol('1')]: 1,
    
    };
    
    Object.defineProperty(obj, 'innumerable', {
    
      enumerable: false, value: '不可枚举属性' }
    
    );
    
    obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
    
    obj.loop = obj    // 设置loop成循环引用的属性
    
    let cloneObj = deepClone(obj)
    
    cloneObj.arr.push(4)
    
    console.log('obj', obj)
    
    console.log('cloneObj', cloneObj)
    

深拷贝的内容差不多都实现了,可以自己将代码放到控制台试试。

参考:

这部分内容参考若离老师关于javascript核心原理整理。