变量或者函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。
注意:上下文在其所有代码都执行完毕后会被销毁,包含定义在它上面的所有变量和函数。【全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器】
1. 作用域
定义:作用域是变量、函数和对象的可访问性规则,决定了代码中哪些部分可以访问某个变量。作用域在代码编写的时候就已经确定了,与函数定义的位置相关,而非调用位置。
核心功能:隔离变量,避免命名冲突,并管理变量的声明周期。
全局作用域:定义在代码最外层,全局可访问。
函数作用域:函数内部定义的变量,仅在函数内有效。
块级作用域:通过 let/const 在{}代码块中定义的变量。
注意:
静态性:作用域在代码解析阶段确定后不可变,属于静态概念
2. 执行上下文
定义:执行上下文是代码执行时的动态环境,包含了当前代码运行所需的所有信息,如变量对象,作用域链,this 指向等。
核心功能:管理代码执行过程中的数据和流程
2.1. 全局执行上下文
全局上下文是最外层的上下文。【默认的最外层上下文,关联 window 对象】
定义: 全局上下文是 js 引擎在解析并执行全局代码时创建的默认执行环境。它是程序运行的唯一全局环境,与全局对象相关联绑定。
创建时机:
- 当 JavaScript 引擎首次加载脚本文件(如 HTML 中的
<script>标签)时,立即创建 GEC,并将其推入 执行上下文栈(ECS) 的栈底; - 在单页应用中,全局执行上下文仅在页面生命周期内创建一次;
| 组件 | 作用与内容 |
|---|---|
| 词法环境(LexicalEnvironment) | 存储全局变量、函数声明及 let/const声明。环境记录类型为 对象环境记录(Object Environment Record) 。 |
| 变量环境(VariableEnvironment) | 存储 var声明的变量,初始值为 undefined。与词法环境分离以实现块级作用域兼容。 |
| 作用域链(Scope Chain) | 仅包含全局词法环境的引用(outer: null),因全局为作用域链的最顶层。 |
this绑定 | 指向全局对象(浏览器中为 window,Node.js 中为 global)。严格模式下为 undefined。 |
2.1.1. 全局执行上下文的生命周期
全局执行上下文的生命周期分为创建阶段和执行阶段。
- 创建阶段
- 创建全局对象:浏览器为 window;
- 初始化词法环境和变量环境:var 变量赋值为 undefined,函数声明完成提升、let/const 变量标记为
暂时性死区; - 绑定 this 到全局对象;
- 执行阶段
- 逐行执行全局代码,为变量赋值;
- 如果遇到函数调用,创建新的函数执行上下文(FEC)并压入执行栈;
- 全局代码执行完毕后,GEC 仍保留在栈中,直到页面关闭或脚本卸载;
生命周期对比 :
| 阶段 | 全局执行上下文(GEC) | 函数执行上下文(FEC) |
|---|---|---|
| 创建次数 | 仅一次(页面生命周期内) | 每次函数调用均创建新上下文 |
| 作用域链 | 仅自身词法环境(outer=null) | 包含自身词法环境 + 外层环境引用(闭包基础) |
| 内存回收 | 页面关闭时释放 | 函数执行完毕后销毁 |
2.1.2. 关键性与规则
- 变量提升的根源
- var 变量和函数声明在创建阶段被提前分配内存,导致在声明前可访问【var 为 undefined,函数可调用】;
- let/const 因暂时性死区(TDZ)的限制,声明前访问会抛出错误。
- 全局对象与变量对象的统一性
- 在全局上下文中,变量对象和全局对象是同一实体。例如
var a = 10等价于window.a = 10;
- 严格模式下的影响
- 在严格模式下,全局执行上下文的 this 为 undefined,而非默认的全局对象。
2.1.3. 实例
- 变量提升和执行阶段赋值
console.log(a);
console.log(b);
var a=10;
let b=20;
在 GEC 创建阶段,使用 var 定义的变量,会在创建阶段提升到作用域的顶部并且初始化为 undefined,也就是说为 a 这个变量提前分配了内存,而 b 是使用 let 创建的,虽然也会提升,但是由于 TDZ 的限制,所以导致 b 保持未初始化状态,导致访问时行为差异,抛出错误。
- 全局对象与变量对象的统一性
var x = 100;
console.log(window.x); // 100(x 被挂载到 window)
全局 var 变量直接成为全局对象的属性。
- 全局作用域链与嵌套函数
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(),所以有两种解释方式:
- window调用了foo(),this指向window。隐式绑定
- foo是直接调用的,this默认绑定为window。默认绑定
- 检查调用方式:
-
- 若函数直接调用(如
foo()),应用默认绑定。 - 若函数通过对象属性调用(如
obj.foo()),应用隐式绑定。
- 若函数直接调用(如
- 注意隐式绑定的丢失:
-
- 当隐式绑定的函数被赋值给变量或作为参数传递时,可能丢失绑定对象,退化为默认绑定。
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);
三、内置函数或异步回调中的隐式丢失
内置函数(如 setTimeout、setInterval)或事件处理器中传入对象方法时,隐式绑定常丢失。
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. 显式绑定的常见用途
- 解决回调函数中的
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 | 对象方法调用 |
| 灵活性 | 可以随时改变 this | this 由调用者决定 |
| 使用场景 | 需要精确控制 this 时 | 常规对象方法调用时 |
显式绑定是 JavaScript 中管理 this 指向的强大工具,特别是在处理回调函数、事件处理程序和需要方法借用的场景中非常有用。
3.1.5. new 绑定
new 绑定是 JavaScript 中 this 绑定的四种方式之一,专门用于构造函数调用。当使用 new 关键字调用函数时,会创建一个全新的对象,并且 this 会绑定到这个新创建的对象上。
3.1.5.1. 基本原理
当使用 new 调用函数时,JavaScript 引擎会执行以下步骤:
- 创建一个全新的空对象
- 将这个新对象的
[[Prototype]]链接到构造函数的prototype对象 - 将新创建的对象绑定到函数调用的
this - 执行构造函数内部的代码(通常用于初始化对象)
- 如果构造函数没有显式返回对象,则自动返回这个新创建的对象
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 绑定的特点
- 自动创建新对象:不需要手动创建对象再赋值
- 自动绑定
this:构造函数内的this自动指向新实例 - 自动设置原型链:新对象的
__proto__指向构造函数的prototype - 自动返回新对象:除非构造函数显式返回另一个对象
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. 注意事项
- 忘记使用
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. 实际应用场景
- 创建多个相似对象:
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 绑定的机制。