函数,是可以通过名称来引用,并且就像自包含了一个微型程序的代码块。利用函数,我们可以实现对代码的复用,降低代码的重复,并且让代码更加容易阅读。
在 JavaScript 中,函数显得尤为的重要。因为函数在 JavaScript 中是一等公民,可以像参数一样传入和返回。所以说函数是 JavaScript 中的一个重点,同时也是一个难点。
本章我们将学习如下的内容:
- JavaScript 中函数的基础知识
- 箭头函数
- 变量提升与函数提升
- 回调函数
5-1 函数基础介绍
学习任何语言,当学习到函数这一块时,往往都离不开以下几个问题:
- 如何创建函数
- 掌握函数的三要素
这里的函数三要素,是指函数的功能,函数的参数,以及函数的返回值。这些知识,可以说是在整个函数的知识体系中最为基础的部分。
在这一节,我们就来一起看一下 JavaScript 中函数的这些基础知识。
5-1-1 为什么需要函数
首先我们来看一下为什么需要函数。函数最大的好处就是可以对代码实现复用。相同的功能不用再次书写,而是只用书写一次就够了。这其实就是编程里面所谓的 DRY 原则。
所谓 DRY 原则,英语全称为 Don‘t repeat yourself,翻译成中文就是不要重复你自己。什么意思呢?也就是说一个程序的每个部分只被编写一次,这样做可以避免重复,不需要保持多个代码段的更新和重复。
并且,我们可以把函数看作是一个暗箱,不需要知道函数的内部是怎么实现的,只需要知道函数的功能,参数以及返回值即可。
5-1-2 声明函数的方式
在 JavaScript 中,声明函数的方式有多种,这里我们先来介绍 3 种声明函数的方式:字面量声明函数,函数表达式声明函数以及使用构造器方式来声明一个函数。
1. 字面量声明函数
这种方式是用得最为广泛的一种方式,使用关键字function
来创建一个函数,具体的语法如下:
function 函数名(形式参数){
// 函数体
}
函数名:就是我们调用函数时需要书写的标识符。一般来讲函数表示的是一个动作,所以在给函数命名的时候,一般都是以动词居多。
形式参数:简称形参,指的是调用函数时需要接收的参数。
实际参数:简称实参,指的是调用函数时实际传递过去的参数。
示例:
function test(name){
console.log("Hello,"+name);
}
test("Mr.Yan"); // Hello,Mr.Yan
在上面的示例中,name 为调用函数时,需要接收的参数,也就是形参。Mr.Yan 为我们调用函数时实际传递过去的参数,也就是实参。
2. 函数表达式声明函数
第二种方式是使用函数表达式来进行声明,也就是将一个匿名函数赋值给一个变量,具体的语法如下:
let 变量 = function(){
// 函数体
}
注:所谓匿名函数,就是没有名称的函数。
函数表达式示例:
let test = function(name){
console.log("Hello,"+name);
}
test("Mr.Yan"); // Hello,Mr.Yan
需要说明的是,这里的变量并不是该函数的名字,一般来讲,我们都是将一个匿名函数赋值给一个变量,然后通过这个变量来对函数进行调用。
当然,我们也可以将一个带有名字的函数赋值给一个变量。这样的声明方式被称之为命名式函数表达式。
示例如下:
let test = function saySth(name){
console.log("Hello,"+name);
}
test("Mr.Yan"); // Hello,Mr.Yan
注意:虽然这种方式的函数表达式拥有函数名,但是调用的时候还是必须要通过变量名来调用,而不能够使用函数名来进行调用。
3. 构造器声明函数
使用构造器来声明函数,又被称之为通过对象的方式来声明函数,具体的语法如下;
let 变量 = new Function("参数","参数","函数体");
具体示例如下:
let test = new Function("name","console.log('Hello,'+name)");
test("Mr.Yan"); // Hello,Mr.Yan
虽然通过这种方式也能够创建出函数,并且调用,但是并不推荐使用这种方式来创建函数。
因为这样会导致 JavaScript 解释器需要解析两次代码。先要解析这个表达式,然后又要解析字符串,效率很低。并且这样将函数体全部书写在一个字符串里面的书写方式对我们程序员也非常的不友好,所以这种方式作为了解即可。
5-1-3 函数的调用
函数的调用在前面介绍函数声明的时候也已经见到过了,就是写上函数名或者变量名,后面加上一对大括号即可。
有一个地方需要注意,那就是如果要调用函数,那么就必须要有括号。这个括号要么在函数名后面,要么在变量名后面,这样才能够将调用函数后的执行结果返回。如果缺少了括号,那就只是引用函数本身。
示例如下:
let test = function(){
console.log("Hello");
}
let i = test; // 没有调用函数,而是将 test 函数赋值给了 i
i(); // Hello
5-1-4 函数的返回值
函数的返回值的关键字为return
。代表要从函数体内部返回给外部的值,示例如下:
let test = function(){
return "Hello";
}
let i = test();
console.log(i); // Hello
即使不写return
,函数本身也会有默认的返回值undefined
,例如:
let test = function(){
console.log("Hello");
}
let i = test(); // Hello
console.log(i); // undefined
需要注意的是,return
后面的代码是不会执行的,因为在函数里面一旦执行到return
,整个函数的执行就结束了。
let test = function(){
return 1;
console.log("Hello");
}
let i = test();
console.log(i); // 1
return
关键字只能返回一个值,如果想要返回多个值,可以考虑返回一个数组,示例如下:
// 1-60的安全数 7 的倍数或者以 7 结尾
let test = function(){
let arr = [];
for(let i=1;i<=60;i++){
if(i%10==7 || i%7==0){
arr.push(i);
}
}
return arr;
}
console.log(test());
// [ 7, 14, 17, 21, 27, 28, 35, 37, 42, 47, 49, 56, 57 ]
5-1-5 函数的参数
函数的参数可以分为两种,一种是实际参数,另外一种是形式参数。这个我们在前面已经介绍过了。
接下来我们来详细看一下形式参数。形式参数简称形参,它就是一种变量,但是这种变量只能被函数体内的语句使用,并在函数调用时被赋值。JavaScript 中的形参的声明是不需要添加关键字的,如果加上关键字反而会报错。
示例:
function test(let i){
console.log(i);
}
test(5);
// SyntaxError: Unexpected identifier
JavaScript 里面关于函数的形参,有以下几个注意点:
1. 参数名可以重复,同名的参数取最后一个参数值
function test(x,x){
console.log(x);
}
test(3,5); // 5
2. 即使函数声明了参数,调用时也可以不传递参数值
function test(x){
console.log(x);
}
test(); // undefined
3. 调用函数时可以传递若干个参数值给函数,而不用管函数声明时有几个参数
function test(x){
console.log(x); // 1
}
test(1,2,3);
那么,这究竟是怎么实现的呢?为什么实参和形参可以不用一一对应呢?
实际上,当一个函数要被执行的时候,系统会在执行函数体代码前做一些初始化工作,其中之一就是为函数创建一个 arguments 的伪数组对象。这个伪数组对象将包含调用函数时传递的所有的实际参数。
因此, arguments 的主要用途是就是用于保存传入到函数的实际参数的。
下面的例子演示了通过 arguments 伪数组对象来访问到所有传入到函数的实参:
function test(x){
for(let i=0;i
所谓伪数组对象,就是长得像数组的对象而已,但是并不是真的数组,我们可以证明这一点
function test(x){
arguments.push(100); // 针对伪数组对象使用数组的方法
}
test(1,2,3);
// TypeError: arguments.push is not a function
不定参数
不定参数是从 ECMAScript 6 开始新添加的功能,在最后一个形式参数前面添加 3 个点,会将所有的实参放入到一个数组里面,示例如下:
function test(a,...b){
console.log(a); // 1
console.log(b); // [2,3]
}
test(1,2,3);
这里的不定参数就是一个真正的数组,可以使用数组的相关方法
function test(a,...b){
console.log(a); // 1
console.log(b); // [2,3]
b.push(100);
console.log(b); // [ 2, 3, 100 ]
}
test(1,2,3);
还有一点需要注意的是,不定参数都是放在形式参数的最后面,如果不是放在最后,则会报错。
function test(...a,b){
console.log(a);
console.log(b);
}
test(1,2,3);
// SyntaxError: Rest parameter must be last formal parameter
默认参数
从 ECMAScript 6 开始,书写函数的时候可以给函数的形式参数一个默认值。这样如果在调用函数时没有传递相应的实际参数,就使用默认值。如果传递了相应的实际参数,则使用传过去的参数。
function test(name = "world"){
console.log("Hello,"+name);
}
test("Mr.Yan"); // Hello,Mr.Yan
test(); // Hello,world
如果参数是一个数组,要为这个数组设置默认值的话,写法稍微有些不同,如下:
let fn = function([a = 1,b = 2] = []){
console.log(a,b);
}
fn(); // 1 2
fn([3,4]); // 3 4
包括后面我们要介绍的对象,也是可以设定默认值的,其写法和上面为数组设置默认值类似,如下:
let fn = function({name = 'Mr.Yan',age = 18} = {}){
console.log(name,age);
}
fn(); // Mr.Yan 18
fn({name:"zhang",age:20}); // zhang 20
有关对象相关内容,请参考第 6 章对象。
5-1-6 函数属性和方法
1. name
属性
表示函数的函数名
function test(){
console.log("Hello");
}
console.log(test.name); // test
我们可以通过这个name
属性来证明函数表达式的变量不是函数名,如下:
let test = function test2(){
console.log("Hello");
}
console.log(test.name); // test2
2. length
属性
表示形式参数的个数。但是形参的数量不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数。
具体示例如下:
const test = function (a, b, c) { }
console.log(test.length); // 3
统计具有默认值之前的参数个数。
const test = function (a, b = 5, c) { }
console.log(test.length); // 1
不会包括剩余参数的个数。
const test = function (a, b, ...c) { }
console.log(test.length); // 2
接下来我们需要看一下函数名.length
与arguments.length
的区别:
函数对象的length
属性是表示形式参数的个数。arguments
伪数组对象的length
属性是调用函数时实际参数的个数。如下:
let test = function(a,b,c){
console.log(arguments.length); // 5
console.log(arguments.callee.length); // 3
}
test(1,2,3,4,5);
3. caller
属性(扩展)
caller
属性是函数对象本身的属性,它显示了函数的调用者。
如果函数是在全局执行环境中(浏览器中)被调用,那么它的值为 null,如果在另一个函数中被调用,它的值就是那个函数。
浏览器中的全局执行环境中被调用:
Node.js 中的全局执行环境中被调用:
let test = function(){
console.log(test.caller);
}
test(); // [Function]
被一个函数所调用:
let test = function(){
let test2 = function(){
console.log(test2.caller);
// [Function: test]
// 因为这个函数的调用者就是 test 函数
}
test2();
}
test();
4. callee
属性(扩展)
callee
是arguments
对象的一个属性,该属性是一个指针,指向拥有这个arguments
对象的函数
let test = function(){
let test2 = function(){
let test3 = function(){
console.log(arguments.callee);
// [Function: test3]
}
test3();
}
test2();
}
test();
callee
的作用在于能够找到arguments
对象所属的函数,不让函数的执行和函数名仅仅的关联在一起,我们来看下面这个例子:
// 计算阶乘的递归函数
let test = function(i){
if(i == 1){
return 1;
}else{
return i * test(i-1); // 这里就和函数名紧紧的关联了起来
}
}
console.log(test(3));
如果我们把上面的写法稍作修改,就可以看到上面写法的缺陷
// 计算阶乘的递归函数
let test = function(i){
if(i === 1){
return 1;
}else{
return i * test(i-1); // 这里就和函数名紧紧的关联了起来
}
}
let test2 = test; // 将阶乘函数赋值给 test2
// 改变 test 这个阶乘函数的函数体
test = function(){
console.log("我已经改变了");
}
console.log(test2(3));
// 我已经改变了
// NaN
所以,这个时候就可以使用arguments
对象的callee
属性来降低这种关联,如下:
// 计算阶乘的递归函数
let test = function(i){
if(i == 1){
return 1;
}else{
return i * arguments.callee(i-1); // callee 指向拥有 arguments 对象的函数
}
}
let test2 = test; // 将阶乘函数赋值给 test2
// 改变 test 这个阶乘函数的函数体
test = function(){
console.log("我已经改变了");
}
console.log(test2(3)); // 6
Comments