你将知道:
- 函数声明和表达式
- 函数声明和表达式之间的区别
- 什么是匿名函数
- 什么是 IIFE
- 命名函数表达式
this
关键字
函数是调用该函数时执行的代码块 。
函数声明和表达式
让我们回顾一下它的语法:
functionfunctionName(param1, param2, ..., paramN) {// Function's body
}
那么什么是函数声明?
函数声明是指作为独立语句而不是表达式存在的函数定义。
// Standalone statement,
// hence a function declaration.
function sum(a, b) {return a + b;
}// Another function declaration.
function outerFn() {// A function declaration within outerFn().function innerFn() {console.log('Working with functions.')}}
这里 sum()
、 outerFn()
和 innerFn()
都是函数声明,因为它们是独立的语句:
在 JavaScript 中,还有另一种创建函数的方式,尽管其语法与函数声明相同,但工作方式略有不同。我们称之为函数表达式 。
函数声明与函数表达式最重要的区别在于,后者可以定义一个没有名称的函数,这样的函数通常被称为匿名函数 。无论如何,假设函数被分配给一个变量,那么函数表达式在语法上看起来是这样的:
var identifierName = function [functionName](param1, param2, ..., paramN) {// Function's body
}
请注意,使用函数表达式时,可以选择在 function
关键字后包含函数名称(即 functionName
),如上面的语法所示( functionName
周围有 [ ]
)。
让我们创建相同的函数 sum()
,这次使用函数表达式:
var sum = function(a, b) {return a + b;
}
这里发生的事情是,一个没有名称的函数被赋值给了变量 sum
。由于 sum
指向内存中的一个函数对象,所以我们可以使用表达式 sum()
。
下面我们将一个函数表达式作为参数传递给另一个函数:
function execute(fn) {fn();
}execute(function() { console.log('Hello World!'); })
这是 JavaScript 应用程序中常见的另一种编码实践。将一个函数作为参数传递给另一个函数,我们称之为回调函数 ,或者简称为回调 。
function getLogger() {return function(val) {console.log('We are learning ' + val + '.');}
}var langIntro = getLogger();langIntro('JavaScript');
回调在 JavaScript 的事件 API 以及大量其他 API 中扮演着至关重要的角色。
在第 7 行,当调用 getLogger()
时,执行会转到第 1 行中它的定义处,并退出并返回另一个函数的返回值。这个 “另一个” 函数接受一个参数 val
,并创建一个包含该参数和其他文本的日志。
由于 getLogger()
返回一个函数,因此 langIntro
中保存的值是对函数的引用——具体来说,是对第 2 行定义的匿名函数的引用。这意味着我们可以使用表达式 langIntro ()
来调用此函数。这正是我们在最后一行所做的。
从其他函数返回的函数构成了 JavaScript 中的另一个重要概念,称为闭包 ,尽管这个概念不仅存在于从其他函数返回的函数中;它也存在于其他地方。
从核心执行层面来看,函数声明和函数表达式之间没有任何区别。调用时,两者的执行方式相同。区别仅在于脚本编译时解析它们的方式。
函数声明和表达式之间的区别
函数声明和函数表达式之间主要有两个区别。
- 其中一个被 提升,而另一个则没有。
- 一个可以创建匿名函数,而另一个则不能。
1. Hoisting 提升
提升是指在执行任何其他代码之前处理各自范围内的所有变量声明的行为。
变量提升是以下代码有效并且不会引发错误的原因:
// We think that x doesn't exist at this point, likewise
// the following statement would throw an error.
// However, that's not the case.
console.log(x);var x = 10;
console.log(x);
内部实现是这样的:引擎首先在代码中搜索所有变量声明语句,并在运行任何其他代码之前执行它们。完成后,它会从头开始解析代码。
这意味着在实际执行上述第 4 行之前,变量 x
已被有效声明,因此可以访问。由于 var x
将 x
初始化为 undefined
,因此在第 6 行赋值语句之前访问 x
会返回 undefined
。
对于函数声明,也会发生类似的情况。我们称之为函数提升 (function hoisting) 。
整个声明连同函数主体都被置于其范围的最顶部。这意味着下面的代码将完全正常工作:
console.log(sum(10, 20));function sum(a, b) {return a + b
}
在第 3 行中,函数 sum()
在实际定义之前就被访问了。然而,由于函数提升,这次调用并没有标记任何错误。
块语句内的函数声明
我们知道,所有函数声明都会被提升到其作用域的顶部。然而,这个想法有一个小例外,事实上,不同浏览器之间存在不兼容性。
也就是说,如果函数声明出现在块语句内,那不是另一个函数的主体(也是一个块语句),则函数声明在大多数浏览器中不会正常提升 - 只有其名称可以通过值 undefined
访问。
这种行为有一个很好的理由,从下面的代码中可以看出:
var userIsNew = false;if (userIsNew) {function greet() {console.log('Hello!');}
}
函数 greet()
被放在 if
语句块内。该语句块执行的条件是 userIsNew
为 true
。然而,事实显然并非如此。因此,理想情况下,这段代码的作者希望该函数不会出现在全局作用域中,因为它的出现条件尚未满足。同样,在这种情况下,大多数浏览器不会提升函数声明,但函数名称在相应的范围内可用作 undefined
标识符。
这里要认识到的最重要的一点是,函数提升仅限于函数声明 — — 而不是函数表达式 。
引擎在第一次遍历一段代码时仅查找函数声明,而不是表达式,并将它们提升。
console.log(sum(10, 20));var sum = function(a, b) {return a + b
}//Uncaught TypeError: sum is not a function
我们有一个变量声明语句 var sum
,同样,它在运行任何其他代码之前会被提升。接下来,从脚本开头的第 1 行开始执行。此时, sum
等于 undefined
,因此无法调用;同样, sum()
会抛出错误。
可能会认为可以通过在表达式中为函数添加名称来缓解此问题。然而,这也行不通:
console.log(sum(10, 20));var sumRef = function sum(a, b) {return a + b
}
//Uncaught TypeError: sum is not a function
引擎只会在给定代码片段中搜索函数声明, 并最终将其提升。函数表达式,无论是命名的还是匿名的,都无关紧要。它们最终还是表达式,同样会在提升阶段被忽略。
2. 匿名函数
没有名称的函数称为匿名函数 。
当 JavaScript 遇到函数声明时,它期望也看到函数的名称。否则会导致语法错误。
只有在函数表达式中才可以省略函数名称,如下所示:
var sum = function(a, b) {return a + b;
}
匿名函数只能使用函数表达式来表示,是 JavaScript 中的一个重要特性。几乎所有涉及事件和回调的程序都会用到它们.我们将广泛使用匿名函数。
IIFE
在 JavaScript 中,有一种特殊情况,即函数表达式在定义后立即被调用。我们称之为 IIFE ,即立即调用函数表达式 。
IIFE 是一个在定义后立即调用的函数表达式。
(function() {console.log('I am in an IIFE.')
})();
IIFE 用非常简洁的语法解决了这个问题。它们将代码封装在本地环境中,不会干扰全局作用域。
最棒的是,这个过程非常快捷——我们无需命名这些函数,也无需担心如何将它们赋值给任何标识符。只需创建一个匿名(或命名)函数,用一对括号将其封装起来,将代码放入其中,然后立即调用即可。 瞧!
每个库的代码都封装在 IIFE 中,并且彼此之间完全互不干扰。 简直太棒了。
this
关键字
JavaScript 中有很多令人困惑的概念,其中之一就是 this
的概念。本节我们将深入介绍并解释 this 的核心。
首先, this
是 JavaScript 中的保留关键字,这意味着它不能用作变量的名称。
回到 this
背后的想法,它是为了提供一种引用调用函数的对象的方式。这就是它被创建的初衷。事实上,编程语言 Java 无疑是 JavaScript 的影响源泉,它也包含 this
功能,本质上服务于非常相似的用途。
当在函数内部使用时, this
指的是调用该函数的对象 。
例如,考虑下面的对象 o
。它有一个属性 x
和一个方法 f()
:
var o = {x: 10,f: function() { console.log(this.x); }
};var x = 20;
o.f();
一旦执行此代码,我们就会在控制台中记录值 10
。
怎么样? 我们来看看吧。
在第 7 行中,调用表达式 of()
在对象 o
上调用存储在 of
中的函数,同样,它的 this
也指向 o
。这仅仅意味着函数对象内部的表达式 this.x
转换为 o.x
,等于值 10
,因此控制台中打印的是 10
。
这是因为 this
会自动指向调用对象——我们不需要自己将其配置为所需的对象。相反,如果我们使用 ox
(而不是 this.x
),当 o
的名称将来发生变化或被分配给其他标识符时,我们必须重写这个表达式。理想情况下,只要有可能,就使用 this
关键字引用方法定义中的容器对象 - 而不是使用对象本身的名称。
当直接调用函数对象(而不是作为对象的一部分)时,如果脚本在非严格模式下运行,则其 this
值将解析为全局 window
对象。
// The global context.
console.log(this);
Window {window: Window, self: Window, document: document, name: "", location: Location, …}