tt-up / fed-in-depth

经验、知识、笔记——让坚持学习成为一种习惯
6 stars 1 forks source link

【红宝书第4版读书笔记】04-变量、作用域、内存管理 #10

Open yuqingc opened 4 years ago

yuqingc commented 4 years ago

04 变量、作用域、内存管理

基本类型和引用类型

很多语言的 String 类型是引用类型,但是 ECMAScript 比较特殊

动态属性

函数传参

类型判断

注意 typeof 函数 会返回 "function" 是因为标准规定,如果一个对象实现了内部的 [[Call]] 方法就会返回 "function"。在老版本的 Chrome 和 Safari 中,因为正则表达式实现了 [[Call]],所以在这些浏览其内,typeof 正则表达式 会返回 "function"

执行上下文(context)与作用域链

作用域链延长

变量声明

变量查找

垃圾回收 (GC)

浏览器为了确定不再使用的变量,传统上有 2 种策略

Mark-and-Sweep

引用计数

现在主流浏览器已经不使用这种策略了

定义

循环引用的问题

当变量存在循环引用时,引用计数永远不会到 0,则变量永远不会被回收。如果函数多次调用,会造成大量的内存无法被释放,造成内存泄漏

function problem() {
  let objectA = new Object();
  let objectB = new Object();
  objectA.someOtherObject = objectB;
  objectB.anotherObject = objectA;
}

在这个例子中,objectAobjectB 互相引用,使得他们的引用计数都为 2。如果需要释放这两个变量,需要手动清除

myObject.element = null;
element.someObject = null;

老的浏览器,内置对象(如 DOM 和 BOM)对象,并非 JS 对象,而是 C++ 的 COM 对象,因此使用引用计数的策略,会存在内存泄漏的问题。后续浏览器把内置对象改为了 JS 对象,统一使用一套 GC 策略。

还有其他情况会造成内存泄漏,后面的内容会说。

性能优化

内存管理

使用 const 和 let 可以提升性能

因为使用 constlet 定义的变量的作用域是块级的,在代码块结束之后会被释放。而 var 只有在函数作用域结束后才释放。

Hidden Class 和 delete 操作

了解一下 V8 引擎的编译时使用的 Hidden Class 工具

function Article() {
  this.title = 'Inauguration Ceremony Features Kazoo Band';
}
let a1 = new Article();
let a2 = new Article();

上述代码的 a1a2 有相同的属性,所以在 V8 下,他们使用了同一个 hidden class。这样 V8 对其有优化,性能更好。

但是如果给 a2 添加了一个属性,而 a1 没有,则他俩的 hidden class 就不同了,这样性能也会不好

a2.author = 'Jake';

如果两个对象的属性一样,但是用 delete 删除了一个属性,也会导致 hidden class 不同,也会对性能造成不好的影响

function Article() {
  this.title = 'Inauguration Ceremony Features Kazoo Band';
  this.author = 'Jake';
  }
let a1 = new Article();
let a2 = new Article();
delete a1.author;

因此,为了性能优化,不要的变量可以赋值 null,不要用 delete 删掉。

内存泄漏

无用的变量仍在内存中,就叫内存泄漏。以下是常见的内存泄漏的情况及解决方法

全局变量

function setName() {
  name = 'Jake';
}

name 会被绑定在 window 上,而无法释放。因此要用 varletconst 来声明

Interval 函数引用的外部变量

let name = 'Jake';
setInterval(() => {
  console.log(name);
}, 100);

name 被回调函数引用,无法释放。

闭包

let outer = function() {
  let name = 'Jake';
  return function() {
    return name;
  };
};

即使调用完了 outername 仍被其返回的函数引用,导致无法被释放。

静态分配、Object Poll(对象池)

如果对性能有极致要求才这么做,一般情况用不到

举例

如果某个函数内部会创建局部的对象,而这个函数会被频繁调用,GC 就会更频繁地被触发

// 因为函数内部创建了新的对象
// 如果频繁调用这个函数
// 会导致 GC 更频繁的被调度
function addVector(a, b) {
  let resultant = new Vector();
  resultant.x = a.x + b.x;
  resultant.y = a.y + b.y;
  return resultant;
}

为了防止 GC 被频繁调度,可以维护一个对象池,提前把需要用的对象创建好,需要用到时,从对象池里面取,并且可以复用。这样就避免大量频繁的创建新的对象了。

贪婪算法,如果对象不存在,就创建一个新的,如果存在,就直接复用。

这样会导致内存一直增长,因此可以使用数组来管理对象池。但是使用数组时,也要注意,不要造成额外的垃圾回收

let vectorList = new Array(100);
let vector = new Vector();
vectorList.push(vector);

因为 JS 的数组是动态的。数组的长度原本是 100,但是 push 操作,会导致 JS 把原有的数组删掉,然后在内存中分配一个新的长度为 200 的数组。之前的数组的删除需要调用额外的 GC 操作。因此最开始就要创建长度足够长的数组。

上述方法是当你对性能优化有极致要求的时候才用到。一般用不到。了解一下就行了。