回顾JavaScript执行上下文、作用域链与闭包

一、执行上下文

执行上下文是JavaScript代码在运行时所处的环境,它决定了变量和函数如何被访问和执行。执行上下文可以分为三种类型:全局执行上下文、函数执行上下文和Eval函数执行上下文。

1.1 全局执行上下文

当JavaScript代码开始执行时,首先会创建一个全局执行上下文。这个上下文包含了全局对象(在浏览器中是window对象)以及由var声明的全局变量和函数。全局执行上下文在程序运行期间始终存在,它是所有执行上下文的根节点。

javascript 复制代码
	var globalVar = "I am global";
	function globalFunc() {
	    console.log("This is a global function");
	}

在上面的代码中,globalVarglobalFunc都是在全局执行上下文中定义的。

1.2 函数执行上下文

每当一个函数被调用时,都会创建一个新的函数执行上下文。这个上下文包含了函数的参数、局部变量以及this的值。函数执行上下文在函数执行完毕后会被销毁,除非有闭包存在。

javascript 复制代码
	function foo(param) {
	    var localVar = "I am local";
	    console.log(param + " " + localVar);
	}

	foo("Hello"); // 输出: Hello I am local

在上面的代码中,当foo函数被调用时,会创建一个新的函数执行上下文,其中包含参数param和局部变量localVar

1.3 Eval函数执行上下文

eval函数会创建一个新的执行上下文,但由于eval的使用会带来安全和性能问题,因此在实际开发中应尽量避免使用。

ini 复制代码
	eval("var evalVar = 'I am eval';");
	console.log(evalVar); // 输出: I am eval

需要注意的是,eval函数中的代码是在全局执行上下文中执行的,除非eval是在一个函数内部被调用的,此时它会在该函数的执行上下文中执行。

1.4 执行上下文的创建过程

执行上下文的创建过程分为两个阶段:创建阶段和执行阶段。

  • 创建阶段 :确定this的值、创建词法环境和变量环境。词法环境包含了变量和函数声明的信息,而变量环境则包含了变量的实际存储位置。
  • 执行阶段:执行代码,并可以访问到创建阶段确定的变量和函数。

二、作用域链

作用域链是JavaScript中用于解析变量和函数引用的机制。它决定了在当前执行上下文中如何访问变量和函数。作用域链是由当前执行上下文的词法环境以及所有父级词法环境组成的链式结构。

2.1 作用域链的构成

作用域链的构成方式如下:

  • 当访问一个变量时,JavaScript引擎会首先在当前执行上下文的词法环境中查找该变量。
  • 如果在当前词法环境中找不到该变量,则沿着作用域链向上查找,直到找到该变量或者到达全局执行上下文为止。
  • 如果在全局执行上下文中也找不到该变量,则会抛出ReferenceError异常。
scss 复制代码
	function outerFunc() {
	    var outerVar = "I am outer";	
	    function innerFunc() {
	        console.log(outerVar); // 输出: I am outer
	    }	 
	    innerFunc();
	}	 
	outerFunc();

在上面的代码中,当innerFunc函数被调用时,它会首先在自己的词法环境中查找outerVar变量。由于outerVar是在outerFunc的词法环境中定义的,因此innerFunc会沿着作用域链向上查找,并最终找到outerVar

2.2 作用域链与词法作用域

JavaScript采用的是词法作用域(也称为静态作用域),这意味着函数的作用域在函数定义的时候就确定了,而不是在函数调用的时候确定的。词法作用域决定了函数能够访问哪些变量,以及这些变量的可见性。

scss 复制代码
	var value = 1; 
	function foo() {
	    var value = 2;
	    function bar() {
	        console.log(value); // 输出: 2
	    }
	    bar();
	}
	foo();

在上面的代码中,尽管value变量在全局作用域和foo函数的作用域中都定义了,但由于bar函数是在foo函数内部定义的,因此bar函数只能访问到foo函数作用域中的value变量。

三、闭包

闭包是JavaScript中的一个重要概念,它指的是一个可以访问其外部作用域(即词法环境)的内部函数。即使外部函数已经执行完毕,闭包仍然可以访问那些外部变量。

3.1 闭包的实现原理

闭包的实现依赖于作用域链。当一个内部函数被返回或者传递给其他函数时,它会携带一个指向其外部词法环境的引用。这个引用使得内部函数能够在任何时候访问到其外部作用域中的变量。

javascript 复制代码
	function createCounter() {
	    let count = 0; // 外部变量
	    return function() {
	        count++; // 内部函数访问外部变量
	        return count;
	    };
	}
	const counter = createCounter();
	console.log(counter()); // 输出1
	console.log(counter()); // 输出2

在上面的代码中,createCounter函数返回了一个内部函数。这个内部函数形成了一个闭包,它可以访问到createCounter函数中的局部变量count。每次调用counter函数时,count的值都会递增,并返回新的值。

3.2 闭包的应用场景

闭包的应用非常广泛,它可以用于实现私有变量、封装功能、创建工厂函数等。

  • 实现私有变量:通过闭包,我们可以将变量封装在函数内部,从而避免外部代码直接访问这些变量。
javascript 复制代码
	function createCounter() {
	    let count = 0; // 私有变量
	    return {
	        increment: function() {
	            count++;
	            return count;
	        },
	        decrement: function() {
	            count--;
	            return count;
	        },
	        getValue: function() {
	            return count;
	        }
	    };
	}
	const counter = createCounter();
	console.log(counter.getValue()); // 输出0
	console.log(counter.increment()); // 输出1
	console.log(counter.decrement()); // 输出0

在上面的代码中,count变量是私有的,外部代码只能通过incrementdecrementgetValue方法来访问和修改它。

  • 封装功能:闭包可以用于将一组相关的函数封装在一起,形成一个模块。这样不仅可以提高代码的可读性,还可以避免全局变量的污染。
javascript 复制代码
	function createModule() {
	    let privateVar = "I am private";
	    return {
	        getPrivateVar: function() {
	            return privateVar;
	        },
	        setPrivateVar: function(value) {
	            privateVar = value;
	        }
	    };
	}
	const module = createModule();
	console.log(module.getPrivateVar()); // 输出: I am private
	module.setPrivateVar("New value");
	console.log(module.getPrivateVar()); // 输出: New value

在上面的代码中,createModule函数返回了一个对象,该对象包含了两个方法:getPrivateVarsetPrivateVar。这两个方法用于访问和修改私有变量privateVar

  • 创建工厂函数:闭包可以用于创建具有相同结构的对象实例。通过闭包,我们可以将对象的创建逻辑封装在一个函数中,并通过返回不同的对象实例来实现工厂模式。
javascript 复制代码
	function createPerson(name, age) {
	    return {
	        getName: function() {
	            return name;
	        },
	        getAge: function() {
	            return age;
	        },
	        setName: function(newName) {
	            name = newName;
	        },
	        setAge: function(newAge) {
	            age = newAge;
	        }
	    };
	}
	const person1 = createPerson("Alice", 30);
	console.log(person1.getName()); // 输出: Alice
	console.log(person1.getAge()); // 输出: 30
	const person2 = createPerson("Bob", 25);
	console.log(person2.getName()); // 输出: Bob
	console.log(person2.getAge()); // 输出: 25

在上面的代码中,createPerson函数是一个工厂函数,它接收两个参数:nameage。函数返回一个对象,该对象包含了获取和设置nameage的方法。通过调用createPerson函数,我们可以创建具有相同结构的对象实例。

3.3 闭包的性能问题

虽然闭包非常强大和灵活,但它也会带来一些性能问题。由于闭包会持有其外部作用域的引用,因此如果外部作用域中的变量不再需要被访问时,这些变量仍然会被保留在内存中,直到闭包被销毁为止。这可能会导致内存泄漏的问题。 为了避免内存泄漏,我们需要注意以下几点:

  • 及时释放闭包引用 :当不再需要访问闭包中的变量时,应及时解除对闭包的引用。这可以通过将闭包变量设置为null来实现。
ini 复制代码
	function createClosure() {
	    let largeObject = { /* 一些大数据 */ };
	    return function() {
	        console.log(largeObject);
	    };
	}
	const closure = createClosure();
	closure(); // 输出largeObject
	closure = null; // 解除对闭包的引用,释放内存
  • 使用弱引用 :在ES6中,我们可以使用WeakMapWeakSet来实现弱引用。弱引用不会阻止垃圾回收器回收键所指向的对象,从而有效地减少内存泄漏的风险。
javascript 复制代码
	function createClosure() {
	    const privateData = new WeakMap();
	    return function(key, value) {
	        if (value !== undefined) {
	            privateData.set(key, value);
	        } else {
	            return privateData.get(key);
	        }
	    };
	}

	const closure = createClosure();
	closure("key1", "value1");
	console.log(closure("key1")); // 输出: value1

在上面的代码中,privateData是一个WeakMap对象,它用于存储闭包中的私有数据。由于WeakMap的弱引用特性,即使闭包被销毁,privateData中的数据也可以被垃圾回收器回收。

四、实际案例分析

为了更深入地理解执行上下文、作用域链和闭包的概念,我们将通过几个实际案例来进行分析。

4.1 案例一:闭包与变量提升
scss 复制代码
	function foo() {
	    var a = 1;
	    function bar() {
	        console.log(a); // 输出: undefined
	        var a = 2;
	    }
	    bar();
	}
	foo();

在上面的代码中,尽管var a = 1;bar函数之前被声明,但由于变量提升(hoisting)的作用,var a;的声明会被提升到bar函数的顶部。因此,在bar函数内部访问a时,实际上访问的是提升后的a变量(此时其值为undefined),而不是外部函数foo中的a变量。

这个例子展示了变量提升对闭包行为的影响。在实际开发中,我们应尽量避免使用var声明变量,而是使用letconst来声明块级作用域变量,从而避免变量提升带来的问题。

4.2 案例二:闭包与定时器
css 复制代码
	for (var i = 0; i < 5; i++) {
	    setTimeout(function() {
	        console.log(i);
	    }, 1000);
	}

在上面的代码中,由于var声明的变量具有函数作用域,因此i变量在循环结束后被提升为5。当定时器回调函数执行时,它们访问的都是同一个i变量(其值为5),因此输出结果为5, 5, 5, 5, 5

为了解决这个问题,我们可以使用闭包来捕获每次循环中的i值:

javascript 复制代码
	for (var i = 0; i < 5; i++) {
	    (function(j) {
	        setTimeout(function() {
	            console.log(j);
	        }, 1000);
	    })(i);
	}

在上面的代码中,我们通过一个立即执行函数表达式(IIFE)来创建一个闭包,将每次循环中的i值传递给闭包中的j变量。这样,每个定时器回调函数都有自己独立的j变量值,从而输出正确的结果0, 1, 2, 3, 4

4.3 案例三:闭包与事件处理
javascript 复制代码
	function createButtonHandler(message) {
	    return function(event) {
	        console.log(message);
	    };
	}

	const button = document.getElementById("myButton");
	button.addEventListener("click", createButtonHandler("Hello, world!"));

在上面的代码中,createButtonHandler函数返回了一个闭包函数。这个闭包函数可以访问到外部函数createButtonHandler中的message变量。当按钮被点击时,闭包函数被调用,并输出Hello, world!

这个例子展示了闭包在事件处理中的应用。通过闭包,我们可以将事件处理函数与外部数据(如按钮的文本或状态)关联起来,从而实现更加灵活和动态的事件处理机制。

相关推荐
LTPP6 分钟前
Hyperlane 是一个轻量级、高性能的 Rust HTTP 服务器库
后端·面试·架构
呆萌呆萌怪兽10 分钟前
Swift中关于协议的使用总结
面试
最懒的菜鸟11 分钟前
spring boot jwt生成token
java·前端·spring boot
天天扭码22 分钟前
基于原生JavaScript实现H5滑屏音乐播放器开发详解
前端·css·html
Carlos_sam23 分钟前
canvas学习:如何绘制带孔洞的多边形
前端·javascript·canvas
文岂_23 分钟前
不可解的Dom泄漏问题,Dom泄漏比你预期得更严重和普遍
前端·javascript
本地跑没问题23 分钟前
HashRouter和BrowserRouter对比
前端·javascript·react.js
很酷爱学习23 分钟前
ES6 Promise怎么理解?用法?使用场景?
前端·javascript
76756047923 分钟前
深入剖析 JavaScript 中的 `Number.isNaN` 和 `isNaN`:区别与应用场景
前端
忆柒24 分钟前
Vue自定义指令:从入门到实战应用
前端·javascript·vue.js