本文以《JavaScript高级程序设计》第4版作为基础参考,整理使用JavaScript开发过程中,函数使用相关的知识点。
本文是开发知识点系列第六篇。
- 第一篇:JavaScript开发中变量、常量声明的规矩总结
- 第二篇:JavaScript开发:数据类型知识总结
- 第三篇:JavaScript开发:使用Number数据类型需要注意的问题
- 第四篇:JavaScript开发:操作符在实际开发中的使用总结
- 第五篇:JavaScript开发:流程控制语句在实际开发中的使用总结
函数在JavaScript开发中视为一等公民,足见其重要性,不可不察。
终于写到函数了,前面的五篇可以说是JavaScript内置规则,也可以说是JavaScript语言的基础,给予开发者发挥的空间并不多。一直到函数,开发者可以发挥的余地增大,开发变得异彩纷呈,变得有意思;当然能够与函数在这方面旗鼓相当的还有后面的对象。
什么是函数?函数是具备一定功能的可以被重复调用的代码块。函数可以接收输入(称为参数),并返回一个结果(称为返回值)
函数的基本性质
函数声明&定义
函数可以通过多种方式定义:
- 函数声明(Function Declaration):
javascript
function myFunction(a, b) {
return a + b;
}
- 函数表达式(Function Expression):
javascript
const myFunction = function(a, b) {
return a + b;
};
- 箭头函数(Arrow Function):
javascript
const myFunction = (a, b) => {
return a + b;
};
如果箭头函数函数体只有一条语句,可以简写为:
javascript
const myFunction = (a, b) => a + b;
- 使用Function构造函数:
javascript
const myFunction = new Function('a', 'b', 'return a + b');
这种因为会影响性能,所以不被推荐。
需要注意的是,普通函数声明会被JavaScript引擎提升(hoisted),这意味着你可以在声明之前调用函数。而函数表达式和箭头函数则不会被提升,必须在调用函数之前定义函数。
此外,箭头函数和其他两种方式定义的函数在行为上也有一些差异,例如箭头函数没有自己的this,this的值在箭头函数被定义时就已经确定,等于箭头函数定义时所在的上下文。
另外,箭头函数不能使用argumens、super和new.target,也不能作为构造函数,也没有prototype属性。
函数名
ECMAScript 6的所有函数对象都会暴露一个只读的name属性。多数情况下这个属性保存的是函数标识符,或者是一个字符串化的变量名。如果没有名称即匿名函数,则显示成空字符串。如果函数是使用Function构造函数创建的,则会标识成"anonymous"。
如果函数是一个获取函数、设置函数或者使用bind()实例化了,则标识符前面会加上一个前缀:对于获取函数(getter)和设置函数(setter),name属性返回 "get " 或 "set " 加上函数的名称。对于使用bind()方法创建的函数,name属性返回 "bound " 加上原函数的名称。
- 普通函数:
javascript
function myFunction() {}
console.log(myFunction.name); // 输出:"myFunction"
- 获取函数和设置函数:
javascript
var obj = {
get value() {},
set value(v) {}
};
var descriptor = Object.getOwnPropertyDescriptor(obj, 'value');
console.log(descriptor.get.name); // 输出:"get value"
console.log(descriptor.set.name); // 输出:"set value"
- 使用
bind()创建的函数:
javascript
function myFunction() {}
var boundFunction = myFunction.bind(null);
console.log(boundFunction.name); // 输出:"bound myFunction"
此外,name属性的值不一定能反映函数的当前名称,因为函数的名称可以在运行时被改变。
参数
JavaScript中的函数参数不是必须写的,提前写只是为了方便。
非箭头函数可以用arguments获取到参数,其本身就是一个类数组。可通过arguments[0]、arguments[1]、arguments[2]......获取到参数。
箭头函数因为不能使用arguments,也就不能通过arguments获取参数。但是箭头函数可以通过使用剩余参数的方式,获取参数,后面会说到。
关于函数的默认值,ECMAScript 6可以显式的定义参数的默认值了。不仅限于普通函数,箭头函数也可以。
另外因为参数是按照顺序初始化的,所以后定义默认值的参数可以引用先定义的参数。
js
function greet(name = 'World', message = `Hello, ${name}!`) {
console.log(message);
}
扩展运算符:扩展运算符可以将数组展开,数组成员直接作为参数传递给函数,不仅限于普通函数。
js
let numbers = [1, 2, 3];
function sum(a, b, c) {
return a + b + c;
}
console.log(sum(...numbers)); // 输出 6
收集参数/剩余参数:在ES6中,你可以使用剩余参数(rest parameters)语法来表示任意数量的参数。剩余参数在参数列表中的最后一个参数前面加上...,在函数内部,剩余参数表现为一个数组。
js
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3)); // 输出 6
没有重载
所谓重载在传统编程中,比如Java开发中,一个函数名相同的函数可以有两个甚至以上的定义,只要参数的类型或者数量不同就可以。但是JavaScript不可以,只要函数名相同后面定义的会覆盖前面定义的。
函数的属性和方法
在JavaScript中,函数是一种特殊的对象,除了上面提到的name属性,它还有一些其它的属性和方法。
-
length:返回函数的参数个数,即在函数定义时给出的形参个数。 -
prototype:非箭头函数都有一个prototype属性,它指向函数的原型对象。当使用new操作符创建一个新对象时,新对象会从它的构造函数的原型对象上继承属性和方法。
下面则是一些函数对象的内置方法:
-
call():调用一个函数,并将函数的this值设置为提供的值。所有的参数都会作为函数的参数传递。 -
apply():调用一个函数,并将函数的this值设置为提供的值。接受一个数组或类数组对象,其中的元素会作为函数的参数传递。 -
bind():创建一个新的函数,新函数的this值和参数被预设为bind()的参数。 -
toString():返回一个表示函数源代码的字符串。
需要注意的是,虽然函数是对象,但它们和普通的对象有一些重要的区别。最重要的区别是函数可以被调用执行,而普通的对象不能。
还有箭头函数this因为一开始就确定好了,所以不可以通过call、apply、bind改变。
函数内部
函数内部有一些特殊的对象和变量,它们在函数执行时被创建,并在函数执行完毕后被销毁。除了上面提到的arguments,还有以下特殊对象和变量:
-
this:这是一个特殊的变量,它引用了函数被调用时的上下文对象。this的值在函数被调用时确定,取决于函数的调用方式。例如,当一个函数作为对象的方法被调用时,this引用了那个对象;当一个函数直接被调用(即不通过对象或其他函数调用)时,this引用了全局对象(在非严格模式下)或undefined(在严格模式下)。当然箭头函数因为this被提前确定了,所以不具备这样的特性。 -
return:这是一个关键字,用于指定函数的返回值。当执行到return语句时,函数会立即停止执行,并返回return后面的表达式的值。如果函数没有return语句,或者return后面没有表达式,那么函数会返回undefined。 -
局部变量:在函数内部声明的变量是局部变量,它们只在函数内部可见。每次函数被调用时,都会创建新的局部变量。
-
内部函数:在函数内部可以声明其他的函数,这些函数被称为内部函数或嵌套函数。内部函数可以访问外部函数的变量和参数,这称为词法作用域或静态作用域。
函数的种类
除了上面函数声明提到的三种函数:function声明式函数,函数表达式以及箭头函数,还有以下几种函数类型:
- 立即调用的函数表达式(Immediately Invoked Function Expression,IIFE):这种函数在定义后立即调用。
javascript
(function() {
console.log('Hello, world!');
})();
- 生成器函数(Generator Function):生成器函数是ES6引入的新特性,它可以返回一个生成器对象。生成器对象可以按需产生一系列的值。
javascript
function* idGenerator() {
let id = 0;
while (true) {
yield id++;
}
}
- 构造函数(Constructor Function):构造函数用于创建新的对象。构造函数通常首字母大写,使用
new关键字调用。
javascript
function Person(name) {
this.name = name;
}
let john = new Person('John');
- 方法(Method):方法是定义在对象或类中的函数。
javascript
var obj = {
greet: function() {
console.log('Hello, world!');
}
};
递归函数和尾调用优化
递归函数
递归函数是一种调用自身的函数。
javascript
function factorial(n) {
if (n === 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}
console.log(factorial(5)); // 输出:120
factorial函数接受一个参数n,并返回n的阶乘。如果n等于0,那么函数返回1;否则,函数返回n乘以factorial(n - 1)的结果。这里,factorial(n - 1)是一个递归调用,它计算(n - 1)的阶乘。
递归函数必须有一个终止条件,否则函数会无限递归,导致栈溢出错误。
尾调用优化
尾调用优化是一种编程语言的特性,它允许在函数的最后一步调用另一个函数时,不增加调用栈。这意味着,即使在无限递归的情况下,也不会出现栈溢出错误。
以下是一个使用尾调用的例子:
javascript
function factorial(n, acc = 1) {
if (n === 0) {
return acc;
} else {
return factorial(n - 1, n * acc);
}
}
console.log(factorial(5)); // 输出:120
在这个例子中,factorial函数在最后一步调用了自身,这是一个尾调用。这里使用了一个累积器acc来保存中间结果,这样在每次递归调用时,都只需要计算一次乘法,而不需要保存中间的乘法结果。这使得函数可以被尾调用优化。
尾调用优化失败的一些例子:
js
function foo(x) {
return bar(x) + 1; // 尾调用后还有加法操作,无法优化
}
function foo(x) {
bar(x); // 尾调用的结果不是函数的返回值,无法优化
}
function foo(x) {
let y = x * 2;
return function() { return y; } // 尾调用函数是一个闭包,无法优化
}
function foo(x) {
return new bar(x); // 尾调用函数是一个构造函数,无法优化
}
function foo(x) {
return obj.bar(x); // 尾调用函数是一个方法,无法优化
}
function factorial(n) {
if (n === 0) {
return 1;
} else {
// 尾调用自身,但是调用后还有乘法操作,所以不能被尾调用优化
return n * factorial(n - 1);
}
}
需要注意的是,只有在严格模式下,JavaScript引擎才会进行尾调用优化。通过在文件或函数的开始处添加'use strict';来开启严格模式。
总结一下
虽然说到了函数就有意思了,但这篇主要写的还是函数的基本内置规则。好吧,总结一下要点:
- 函数是一个具有一定功能的代码块,可以有输入值参数,也可以有返回值,默认返回
undefined - 函数声明有几种,注意区别,根据实际开发需要使用
- 函数参数可以写也可以不写,写是为了方便,推荐写
- 非箭头函数可以通过
arguments获得参数 - 函数参数可以通过剩余参数收集参数
- JavaScript函数没有重载
- 注意非箭头函数中的
this指向,这个需要总结,一句话总结的话:决定于调用环境 - 箭头函数
this提前确定,不可以被bind、call、apply改变,没有property属性,没有arguments,也不能作为构造函数 - 函数的种类有立即调用函数、构造函数还有生成器函数
- 递归和尾调用函数学会使用
本文完。