如何理解JavaScript中的执行上下文

什么是执行上下文

执行上下文是 JavaScript 中一个重要的概念,它是在代码执行期间用于管理和执行代码的环境。每当 JavaScript 代码在运行时,都会创建一个执行上下文,并将其压入执行上下文栈中。
JavaScript中有三种执行上下文:
全局上下文:这是默认的上下文,任何不在函数内部的代码都再全局上下文中。在浏览器的情况下一般指向window。
函数上下文:每当应该函数被调用时,就会创建一个指向上下文。每个函数都有自己的执行上下文。
Eval:在执行eval函数内部的代码也有自己的上下文。由于平时用的不多,就不深入讨论。

执行上下文的生命周期:创建阶段执行阶段回收阶段

创建执行上下文包含以下三个重要的组成部分:

  1. 变量对象
    在执行上下文中,会创建一个变量对象,用于存储变量、函数声明和函数参数。变量对象是一个存储变量和函数声明的容器,它在代码执行过程中被用来查找变量和函数。变量对象的创建过程被称为变量提升,即在代码执行之前,JavaScript 引擎会将变量声明和函数声明提升到当前执行上下文的顶部,从而在整个执行上下文中都可以访问。

  2. 作用域链
    作用域链是一组连接到当前执行上下文的变量对象的列表。它用于解析变量标识符的访问权限。当查找一个变量时,JavaScript 引擎首先在当前执行上下文的变量对象中查找,如果找不到,就会继续向上级执行上下文的变量对象中查找,直到找到该变量或者达到全局执行上下文的变量对象为止。

  3. this 值
    this 值是一个指向对象的引用,它指定了当前执行上下文中的 this 关键字的指向。this 的指向根据执行上下文的类型而变化,例如在函数调用中,this 指向调用函数的对象;而在全局执行上下文中,this 指向全局对象(浏览器环境中为 window)。
    看个例子:

var name = "window";
let hello = {
    name:"hello",
    helloThis: function() {
        console.log(this.name);
    }
}
  
hello.helloThis(hello);// window -> hello -> helloThis。由 hello 调用

let ht = hello.helloThis;
ht();//window -> helloThis,由 window 调用

这段代码会打印以下结果:

  1. hello.helloThis(hello)
    当调用 hello.helloThis(hello) 时,helloThis 方法被当做普通函数调用,它的执行上下文是 hello 对象。在 hello 对象中,有一个名为 name 的属性,所以 this.name 指向 hello 对象的 name 属性,输出结果为 “hello”。

  2. ht()
    let ht = hello.helloThis; 这行代码中,将 helloThis 方法赋值给了变量 ht,此时 ht 是一个函数。当调用 ht() 时,这个函数被当做普通函数调用,它的执行上下文是全局环境(即全局对象 window,浏览器环境中)。在全局环境中,有一个名为 name 的全局变量,因此 this.name 指向全局变量 name,输出结果为 “window”。

需要注意的是,this 的指向是根据函数的调用方式和上下文来决定的:

  • 当通过对象的方法调用函数(例如 hello.helloThis()),函数内部的 this 指向调用该方法的对象(在这里是 hello 对象)。
  • 当将对象的方法赋值给变量,并通过变量调用函数(例如 let ht = hello.helloThis; ht()),函数内部的 this 默认指向全局对象(浏览器环境中是 window)。在严格模式下,默认绑定的情况下 thisundefined。使用箭头函数也可以继承外层函数的 this 值。

变量提升和函数提升

在JavaScript中,使用var 定义的变量会被提升到该作用域的最顶部,这意味着先使用后赋值不会报错,比如有下面的例子:

console.log(a); // undefined,定义但未被赋值
var a = '123';
console.log(a); // 123, 已赋值

函数提升和变量提升是一个意思,但是使用函数声明的函数才会被提升,而函数表达式就不能。比如

sayHello(); // 输出 "Hello"

function sayHello() {
  console.log('Hello');
}

fun(); // 会报错,不是函数
var fun = function(){
console.log("hello")
}

作用域链

作用域链是 JavaScript 中用于解析变量标识符的访问权限。当在代码中引用一个变量时,JavaScript 引擎会按照作用域链的顺序来查找该变量的值。

在 JavaScript 中,作用域是变量和函数可访问的范围。每个函数都会创建一个新的作用域,而全局代码则位于全局作用域中。作用域链是一种连接了所有父级作用域的链式结构,它按照词法层次(代码在源代码中的位置)进行组织。

当在当前作用域中无法找到某个变量时,JavaScript 引擎会顺着作用域链向上查找,直到找到该变量或者到达全局作用域为止。这样的过程称为作用域查找。

举个例子:

function outerFunction() {
  const outerVar = 'I am from outerFunction';

  function innerFunction() {
    const innerVar = 'I am from innerFunction';
    console.log(innerVar); // 输出 "I am from innerFunction"
    console.log(outerVar); // 输出 "I am from outerFunction"
    console.log(globalVar); // 输出 "I am a global variable"
  }

  innerFunction();
}

const globalVar = 'I am a global variable';
outerFunction();

这段代码中,定义了三个函数:outerFunctioninnerFunction 和全局代码。outerFunctioninnerFunction 分别创建了它们自己的作用域。当 innerFunction 在执行时,它会在自己的作用域中查找变量 innerVar,如果找不到,就会向上级作用域(即 outerFunction 的作用域)继续查找,直到找到 innerVar

同样,当 innerFunction 查找变量 outerVar 时,它会先在自己的作用域中查找,找不到时就会向上级作用域(即全局作用域)继续查找,直到找到 outerVar

在全局作用域中定义的变量 globalVar 可以在任何函数内部访问,因为全局作用域是所有函数作用域的顶级父级。

this指向

理解 this 指向在 JavaScript 中非常重要,因为它决定了函数在执行时的上下文,即当前函数所属的对象或环境。this 的值根据函数的调用方式和上下文而变化,下面介绍几种常见情况:

  1. 默认绑定:如果函数是独立调用的,即没有通过任何对象来调用,那么 this 默认指向全局对象(浏览器环境中是 window,Node.js 环境中是 global)。在严格模式下,默认绑定的情况下 thisundefined

  2. 隐式绑定:如果函数通过对象的属性调用,那么 this 将指向该对象。

const obj = {
  name: 'John',
  sayHello: function() {
    console.log(`Hello, ${this.name}!`);
  }
};

obj.sayHello(); //  "Hello, John!"
  1. 显式绑定:可以通过 call()apply()bind() 方法来显式指定函数执行时的 this
function sayHello() {
  console.log(`Hello, ${this.name}!`);
}

const person = { name: 'Alice' };

sayHello.call(person); //  "Hello, Alice!"
sayHello.apply(person); // "Hello, Alice!"

const boundFunction = sayHello.bind(person);
boundFunction(); //  "Hello, Alice!"
  1. new 绑定:当使用 new 关键字调用构造函数时,会创建一个新的对象,并将该对象作为 this 绑定到构造函数。
function Person(name) {
  this.name = name;
}

const person = new Person('Alice');
console.log(person.name); // Output: "Alice"
  1. 箭头函数:箭头函数没有自己的 this 绑定,它会继承外层函数的 this 值。
const obj = {
  name: 'John',
  sayHello: function() {
    setTimeout(() => {
      console.log(`Hello, ${this.name}!`);
    }, 1000);
  }
};

obj.sayHello(); // "Hello, John!" (1秒后输出)

理解 this 指向有助于正确处理函数中的上下文和作用域,确保代码的正确执行。在不同的情况下,要注意 this 的指向,避免出现错误的结果。

var let const

在执行上下文中,varletconst 在声明变量时有一些区别。

  1. var 声明:
    • 在全局作用域下用 var 声明的变量会成为全局对象的属性。
    • 在函数内部用 var 声明的变量会成为函数作用域中的局部变量。
    • var 声明的变量会存在变量提升,即在函数或作用域内的任意位置声明都会在代码执行之前被处理。
console.log(a); // undefined,变量提升,a 被声明但未赋值
var a = 10;
console.log(a); // 10
  1. let 声明:
    • let 声明的变量有块级作用域,即在 {} 内声明的变量只在该块中有效,不会影响外部的同名变量。
    • let 声明的变量不会存在变量提升,它只在声明语句处开始生效。
console.log(b); // ReferenceError: b is not defined
let b = 20;
console.log(b); // 20
  1. const 声明:
    • const 声明的变量也有块级作用域,和 let 的作用域规则一样。
    • const 声明的变量必须在声明时进行初始化,并且不能再被重新赋值,但其指向的对象是可以修改的(即对象属性的修改不会报错)。
const c = 30;
c = 40; // 类型错误: Assignment to constant variable.

总结:

  • var 声明的变量具有函数作用域和变量提升,可以在声明前使用,并且可能会污染全局命名空间。
  • letconst 声明的变量具有块级作用域,不会进行变量提升,并且在声明前使用会报错。
  • const 声明的变量一旦被赋值后就不能再重新赋值,但它指向的对象是可以修改的。

由于 letconst 具有块级作用域,能更好地控制变量的作用范围,推荐在新的代码中优先使用 letconst 来声明变量。只有在特定情况下需要函数作用域或变量提升时,才使用 var 声明。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值