本文以《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
,也不能作为构造函数 - 函数的种类有立即调用函数、构造函数还有生成器函数
- 递归和尾调用函数学会使用
本文完。