【知识体系】 - V8工作原理(1) 之 JS的内存机制

172 阅读6分钟

知识体系

  • 栈内存&堆内存
    • 语言类型
    • JS的数据类型分类
      • Undefined与Null的区别
      • Symbol的延伸
    • JS数据存储(堆/栈存储)
      • 闭包的变量存储
    • 深/浅拷贝
      • 赋值与浅拷贝的区别
    • 类型转换

栈内存&堆内存

语言类型

  • 强/弱

    • 弱类型语言:支持隐式类型转换的语言,如C/JS
    • 强类型语言:不支持隐式类型转换的语言
  • 动/静

    • 静态语言:在声明变量之前需要先定义变量的类型
    • 动态语言:在运行过程中需要检查数据类型的语言

【JS是一个动态的弱类型语言】

弱类型:不需要告诉数据类型,JS引擎在运行代码过程中自己计算
动态:意味着可以用同一个变量存储不同类型的数据

JS的数据类型

数据类型分为【基本数据类型】和【引用数据类型】

  • 基本数据类型:Null | Undefined | Boolean | Number(整数或浮点型) | Symbol | BigInt(大整数) | String

  • 引用数据类型: Object | Array | Function | RegExp | Date

Undefined与Null的区别

  • 概念上
    • undefined表示没有任何值

      • 没有值的return语句(return;),隐式返回undefined
      • 访问不存在的对象属性obj.iDontExit),返回undefined
      • 变量声明时没有初始化(let a),隐式初始化为undefined
    • null表示一个空对象指针,表示变量未指向任何对象

  • 表现形式
    • typeof
      typeof null // 'object'
      typeof undefined // 'undefined'
      
    • == 与 ===
       undefined == null // true
       undefined === null // false
       !!undefined === !!null // true
      
    • +运算与Number()
    let a = undefined + 1 // NaN
    let b = null + 1 // 1
    Number(undefined) // NaN
    Number(null) // 0
    
    null 转化为 number 时,会转换成 `0`
    undefined 转换为 number 时,会转换为 `NaN`
    
  • 其他
    • null是一个关键字,undefined是一个标识符
    意味着:
    let undefined = 'test' // 成立,但不推荐使用
    let null = 'test' // 报错
    
【稍作解释】
Undefined - 一个没有被赋值的变量产生的一个默认值,变量提升时的默认值
Number - 可以存储很大的整数和浮点数
BigInt - 存储一个巨大的,超过Number限制的整数
Symbol - 唯一且不可变的原始值,可以用来作为对象属性的键【解决属性名冲突的问题】。symbol的目的是去创建一个唯一的属性键,保证不会与其他代码中的键产生冲突
    

Symbol的延伸

Symbol的延伸】
// 创建一个Symbol类型的值
const mySymbol = Symbol();
console.log(mySymbol); // Symbol()

const myName = Symbol('Elio');
console.log(typeof myName); // Symbol

// 没有两个Symbol的值是相等的。
console.log(Symbol() === Symbole()); // false

// 对象属性
const person = {
    [myName] = 'Elio'
}
console.log(person[myName]) // Elio

【注意点】

1. typeof Null = Object (JS的一个bug,为了兼容以前版本,未做修复)
2. Object的value值有上述7中基本类型的数据组成
3. 原始类型(基本类型)和引用类型的区分 根本原因是在内存中存放的位置不一样(下面一节介绍他们在JS内存中的存放)

JS的数据存储

前言存放变量,存放复杂对象,存放常量(常量池)

栈内存

  • 先进后出,存放临时变量一片内存块
  • 栈顶进行元素的进出栈

元素存储

  • 栈内存存储各种基本类型的变量以及对象变量的指针

堆内存

  • 数据都存在内存中,无规则可言。

元素存储

  • 存储Obeject类型的变量。
  • 对象的指针地址存储在中,通过栈中的指针中进行获取查找。
  • 指针 & 值

栈/堆内存的优缺点

  • 大小分配:基本数据类型变量大小固定,故存放在栈中;引用类型变量大小不固定,故存放在堆中,自己申请大小。
  • 效率:引用类型数据的存储,需要将指针地址存放在栈中,堆内存也需分配空间,效率低于栈。
  • 内存回收栈内存中的变量在当前执行环境结束就会被销毁且回收;堆不会

【以上的优缺点可以回答一个问题】

问题:为什么会分“堆”和“栈”两个空间呢?
回答:
    栈太大 => 影响上下文切换效率 => 影响程序运行效率
    【所以】栈不能太大 => 主要存放一些基本类型的小数据
    【因为】引用类型的数据占用空间都比较大(存放指针地址&值)=> 使用堆空间进行存储
    【但是】堆的内存分配和回收会占用一定的时间

闭包与堆内存

  • 产生闭包的核心
    • 预扫描内部函数
    • 把函数内部引用的外部变量保存在堆内存
  • 闭包中的变量保存在堆内存中(这也可以解释,函数调用之后,闭包还能引用函数内部变量)

深/浅拷贝

  • 深/浅拷贝只针对引用数据类型的。
  • 浅拷贝:只复制对象的指针,不复制对象本身,新旧对象依然共享一个内存空间。
  • 深拷贝:创建一个一样的对象,与原来对象不共享内存空间。独立的!

浅拷贝实现方式

1. Object.assign()

// Object.assign()方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,返回目标对象。

// 创建一个源对象
var obj = {
    username: 'elio',
    userInfo: {
        age: 25,
        address: 'china'
    }
};

// 创建目标对象,使用Object.assign()进行对象的拷贝。
var targetObj = Object.assign({}, obj);
targetObj.username = 'elioCopy';
targetObj.userInfo.age = 26;
console.log(obj.username); // elio - 第一层的对象属性是深拷贝
console.log(obj.userInfo.age); // 26

2. Array.prototype.concat()

let arr = [1,2,{
    username: 'elio'
}];
let arrCopy = arr.concat();
arrCopy[2].username = 'elioCopy';
arrCopy[0] = 100;
console.log(arr); // [1,2,{username: 'elioCopy'}]

3.Array.prototype.slice()

let arr = [1,2,{
    username: 'elio'
}];
let arrCopy = arr.slice();
arrCopy[2].username = 'elioCopy';
arrCopy[0] = 100;
console.log(arr); // [1,2,{username: 'elioCopy'}]

【补充】Array的slice和concat不会修改原数组(针对字符串、数字、布尔值,对象是会被改变的),只会返回一个浅拷贝了原数组中的元素的一个新数组。

深拷贝的实现方式

1.JSON.parse(JSON.stringfy())

let arr = [1,2,{
    username: 'elio'
}];
let arrCopy = JSON.parse(JSON.stringfy(arr));
arrCopy[2].username = 'elioCopy';
arrCopy[0] = 100;
console.log(arr); // [1,2,{username: 'elio'}]

// 【原理】用JSON.stringfy()将对象转成JSON字符串,在用JSON.parse()把字符串解析成对象。

赋值与浅拷贝

  • 赋值:赋的其实是该对象在栈中的地址,不是堆中的数据。两个对象指向同一个内存空间,两个对象是联动的。
  • 浅拷贝:会创建一个新对象。如果属性是基本类型,则拷贝的是基本类型的值;如果属性是引用类型,则拷贝的是内存地址(其中一个对象改变这个地址,会有联动影响)

【总结】

和原数据是否指向同一对象第一层数据为基本类型原数据中包含子对象
赋值改变会使原数据一同改变改变会使原数据一同改变
浅拷贝改变不会使原数据一同改变改变会使原数据一同改变
深拷贝改变不会使原数据一同改变改变不会使原数据一同改变

JS类型转换

两个方法

  • typeof:查看变量的数据类型
typeof 3.14 // number
typeof 'elio' // string
typeof true // boolean

-------------------------

typeof null // object
typeof [1,2,3] // object
typeof NaN // number

// 使用typeof进行引用类型数据的查看,都会返回Object
// 对于这种情况,我们引入instanceof来清晰判断引用数据类型
  • instanceof:清晰判断引用类型对象
console.log([1,2,3] instanceof Array) // true
console.log(Function instanceof Function) // true

类型转换

  • 数字转字符串
    • String()
    • toString() : null&undefined除外(会报错)
String(123);
(123).toString();
  • 字符串转数字
    • Number()
    • parseFloat() - 返回浮点数
    • parseInt() - 返回整数
  • 自动转换类型
'5' + 1 // '51'
'5' - 1 // 4
'5' + null // '5null'
5 + null // 5 null转换为0