JavaScript 高阶知识:从 V8 引擎到闭包与 this 绑定
一、V8 引擎工作原理
JS 源码 │ ▼ 解析器 Parser │ ▼ AST 抽象语法树 │ ▼ 解释器 Ignition │ ▼ 字节码 Bytecode │ ├─ 不是热点代码 → 直接执行字节码 │ └─ 是热点代码 → 优化编译器 TurboFan │ ▼ 高效机器码 │ (某些情况下) └→ 反优化回到字节码
简单来说:源码 → AST → 字节码 → 热点代码会被优化成机器码,非热点直接执行字节码,优化后的机器码如果条件不满足还会退回到字节码重新执行。
预解析(Preparse)
-
立即执行的代码 → 完整解析和编译
-
函数内部嵌套的函数(暂时不会执行) → 只进行预解析,仅检查语法错误,不生成完整 AST
-
只有当内层函数真正被调用时,才会完整解析
function outer() {
// outer 被完整解析
function inner() {
// inner 初始只做预解析
console.log('Hello');
}
inner(); // 这里才触发 inner 的完整解析
}
因为大部分页面中的函数并不会在首屏就被调用,这种设计能显著减少初始加载时间。
二、全局代码执行过程
JavaScript 代码执行分两个阶段:编译阶段和执行阶段,整体过程如下图所示:
编译阶段: 创建全局对象 GO │ ▼ 扫描 var 和 function 声明 │ ├─ var 变量 → 提升为 undefined │ └─ function 声明 → 直接提升为函数对象 ↓(编译阶段结束后进入执行阶段) 执行阶段: 逐行执行代码 │ ├─ 对变量进行实际赋值 │ └─ 调用函数时 → 创建函数执行上下文
关于 VO、AO、GO 的简单说明
-
变量对象 (VO, Variable Object):每个执行上下文都有一个变量对象,用来存储该上下文中的变量、函数声明等。编译阶段创建的就是 VO。
-
全局对象 (GO, Global Object):全局执行上下文的 VO 就是全局对象。在浏览器中是
window,在 Node.js 中是global。 -
活动对象 (AO, Activation Object):当函数执行时,它的变量对象被激活,称为活动对象。AO 可以理解为函数执行上下文中“活”的 VO,包含参数、局部变量等。
console.log(window.window.window === window); // true // GO 的 window 属性指向 GO 自身,形成循环引用
三、作用域链与变量查找
3.1
function foo() {
var a = b = 10;
// 等价于:
// var a = 10; ← a 被 var 声明,是局部变量
// b = 10; ← b 无任何声明关键字,成为全局变量
}
foo();
console.log(a); // ReferenceError: a is not defined
console.log(b); // 10 ← b 泄漏到了全局!
b = 10前面没有var / let / const,JS 引擎沿着作用域链向上一直找到全局都没找到,最终在全局对象上创建了b属性。
3.2 作用域链查找规则
当查找一个变量时,JavaScript 引擎会:
-
从当前作用域开始查找;
-
如果找不到,就向上一层作用域查找;
-
一直向上,直到全局作用域;
-
如果全局也没有:
-
非严格模式下,会在全局自动创建一个同名变量(容易产生意外全局变量);
-
严格模式下,抛出
ReferenceError。
-
四、内存管理
GC 垃圾回收算法
主要有两种垃圾回收算法:
-
引用计数:每个对象记录被引用的次数,引用次数为 0 时立即回收。但缺点是无法处理循环引用(两个对象互相引用,即使不再被使用也无法回收)。
-
标记清除(Mark-Sweep):从根对象(如
window/global)出发,遍历并标记所有能到达的对象,然后清除未被标记的对象。即使对象间存在循环引用,只要从根出发不可达,也会被回收。
V8 使用的是标记清除算法,因此可以很好地解决循环引用问题。
两个互相引用的对象,只要从根对象出发无法被访问到,都会被标记清除算法回收——这正是它优于引用计数的地方。
五、闭包
5.1 什么是闭包?
闭包 = 函数 + 该函数能够访问的外部自由变量。 JavaScript 中两种常见产生方式:
// 方式一:函数作为返回值
function createCounter() {
let count = 0; // 自由变量
return function() {
return ++count; // 内部函数引用外部变量
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
// 方式二:函数作为参数
function log(fn) { console.log(fn()); }
function outer() {
const msg = 'Hello Closure';
log(function() { return msg; }); // 保留对 msg 的访问
}
outer(); // 'Hello Closure'
5.2 闭包的内存泄漏问题
function foo() {
const largeData = new Array(10000000).fill('1'); // 大内存
const name = 'hello';
return function bar() {
console.log(name); // 只用到了 name
};
}
const fn = foo();
fn();
// foo 的整个活动对象(包含 largeData)可能无法被回收
闭包会导致外层函数的活动对象无法被正常回收,因为内层函数通过 [[Scope]] 属性始终保持着对它的引用。在上面的例子中,bar 只使用了 name,但整个 foo 的作用域都被保留了下来,包括那个巨大的 largeData 数组,从而造成内存浪费。
现代 V8 会分析闭包实际使用了哪些外部变量,未引用的变量会被回收。但为了代码健壮性,建议手动释放。
5.3 解决内存泄漏
fn = null; // 释放对 bar 的引用 foo = null; // 释放对 foo 的引用,整个作用域即可被 GC 回收
V8 中的小整数(Smi)只占 4 字节,利用指针标记技术直接编码整数值,非常高效。
六、this 指向
核心原则:this 的指向与函数在何处定义无关,只与函数如何被调用有关。
6.1 全局作用域下的 this
console.log(this); // 浏览器: window Node.js: {}(module.exports)
6.2 四大绑定规则及优先级
JavaScript 中 this 的指向由以下规则决定,优先级从高到低为:
-
new 绑定(最高) 当函数通过
new调用时,this指向新创建的那个对象。function Person(name) { this.name = name; } const p = new Person('V8'); // this → 新对象 p -
显式绑定 使用
call、apply或bind手动指定this指向的对象。function greet() { console.log(this.name); } greet.call({ name: 'JS' }); // this → { name: 'JS' } -
隐式绑定 当函数以
obj.method()的形式调用时,this指向调用该方法的对象。const obj = { bar() { console.log(this); } }; obj.bar(); // this → obj -
默认绑定(最低) 普通函数调用,非严格模式下
this指向全局对象(浏览器中是window),严格模式下this为undefined。function foo() { console.log(this); } foo(); // 非严格:window,严格:undefined
6.3 常见内置函数的 this
// setTimeout 普通函数指向 window
setTimeout(function() { console.log(this); }, 100); // window
// 事件监听指向触发的 DOM 元素
button.addEventListener('click', function() {
console.log(this); // button 元素
});
// forEach / map 等可传入第二个参数绑定 this
[1, 2, 3].forEach(function(item) {
console.log(item * this.multiplier);
}, { multiplier: 2 }); // 2, 4, 6
6.4 this规则之外
-
忽略显示绑定:显示绑定传入null/undefined,自动将this绑定成全局对象。
-
间接函数引用 : eg.
(obj2.bar=obj1.foo)()会将其看作一个独立函数调用。
更多推荐
所有评论(0)