javaScript执行上下文与作用域

91 阅读10分钟

变量或者函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。

注意:上下文在其所有代码都执行完毕后会被销毁,包含定义在它上面的所有变量和函数。【全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器】

1. 作用域

定义:作用域是变量、函数和对象的可访问性规则,决定了代码中哪些部分可以访问某个变量。作用域在代码编写的时候就已经确定了,与函数定义的位置相关,而非调用位置。

核心功能:隔离变量,避免命名冲突,并管理变量的声明周期。

全局作用域:定义在代码最外层,全局可访问。

函数作用域:函数内部定义的变量,仅在函数内有效。

块级作用域:通过 let/const 在{}代码块中定义的变量。

注意:

静态性:作用域在代码解析阶段确定后不可变,属于静态概念

2. 执行上下文

定义:执行上下文是代码执行时的动态环境,包含了当前代码运行所需的所有信息,如变量对象,作用域链,this 指向等。

核心功能:管理代码执行过程中的数据和流程

2.1. 全局执行上下文

全局上下文是最外层的上下文。【默认的最外层上下文,关联 window 对象】

定义: 全局上下文是 js 引擎在解析并执行全局代码时创建的默认执行环境。它是程序运行的唯一全局环境,与全局对象相关联绑定。

创建时机:

  1. 当 JavaScript 引擎首次加载脚本文件(如 HTML 中的 <script> 标签)时,立即创建 GEC,并将其推入 执行上下文栈(ECS) 的栈底;
  2. 单页应用中,全局执行上下文仅在页面生命周期内创建一次;
组件作用与内容
词法环境(LexicalEnvironment)存储全局变量、函数声明及 let/const声明。环境记录类型为 对象环境记录(Object Environment Record)
变量环境(VariableEnvironment)存储 var声明的变量,初始值为 undefined。与词法环境分离以实现块级作用域兼容。
作用域链(Scope Chain)仅包含全局词法环境的引用(outer: null),因全局为作用域链的最顶层。
this绑定指向全局对象(浏览器中为 window,Node.js 中为 global)。严格模式下为 undefined

2.1.1. 全局执行上下文的生命周期

全局执行上下文的生命周期分为创建阶段执行阶段

  1. 创建阶段
  • 创建全局对象:浏览器为 window;
  • 初始化词法环境和变量环境:var 变量赋值为 undefined,函数声明完成提升、let/const 变量标记为暂时性死区
  • 绑定 this 到全局对象;
  1. 执行阶段
  • 逐行执行全局代码,为变量赋值;
  • 如果遇到函数调用,创建新的函数执行上下文(FEC)并压入执行栈;
  • 全局代码执行完毕后,GEC 仍保留在栈中,直到页面关闭或脚本卸载;

生命周期对比

阶段全局执行上下文(GEC)函数执行上下文(FEC)
创建次数仅一次(页面生命周期内)每次函数调用均创建新上下文
作用域链仅自身词法环境(outer=null)包含自身词法环境 + 外层环境引用(闭包基础)
内存回收页面关闭时释放函数执行完毕后销毁

2.1.2. 关键性与规则

  1. 变量提升的根源
  • var 变量和函数声明在创建阶段被提前分配内存,导致在声明前可访问【var 为 undefined,函数可调用】;
  • let/const 因暂时性死区(TDZ)的限制,声明前访问会抛出错误。
  1. 全局对象与变量对象的统一性
  • 在全局上下文中,变量对象和全局对象是同一实体。例如var a = 10 等价于 window.a = 10
  1. 严格模式下的影响
  • 在严格模式下,全局执行上下文的 this 为 undefined,而非默认的全局对象。

2.1.3. 实例

  1. 变量提升和执行阶段赋值
console.log(a);
console.log(b);
var a=10;
let b=20;

在 GEC 创建阶段,使用 var 定义的变量,会在创建阶段提升到作用域的顶部并且初始化为 undefined,也就是说为 a 这个变量提前分配了内存,而 b 是使用 let 创建的,虽然也会提升,但是由于 TDZ 的限制,所以导致 b 保持未初始化状态,导致访问时行为差异,抛出错误。

  1. 全局对象与变量对象的统一性
var x = 100;
console.log(window.x); // 100(x 被挂载到 window)

全局 var 变量直接成为全局对象的属性。

  1. 全局作用域链与嵌套函数
function outer(){
	const y=200;
	function inner(){
		console.log(y)
	}
	inner();
}
outer();

inner的作用域链为inner-outer-global,全局上下文始终作为作用域链终点。

2.1.4. 特性

  • 唯一性:页面生命周期内仅存在一个 GEC。
  • 全局对象绑定:通过 window(浏览器)或 globalThis(跨环境)访问全局变量。
  • 变量提升规则var 与函数声明提前初始化,let/const 受 TDZ 限制。
  • 作用域链终点:所有函数作用域链最终指向全局词法环境

2.2. 函数执行上下文

每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。 在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。

接下来就来详细的看看函数调用的上下文:函数调用的上下文,也就是 this 的指向,主要是由调用方式决定的,具体的规则如下:

2.2.1. 直接调用

函数通过圆括号直接调用时,this 默认指向全局对象 window

function foo(){
	console.log(this)
}
foo()1

2.2.2. 对象方法调用

当函数作为对象的方式调用时,this 指向调用该方法的对象。

var o = {
		sayHi: function() {
				console.log('对象方法的this:' + this);
		}
}
o.sayHi();

2.2.3. 定时器/IIFE 调用

在定时器回调或立即执行函数中,this 指向 window

setTimeout(
	function(){
		console.log(this)
	},0)

2.2.4. 事件处理函数

DOM 事件处理函数中,this 指向触发事件的 DOM 元素

<button class="btn">点击</button>
const btn = document.querySelector('.btn')
btn.addEventListener('click',function(){
	console.log(this)
})

2.2.5. 显示绑定

通过 call()或者 apply()调用时,this 可被显示指定为第一个参数

2.2.6. 构造函数调用

使用 new 调用函数时,this 指向新创建的实例对象

2.2.7. 箭头函数

箭头函数的 this 继承自外层作用域,不受调用方式影响

2.3. Eval 执行上下文

eval 是 js 中的一个特殊函数,它可以执行传入的字符串代码。

2.3.1. 直接调用 eval 的上下文

当直接调用 eval 的时候,它会在当前调用上下文中执行代码:

let x=10;
function test(){
	let x=20;
	eval('console.log(x)')//访问局部变量,输出20
}
test()

2.3.2. 间接调用 eval 是上下文

当间接调用 eval 时,它会在全局上下文中执行代码:

let x=10;
function test(){
  let x=20;
  const evalAlias = eval;
  evalAlias('console.log(x)')//访问全局变量,输出10
}
test()

3. 扩展

3.1. this 绑定的四种方式

3.1.1. 默认绑定

默认绑定作用于函数直接调用的情况下,此时this指向全局对象,但严格模式下this指向undefined。

function foo () {
    console.log(this)
}
foo() // => window

3.1.2. 隐式绑定

this 指向它的调用者,也就是谁调用了这个函数,this 就指向谁。

function foo () {
	console.log(this)
}
const obj = {
	foo:foo
}
obj.foo()

3.1.3. 区分隐式绑定和默认绑定

查看是直接调用还是调用时有所依赖,如果时直接调用,那么就是默认绑定,但如果调用时有所依赖,则为隐式绑定

特殊的是由于全局变量的特殊性,foo()等价于 window.foo(),所以有两种解释方式:

  1. window调用了foo(),this指向window。隐式绑定
  2. foo是直接调用的,this默认绑定为window。默认绑定
  1. 检查调用方式
    • 若函数直接调用(如 foo()),应用默认绑定
    • 若函数通过对象属性调用(如 obj.foo()),应用隐式绑定
  1. 注意隐式绑定的丢失
    • 当隐式绑定的函数被赋值给变量作为参数传递时,可能丢失绑定对象退化为默认绑定
const bar = obj.foo;
bar(); // 输出:2(默认绑定到全局对象)[[5, 7]]

隐式绑定的丢失导致退化为默认绑定

核心区别:调用时是否有明确的上下文对象(如 obj.foo()

3.1.3.1. 优先级关系

隐式绑定的优先级高于默认绑定。但当隐式绑定丢失时,默认绑定会生效:

function foo() {
  console.log(this.a);
}
const obj = { a: 3, foo: foo };
const a = 2;

// 隐式绑定(优先级高)
obj.foo(); // 输出:3 [[2]]

// 默认绑定(隐式绑定丢失)
setTimeout(obj.foo, 100); // 输出:2 [[7]]
3.1.3.2. 导致隐式绑定丢失的原因

一、将对象方法赋值给变量后调用

当对象方法被赋值给变量或属性时,函数与原始对象的关系被切断,导致隐式绑定丢失。此时调用函数会应用默认绑定规则。

function foo() { console.log(this); }
const obj = { a: 1, foo: foo };
const bar = obj.foo; // 赋值操作,bar 仅是函数的引用
bar();

二、函数作为参数传递(回调函数)

当函数作为参数传递给其他函数时,隐式绑定的上下文可能丢失。尤其在回调函数或高阶函数中常见。

function foo() { console.log(this); }
function wrapper(func) { 
	func(); 
} // 接收函数参数并直接调用
const obj = { 
	a: 2, 
	foo: foo 
};
wrapper(obj.foo);

三、内置函数或异步回调中的隐式丢失

内置函数(如 setTimeoutsetInterval)或事件处理器中传入对象方法时,隐式绑定常丢失。

function foo() { console.log(this); }
	const obj = { a: 3, foo: foo };//隐式绑定

	setTimeout(obj.foo, 100);//隐式绑定丢失,导致隐式绑定退化为默认绑定

由于在函数执行时,会将上下文推入上下文栈,这时函数的调用是直接调用,并没有上下文对象,所以出发默认绑定

3.1.4. 显示绑定

显式绑定是 JavaScript 中明确指定函数调用时 this 值的一种方式,与隐式绑定(通过对象调用方法自动绑定)相对。当我们需要完全控制函数调用时的 this 指向时,显式绑定非常有用。

3.1.4.1. 三种显式绑定方法
3.1.4.1.1. call() 方法

语法func.call(thisArg, arg1, arg2, ...)

特点

  • 立即调用函数
  • 第一个参数指定 this
  • 后续参数作为函数的参数逐个传递

示例

function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`);
}

const person = { name: 'Alice' };

greet.call(person, 'Hello', '!'); // 输出: "Hello, Alice!"
3.1.4.1.2. apply() 方法

语法func.apply(thisArg, [argsArray])

特点

  • 立即调用函数
  • 第一个参数指定 this
  • 第二个参数是包含参数的数组(或类数组对象)

示例

function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`);
}

const person = { name: 'Bob' };
const args = ['Hi', '!!!'];

greet.apply(person, args); // 输出: "Hi, Bob!!!"
3.1.4.1.3. bind() 方法

语法func.bind(thisArg[, arg1[, arg2[, ...]]])

特点

  • 不立即调用函数,而是返回一个新函数
  • 永久绑定 this 值(无法再次更改)
  • 可以预先设置部分参数(柯里化)

示例

function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`);
}

const person = { name: 'Charlie' };
const boundGreet = greet.bind(person, 'Hey');

boundGreet('?'); // 输出: "Hey, Charlie?"
3.1.4.2. 显式绑定的常见用途
  1. 解决回调函数中的 this 丢失问题
const obj = {
  value: 42,
  getValueLater() {
    setTimeout(function() {
      console.log(this.value);
    }.bind(this), 1000);
  }
};

2. 借用方法

// 借用数组的slice方法处理类数组对象
function toArray() {
  return [].slice.call(arguments);
}

3. 实现函数柯里化

function multiply(a, b) {
  return a * b;
}
const double = multiply.bind(null, 2);
console.log(double(5)); // 10

4. 在类构造函数中绑定方法

class Button {
  constructor() {
    this.click = this.click.bind(this);
  }
  click() {
    console.log(this); // 总是指向Button实例
  }
}
3.1.4.3. 显式绑定 vs 隐式绑定
特性显式绑定隐式绑定
控制权开发者完全控制由调用方式决定
方法call/apply/bind对象方法调用
灵活性可以随时改变 thisthis 由调用者决定
使用场景需要精确控制 this常规对象方法调用时

显式绑定是 JavaScript 中管理 this 指向的强大工具,特别是在处理回调函数、事件处理程序和需要方法借用的场景中非常有用。

3.1.5. new 绑定

new 绑定是 JavaScript 中 this 绑定的四种方式之一,专门用于构造函数调用。当使用 new 关键字调用函数时,会创建一个全新的对象,并且 this 会绑定到这个新创建的对象上。

3.1.5.1. 基本原理

当使用 new 调用函数时,JavaScript 引擎会执行以下步骤:

  1. 创建一个全新的空对象
  2. 将这个新对象的 [[Prototype]] 链接到构造函数的 prototype 对象
  3. 将新创建的对象绑定到函数调用的 this
  4. 执行构造函数内部的代码(通常用于初始化对象)
  5. 如果构造函数没有显式返回对象,则自动返回这个新创建的对象
3.1.5.2. 代码示例
function Person(name, age) {
  // 这里的 this 指向新创建的对象
  this.name = name;
  this.age = age;
  this.introduce = function() {
    console.log(`Hi, I'm ${this.name}, ${this.age} years old.`);
  };
  // 不需要 return,会自动返回新对象
}

// 使用 new 调用构造函数
const alice = new Person('Alice', 25);
alice.introduce(); // 输出: "Hi, I'm Alice, 25 years old."

const bob = new Person('Bob', 30);
bob.introduce(); // 输出: "Hi, I'm Bob, 30 years old."
3.1.5.3. new 绑定的特点
  1. 自动创建新对象:不需要手动创建对象再赋值
  2. 自动绑定 this:构造函数内的 this 自动指向新实例
  3. 自动设置原型链:新对象的 __proto__ 指向构造函数的 prototype
  4. 自动返回新对象:除非构造函数显式返回另一个对象
3.1.5.4. 手动实现 new 操作符

理解 new 的工作原理可以通过手动实现一个类似的函数:

function myNew(constructor, ...args) {
  // 1. 创建一个新对象,并将其 [[Prototype]] 链接到构造函数的 prototype
  const obj = Object.create(constructor.prototype);
  
  // 2. 将 this 绑定到新对象并执行构造函数
  const result = constructor.apply(obj, args);
  
  // 3. 如果构造函数返回了一个对象,则返回该对象,否则返回新创建的对象
  return result instanceof Object ? result : obj;
}

// 使用示例
const charlie = myNew(Person, 'Charlie', 35);
charlie.introduce(); // 输出: "Hi, I'm Charlie, 35 years old."
3.1.5.5. 注意事项
  1. 忘记使用 new 的后果
const bad = Person('Oops', 99); // 忘记 new
console.log(bad); // undefined
console.log(name); // "Oops" (污染了全局作用域)

2. 构造函数返回对象的情况

function Car(model) {
  this.model = model;
  return { custom: 'object' }; // 显式返回对象会覆盖 new 创建的默认对象
}

const myCar = new Car('Tesla');
console.log(myCar); // { custom: 'object' } 而不是 Car 实例

3. 箭头函数不能用作构造函数

const Foo = () => {};
const bar = new Foo(); // TypeError: Foo is not a constructor
3.1.5.6. 实际应用场景
  1. 创建多个相似对象
function Product(name, price) {
  this.name = name;
  this.price = price;
  this.getInfo = function() {
    return `${this.name} - $${this.price}`;
  };
}

const p1 = new Product('Laptop', 999);
const p2 = new Product('Phone', 699);

2. 构建类层次结构(ES5 方式):

function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function() {
  console.log(`${this.name} makes a noise.`);
};

function Dog(name) {
  Animal.call(this, name); // 调用父类构造函数
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.speak = function() {
  console.log(`${this.name} barks.`);
};

const d = new Dog('Rex');
d.speak(); // "Rex barks."

new 绑定是 JavaScript 面向对象编程的基础,虽然在 ES6 中引入了 class 语法糖,但底层仍然基于这种原型继承和 new 绑定的机制。