从头梳理JS基础(一)数据类型及深浅拷贝

1,029 阅读16分钟

前言

这个系列呢,说是博客其实就是笔记哈哈哈哈,感觉上班久了以后很多基础的东西反而不那么扎实了,也是进行一个梳理吧,站在巨人的肩膀上加一些自己的理解【虽然可能我会把自己绕进去,这不重要 🙈】,但我会努力讲明白哒 😘

本文主要梳理JS的基本数据类型和引用数据类型,显隐式转换规则及深浅拷贝,会持续补充更新哦!

数据存储结构

先来看看三种常见的数据存储结构:

  • 栈:只允许在一段进行插入或者删除操作的线性表,是一种先进后出的数据结构。(基本数据类型)
  • 队列:队列是一种先进先出(FIFO)的数据结构。(事件循环)
  • 堆:堆是基于散列算法的数据结构。(引用数据类型)

基本数据类型

基本类型值指的是那些保存在栈内存中的简单数据段,即这种值是完全保存在内存中的一个位置。

Underfined 类型

只有一个值,当 var 声明变量但未初始化时,这个值为 underfined(没必要显式设置)
对于尚未声明过的变量,只能执行一项操作,即使用typeof操作符检测其数据类型,使用其他的操作都会报错。

Null 类型

只有一个值,空对象指针,当定义的变量将来用于保存对象时,建议初始化为 null 而不是其他值

underfined == null  //true

Boolean 类型

true 和 false,注意是区分大小写的,也就是说 True 和 False(以及其他混合大小写形式)都不是 Boolean 的值

转换规则

我们常用的 if(变量名),表示变量不为 false,"",underfined,0 和 NaN

Boolean(underfined) //false 
Boolean(null) //false 
Boolean(underfined) //false
Boolean("") //false 
Boolean(0) //false 
Boolean(NaN) //false 
Boolean({}) //true

Number 类型

IEEE754 格式来表示整数和浮点数值

整数

var num=56 //十进制56 var num=070 //八进制56 var num=0x38 //十六进制56

  • 八进制第一位必须是 0,后面跟八进制序列 0 到 7,如果超出了范围,则忽略前导 0,后面的数值当做十进制解析,例如:089 会被解析为 89。(八进制字面量在严格模式下是无效的,会抛出错误。)
  • 十六进制前两位必须是 0x 或 0X,后跟十六进制序列 0~9、a~f(不区分大小写),如果超出了范围,则会报语法错误。

浮点数

浮点数值精度最高 17 位,计算会产生舍入误差

正无穷、负无穷

正数除以 0 返回正无穷(Infinity),负数除以 0 返回负无穷(-Infinity) JavaScript 提供了 isFinite() 函数,来确定一个数是不是有穷的。例如:

isFinite(500)         // true 
isFinite(Infinity);   // false

NaN

NaN(非数值)是一个特殊的数值,用于表示一个本来要返回数值的操作树未返回数值的情况(这样就不会抛出错误了)

//NaN及其本身不相等 
NaN == NaN //false

会出现 NaN 的几种情况:通过isNaN()函数来确定是不是 NaN

//isNaN 
isNaN(0/0) //true 
isNaN(NaN/10) //true (任何涉及NaN的操作) 
isNaN(10) //false 
isNaN("blue") //false 
isNaN(true) //false(转换为1)

转换规则

//Number() 
Number(true) //1 
Number(false) //0 
Number(null) //0 
Number(underfined) //NaN 
Number("0011") //11 
Number("124") //124 
Number("") //0 Number("we1") //NaN 
//parseInt() 
parseInt(""); // NaN 
parseInt("12aa"); // 12 
parseInt("13.8"); // 13 
//parseFloat() 
parseFloat("077.2")      // 77.2 
parseFloat("123.11.22")   // 123.11

String 类型

字符字面量(转义序列)

\n 换行、\t 制表、\b 空格、\r 回车、\f 进纸、\ 斜杠、' 单引号,在用单引号表示的字符串中使用、" 双引号,在用双引号表示的字符串中使用

转换规则

//toString()方法(undefined 和 null 值没有) 
//String() 
var num; 
String(10) // "20"  如果值有 toString() 方法,则调用该方法(没有参数)并返回相应的结果 
String(true) // "true" 
String(null) // "null" (如果值是 null,则返回 "null") 
String(num) // "undefined" (如果值是 undefined,则返回 "undefined")
//String()基本包装类型方法
var str='ceshi str'
str.length //字符串长度
str.trim() //删除前后所有空格
str.replace() //替换,默认只替换第一个,如果要全局替换匹配正则设为g
str.split() //分隔,指定分隔符将一个串拆分为多个串并放入数组

Synbol类型

Symbol 是一种特殊的、不可变的数据类型,可以作为对象属性的标识符使用,表示独一无二的值。

let a = Symbol('a');
let b = Symbol('a');
a===b //false

注意

  • Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。
  • 定义属性的时候只能将Symbol值放在方括号里面,否则属性的键名会当做字符串而不是Symbol值。同理,在访问Symbol属性的时候也不能通过点运算符去访问,点运算符后面总是字符串,不会读取Symbol值作为标识符所指代的值.
  • 常量的使用Symbol值最大的好处就是其他任何值都不可能有相同的值,用来设计switch语句是一种很好的方式。

BigInt类型(第3阶段提案,暂且不论)

引用数据类型

引用类型值指的是那些保存在堆内存中的对象,所以引用类型的值保存的是一个指针,这个指针指向存储在堆中的一个对象。当查询引用类型的变量时, 先从栈中读取内存地址, 然后再通过地址找到堆中的值(按引用访问)。

  • 除了上面的 6 种基本数据类型外,剩下的就是引用类型了,统称为 Object 类型。细分的话,有:Object 类型、Array 类型、Date 类型、RegExp 类型、Function 类型 等。

Object类型

var obj = new Object(); 
var obj = {};   // 优先对象字面量 
valueOf() //返回对象的字符串、数值或布尔值表示(通常与toString()方法的返回值相同)。

Array类型(*)

var arr = new Array(); 
var arr = [];   // 优先数组字面量 

转换方法

Array.isArray() //判断是不是数组
Array.toString() //逗号分隔数组为字符串
Array.join(',') //指定分隔符分隔数组为字符串(默认为逗号)

处理方法

Array.unshift() //开头添加项,return修改后长度
//数组实现类似栈的行为(后进先出)
Array.push() //末尾添加项,return修改后长度
Array.pop() //末尾移除一项,return移除的项
//数组实现类似队列的行为(先进先出)
Array.push() //末尾添加项,return修改后长度
Array.shift() //开头移除一项,return移除的项

重排序方法

Array.sort() //默认升序(先toString()再比较)
Array.sort(comp) //==>comp 为比较函数,可以指定排序效果
let a=[1,2,11,4,23]
a.sort() //[1, 11, 2, 23, 4]
a.sort((a,b)=>a-b) //[1, 2, 4, 11, 23]
Array.reverse() //反转数组顺序

操作方法

Array.concat() //创建副本将参数依次添加到末尾,retrun新数组
Array.slice() //return 从start到end(不包括end)的项
Array.splice() //* return删除项,不删除则为[]
//参数的三种操作(起始位置,删除项数,插入值)
let a=[1,2,11,4,23]
//1.删除
a.splice(1,1) //[1, 11, 4, 23]
//2.插入
a.splice(1,0,5) //[1, 5, 2, 11, 4, 23]
//3.替换
a.splice(1,1,5) //[1, 5, 11, 4, 23]

位置方法

Array.indexOf() //从头开始查找项,return项所在位置,没找到为-1(全等比较)
Array.lastIndexOf() //从尾部开始查找项

迭代方法

//不会对原数组进行修改 运行函数的参为(item,index,array)
Array.filter() //过滤,retrun满足条件(为true)的项组成的数组
Array.foreach() //遍历,无return值
Array.map() //映射,return调用结果所组成的数组
Array.every() //当每一项都满足条件时,return true
Array.some() //当存在一项满足条件时,return true

归并方法

//不会对原数组进行修改 运行函数的参为(prev,cur,index,array)
Array.reduce() //从前遍历,迭代所有项返回一个最终值
Array.reduceRight() //从后向前遍历

Date类型

使用UTC(国际协调时间)

new Date() //不传参,自动获取当前日期和时间 Sun Jul 05 2020 15:20:11 GMT+0800 (中国标准时间)
Date.now() //retrun 调用这个方法时的日期和时间的毫秒数(时间戳) 1593933631402

RegExp类型

支持正则表达式(正则的相关规则单独整理)

var a=/pattern/flags
//pattern 匹配规则
//flags 标志-表明行为 
//g-全局(而非匹配第一个就停止) i-不区分大小写 m-多行(继续向下查下一行)
a.exec(str) //捕获组,str为待匹配字符串,return 结果Array
a.test(str) //str为待匹配字符串,若匹配return true

Function类型(*)

函数没有重载,当函数重名时,取最后一次定义,建议使用函数声明来定义函数。
函数调用优先级
new 调用 > call、apply、bind 调用 > 对象上的函数调用 > 普通函数调用
函数内部属性

  • arguments:参数数组
    • arguments.length-传入参的个数,没有传值的命名参数为underfined,arguments不能重写值,但命名参数可以
    • 递归时,用arguments.callee(指向拥有该对象的函数)来代替函数名,可以消除紧密耦合,但只能用于非严格模式。
  • this:执行函数对象(全局时为window)

隐性转换和显性转换

强制(显性)类型转换

强制类型转换主要是指通过String、Number和Boolean等构造方法手动转换成对应的字符串、数字和布尔值。

自动(隐性)类型转换

自动类型转换就是不需要人为强制的进行转换,js会自动将类型转换为需要的类型,所以该转换操作用户是感觉不到的,因此又称为隐性类型转换

数据的深浅拷贝

  • 浅拷贝(Shallow Copy) 只会将对象的各个属性进行依次复制,并不会进行递归复制,也就是说只会赋值目标对象的第一层属性。
  • 深拷贝(Deep Copy) 不同于浅拷贝,它不只拷贝目标对象的第一层属性,而是递归拷贝目标对象的所有属性。(两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性)

浅拷贝

赋值运算符(=)

只拷贝对象的引用值

首层拷贝实现

(只有第一层是深拷贝)

//1.Object.assign()
const obj2 = Object.assign({}, obj1);\\ES6,拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。
//2.... 展开运算符
const obj2 = [...obj1];只是对对象的第一层进行深拷贝
//3.Array.prototype.slice()
const obj2 = obj1.slice();
//4.Array.prototype.concat()
const obj2 = obj1.concat();

手写一个浅拷贝

function shallowClone(obj){
    let result=Array.isArray(obj)?[]:{}
    Object.keys(obj).forEach(element => {
        result[element]=obj[element]
    });
    return result;
}

深拷贝(*)

JSON.parse()和JSON.stringify() (对目标对象有要求)

const obj2 = JSON.parse(JSON.stringify(obj1));

缺点:

  • undefined、function,正则表达式类型以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时);
  • 它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object;
  • 当出现循环引用时会报错

递归(真正意义上的深拷贝)

递归中可能出现的问题:循环引用

  • 父级引用

这里的父级引用指的是,当对象的某个属性,正是这个对象本身,此时我们如果进行深拷贝,可能会在子元素->父对象->子元素...这个循环中一直进行,导致栈溢出。
解决办法:判断一个对象的字段是否引用了这个对象或这个对象的任意父级

  • 同级引用

假设对象obj有a,b,c三个子对象,其中子对象c中有个属性d引用了对象obj下面的子对象a。
解决办法:父级的引用是一种引用,非父级的引用也是一种引用,那么只要记录下对象A中的所有对象,并与新创建的对象一一对应即可。

手写一个深拷贝(*)

已经处理了相关边界及循环引用问题

function isObject(obj) { //判断obj是不是一个对象,且当obj为null时原样返回而不是返回{}
    return typeof obj === 'object' && obj != null;
}
function deepClone(obj, hash = new WeakMap()) {  //hash用于解决循环引用
    if (!isObject(obj)) return obj;
    if (hash.has(obj)) return hash.get(obj); // 查hash,如果当前obj已经存在则直接取拷贝过的值
    let result = Array.isArray(obj) ? [] : {} //对数组和对象进行区分
    hash.set(obj, result) //obj不存在时存入hash
    Object.keys(obj).forEach(element => { //遍历obj的key进行拷贝
            if (isObject(obj[element])) {
                result[element] = deepClone(obj[element], hash) //当key对应值仍为对象时,递归拷贝
            } else {
                result[element] = obj[element] //为基本数据类型则直接拷贝
            }
    });
    return result;
}

拷贝Symbol()的情况
将Object.keys(obj)遍历key值改变为:

  • 方法一:Object.getOwnPropertySymbols(...)
  • 方法二:
Reflect.ownKeys(...)
//等价于
Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))

拷贝原型链上数据的情况

  • for..in 进行遍历

问几个问题

Q1:为什么基础数据类型存在栈中,而引用数据类型存在堆中呢?

  • 堆比栈大,栈比堆速度快。
  • 基础数据类型比较稳定,而且相对来说占用的内存小。
  • 引用数据类型大小是动态的,而且是无限的。
  • 堆内存是无序存储,可以根据引用直接获取。

Q2:下面的代码会输出什么?

var a = { name: '前端开发' }
var b = a;
a = null;
console.log(b)

{ name: '前端开发' } null是基本类型,a = null之后只是把a存储在栈内存中地址改变成了基本类型null,并不会影响堆内存中的对象,所以b的值不受影响

**Q3:**从内存来看 null 和 undefined 本质的区别是什么?

  • 给一个全局变量赋值为null,相当于将这个变量的指针对象以及值清空,如果是给对象的属性 赋值为null,或者局部变量赋值为null,相当于给这个属性分配了一块空的内存,然后值为null, JS会回收全局变量为null的对象。
  • 给一个全局变量赋值为undefined,相当于将这个对象的值清空,但是这个对象依旧存在,如果是给对象的属性赋值 为undefined,说明这个值为空值

Q4:JS判断一下数据类型?

typeof (检测基本数据类型的最佳选择)

  • 对于基本类型,除 null 以外,均可以返回正确的结果。
  • 对于引用类型,除 function 以外,一律返回 object。
  • 对于 null ,返回 object。
  • 对于 function 返回 function。

instanceof (判断 A 是否为 B 的实例)

  • A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false
  • instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型,不同环境下不是同一个构造函数
  • Array.isArray() 方法 。该方法用以确认某个对象本身是否为 Array 类型,而不区分该对象在哪个环境中创建

constructor (构造函数)

[ ].constructor == Array   true   
" ".constructor == String   true
  • null 和 undefined 是无效的对象,因此是不会有 constructor 存在的,这两种类型的数据需要通过其他方式来判断。
  • 使用它是不安全的,因为contructor的指向是可以改变的

使用Object.prototype.toString.call(目前最优解)

能够生成固定的返回格式,进行截取得到数据类型,目前基本和引用类型全部支持

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

想了两种取type的方法

let type = Object.prototype.toString.call('111') 
let name = type.slice(8,Object.prototype.toString.call('111').length-1)//截取 
let name = type.match(/^\[object (\w+)\]$/)[1]//正则

Q5:什么样的数据值在判断时会被转换为false?

我们使用 Boolean 函数将类型转换成布尔类型,在 JavaScript 中,只有 6 种值可以被转换成 false,其他都会被转换成 true。

console.log(Boolean()) // false
console.log(Boolean(false)) // false
console.log(Boolean(undefined)) // false
console.log(Boolean(null)) // false
console.log(Boolean(+0)) // false  
console.log(Boolean(-0)) // false
console.log(Boolean(NaN)) // false
console.log(Boolean("")) // false

Q6:函数中的arguments是数组吗?若不是,如何将它转化为真正的数组?

typeof arguments=="object"
Object.prototype.toString.call(arguments)=="[object Arguments]"

不是,是类数组对象

var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }     
// 1. slice     
Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"]     
// 2. splice     
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"]      
// 3. ES6 Array.from     
Array.from(arrayLike); // ["name", "age", "sex"]      
// 4. apply     
Array.prototype.concat.apply([], arrayLike) // ["name", "age", "sex"]   
// 5. ES6扩展运算符     
[...arrayLike] // ["name", "age", "sex"]   

Q7:函数传参数是按值还是引用?数据类型或者对象类型都一样吗?

ECMAScript中所有函数的参数都是按来传递的,把函数外部的值复制给函数内部的参数,函数只能操作对象的属性和值,而不能操作对象本身。

  • 原始值:只是把变量里的值传递给参数,之后参数和这个变量互不影响,这个很好理解,不再赘述。
  • 引用值:对象变量里面的值是这个对象在堆内存中的内存地址,因此它传递的值也就是这个内存地址,这也就是为什么函数内部对这个参数的修改会体现在外部的原因了,因为它们都指向同一个对象。

巨人的肩膀

最后

欢迎纠错,看到会及时修改哒! 温故而知新,希望我们都可以保持本心,念念不忘,必有回响。

最后的最后

给自己的小组卖个安利,欢迎热爱前端的小姐妹们加入我们,一起学习,共同进步【有意请留言或私信,社畜搬砖不及时,但看到会立刻回复】 💗💗💗💗💗


本文使用 mdnice 排版