前言
JavaScript作为一门灵活而强大的语言,其底层运行机制常常让开发者感到困惑。为什么变量有时能访问,有时不能?为什么this的指向会"飘忽不定"?本文将深入剖析JavaScript的三大核心机制------执行栈与执行上下文 、作用域与作用域链 、this指向,帮你彻底理解JavaScript的运行原理。
一、执行栈与执行上下文:代码执行的舞台
1.1 面试直击:谈谈你对JS执行上下文栈的理解
当面试官问这个问题时,他们想考察的是你对JavaScript引擎如何管理和执行代码 的理解深度。执行上下文栈是引擎追踪所有函数调用、管理执行流程的核心调度中心。
*什么是执行上下文?
简而言之,执行上下文是评估和执行 js 代码的环境的抽象概念。每当 js 代码在运行的时候,它都是在执行上下文中执行。
执行上下文的类型
js 中有三种执行上下文类型
- 全局执行上下文:这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事,创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
- 函数执行上下文:每当一个函数被调用时,都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每单一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
- eval 函数执行上下文:执行在 eval 函数内部的代码也会有它属于自己的执行上下文。
调用栈
调用栈是解析器(如浏览器中的 js 解析器)的一种机制,可以在脚本调用多个函数时,跟踪每个函数在完成执行时应该返回控制的点。(如什么函数正在执行,什么函数被这个函数调用,下一个调用的函数是谁)
- 当脚本要调用一个函数时,解析器把该函数添加到栈中并且执行这个函数。
- 任何被这个函数调用的函数会进一步添加到调用栈中,并且运行到它们被上个程序调用的位置。
- 当函数运行结束后,解释器将它从堆栈中取出,并且主代码列表中继续执行代码。
- 如果栈占用的空间比分配给它的空间还大,那么则会导致"栈溢出"错误
1.2 什么是执行上下文?
简单来说,执行上下文是评估和执行JavaScript代码的环境的抽象概念。所有的JavaScript代码都是在某个执行上下文中运行的。
1.3 执行上下文的三种类型
javascript
// 1. 全局执行上下文
var globalVar = "全局变量"; // 在全局上下文中
// 2. 函数执行上下文
function foo() {
var localVar = "局部变量"; // 在函数上下文中
console.log(localVar);
}
// 3. Eval函数执行上下文(不推荐使用)
eval("var evalVar = 'eval变量'");
全局执行上下文的特点:
-
有且仅有一个
-
创建时会做两件事:
- 在浏览器中创建全局对象
window - 将
this的值设置为这个全局对象
- 在浏览器中创建全局对象
1.4 执行栈(调用栈):后进先出的管理机制
执行栈是一种LIFO(后进先出)的数据结构,用于存储和管理代码执行期间创建的所有执行上下文。
javascript
function first() {
console.log('第一个函数开始');
second();
console.log('第一个函数结束');
}
function second() {
console.log('第二个函数开始');
third();
console.log('第二个函数结束');
}
function third() {
console.log('第三个函数开始');
console.log('第三个函数结束');
}
first();
// 执行栈的变化过程:
// 1. 全局上下文入栈
// 2. first()调用,first上下文入栈
// 3. second()调用,second上下文入栈
// 4. third()调用,third上下文入栈
// 5. third()执行完毕,出栈
// 6. second()执行完毕,出栈
// 7. first()执行完毕,出栈
1.5 执行上下文的生命周期
每个执行上下文都经历两个阶段:
创建阶段(此时函数被调用,但未执行内部代码):
javascript
function example(a, b) {
var c = "hello";
function d() {}
var e = function() {};
}
// 在创建阶段,执行上下文会进行以下准备工作:
exampleExecutionContext = {
// 1. 创建变量对象
variableObject: {
arguments: { 0: a, 1: b, length: 2 },
a: undefined, // 形参
b: undefined, // 形参
c: undefined, // 变量声明
d: pointer_to_function_d, // 函数声明
e: undefined // 变量声明
},
// 2. 建立作用域链
scopeChain: [...],
// 3. 确定this指向
this: window
}
执行阶段(逐行执行代码):
javascript
function example(a = 1, b = 2) {
console.log(c); // undefined(变量提升)
console.log(d); // function d() {}(函数提升)
var c = "hello";
function d() {
console.log("我是函数d");
}
var e = function() {
console.log("我是函数表达式");
};
console.log(c); // "hello"
console.log(d); // function d() {}
console.log(e); // function() { ... }
}
example();
重要规则:
- 函数声明会完全提升
- 变量声明会部分提升(只提升声明,不提升赋值)
- 函数声明的优先级高于变量声明
1.6 栈溢出错误
javascript
// 递归调用没有终止条件会导致栈溢出
function infiniteRecursion() {
infiniteRecursion(); // 不断创建新的执行上下文
}
// 调用会抛出:RangeError: Maximum call stack size exceeded
// infiniteRecursion();
二、作用域与作用域链:变量的可见性规则
2.1 面试题:谈谈你对作用域和作用域链的理解
作用域和作用域链决定了变量在何处以及如何被访问,这是JavaScript的基础概念。
什么是作用域?
作用域是在运行时代码中某些特定部分变量、函数和对象的可访问性,决定了代码区块中变量和其他资源的可见性。ES5 中只存在两种作用域:全局作用域和函数作用域,ES6 新增了块级作用域。
什么是作用域链?
当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止。
而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域链对符合访问权限的变量和函数的有序访问。
作用域链有一个非常重要的特征,那就是作用域中的值是在函数创建的时候,就已经被存储了,是静态的。 所谓静态,就是是作用域中的值一旦被确定了,永远不会变。函数可以永远不被调用,但是作用域中的值在函数创建的时候就已经被写入了,并且存储在函数作用域链对象里面。
2.2 什么是作用域?
作用域定义了变量、函数和对象的可访问性范围。
javascript
// 全局作用域
var globalVar = "我是全局变量";
function outer() {
// 函数作用域
var outerVar = "我是外部函数变量";
function inner() {
// 另一个函数作用域
var innerVar = "我是内部函数变量";
console.log(globalVar); // 可以访问
console.log(outerVar); // 可以访问
console.log(innerVar); // 可以访问
}
inner();
console.log(globalVar); // 可以访问
console.log(outerVar); // 可以访问
// console.log(innerVar); // 错误!无法访问内部变量
}
outer();
2.3 ES6的块级作用域
javascript
// ES5只有全局和函数作用域
function es5Example() {
if (true) {
var varVariable = "var声明的变量";
let letVariable = "let声明的变量";
const constVariable = "const声明的变量";
}
console.log(varVariable); // "var声明的变量" - 可以访问
// console.log(letVariable); // ReferenceError - 不能访问
// console.log(constVariable); // ReferenceError - 不能访问
}
// ES6的块级作用域
{
let blockScoped = "块级作用域变量";
const constant = "常量";
}
// console.log(blockScoped); // ReferenceError
// console.log(constant); // ReferenceError
2.4 作用域链
当访问一个变量时,JavaScript引擎会沿着作用域链从内到外查找:
javascript
var global = "全局";
function outer() {
var outer = "外层";
function inner() {
var inner = "内层";
// 查找顺序:inner → outer → global
console.log(inner); // "内层" - 在当前作用域找到
console.log(outer); // "外层" - 在父作用域找到
console.log(global); // "全局" - 在全局作用域找到
console.log(notExist); // ReferenceError - 找不到
}
inner();
}
outer();
2.5 自由变量和词法作用域
自由变量:在当前作用域未定义,需要去父作用域查找的变量。
javascript
var x = 10;
function foo() {
console.log(x); // x是自由变量
}
function bar() {
var x = 20;
foo(); // 输出10,而不是20!
}
bar();
// 这是因为JavaScript采用词法作用域(静态作用域)
// 函数的作用域在定义时就确定了,而不是调用时
2.6 作用域 vs 执行上下文
关键区别:
- 作用域 :函数定义时确定,静态不变
- 执行上下文 :函数调用时创建,动态变化
javascript
var color = "blue";
function getColor() {
console.log(this.color); // 执行上下文决定this
console.log(color); // 作用域决定color的值
}
var obj = {
color: "red",
getColor: getColor
};
// 同样的函数,不同的调用方式
getColor(); // this: window, color: "blue"
obj.getColor(); // this: obj, color: "blue"(作用域不变!)
三、this指向:动态绑定的上下文
3.1 this的本质
this关键字总是指向一个对象,具体指向哪个对象取决于函数的调用方式。
关于 this 的指向,有一种广为流传的说法就是"谁调用它,this 就指向谁"。
- 在函数体中,非显示或隐式地简单调用函数时,在严格模式下,函数内的 this 会被绑定到 undefined 上,在非严格模式下则会被绑定到全局对象 window/global 上。
- 一般使用 new 方法调用构造函数时,构造函数内的 this 会被绑定到新创建的对象上。
- 一般通过 call/apply/bind 方法显示调用函数时,函数体内的 this 会被绑定到指定参数的对象上。
- 一般通过上下文对象调用函数时,函数体内的 this 会被绑定到该对象上。
- 在箭头函数中,this 的指向是由外层(函数或全局)作用域来决定的。
3.2 this的绑定规则
1. 默认绑定(普通函数调用)
javascript
// 非严格模式
function showThis() {
console.log(this);
}
showThis(); // 浏览器中输出:Window对象
// 严格模式
"use strict";
function strictShowThis() {
console.log(this);
}
strictShowThis(); // undefined
2. 隐式绑定(方法调用)
javascript
var obj = {
name: "张三",
sayName: function() {
console.log(this.name);
}
};
obj.sayName(); // "张三" - this指向obj
// 隐式丢失的常见情况
var sayName = obj.sayName;
sayName(); // "" 或 undefined - this指向全局
// 回调函数中的隐式丢失
setTimeout(obj.sayName, 100); // this指向全局
3. 显式绑定(call/apply/bind)
javascript
function introduce(lang, year) {
console.log(`我叫${this.name},擅长${lang},${year}年开始编程`);
}
var person1 = { name: "Alice" };
var person2 = { name: "Bob" };
// call - 立即调用,参数逐个传递
introduce.call(person1, "JavaScript", 2015);
// 输出:我叫Alice,擅长JavaScript,2015年开始编程
// apply - 立即调用,参数数组传递
introduce.apply(person2, ["Python", 2018]);
// 输出:我叫Bob,擅长Python,2018年开始编程
// bind - 创建新函数,不立即调用
var boundIntroduce = introduce.bind(person1, "Java");
boundIntroduce(2020);
// 输出:我叫Alice,擅长Java,2020年开始编程
4. new绑定(构造函数调用)
javascript
function Person(name, age) {
// new调用时,this指向新创建的对象
this.name = name;
this.age = age;
this.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
}
var p1 = new Person("张三", 25);
p1.sayHello(); // "你好,我是张三"
5. 箭头函数的this
javascript
var obj = {
name: "对象",
regularFunc: function() {
console.log("普通函数this:", this.name);
setTimeout(function() {
console.log("setTimeout普通函数this:", this.name);
}, 100);
setTimeout(() => {
console.log("setTimeout箭头函数this:", this.name);
}, 200);
},
arrowFunc: () => {
console.log("箭头函数this:", this.name);
}
};
obj.regularFunc();
// 普通函数this: 对象
// setTimeout普通函数this: (全局的name,通常是空)
// setTimeout箭头函数this: 对象
obj.arrowFunc();
// 箭头函数this: (全局的name)
3.3 this绑定的优先级
javascript
// 优先级测试
function test() {
console.log(this.value);
}
var obj1 = { value: "obj1", test: test };
var obj2 = { value: "obj2", test: test };
// 1. 默认绑定(优先级最低)
test(); // undefined 或 全局的value
// 2. 隐式绑定
obj1.test(); // "obj1"
// 3. 显式绑定 > 隐式绑定
obj1.test.call(obj2); // "obj2"
// 4. new绑定 > 显式绑定
var boundTest = test.bind(obj1);
var newObj = new boundTest(); // undefined(new绑定覆盖了bind绑定)
3.4 常见应用场景
DOM事件处理
javascript
// HTML: <button id="btn">点击我</button>
document.getElementById('btn').addEventListener('click', function() {
console.log(this); // 指向被点击的button元素
});
// 箭头函数会改变this指向
document.getElementById('btn').addEventListener('click', () => {
console.log(this); // 指向定义时的上下文,通常是window
});
类中的this
javascript
class Counter {
constructor() {
this.count = 0;
// 需要绑定this,否则作为回调时会丢失
this.increment = this.increment.bind(this);
}
increment() {
this.count++;
console.log(this.count);
}
// 使用箭头函数自动绑定
decrement = () => {
this.count--;
console.log(this.count);
}
}
const counter = new Counter();
document.getElementById('inc').addEventListener('click', counter.increment);
document.getElementById('dec').addEventListener('click', counter.decrement);
3.5 call、apply、bind的经典应用
javascript
// 1. 借用数组方法处理类数组对象
function sum() {
// arguments是类数组对象,没有数组方法
return Array.prototype.reduce.call(arguments, function(total, current) {
return total + current;
}, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
// 2. 获取数组最大/最小值
var numbers = [5, 6, 2, 3, 7];
var max = Math.max.apply(null, numbers); // 7
var min = Math.min.call(null, ...numbers); // 2(使用扩展运算符)
// 3. 继承和构造函数链式调用
function Animal(name) {
this.name = name;
}
function Dog(name, breed) {
Animal.call(this, name); // 调用父类构造函数
this.breed = breed;
}
var myDog = new Dog("旺财", "金毛");
console.log(myDog.name); // "旺财"
// 4. 函数柯里化(Currying)
function multiply(a, b, c) {
return a * b * c;
}
var double = multiply.bind(null, 2); // 固定第一个参数
console.log(double(3, 4)); // 24 (2 * 3 * 4)
console.log(double(5, 6)); // 60 (2 * 5 * 6)
四、综合案例分析
4.1 经典面试题解析
javascript
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn(); // 1. 直接调用,this指向全局
arguments[0](); // 2. 通过arguments调用
fn.call(obj); // 3. 显式绑定到obj
}
};
obj.method(fn, 1);
// 输出:
// 10 (全局length)
// 2 (arguments.length)
// 5 (obj.length)
4.2 复杂场景分析
javascript
var name = "全局";
var person = {
name: "张三",
sayName: function() {
console.log("外层this:", this.name);
return function() {
console.log("内层this:", this.name);
};
},
sayNameArrow: function() {
console.log("外层this:", this.name);
return () => {
console.log("内层箭头函数this:", this.name);
};
}
};
// 情况1:普通函数嵌套
var func1 = person.sayName();
func1();
// 外层this: 张三
// 内层this: 全局
// 情况2:箭头函数解决
var func2 = person.sayNameArrow();
func2();
// 外层this: 张三
// 内层箭头函数this: 张三
// 情况3:使用bind绑定
var func3 = person.sayName().bind(person);
func3();
// 外层this: 张三
// 内层this: 张三
4.3 性能优化建议
- 避免过度嵌套:深度嵌套的作用域链会增加变量查找时间
- 合理使用闭包:不必要的闭包会延长变量的生命周期
- 注意内存泄漏:意外的全局变量和未清除的引用会导致内存泄漏
- 适当使用严格模式:避免意外的全局绑定和不可预测的this
五、总结
5.1 核心概念回顾
- 执行栈:管理执行上下文的后进先出数据结构
- 执行上下文:代码执行的环境,包含变量对象、作用域链和this
- 作用域:变量和函数的可访问范围,在定义时确定
- 作用域链:从当前作用域到全局作用域的链式结构
- this指向:由调用方式决定,遵循特定的绑定规则
5.2 记忆口诀
- 执行上下文:调用函数才创建,入栈出栈管流程
- 作用域链:内层可访外层,定义位置就决定
- this指向:谁调用就指谁,箭头函数看外层
- 绑定优先级:new > 显式 > 隐式 > 默认
5.3 实战建议
- 使用
const和let替代var,利用块级作用域 - 在回调函数中注意this绑定,使用箭头函数或bind
- 理解闭包原理,避免不必要的内存占用
- 掌握call/apply/bind的适用场景,灵活改变this指向
通过深入理解这三个核心概念,你将能够:
- 准确预测代码的执行结果
- 避免常见的this绑定错误
- 编写更高效、可维护的JavaScript代码
- 在面试中从容应对相关技术问题
JavaScript的运行机制虽然复杂,但只要掌握了这些核心概念,你就能从"知其然"进阶到"知其所以然",真正驾驭这门灵活而强大的语言。