关于数据类型及堆栈内存

130 阅读8分钟

一、数据类型转换

数据类型分类:

  1. 基本数据类型:Number String Boolean null undefined symbol bigint
  2. 引用数据类型:object function

1.1 其它数据类型转换为Number类型规则

  • 手动转换
    • Number([value])
    • ParseInt/ParseFloat([value])
  • 隐式转换(浏览器内部默认要先转换成Number再进行计算的)
    • isNaN([value])
    • 数学运算(+前后如果出现字符串就是字符串拼接,不属于数学运算)
    • 在 == 比较时,有些值需要转换为数字再进行比较
1.2.1 其他类型转换为数字规则
  • Boolen转换为数字
    • true -> 1
    • false -> 0
  • null 转换为数字
    • null -> 0
  • undefined 转换为数字
    • undefined -> NaN
  • 字符串转换为数字
    • 字符串中只有全是有效数字,才能正常转换 "12" -> 12 "000012" -> 12
    • 字符串中只要出现任何一个非有效数字字符,结果都为NaN "12px" -> NaN
  • 对象转换为数字,会先转换成字符串,再转换为数字
1.2.2 parseInt / parseFloat
  • parseInt([value]) 把value转换为数字(内核机制:需要把value先转换为字符串,然后从字符串左侧第一个字符开始查找,把找到的有效数字字符转换为数字,直到遇到一个非有效数字字符为止);如果value不是字符串先转换为字符串,再转换为数字
  • parseInt([value], [n]) 把[value]看做[n]进制的数据,最后转换为十进制;
    • 如果[n]不写,默认是10(即默认十进制),0和10一样,都是十进制

    • 特殊情况:如果字符串是以 0x 开头,则默认是16进制

    • [n] 范围 0~36之间,如果不在这个范围,则结果是NaN 2

1.2.3 关于NaN
  • 任何一个数和NaN相加,结果都是NaN

1.2 其它数据类型转换为String类型规则

  • 能使用的方法
    • toString()
    • String()
  • 隐式转换(一般都是调用toString())
    • 加号运算时候,如果某一边出现字符串,则是字符串拼接
    • 把对象转换为数字,需要先toString()转换为字符串,再转换为数字
    • 基于alert/confirm/prompt/document.write...这些方式输出内容,都是先转换为字符串,再输出内容的
  • 空数组转换为字符串 [] -> ""
  • 空对象转换为字符串(调用toString()) {} -> "[object Object]"(检测数据类型)

1.3 其它数据类型转换为Boolean类型规则

  • 基于以下方式可以把其他数据类型转换为Boolean
    • ! 转换为布尔值后取反
    • !! 转换为布尔类型
    • Boolean([value])
  • 隐式转换
    • 在循环或者条件判断中,条件处理的结果就是Boolean值类型
  • 规则:只有 0 NaN null undefined 空字符串"" 这五个值转换成布尔值为false,其余都为true

1.4 在==(弱比较)比较的过程中,数据转换的规则

  • 类型一样的几个特殊点:
    • {} == {} -> false 对象比较的是堆内存的地址
    • [] == [] -> false
    • NaN == NaN -> false
  • 类型不一样的转换规则
    • null == undefined -> true 但是换成===结果是false(因为数据类型不一致),剩下null/undefined和其他任何数据类型都不相等
    • 字符串 == 对象 要把对象转换为字符串
    • 剩下如果==两边数据类型不一致,都需要转换为数字再进行比较

二、数据类型面试题

面试题1

let res = 100 + true + 21.1 + null + undefined + "Hello" + [] + null + 9 + false;
console.log(res) // NaNHellonull9false

面试题2

console.log([] == false) // true
console.log(![] == false) // true

面试题3

var a = ?;(在a == 什么的情况下才能让以下判断成立)
if(a == 1 && a == 2 && a == 3){
    console.log(1);
}
var a = {
    i: 0,
    toString() {
        return ++this.i
    }
};
if (a == 1 && a == 2 && a == 3) {
    console.log('ok');
}

// 对象转换为字符串:valueOf() toString()
// a.toString() //=> Object.prototype.toString() 检测数据类型的
// 总体实现思路:重写对象原型上的toString方法,给一个初始值,然后让a的值每次累加

面试题4

let arr = [10.18, 0, 10, 25, 23];
arr = arr.map(parseInt);
console.log(arr); // [10, NaN, 2, 2, 11]
 
// 把parseInt当做一个回调函数,第一个参数是数组中的当前项,第二个参数是当前项的索引
/*
* parseInt(10.18, 0) => 10
* parseInt(0, 1) => NaN
* parseInt(10, 2) => '10' 二进制 转换为 十进制  2
* parseInt(25, 3) => '25' 三进制 转换为 十进制  2
* parseInt(23, 4) => '23' 四进制 转换为 十进制  11
* 输出 [10, NaN, 2, 2, 11]
*/

三、堆栈内存

3.1 关于堆栈内存
  • 浏览器会在计算机内存中开辟一块内存,专门用来执行JS代码的 => 栈内存:执行环境栈(ECStack)
  • EC(G) => 全局执行上下文,用来供全局代码执行
  • 形成上下文的目的:区分不同区域的代码执行
  • VO(G) => 全局变量对象:存储全局上下文中声明的变量
  • 当前区域的执行上下文形成后会进栈执行,有的执行上下文在代码执行完成之后会出栈
  • 在代码执行之前,先做词法分析及变量提升,然后代码执行;如果是函数执行,还要做初始化作用域链,形参赋值,初始化arguments,初始化this,变量提升等
  • 基本数据类型直接存储到栈内存;引用数据类型,浏览器会为它单独开辟一块内存:堆内存
  • 每一个堆内存都有个16进制堆内存地址,把键值对分别存储到堆中,把16进制地址放到栈内存中存储:方便后期变量的关联
  • 栈内存 还是 堆内存,都是浏览器从计算机中分配出来的内存,开辟的越多,电脑性能越慢 -> 性能优化:内存优化
    • 栈内存:代码执行 & 存储基本类型值
    • 堆内存:存储引用数据类型值
3.2 关于变量提升
  • 声明就是创建变量,定义就是给变量赋值
  • 变量提升:当前上下文代码执行之前,会把var/function声明或定义,带var的只声明,带function的声明+定义;如果遇到了{}(块级作用域),新老浏览器的表现不一致(兼容ES3、ES6)
  • 老浏览器:IE浏览器 <= IE10:
    • 不管{},还是一如既往地function声明+定义,而且也不会存在块级作用域
  • 新版本浏览器
    • {}块级作用域下的function只声明不定义
    • {}块级作用域中出现function/let/const会创建一个块级上下文

四、堆栈内存面试题

面试题1
let a = { n: 1 };
let b = a;
a.x = a = { n: 2 };
console.log(a.x); // undefined
console.log(b); // {n: 2}

堆栈内存.png

解析:

代码运行时,浏览器会为运行代码提供一个环境,叫做ECStack执行环境栈,即栈内存

ECStack执行环境栈中有EC(G)全局执行上下文,在EC(G)全局执行上下文中的执行变量赋值操作

因为变量赋值永远是,先创建值,再赋值给变量

基本数据类型在栈内存即全局上下文中,引用数据类型因为数据过于复杂,浏览器会为它专门开辟堆内存

所以本题,第一步let a = { n: 1 };,浏览器会先开辟一个堆内存,把键值对n: 1存储到这个堆内存中,堆内存有个十进制的内存地址为AAAFFF000,变量a指向地址AAAFFF000

第二步let b = a;, 变量b也指向a的内存地址,即 b = AAAFFF000

第三步a.x = a = { n: 2 };,由于带成员访问的要优先处理,即此行可拆解为 a.x = {n:2}a = {n:2},浏览器又开辟一个堆内存,把键值对{n: 2}存储到这个堆内存中, 此堆内存有个新的地址为AAAFFF111;

  • a.x = {n:2}: 在a即AAAFFF000中,创建属性x, 属性值为AAAFFF111
  • a = {n:2}: a又重新指向内存地址AAAFFF111(即原内存地址AAAFFF000已不存在)

此时 a 指向内存地址 AAAFFF111,其中只有键值对 {n: 2},所以a.x 为undefined b 也为 AAAFFF111,即 {n: 2}

面试题2
var a = 0;
if (true) {
    a = 1;
    function a() { };
    a = 21;
    console.log(a);
}
console.log(a); // 21 1

解析:

  • 如果是<=IE10版本浏览器:
    1. 变量提升:function a() { };
    2. a = 0
    3. a = 1
    4. a = 21
    5. 输出 21 21
  • 如果是新版本浏览器:

变量提升-块级作用域.png

解析:

var a = 0; 首先,在EC(G)全局执行上下文进行变量提升,声明var a 及 function a; 在VO(G)全局变量对象中,把0赋值给a

if (true) { } -> 浏览器开辟了一个块级作用域EC(BLOCK)(私有作用域),里面放置私有变量AO(BLOCK)

在块级作用域中,同样先进行变量提升:首先声明+定义function a(){},即浏览器开辟的一个函数a的堆内存,并把a指向这个堆内存地址

然后按照顺序执行代码:a = 1; a进行重新赋值,原内存地址舍弃 function a() { }; ,此时函数a已经在块级作用域下进行过变量提升,不再执行,但是遇到这行代码后,会把代码之前所有对a的操作映射给全局一份,后面的则不会再处理了(认为后面的都是自己私有的了),即此时,全局下的a也指向了1

继续执行代码 a = 21;,即私有域中,21又赋值给了a

所以此时输出,21 (私有域) 1(全局)

面试题3
var x = 1;
function func(x, y = function anonymous1() { x = 2 }) {
    x = 3;
    y();
    console.log(x);
}
func(5)
console.log(x);
//  2 1 

闭包.png

var x = 1;
function func(x, y = function anonymous1() { x = 2 }) {
    var x = 3;
    y();
    console.log(x);
}
func(5)
console.log(x);
//  3 1