JavaScript 执行上下文
当一段 JavaScript 代码在运行的时候,它实际上是运行在执行上下文中。下面 3 种类型的代码会创建一个新的执行上下文:
window
对象。eval
函数: 指的是运行在 eval 函数中的代码,很少用而且不建议使用。每一个上下文在本质上都是一种作用域层级。每个代码段开始执行的时候都会创建一个新的上下文来运行它,并且在代码退出的时候销毁掉。JavaScript 引擎通过创建执行上下文栈(Execution Context Stack,ECS) 管理执行上下文。
创建阶段和执行阶段
当我们调用一个函数时,一个新的执行上下文就会被创建。一个执行上下文可分为创建阶段和执行阶段。我们用下面的代码分析一下两个过程:
let x = 10;
function timesTen(a){
return a * 10;
}
let y = timesTen(x);
console.log(y); // 100
创建阶段
当 JavaScript 引擎第一次执行代码时,它会创建全局执行上下文,在创建阶段会进行以下操作:
window
, Node.js 中的 global
(第一次)undefined
作为变量的初始值 this
对象指向,第一次执行代码 this
绑定在全局对象上。如果 JavaScript 引擎执行上面的代码:
执行阶段
代码执行阶段进行以下操作:
函数经过创建阶段和执行阶段,并在执行后返回结果。
如下面图所示:
垃圾回收是删除任何其他对象未使用的对象的过程。垃圾收集通常缩写为 "GC"。它是内存生命周期的一部分:
内存生命周期分为:
垃圾回收需要去寻找内存“不需要”的对象,这里涉及到引用概念。我们也可以简单理解为“链接”。一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。
引用计数垃圾回收
这是最初级的垃圾收集算法,只判断“对象有没有其他对象引用到它”。如果没有,对象就会被垃圾回收机制回收。
// 创建两个对象,一个变量 o1 和另一个被引用的属性 a
let o1 = {
a: {
b: 2
}
};
let o2 = o1; // o2 是 o1 的引用
如下图:
我们执行下面代码后:
o2 = 1;
o1 = null; // 可以被垃圾回收了
限制:循环引用
引用计数垃圾回收无法处理循环引用的问题。因为对象始终互相应用,形成循环,无法被回收。
function f() {
var o1 = {};
var o2 = {};
o1.a = o2; // o1 的属性 a 引用 o2
o2.a = o1; // o2 的属性 a 引用 o1
return "azerty";
}
f();
标记清除算法
这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。算法假定设置一个叫做根的对象,从根开始,找对象的引用,以此类推。循环引用的问题也得以解决。
整个标记清除算法大致过程就像下面这样:
除此之外,JavaScript 引擎还有一些优化处理:
在浏览器中,顶级作用域是全局作用域,即可以使用 var
定义一个全局变量。然而 Node.js 却不一样,它的顶级作用域不是全局作用域,var
声明的变量也是该模块的局部变量。
var foo = "foobar";
foo === window.foo; // true
Object.prototype.toString.call()
可以用于判断当前脚本运行的环境:
Object.prototype.toString.call(window); // "[object Window]"
Object.prototype.toString.call(global); // "[object global]"
因此,我们如下判断:
let isBrowser = typeof window !== "undefined" && ({}).toString.call(window) === "[object Window]";
let isNode = typeof global !== "undefined" && ({}).toString.call(global) == "[object global]";
JavaScript 是单线程
JavaScript 是单线程编程语言,意味着它一个时间点只能做一件事情,即“一心不可二用”。
JavaScript 引擎执行代码时,从文件的上面开始,到文件结束为止。它创建执行上下文、解析、在执行阶段将函数压入和弹出调用栈等。
如果函数花费太长时间执行,浏览器页面就会“卡死”。这对用户来说体验太糟糕了。我们可以模拟一个执行时间略长的函数
function task(message) {
// 模拟一段长时间
let n = 10000000000;
while (n > 0){
n--;
}
console.log(message);
}
console.log("Start script..."); // "Start script.."
task("Call an API"); // "Call an API"
console.log("Done!"); // "Done"
task
函数要遍历结束后,才会继续执行下面的 console.log("Done!")
。
事件循环
我们可以将需要长时间执行的代码放到回调函数稍后执行。
console.log("Start script...");
setTimeout(() => {
task("Download a file.");
}, 1000);
console.log("Done!");
// Start script...
// Done!
// Download a file.
之前提到 JavaScript 是单线程的,然而更准确的说,JavaScript 运行时一次只能做一件事情。
当我们调用 setTimeout()
时,或者向服务器发送一个请求,或者触发 DOM 事件,都是浏览器中 Web API 的一部分。
在我们上面的例子里,调用 setTimeout()
时, Web API 创建一个 1 秒后过期的定时器。
定时器到时间后,JavaScript 引擎将 task 放到的任务队列里面:
更详细的事件循环如下:
script
DOM 事件,如 mousemove
setTimeout
setInterval
queueMicrotask(f)
promise
小练习
给出下面代码的输出顺序:
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
let promise = new Promise((resolve) => {
console.log(3);
resolve();
})
.then((res) => {
console.log(4);
})
.then((res) => {
console.log(5);
});
console.log(6);
答案: 依次输出 1 3 6 4 5 2。按照顺序执行代码:
console.log
打印 1setTimeout
是 Web API,设置定时器,时间到后加入调用队列中new Promise()
中传递的函数立即执行,因此打印 3.then()
调用都加入任务队列 console.log
打印 6setTimeout
的任务加入任务队列。最后执行打印 2。内存泄漏指申请的内存执行完后没有及时的清理或者销毁,占用空闲内存,内存泄漏过多的话,就会导致后面的进程申请不到内存。因此内存泄漏会导致内部内存溢出。
一般是堆区内存泄漏,栈区不会泄漏。JavaScript 原始数据类型的值保存在栈中,引用数据类型保存在堆中。所以对象、数组等才会发生内存泄漏。
常见的内存泄漏的原因:
我们可以使用下面的方式进行调试:
打开 Chrome 浏览器 -> More Tools
-> Developer Tools
-> Performance/Memory
,一般先在 Performance
面板录制页面内存占用情况随时间变化的图像,对内存泄漏有个直观的判断,然后在 Memory
面板定位问题发生的位置。
这些场景可能存在内存泄漏隐患:
function foo(arg) {
bar = "this is a hidden global variable";
}
bar
被添加到 window
对象上了,如果 bar 指向一个巨大的对象或 DOM
节点,那就是安全隐患。
function getData() {
return "Hello World!";
}
var someResource = getData();
setInterval(function () {
var node = document.getElementById("Node");
if (node) {
node.innerHTML = JSON.stringify(someResource);
}
}, 1000);
如果后续 id
为 Node
的节点被移除了,定时器里的 Node
变量仍然持有其引用,导致游离的 DOM 子树无法释放。
var elements = {
button: document.getElementById("button")
};
function doStuff() {
button.click();
}
function removeButton() {
// button 是 body 的子节点.
document.body.removeChild(document.getElementById("button"));
// 因为 elements 对象中缓存了 DOM 节点引用,这里我们始终有对 id 是 button 的引用
}
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) console.log("hi");
};
theThing = {
longStr: new Array(1000000).join("*"),
someMethod: function () {
console.log(someMessage);
}
};
};
setInterval(replaceThing, 1000);
定义在 replaceThing
里的函数都实际使用了 originalThing
,那就有必要保证让它们都取到同样的对象,即使 originalThing 被一遍遍地重新赋值,所以这些(定义在 replaceThing 里的)函数都共享相同的词法环境。
只要变量被任何一个闭包使用了,就会被添到词法环境中,被该作用域下所有闭包共享。这是闭包引发内存泄漏的关键。
我们可以打开浏览器的开发者工具查看内存数据,选择如下图所示:
点击 Start 按钮后:
图中有两列:
可以看到图中蓝色的条均匀分布,这些蓝色条代表新的内存分配。这些新的内存分配是内存泄漏的候选对象,需要重点关注。除此之外,还可以通过对比内存变化等手段查出具体内存泄漏的原因。
javaScript 本地存储的方法我们主要讲述以下四种:
cookie
localStorage
sessionStorage
indexedDB
cookie
cookie
指某些网站为了辨别用户身份而储存在用户本地终端上的数据。是为了解决 HTTP 无状态导致的问题。
作为一段一般不超过 4KB 的小型文本数据,它由一个名称(Name)、一个值(Value)和其它几个用于控制 cookie 有效期、安全性、使用范围的可选属性组成。
localStorage
一个可被用于访问当前源的本地存储空间的 Storage 对象
可以进行以下操作:
// 增加数据
localStorage.setItem('myCat', 'Tom');
// 读取数据
let cat = localStorage.getItem('myCat');
// 移除单个数据
localStorage.removeItem('myCat');
// 移除所有数据
localStorage.clear();
localStorage
有两个缺点:
Cookie
一样设置过期时间sessionStorage
sessionStorage
和 localStorage
使用方法基本一致,唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage
将会删除数据。
indexedDB
MDN 对其的定义如下:
indexedDB
是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。
优点:
localStorage
同步操作性能更高,尤其是数据量较大时缺点:
应用
在了解了上述的前端的缓存方式后,我们可以看看针对不对场景的使用选择:
阅读量:888
点赞量:0
收藏量:0