执行上下文是 JavaScript 中一个重要的概念,它是在代码执行期间用于管理和执行代码的环境。每当 JavaScript 代码在运行时,都会创建一个执行上下文,并将其压入执行上下文栈中。
JavaScript中有三种执行上下文:
全局上下文:这是默认的上下文,任何不在函数内部的代码都再全局上下文中。在浏览器的情况下一般指向window。
函数上下文:每当应该函数被调用时,就会创建一个指向上下文。每个函数都有自己的执行上下文。
Eval:在执行eval函数内部的代码也有自己的上下文。由于平时用的不多,就不深入讨论。
执行上下文的生命周期:创建阶段—执行阶段—回收阶段;
创建执行上下文包含以下三个重要的组成部分:
变量对象:
在执行上下文中,会创建一个变量对象,用于存储变量、函数声明和函数参数。变量对象是一个存储变量和函数声明的容器,它在代码执行过程中被用来查找变量和函数。变量对象的创建过程被称为变量提升,即在代码执行之前,JavaScript 引擎会将变量声明和函数声明提升到当前执行上下文的顶部,从而在整个执行上下文中都可以访问。
作用域链:
作用域链是一组连接到当前执行上下文的变量对象的列表。它用于解析变量标识符的访问权限。当查找一个变量时,JavaScript 引擎首先在当前执行上下文的变量对象中查找,如果找不到,就会继续向上级执行上下文的变量对象中查找,直到找到该变量或者达到全局执行上下文的变量对象为止。
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 调用
这段代码会打印以下结果:
hello.helloThis(hello)
:
当调用 hello.helloThis(hello)
时,helloThis
方法被当做普通函数调用,它的执行上下文是 hello
对象。在 hello
对象中,有一个名为 name
的属性,所以 this.name
指向 hello
对象的 name
属性,输出结果为 “hello”。
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
)。在严格模式下,默认绑定的情况下 this
是 undefined
。使用箭头函数也可以继承外层函数的 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();
这段代码中,定义了三个函数:outerFunction
、innerFunction
和全局代码。outerFunction
和 innerFunction
分别创建了它们自己的作用域。当 innerFunction
在执行时,它会在自己的作用域中查找变量 innerVar
,如果找不到,就会向上级作用域(即 outerFunction
的作用域)继续查找,直到找到 innerVar
。
同样,当 innerFunction
查找变量 outerVar
时,它会先在自己的作用域中查找,找不到时就会向上级作用域(即全局作用域)继续查找,直到找到 outerVar
。
在全局作用域中定义的变量 globalVar
可以在任何函数内部访问,因为全局作用域是所有函数作用域的顶级父级。
理解 this
指向在 JavaScript 中非常重要,因为它决定了函数在执行时的上下文,即当前函数所属的对象或环境。this
的值根据函数的调用方式和上下文而变化,下面介绍几种常见情况:
默认绑定:如果函数是独立调用的,即没有通过任何对象来调用,那么 this
默认指向全局对象(浏览器环境中是 window
,Node.js 环境中是 global
)。在严格模式下,默认绑定的情况下 this
是 undefined
。
隐式绑定:如果函数通过对象的属性调用,那么 this
将指向该对象。
const obj = {
name: 'John',
sayHello: function() {
console.log(`Hello, ${this.name}!`);
}
};
obj.sayHello(); // "Hello, John!"
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!"
new
关键字调用构造函数时,会创建一个新的对象,并将该对象作为 this
绑定到构造函数。function Person(name) {
this.name = name;
}
const person = new Person('Alice');
console.log(person.name); // Output: "Alice"
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
在声明变量时有一些区别。
var
声明:
var
声明的变量会成为全局对象的属性。var
声明的变量会成为函数作用域中的局部变量。var
声明的变量会存在变量提升,即在函数或作用域内的任意位置声明都会在代码执行之前被处理。console.log(a); // undefined,变量提升,a 被声明但未赋值
var a = 10;
console.log(a); // 10
let
声明:
let
声明的变量有块级作用域,即在 {}
内声明的变量只在该块中有效,不会影响外部的同名变量。let
声明的变量不会存在变量提升,它只在声明语句处开始生效。console.log(b); // ReferenceError: b is not defined
let b = 20;
console.log(b); // 20
const
声明:
const
声明的变量也有块级作用域,和 let
的作用域规则一样。const
声明的变量必须在声明时进行初始化,并且不能再被重新赋值,但其指向的对象是可以修改的(即对象属性的修改不会报错)。const c = 30;
c = 40; // 类型错误: Assignment to constant variable.
总结:
var
声明的变量具有函数作用域和变量提升,可以在声明前使用,并且可能会污染全局命名空间。let
和 const
声明的变量具有块级作用域,不会进行变量提升,并且在声明前使用会报错。const
声明的变量一旦被赋值后就不能再重新赋值,但它指向的对象是可以修改的。由于 let
和 const
具有块级作用域,能更好地控制变量的作用范围,推荐在新的代码中优先使用 let
或 const
来声明变量。只有在特定情况下需要函数作用域或变量提升时,才使用 var
声明。