javaScript数据类型相关底层原理

120 阅读9分钟

一,数据类型的分类

1.简单数据类型

undefined: 未初始化的变量

null: 空值

String:字符串

Number:数值

Boolean 布尔值

Symbol:唯一标识值(es6引入)

Bigint 大整数(es11引入,比Number数据类型支持的范围更大的整数值,以任意精度表示整数。解决Number类型整数溢出的问题)

2.复杂数据类型

Object:对象 Arrary:数值 Function:函数 Regexp:正则 Date:日期 数据类型.png

二,数据类型的判断

1.typeof

可以检测除了null以外的简单数据类型,检测null返回object,复杂数据类型只能检测Function。

typeof判断null为object的原因:这是一个历史遗留问题(bug),typeof方法是按照数据在计算机底层存储的二进制结果来进行检测的,在javaScript中二进制前三位都为0会被判断为object,但null的二进制为全0,所以使用typeof判断null也会判断为object。

注意点:除开null外,typeof检测Array数组类型返回的结果也是object,这是因为Array数组是一种具有特殊行为的对象,typeof无法对两者进行详细的区分。

使用方式:typeof 需要判断数据类型的变量

//例子
let numberValue = 1;
let stringValue = '1';
let booleanValue = true;
let symbolValue = Symbol();
let bigintValue = BigInt(12500);
let nullValue = null;
let undefinedValue = undefined;
let objectValue = {a:1};
let arrayValue = [1,2,3];
let functionValue = (()=>{ return 1 });
let dateValue = new Date();
let regExpValue = /^[a-z]$/;

console.log('numberValue 数值------',typeof numberValue);
console.log('stringValue 字符串------',typeof stringValue);
console.log('booleanValue 布尔值------',typeof booleanValue);
console.log('symbolValue Symbol唯一标识符------',typeof symbolValue);
console.log('bigintValue 大整数Bigint------',typeof bigintValue);
console.log('nullValue 空值------',typeof nullValue);
console.log('undefinedValue 未定义------',typeof undefinedValue);
console.log('objectValue 对象------',typeof objectValue);
console.log('arrayValue 数组------',typeof arrayValue);
console.log('functionValue 函数------',typeof functionValue);
console.log('dateValue 日期------',typeof dateValue);
console.log('regExpValue 正则------',typeof regExpValue);

typeof.png

2.instanceof

只能用于检测复杂数据类型的数据,无法检测简单数据类型(instanceof只关注对象的原型链,简单数据类型的数据不是对象,没有原型链)。

其原理为检测指定的构造函数的原型是否存在给定的实例对象的原型链上

使用方式:被检测的复杂数据类型数据 instanceof 数据类型

重写instanceof方法:

/**
 * 原理:判断指定的构造函数的原型是否在给定的实例对象的原型链上
 * @param object 实例对象   被检测的引用类型数据
 * @param constructor  构造函数   数据类型
 * @returns {boolean} ture:这个构造函数的原型处于这个实例对象的原型链上
function instanceRewriter(object,constructor){
  let type = typeof left;
  //Function函数类型单独处理(typeof判断函数类型返回的是function)
  if(type.toString() === 'function' && typeof right === 'function') return true;
  //排除简单数据类型
  //注意:null和undefined没有包装类型的对象,直接通过instanceof判断是否为null或undefined会抛出一个错误, 显示instanceof的右侧不是一个对象,这里直接统一返回false,不做过多处理
  if(type !== 'object' || left === null)return false;
  //getPrototypeOf获取参数的原型对象
  let proto = Object.getPrototypeOf(object);
  while (true){   //循环,在object的原型链上寻找与constructor相同的原型
    if(proto === null)return false;   //排查到原型链的顶部还没有发现相同的原型对象
    if(proto === constructor.prototype)return true;//找到相同的原型对象
    proto = Object.getPrototypeOf(proto);
  }
}
//功能测试
console.log('numberValue 数值------',instanceofFun(numberValue,Number)   );
console.log('stringValue 字符串------',instanceofFun(stringValue,String)   );
console.log('booleanValue 布尔值------',instanceofFun(booleanValue,Boolean)   );
console.log('symbolValue Symbol唯一标识符------',instanceofFun(symbolValue,Symbol));
console.log('bigintValue 大整数Bigint------',instanceofFun(bigintValue,BigInt)   );
console.log('nullValue 空值------',instanceofFun(nullValue,null)   );
console.log('undefinedValue 未定义------',instanceofFun(undefinedValue,undefined)    );
console.log('objectValue 对象------',instanceofFun(objectValue,Object)    );
console.log('arrayValue 数组------',instanceofFun(arrayValue,Array)   );
console.log('functionValue 函数------',instanceofFun(functionValue,Function)   );
console.log('dateValue 日期------',instanceofFun(dateValue,Date)   );
console.log('regExpValue 正则------',instanceofFun(regExpValue,RegExp)   );

instanceof.png

3.constructor

构造函数,其原理为所有对象都会从原型上继承constructor属性;只能用来检测复杂类型数据和除了undefined和null的简单类型数据(null和undefined没有构造函数)。

constructor属性可以检测简单数据类型的原因:javaScript引擎在访问简单数据类型的constructor属性时会将简单数据类型的数据包装成对应的包装对象来处理,这些临时创建的包装对象也具有与其对应的构造函数(装箱操作)。

此外,因为constructor属性可以被改写,因此用constructor检测出来的结果不一定正确。

用法:被检测的数据.constructor === 数据类型

console.log('numberValue 数值------',numberValue.constructor === Number   );
console.log('stringValue 字符串------',stringValue.constructor === String  );
console.log('booleanValue 布尔值------',booleanValue.constructor === Boolean   );
console.log('symbolValue Symbol唯一标识符------',symbolValue.constructor === Symbol);
console.log('bigintValue 大整数Bigint------',bigintValue.constructor === BigInt  );
console.log('objectValue 对象------',objectValue.constructor === Object    );
console.log('arrayValue 数组------',arrayValue.constructor  === Array  );
console.log('functionValue 函数------',functionValue.constructor === Function   );
console.log('dateValue 日期------',dateValue.constructor === Date   );
console.log('regExpValue 正则------',regExpValue.constructor === RegExp   );
regExpValue.constructor = Function;
console.log('regExpValue 改写正则类型为函数类型------',regExpValue.constructor === Function)

constructor.png

4.Object.prototype.toString.call(xxx)

对象的原型方法,使用@@toStringTag符号(内部属性)来确定对象的类型,返回格式为[Object XXX]的字符串,XXX就是对应的数据类型;这是判断数据类型的终极方法。

用法:Object.prototype.toString.call(需要被检测的数据)

优化:

const PrototypeOfReWrite=(value)=>{
  //为了防止Object.prototype.toString判断简单数据类型产生装箱操作,我们直接对简单数据类型单独分析
  //排除null值
  if(value === null)return null;
  //检查是否为简单数据类型和函数类型,是则直接返回typeof检测的值
  let result = typeof value;
  if(result !== "object"){
    return result;
  }
  //Object.prototype.toString.call(value) 获取复杂类型数据的数据类型 返回[Object XXX]的字符串  XXX为value的数据类型
  //使用call()是为了判断其他对象时改变Object的this指向;判断Object对象,直接调用即可,返回[Object Object]
  //最后再使用正则匹配出value的数据类型XXX即可
  return Object.prototype.toString.call(value).replace(/^[object (\S+)]$/, '$1');
}
//功能测试
console.log('numberValue 数值------',PrototypeOfReWrite(numberValue) );
console.log('stringValue 字符串------',PrototypeOfReWrite(stringValue)   );
console.log('booleanValue 布尔值------',PrototypeOfReWrite(booleanValue)    );
console.log('symbolValue Symbol唯一标识符------',PrototypeOfReWrite(symbolValue));
console.log('bigintValue 大整数Bigint------',PrototypeOfReWrite(bigintValue)   );
console.log('nullValue 空值------',PrototypeOfReWrite(nullValue) );
console.log('undefinedValue 未定义------',PrototypeOfReWrite(undefinedValue));
console.log('objectValue 对象------',PrototypeOfReWrite(objectValue)  );
console.log('arrayValue 数组------',PrototypeOfReWrite(arrayValue)  );
console.log('functionValue 函数------',PrototypeOfReWrite(functionValue) );
console.log('dateValue 日期------',PrototypeOfReWrite(dateValue) );
console.log('regExpValue 正则------',PrototypeOfReWrite(regExpValue)    );

ObjectProperty.png

三,数据类型的转换

1.强制类型转换

1.1 字符串显示转换:String(),被转换数据.toString()

1.2 数值转换: Number(), paseInt(), paseFloat()

1.3布尔值转换: Boolean(), 逻辑运算符 被转换值(!a)

1.4 数组转换:Array.form()

1.5 日期转换: new Date()

2.隐式类型转换(常见)

2.1 运算符类型转换

2.1.1 +隐式转换

存在三种情况。

左侧为String类型:则+识别为拼接字符串,返回左右两侧相拼后的字符串。

左侧为Number类型,右侧为简单数据类型:+识别为数值相加,将右侧的变化强制转换为Number类型然后进行数值的相加;第三种情况为左侧。

左侧为Number类型,右侧为object对象数据类型:+识别为字符串拼接,将Number类型和object对象数据类型拼接为字符串返回。

2.1.2- * /隐式转换

将右侧非Number类型转换为Number类型,再进行计算。

2.2 逻辑运算符隐式转换

2.2.1 单个变量转换

Nan,undefined,false,0 null,转换为false,其余皆为true。

2.2.2 多个变量转换

Nan与任何值比较懂返回false,包括他自身

Boolean类型与其他任何值进行比较,自身先转换为Number类型再进行比较

String类型和Number类型进行比较,String类型转换为Number类型后再进行比较

null == undefined,但是null和undefined与其他任何值进行比较都返回false

简单数据类型和引用数据类型进行比较,引用数据类型先根据toprive规则转换为简单数据类型后再进行相应的比较

四,数据类型的拷贝

简单数据类型存储于栈内存中,存储的是值,被引用和拷贝时直接生成一个值完全相等的变量即可;

复杂数据类型存储于堆内存中,存储的是引用地址;其拷贝方式区分为浅拷贝和深拷贝。

1.浅拷贝

创建一个新的对象或数字,将原对象或数组的第一层属性的值复制到新的对象或数组,如果第一层属性的值有复杂数据类型,则复制他的引用地址。

缺陷:只适用于拷贝只有一层属性的对象,且对象属性的值如果是复杂数据类型需注意拷贝的是其引用地址,新对象的某个属性的引用发生变化,被拷贝的对象的对应的属性的引用也会发生变化。

1.1 对象浅拷贝;
es6扩展运算符...
 let a = {a:1,b:2,c:3,d:{a:2}};
 let b = {...a};
 b.a = 2;
 b.d.a=3;
console.log('a:',a)
console.log('b:',b)

es6.png

Object.assign()

将所有可枚举属性的值从一个或多个源对象返回目标对象,最后返回这个目标对象

let a ={ b:1,c:2,d:5,e:{a:7} };
let c = Object.assign(a);
c.e.a = 9;
console.log('a:',a)
console.log('c:',c)

objectAssign.png

Object.create()

创建一个新的对象

 function deepCopy(obj){
   //创建一个新对象,其原型使用obj的原型
   let copy = Object.create(Object.getPrototypeOf(obj));
   //Object.getOwnPropertyNames(obj) 返回参数对象obj自身的所有属性,无论是否可枚举
   let propNames = Object.getOwnPropertyNames(obj);
   //遍历参数对象的所有属性
   propNames.forEach((el)=>{
     //Object.getOwnPropertyDescriptor(object,value) object:返回目标对象中object的所有属性的属性描述符 value:返回目标对象中object的value属性的属性描述符
     //获取obj对象中每个属性的属性描述符
     let desc = Object.getOwnPropertyDescriptor(obj,el);
     Object.defineProperty(copy,el,desc);
     //Object.defineProperty(a,b,c) 在一个对象上添加一个新的属性,或者修改一个已存在的属性,最后返这个对象
     //a:需要定义属性的当前对象 copy
     //b:需要定义的属性名  el
     //c:描述符,一般为一个对象 desc 
     //{ configurable:true,//控制属性是否可以被删除,默认false  
     //enumerable:true,//控制属性是否可以枚举,默认false 
     //writable:true//控制属性是否可以被修改,默认false }
   });
   return copy
 }
//功能测试
let a = { b:1,c:2,d:{c:4}};
let b = deepCopy(a);
b.d.c=5;
console.log('a:',a)
console.log('b:',b)

deepCopy.png

for......in......和hasOwnProperty()
 function ForIn(obj){
   let newObj = {};
   for(let k in obj){
     //使用hasOwnProperty方法判断k是否是obj自身的属性,而不是继承过来的属性
       if(obj.hasOwnProperty(k)){
         newObj[k] = obj[k];
       }
   };
   return newObj;
 }
//功能测试
let f1 = { b:1,c:2,d:{c:4}};
let f2 = ForIn(f1);
f2.d.c = 5;
console.log('f1:',f1)
console.log('f2:',f2)

f1.png

1.2 数组浅拷贝:
es6扩展运算符...
let a = [1,2,3,4];
let b = [...a];
console.log('a:',a)
console.log('b:',b)

es6扩展运算符.png

slice:

从一个数组中截取某一部分内容,然后返回一个新的数组,接收两个参数,起始索引,结束索引。

 let arr = [1,2,3,4,5];
 let b = arr.slice(0);
console.log('arr:',arr)
console.log('b:',b)

slice.png

concat:

用于连接两个数组,返回一个新构建的数组

let a = [1,2,3];
let b = a.concat(4,[1,6]);
let c = a.concat([4,[1,6]]) //如果包含二维数组,二维数组整体添加进新数组
console.log('a:',a)
console.log('b:',b)
console.log('c:',c)

concat.png

Array.from() :
let a = [1,2,3,4];
let b = Array.from(a);
console.log('b:',b)
console.log('a:',a)

arrayfrom.png

2.深拷贝(浅拷贝+递归)

在堆内存中开辟一块空间用于存放拷贝的对象,并将其数据拷贝过来;新拷贝的对象和原对象的引用地址为两个不同的引用地址,互不影响。

JSON序列号和反序列化

利用 JSON.stringify() 将对象序列化为 JSON 字符串,然后再用 JSON.parse() 将字符串解析为新的对象

缺点:无法处理函数,正则等特殊对象,无法处理循环引用导致的栈溢出的问题

function jsonCopy(ojb){
  return JSON.parse(JSON.stringify(ojb));
}
let a  =  {a:1,b:2,c:{d:3}};
let b = jsonCopy(a);
b.c.d = 4;
console.log('a:',a)
console.log('b:',b)

JSON.png

循环引用: 两个或多个对象存在相互引用的现象即为循环引用。由于循环引用这些对象不能内垃圾回收机制回收,会导致内存泄露。

let f1 = {a:1,b:2};
let f2 = {c:3,d:3};
let f3 = {};
f3.info = f2;
f2.info = f1;
f1.info = f3
let f4 = recursionCopy2(f3);
console.log(f4,165)
function recursionCopy2(obj){
  if(typeof obj !== 'object' || obj === null)return obj;
  //判断传入的参数是数组还是对象
  let copy = Array.isArray(obj)?[]:{};
  for(let key in obj){
    if(obj.hasOwnProperty(key)){
      copy[key] = recursionCopy2(obj[key])
    }
  }
  return  copy
}

循环引用.png

递归

缺点:无法处理循环引用导致的栈溢出的问题

function recursionCopy(obj){
  if(typeof obj !== 'object' || obj === null)return obj;
  //判断传入的参数是数组还是对象
  let copy = Array.isArray(obj)?[]:{};
  for(let key in obj){
    if(obj.hasOwnProperty(key)){
      copy[key] = recursionCopy(obj[key])
    }
  }
  return  copy
}
//功能测试
let f1 = {a:1,b:2,c:{d:3}};
let f2 = recursionCopy(f1);
f2.c.d = [1,2,3]
  console.log('f1:',f1)
  console.log('f2:',f2)

递归.png

递归 改进版
function recursionCopy(obj,recordMap = new Map()){
  //缓存一个Map结构用以跟踪已经复制过的对象,以避免陷入无限递归
  if(typeof obj !== 'object' || obj === null)return obj;
  //如果已经拷贝过改对象,则直接返回拷贝后的对象引用
   if(recordMap.has(obj)){
     return recordMap.get(obj);
   }
  //判断传入的参数是数组还是对象
  let copy = Array.isArray(obj)?[]:{};
   //将当前对象添加到已拷贝过的对象列表中
  recordMap.set(obj,copy);
  for(let key in obj){
    if(obj.hasOwnProperty(key)){
      copy[key] = recursionCopy(obj[key],recordMap)
    }
  }
  return  copy
}
//功能测试
let f1 = {a:1,b:2,c:{d:3}};
let f2 = recursionCopy(f1);
f2.d = [1,2,3]
  console.log('f1:',f1)
  console.log('f2:',f2)

MAP递归.png

最终版:weakmap弱映射 + 递归
function weakMapCopy(obj,wm = new WeakMap()){
  /**
   * 缓存一个map结构,WeakMap的键只能是对象且对键名所引用的对象为弱引用关系,不影响垃圾回收,可以减少内存泄露的风险
   *WeakMap的API和Map一致,使用方法参照Map即可
   */
    if(obj === null ||typeof obj !=='object')return obj;
      //如果已拷贝过该对象,则直接返回拷贝后的对象
  if(wm.has(obj)){
    return wm.get(obj);
  }
  //判断传入的参数是数组还是对象
  let copy = Array.isArray(obj)?[]:{};
  //将新对象存入weakMap中,以便后续判断是否已经拷贝过了
  wm.set(obj,copy);
  //遍历obj的属性,并递归进行深拷贝
  for(let key in obj){
    if(obj.hasOwnProperty(key)){
      copy[key] = weakMapCopy(obj[key],wm)
    }
  }
  return copy;
}
//功能测试
let f1 = {a:1,b:2,c:{d:3}};
let f2 = weakMapCopy(f1);
f2.b = [1,2,3]
  console.log('f1:',f1)
  console.log('f2:',f2)

WeakMap递归.png

递归 + 原型链版本
function deepCopy(obj,tempMap=new WeakMap()){
  //缓存一个Map结构用以追踪被拷贝过的对象,防止无限递归造成栈溢出错误

  //创建一个新对象,其原型使用obj的原型
  let copy = Object.create(Object.getPrototypeOf(obj));
  //Object.getOwnPropertyNames(obj) 返回参数对象obj自身的所有属性,无论是否可枚举

  //检测是否存在已被拷贝过的对象,存在则直接返回拷贝过的对象
  if(tempMap.has(obj)){
    return  tempMap.get(obj);
  }
  let propNames = Object.getOwnPropertyNames(obj);
  //将新对象存入weakMap中,以便后续判断是否已经拷贝过了
  tempMap.set(obj,copy);
  //遍历参数对象的所有属性
  propNames.forEach((el)=>{
    //Object.getOwnPropertyDescriptor(object,value) object:返回目标对象中object的所有属性的属性描述符 value:返回目标对象中object的value属性的属性描述符
    //获取obj对象中每个属性的属性描述符
    let desc = Object.getOwnPropertyDescriptor(obj,el);
    //Object.defineProperty(a,b,c) 在一个对象上添加一个新的属性,或者修改一个已存在的属性,最后返回这个对象
    //a:需要定义属性的当前对象 copy
    //b:需要定义的属性名  el
    //c:描述符,一般为一个对象 desc {value:'',//属性值 configurable:true,//控制属性是否可以被删除,默认false  enumerable:true,//控制属性是否可以枚举,默认false  writable:true//控制属性是否可以被修改,默认false }
    Object.defineProperty(copy,el,desc);
    // 如果属性是对象,则递归地复制该对象
    if (typeof obj[el] === 'object' && obj[el] !== null) {
      copy[el] = deepCopy(obj[el],tempMap);
    }
  });
  return copy
}
//功能测试
let f1 = {a:1,b:2,c:{d:3}};
let f2 = deepCopy(f1);
delete f2.c;
f2.b =3;
  console.log('f1:',f1)
  console.log('f2:',f2)

原型链递归.png

数据类型.png