持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情
JavaScript
0.1 + 0.2 === 0.3 ?
究竟是否相等,只需测试一下即可知晓:
console.log( 0.1 + 0.2 == 0.3);
这里输出的结果是 false,说明两边是不相等的,这是浮点数运算的精度问题导致的。
在 IEEE754 中,规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度、与延伸双精确度。像 ECMAScript 采用的就是双精确度,也就是说,会用 64 位来储存一个浮点数。
而计算器存储时会将浮点数转为二进制,但 0.1 转成二进制时是一个无限循环的数,这时就会有误差了。那么当我们用浮点数进行运算的时候,使用的其实是精度丢失后的数。
具体可以看冴羽老师的JavaScript 深入之浮点数精度 #155
所以实际上,这里错误的不是结论,而是比较的方法,正确的比较方法是使用 JavaScript 提供的最小精度值:
console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);
检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。这段代码结果就是 true 了。
声明变量和声明函数的提升有什么区别?
相同点:
只要变量/函数在代码中声明了,无论是在哪个位置声明的,js 引擎都会将它的声明放在范围作用域的顶部,即提升声明。
不同点:
- 变量声明提升:变量声明在进入执行上下文就完成了。
- 函数声明提升:执行代码之前会先读取函数声明,意味着可以把函数声明放在调用它的语句后面。
注意:
函数声明会覆盖变量声明,但不会覆盖变量赋值。例如:
同一个名称表示 a ,既有变量声明 var a ,又有函数声明 function a() {} ,不管二者声明的顺序如何,函数声明始终会覆盖变量声明,也就是说此时 a 的值是 function a() {} 。
如果在变量声明的同时初始化a,或是之后对a进行赋值,此时a的值是变量的值:
var a;
a = 1;
function a() {
return true;
}
console.log(a);
箭头函数与普通函数的区别
- 箭头函数比普通函数更加简洁
- 如果没有参数,就直接写一个空括号即可
- 如果只有一个参数,可以省去参数的括号;如果有多个参数,就用逗号分隔
- 如果函数体的返回值只有一句,可以省略大括号;如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字,最常见的就是调用一个函数:
let fn = () => void doesNotReturn();
- 箭头函数没有自己的 this 箭头函数不会创建自己的 this ,它只会在自己作用域的上一层继承 this 。所以箭头函数中的 this 的指向在它定义时就已经确定了,之后不会更改。
- call()、apply()、bind()等方法不能改变箭头函数中this的指向
var id = 'Global';
let fun1 = () => {
console.log(this.id)
};
fun1(); // 'Global'
fun1.call({id: 'Obj'}); // 'Global'
fun1.apply({id: 'Obj'}); // 'Global'
fun1.bind({id: 'Obj'})(); // 'Global'
- 箭头函数不能作为构造函数使用
- 箭头函数没有自己的 arguments
- 箭头函数没有 prototype
- 箭头函数不能用作 Generator 函数,不能使用 yeild 关键字
new 操作符具体干了什么?
- 创建了一个空对象
var obj = new object();
- 设置原型链
obj._proto_ = fn.prototype;
- 让 fn 的 this 指向空对象,并执行 fn 的函数体
var result = fn.call(obj);
- 判断fn的返回值类型,如果是值类型,返回obj。如果是引用类型,就返回这个引用类型的对象
if (typeof(result) == "object"){
fnObj = result;
} else {
fnObj = obj;}
原型链
当我们在访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就回去 prototype 里找这个属性,这个 prototype 又会有自己的 prototype ,于是就这样一直找下去,直到 Object._proto_ 为止,找不到就返回 undefined ,这个寻找的过程形成了链式查找,就是我们所说的原型链的概念。
闭包
什么是闭包
闭包就是在函数内部可以访问到其外部函数的参数和变量。之所以能访问到外部的参数和变量,是因为它是顺着作用域链向上查找的。
闭包的应用
- 维护函数内的变量安全,避免全局变量的污染。
- 维持一个变量不被回收。
- 封装模块
闭包的优缺点
优点:
- 减少全局环境的污染生成独立的运行环境
- 可以通过返回其他函数的方式突破作用域链
*缺点:
- 常驻内存会增大内存使用量,且有可能造成内存泄露。
- 闭包会影响脚本性能,包括处理速度和内存消耗。
什么是内存泄露
概念:不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak),会导致内存溢出。
内存溢出:指程序申请内存时,没有足够的内存供申请者使用。例如,给一块存储int类型数据的存储空间,但却存储long类型的数据,那么结果就是内存不够用,此时就会报错,即所谓的内存溢出。
常见的内存泄露:
- 意外的全局变量:①未定义的变量会在全局对象创建一个新变量;②通过 this 点方法在全局定义了一个新变量。
- 闭包引起的内存泄漏(闭包可以读取函数内部的变量,然后让这些变量始终保存在内存中。如果在使用结束后没有将局部变量清除,就可能导致内存泄露)。
- 没有清理 DOM 元素的引用(如在一个对象中定义了一个值为 DOM 的键值对)。
- 没有移除计时器或回调函数。
- 循环引用。
垃圾回收
什么是 GC:GC 即 Garbage Collection,程序工作过程中会产生很多 垃圾 ,这些垃圾时程序不用的内存,或者是之前用过了,以后不会再用的内存空间,而 GC 就是负责回收垃圾的。当然不是所有的语言都有 GC ,一般的高级语言里面会自带 GC ,比如 Java、Python、JavaScript 等,也有无 GC 的语言,比如 C、C++ 等,那这种就需要我们自己手动管理内存了,相对比较麻烦。
我们知道写代码时创建一个基本类型、对象、函数......都是需要占用内存的,但是我们并不关注这些,因为这是引擎为我们分配的,我们不需要显式手动的去分配内存。但是,如果当我们不再需要某个东西时会发生什么?JavaScript 引擎又是如何发现并清理它的呢?
基本思路:在 JavaScript 内存管理中有一个概念叫做 可达性 ,就是那些以某种方式可访问或者说可用的值,它们被保证存储在内存中,反之不可访问则需回收。所以思路便是根据可达性先确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。
至于如何回收,其实就是怎样发现这些不可达的对象(垃圾),并给予清理的问题。
回收策略:
- 引用计数:跟踪记录每个值被引用的次数,每次引用的时候加一,被释放的时候就减一,当一个值的引用次数变为零,就可以将其内存空间回收。
- 优点:①发现垃圾时立即回收;②能最大限度减少程序暂停,让空间不会有被占满的时候。
- 缺点:①无法回收循环引用的对象;②由于要对所有对象进行数值的监控和修改,资源消耗开销大。
- 标记清除:分为标记和清除两个阶段。标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
当变量进入执行上下文时,这个变量会被加上 存在于上下文中 的标记,此为标记阶段;当变量离开上下文时,也会被加上 离开上下文 的标记,此为清除阶段。于是当垃圾回收时就会销毁那些带标记的值并回收他们的内存空间。
Q:那么怎么给变量加标记呢?
A:当变量进入执行环境时,反转某一位(通过一个二进制字符来表示标记),又或者可以维护进入环境变量和离开环境变量这样两个列表,可以自由地把变量从一个列表转移到另一个列表,等等很多方法。
算法过程:
- 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为
0 - 然后从各个根对象开始遍历,把不是垃圾的节点改成
1 - 清理所有标记为
0的垃圾,销毁并回收它们所占用的内存空间 - 最后,把所有内存中对象标记修改为
0,等待下一轮垃圾回收
优点:
- ①解决了对象循环引用的问题;
- ②回收速度较快。
缺点:
- ①空间碎片化,因为清除那些被标记为离开上下文状态的变量时,是不管删除的这个位置的;
- ②不会立即回收垃圾对象,清除的时候程序是停止工作的。
- 标记整理:是标记清除的加强版。在标记和清除中间,添加了内存空间的整理,其实就是在执行清除阶段前,移动对象位置,使他们在地址上保持连续。
- 优点:相比较与标记清除回收策略,这个方法减少了碎片化的空间。
- 缺点:不会立即回收垃圾对象,清除的时候程序是停止工作的。
- 分代回收(V8对GC的优化):又分为新生代对象回收和老生代对象回收。
新生代对象回收
概念:新生代就是指存活时间较短的对象,例如:一个局部作用域中,只要函数执行完毕之后变量就会回收。
空间分布:新生代内存区分为两个等大小的空间,分别是 使用空间 From 和 空闲空间 To 。
回收过程:
- 首先会将所有活动对象存储于 From 空间,这个时候 To 空间是空闲状态。
- 当 From 空间使用到一定程度后就会对活动对象进行 标记整理 回收策略,将使用空间 From 变得连续。
- 然后将活动对象拷贝至 To 空间,就可以把 From 空间全部释放了,回收完成。
- 对 From 和 To 名称进行调换,继续重复之前的操作。
缺点:只能使用堆内存的一半。
老生代对象回收
概念:老生代就是指存活时间较长的对象,例如:全局对象,闭包变量数据。
什么情况下对象会出现在老生代空间中:
- 一轮GC之后还存活的新生代对象 就需要晋升。
- 在拷贝过程中,To 空间的使用率超过
25%,就将这次的活动对象都移动至老生代空间。
Q:为什么设置25%这个阈值?
A:当这次回收完成后,这个To空间会变为From空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。
在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工作分解为更小的模块,可以让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行。
清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象向一端移动,直到所有对象都移动完成然后清理掉不需要的内存。
宏任务和微任务
采纳 JSC 引擎的术语,我们把宿主发起的任务称为宏观任务,把 JavaScript 引擎发起的任务称为微观任务。
异步执行的顺序:
- 首先我们分析有多少个宏任务;
- 在每个宏任务中,分析有多少个微任务;
- 根据调用次序,确定宏任务中的微任务执行次序;
- 根据宏任务的触发规则和调用次序,确定宏任务的执行次序;
- 确定整个顺序。