关于 JavaScript 中的函数和作用域
在本文中,我们将深入介绍 JS 函数、回调、作用域、闭包的所有基本概念,这将帮助您:
- 了解不同类型的函数声明。
- 更好地利用功能。
- 了解不同的作用域和作用域链在 JS 中是如何工作的。
- 了解闭包以及如何使用它们。
我们将通过示例了解所有这些概念,并了解它们的实现。让我们从 Javascript 函数开始讨论。
功能:函数 允许我们在一个块中声明和打包一堆代码,以便我们可以在我们的程序中使用(和重用)一个代码块。有时,它们将一些值作为“参数”来执行操作并返回一些值作为操作的结果。
例子:
Javascript
function add(a, b) {
// a and b are the parameters of this
// function code to do the operation
return a + b; // return statement
}
// Invoking the function and 2, 3
// are arguments here
add(2, 3);
Javascript
// Anonymous function expression
const add = function (a, b){
return a + b;
}
// Named function expression
const subtractResult = function subtract(a, b){
return a - b;
}
console.log(add(3, 2)); // 5
console.log(subtractResult(3, 2)); // 1
Javascript
function showLength(name, callback) {
callback(name);
}
// function expression `nameLength`
const nameLength = function (name) {
console.log(`Given Name ${name} which
is ${name.length} chars long`);
};
// Passing `nameLength` as a callback function
showLength("GeeksforGeek", nameLength);
Javascript
const name = "GeeksforGeeks";
function introduceMyself(greet) {
const audience = "Everyone";
function introduce() {
console.log(`${greet} ${audience}, This is ${name} Learning!`);
}
introduce();
}
introduceMyself("Hello");
Javascript
const name = "GeeksforGeeks";
function introduceMyself(greet) {
const audience = "Everyone";
function introduce() {
console.log(`${greet} ${audience}, This is ${name} Learning`);
}
introduce();
}
introduceMyself("Hello");
Javascript
let name = "Abhijit";
var sector = "Government";
{
let name = "Souvik";
// as `var` is NOT block scoped(globally s
// coped here), it'll update the value
var sector = "Private";
console.log(name); //Souvik
console.log(sector); //Private
}
console.log(name); //Abhijit
console.log(sector); //Private
Javascript
let name = "Abhijit";
var sector = "Government";
function showDetails() {
let name = "Souvik";
// `var` is functionally scoped here,
// so it'll create new reference with
// the given value for organization
var sector = "Private";
console.log(name); // Souvik
console.log(sector); // Private
}
showDetails();
console.log(name); // Abhijit
console.log(sector); // Government
Javascript
function closureDemo(){
const a = 3;
return function (){
console.log(a);
}
}
// Returns the definition of inner function
const innerFunction = closureDemo();
innerFunction(); // 3
Javascript
// Module pattern
let greet = (function () {
const name = "GeekforGeeks"; // Private variable
return {
introduce: function () {
console.log(`Hello, This is ${name} Learning!`);
},
};
})();
console.log(greet.name); //undefined
// Hello, This is GeekforGeeks Learning!
greet.introduce();
输出:
5
一等公民:如果任何编程语言能够将函数视为值,将它们作为参数传递并从另一个函数返回一个函数,那么就说编程语言具有一等函数并且这些函数被称为一等使用该编程语言的公民。如果函数满足以下条件,则函数将被视为 JavaScript 中的一等公民:
- 将函数存储在变量中。
- 将一个函数作为参数传递给另一个函数。
- 从另一个函数。
函数表达式:当函数存储在变量中时,称为函数表达式。这可以命名或匿名。如果一个函数没有任何名称并且存储在一个变量中,那么它将被称为匿名函数表达式。否则,它将被称为命名函数表达式。有关详细信息,请参阅 JavaScript函数表达式文章。
例子:
Javascript
// Anonymous function expression
const add = function (a, b){
return a + b;
}
// Named function expression
const subtractResult = function subtract(a, b){
return a - b;
}
console.log(add(3, 2)); // 5
console.log(subtractResult(3, 2)); // 1
输出将分别为 5 和 1。
回调:将函数存储在变量中可以很容易地将函数。将其他函数作为参数或返回函数的函数称为高阶函数。作为参数传递给另一个函数的函数称为回调函数。简单来说,如果我们想在其他函数返回后立即执行一个函数,那么可以使用回调。请参考 theavaScript |回调文章了解更多详情。
例子:
Javascript
function showLength(name, callback) {
callback(name);
}
// function expression `nameLength`
const nameLength = function (name) {
console.log(`Given Name ${name} which
is ${name.length} chars long`);
};
// Passing `nameLength` as a callback function
showLength("GeeksforGeek", nameLength);
输出:
Given Name GeeksforGeek which is 12 characters long
ES6 中的 Template 字面量提供了创建字符串的新功能,可以更好地控制动态字符串。传统上,字符串是使用单引号 (') 或双引号 (") 引号创建的。模板字面量是使用允许声明嵌入表达式的反引号 (`)字符创建的。通常,我们在数组方法中使用回调函数——forEach()、map()、filter()、reduce()。
范围:它是程序中可以访问变量的区域。换句话说,范围决定了变量的可访问性/可见性。由于 JavaScript 看起来像 C 系列语言,很明显认为 JavaScript 中的作用域与大多数后端编程语言(如 C、C++ 或Java)中的作用域相似。请参考什么是 JavaScript 中的变量作用域?文章了解更多详情。 JavaScript 中有 3 种作用域:
- 全局范围:在所有函数之外声明的变量称为全局变量并在全局范围内。全局变量可以在程序的任何地方访问。
- 函数范围:在函数内部声明的变量称为局部变量,在函数范围内。局部变量可以在函数内的任何地方访问。
- 块范围:在特定块内声明的变量&不能在该块外访问。为了访问该特定块的变量,我们需要为其创建对象。
函数内的代码可以访问:
- 函数的参数。
- 函数内部声明的局部变量。
- 在其父函数范围内声明的变量。
- 全局变量。
Javascript
const name = "GeeksforGeeks";
function introduceMyself(greet) {
const audience = "Everyone";
function introduce() {
console.log(`${greet} ${audience}, This is ${name} Learning!`);
}
introduce();
}
introduceMyself("Hello");
输出:
Hello Everyone, This is GeeksforGeeks Learning!
块范围:这告诉我们在块 ({}) 内声明的任何变量只能在该块内访问。
现在,什么是块?块 {} 用于将 JavaScript 语句组合成 1 组,以便它可以在程序中预期只编写 1 条语句的任何地方使用。
Block scope is related to variables declared with `let` and `const` only. Variables declared with `var` do not have block scope.
例子:
{
let a = 3;
var b = 2;
}
console.log(a); //Uncaught ReferenceError: a is not defined
console.log(b); // 2 as variables declared with `var` is
functionally and globally scoped NOT block scoped
作用域链:每当我们的代码在函数调用期间尝试访问变量时,它都会从局部变量开始搜索。如果未找到该变量,它将继续在其外部作用域或父函数的作用域中搜索,直到它到达全局作用域并在那里完成对变量的搜索。搜索任何变量都会沿着作用域链或不同的作用域进行,直到我们得到变量。如果在全局范围内也找不到该变量,则会引发引用错误。
例子:
Javascript
const name = "GeeksforGeeks";
function introduceMyself(greet) {
const audience = "Everyone";
function introduce() {
console.log(`${greet} ${audience}, This is ${name} Learning`);
}
introduce();
}
introduceMyself("Hello");
输出:
Hello Everyone, This is GeeksforGeeks Learning
在上面的示例中,当代码尝试访问 `introduce()`函数内的变量 `name` 时,它没有在那里获取变量并尝试在其父函数的 (`introduceMyself()`) 范围内进行搜索。由于它不存在,它最终上升到全局范围来访问变量并获取变量 `name` 的值。
变量遮蔽:如果我们在作用域链中声明一个与另一个变量同名的变量,则具有局部作用域的变量会在外部作用域上遮蔽该变量。这称为可变阴影。有关详细信息,请参阅 JavaScript 中的变量阴影一文。
示例 1:
Javascript
let name = "Abhijit";
var sector = "Government";
{
let name = "Souvik";
// as `var` is NOT block scoped(globally s
// coped here), it'll update the value
var sector = "Private";
console.log(name); //Souvik
console.log(sector); //Private
}
console.log(name); //Abhijit
console.log(sector); //Private
输出:
Souvik
Private
Abhijit
Private
示例 2:
Javascript
let name = "Abhijit";
var sector = "Government";
function showDetails() {
let name = "Souvik";
// `var` is functionally scoped here,
// so it'll create new reference with
// the given value for organization
var sector = "Private";
console.log(name); // Souvik
console.log(sector); // Private
}
showDetails();
console.log(name); // Abhijit
console.log(sector); // Government
解释:在示例 1 的情况下,`name` 变量在块内的外部范围内隐藏了具有相同名称的变量,因为我们使用了 `let` 来声明变量。但是,在我们使用 var 声明它的同时,`sector` 变量也在更新值。正如我们所知,`var` 是函数式和全局范围的,块内具有相同名称(`sector`)的声明将更新相同引用处的值。而在示例 2 的情况下,函数内部的 `sector` 变量是函数范围的,并且将创建一个新的引用,该引用将隐藏在外部声明的具有相同名称的变量。
输出:
Souvik
Private
Abhijit
Government
闭包:函数能够记住在其外部范围内声明的变量和函数。
MDN 将闭包定义为 -“将函数与对其周围状态或词法环境的引用捆绑在一起的组合”
现在,如果你在想,词汇环境是什么?函数的本地环境连同其父函数的环境形成词法环境。请参阅 JavaScript 中的闭包文章以了解此概念。
例子:
Javascript
function closureDemo(){
const a = 3;
return function (){
console.log(a);
}
}
// Returns the definition of inner function
const innerFunction = closureDemo();
innerFunction(); // 3
输出将是 3。
在上面的示例中,当调用 `closureDemo()`函数时,它将返回内部函数及其词法范围。然后,当我们尝试执行返回的函数时,它会尝试记录 `a` 的值并从其词法范围的引用中获取该值。这称为闭包。即使在外部函数执行之后,返回的函数仍然持有词法作用域的引用。
优点:
- 咖喱
- 记忆
- 模块设计模式
缺点:
- 内存的过度消耗可能会导致内存泄漏,因为最里面的函数持有词法作用域的引用,并且即使在外部函数执行后,在其词法作用域中声明的变量也不会被垃圾回收。
立即调用函数表达式 (IIFE):立即调用函数表达式或 IIFE 是在定义后立即调用的函数。请参考 JavaScript |立即调用函数表达式 (IIFE) 文章了解更多详细信息。
句法:
(function task(){
console.log("Currently writing a blog on JS functions");
})();
我们基本上是在括号中包装一个函数,然后在末尾添加一对括号来调用它。
- 将参数传递给 IIFE:我们也可以将参数传递给 IIFE。第二对括号不仅可以用于立即调用函数,还可以用于将任何参数传递给 IIFE。
(function showName(name){
console.log(`Given name is ${name}`); // Given name is Souvik
})("Souvik");
- IIFE 和私有作用域:如果我们可以将 IIFE 与闭包一起使用,我们可以创建一个私有作用域,并且可以保护一些变量不被外部访问。在模块设计模式中使用相同的想法来保持变量私有。
例子:
Javascript
// Module pattern
let greet = (function () {
const name = "GeekforGeeks"; // Private variable
return {
introduce: function () {
console.log(`Hello, This is ${name} Learning!`);
},
};
})();
console.log(greet.name); //undefined
// Hello, This is GeekforGeeks Learning!
greet.introduce();
IIFE 有助于防止在此处访问 `name` 变量。并且返回对象的 `introduce()` 方法保留了其父函数的范围(由于闭包),我们得到了一个与 `name` 交互的公共接口。
输出:
undefined
Hello, This is GeekforGeeks Learning!