JavaScript中最关键的概念是:函数是第一类对象,或者说一等公民。函数与对象共存,函数也可以被视为其他任意类型的JavaScript对象。函数和那些更普通的JavaScript数据类型一样,它能被变量引用,能以字面量方式声明,能被作为函数参数传递。面向函数编程带来的差异,会发现,在需要调用某函数的位置定义该函数,能让我们编写更紧凑、更易懂的代码
函数式的不同点
函数及函数式概念之所以重要,其原因之一在于函数是程序执行过程中的主要模块单元。除了全局JavaScript代码是在页面构建的阶段执行的,所有的脚本代码都将在一个函数中执行。
首先在JavaScript中对象有以下几种常用功能
- 对象可通过字面量来创建{}
- 对象可以赋值给变量、数组项、其他对象的属性
- 对象可以作为参数传递
- 对象可以作为函数的返回值
- 对象能够具有动态创建和分配的属性
其实,在JavaScript中,我们几乎能够用函数来实现同样的事
函数是第一类对象
JavaScript中函数能够拥有对象的所有能力,也因此函数可被作为任意其他类型对象对待,因此函数也能实现以下功能
- 通过字面量创建
- 赋值给变量、数组项、其他对象的属性
- 作为参数传递
- 作为函数的返回值
- 具有动态创建和分配的属性
对象能做的任何一件事,函数也能做。函数也是对象,唯一的特殊之处在于它是可调用的。
第一类对象的特点之一是它能够作为参数传入函数。对于函数而言,这项特性也表明:如果我们将某个函数作为参数传人另一个函数,传入函数会在应用程序执行的未来某个时间点才执行。这个概念就是回调函数
回调函数
每当我们建立一个将在随后调用的函数时,无论是在事件处理阶段通过浏览器还是通过其他代码,都是在建立一个回调。
举个🌰
const text = 'Domo arigato!';
console.log("Before defining functions");
function useless(ninjaCallback) {
console.log("In useless function");
return ninjaCallback();
}
function getText() {
console.log("In getText function");
return text;
}
console.log("Before making all the calls");
assert(useless(getText) === text, "The useless function works! " + text);
console.log("After the calls have been made");
function assert(value, text){
value&&console.log(text)
}
// Before defining functions
// Before making all the calls
// In useless function
// In getText function
// The useless function works! Domo arigato!
// After the calls have been made
上面代码的执行:
- getText函数作为参数传入useless函数
- useless函数体内通过Callback参数可以获取getText函数的引用
- 回到函数的调用让getText函数得到执行,也就是我们传入的getText函数通过useless函数被回调
过程很容易,表现在JavaScript函数式本质是让我们把函数作为第一类对象。
JavaScript的重要特征之一是可以在表达式出现的任意位置创建函数,除此之外这种方式能使代码更紧凑和易于理解(把函数定义放在函数使用处附近)。当一个函数不会在代码的多处位置被调用时,该特性可以避免用非必须的名字污染全局命名空间。
函数作为对象的乐趣
在集合中存储函数使我们轻易管理相关联的函数。例如,某些特定情况下必须调用的回调函数。记忆函数能记住上次计算得到的值,从而提高后续调用的性能。
存储函数
某些场景中我们需要存储元素唯一的函数集合。当我们向这样的集合中添加函数是会面临两个问题:那个函数对于这个集合来说是一个新函数,从而需要被加入到该集合中?又是那个函数已经存在于集合中,从而不需要再次加入到集合中?
一般来说,管理回调函数集合时,我们并不希望存在重复函数,否则一个事件会导致一个回调函数被多次调用。
举个🌰
var store = {
nextId: 1,
cache: {},
add: function(fn) {
if (!fn.id) {
fn.id = this.nextId++;
this.cache[fn.id] = fn;
return true;
}
}
};
function ninja(){}
assert(store.add(ninja),"Function was safely added.");
assert(!store.add(ninja),"But it was only added once.");
function assert(value, text){
value&&console.log(text)
}
以上代码中,创建了一个对象赋值给变量store。这个对象有两个数据属性:nextId是下一个可用的id,cache为缓存已经保存的函数。函数通过add()方法添加到缓存中
在add函数内部,首先检查该函数是否存在id属性,如果当前函数已经存在id属性,则假设该函数被处理过了,从而忽略该函数,否则为该函数分配一个id。同时增加nextId,保证下个ID不会有重复,然后将该函数增加到cache上,id作为属性名。
代码运行时,程序尝试两次添加函数,而该函数只被添加一次到缓存中
自记忆函数
记忆化是一种构建函数的处理过程,能够记住(缓存)上次计算结果。当函数计算得到结果时就将该结果按照参数存储起来。采用这种方式,如果另外一个调用也使用相同的参数,我们则可以直接返回上次存储的结果而不是再计算一遍,这样避免即重负又复杂的计算可以显著的提高性能。
举个🌰
计算素数(素数:素数一般指质数。 质数是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。)
function isPrime(value) {
if (!isPrime.answers) {
isPrime.answers = {};
}
if (isPrime.answers[value] !== undefined) {
return isPrime.answers[value];
}
var prime = value !== 1;
for (var i = 2; i < value; i++) {
if (value % i === 0) {
prime = false;
break;
}
}
return isPrime.answers[value] = prime;
}
assert(isPrime(5), "5 is prime!" );
assert(isPrime(5), "5 is prime!" );
assert(isPrime.answers[5], "The answer was cached!" );
function assert(value, text){
value&&console.log(text)
}
// 5 is prime!
// 5 is prime!
// The answer was cached!
以上代码执行流程:
- 在isPrime函数中,首先检查answers属性来确认是否创建了一个缓存,如果没有创建,则创建一个
- 只有第一次函数调用才会创建这个初始空对象,之后之歌缓存就已经存在了。然后检查传入的值是否存储在缓存中
- 这个缓存会针对参数中的值value来存储该值是否为素数(true或false)。如果在缓存中找到该值,函数会直接返回。
- 由于缓存是函数自身的一个属性,所以只要该函数还存在,缓存也就存在
函数定义
JavaScript中提供了几种定义函数的方式,可以分为四类
-
函数定义和函数表达式(最常用)
function myFun(a,b){return a+b} -
箭头函数
(a,b)=>a+b -
函数构造函数(最不常用)
new Function('a','b','return a+b') -
生成器函数—ES6新增功能,不同于普通函数的函数,在执行过程中,这种函数能够退出再重新进入
function* myGen(){yield 1}
函数声明和函数表达式
JavaScript中定义函数最常用的方式是函数声明和函数表达式。
函数声明
以强制性的function开头,紧接着强制性的函数名,以及括号和括号内一系列逗号分隔的可选参数名。函数体是一列可以为空的表达式,这些表达式必须包含在花括号内。
每个函数声明必须包含一个条件:作为一个单独的JavaScript语句,函数声明必须独立
举个🌰
function samurai() {
return "samurai here";
}
function ninja() {
function hiddenNinja() {
return "ninja here";
}
return hiddenNinja();
}
函数表达式
JavaScript中函数是第一类对象,除此之外也就意味着它们可以通过字面量创建,可以赋值给变量和属性,可以作为参数或返回值。
总是是其他表达式的一部分的函数叫做函数表达式(作为赋值表达式的右值,或者作为其他函数的参数)
举个🌰
var myFunc = function(){};
myFunc(function(){
return function(){};
});
箭头函数
箭头函数是函数表达式的简化版。
举个🌰
var greet = name => "Greetings " + name;
var anotherGreet = function(name){
return "Greetings " + name;
}
箭头函数的定义以一串可选参数名列表开头,参数名以逗号分隔,当中有一个参数时,括号不是必须的,否则必须有括号或者将参数包裹在括号内
箭头操作符后面有两种可选方式。
- 如果要创建一个简单函数,那么可以把表达式放在这里,该函数的返回值即为此表达式的返回值
- 当箭头函数没那么简单从而需要更多代码时,箭头操作符则可以跟一个代码块。当代码块中没有return语句,返回值时undefined,否则就是return表达式的值
函数的实参和形参
形参时定义函数时所列举的变量
实参时调用函数时所传递给函数的值
当函数调用时提供了一系列实参,这些实参就会以形参在函数中定义的顺序被赋值到形参上。
如果实参的数量大于形参,那么额外的实参不会赋值给任何形参
如果形参的数量大于实参,那么没有对应实参的形参会被设为undefined
剩余参数
为函数的最后一个命名参数前加上参略号(...)前缀,这个参数就变成了一个叫做剩余参数的数组。数组内包含着传入的剩余的参数
举个🌰
获取第一个参数的值与余下参数中最大值的数做乘法
function multiMax(first, ...remainingNumbers){
var sorted = remainingNumbers.sort(function(a, b){
return b - a;
});
return first * sorted[0];
}
assert(multiMax(3, 1, 2, 3) == 9, "3*3=9 (First arg, by largest.)");
function assert(value, text){
value&&console.log(text)
}
以上代码中,用四个参数调用了multiMax函数,在multiMax函数内第一个参数的值3被复制给了第一个形参first。由于第二个形参时剩余参数,所以剩余所有的参数都被放在数组remainingNumbers中。
默认参数
函数中的形参可以设定默认的值,当对应的实参没有传入的时候,就会采用默认的值,当对应的实参传入时会采用传入的实参
举个🌰
function performAction(ninja, action = "skulking"){
return ninja + " " + action;
}
assert(performAction("Fuma") === "Fuma skulking","The default value is used for Fuma");
assert(performAction("Yoshi") === "Yoshi skulking","The default value is used for Yoshi");
assert(performAction("Hattori") === "Hattori skulking","The default value is used for Hattori");
assert(performAction("Yagyu", "sneaking") === "Yagyu sneaking","Yagyu can do whatever he pleases, even sneak!");
// The default value is used for Fuma
// The default value is used for Yoshi
// The default value is used for Hattori
// Yagyu can do whatever he pleases, even sneak!
以上代码可以看到创建默认参数的方式是为函数的形参赋值
总结
把JavaScript看作函数式语言你就能书写复杂的代码。作为第一类对象,函数和JavaScript其他对象一样,具有以下功能
- 通过字面量创建
- 赋值给变量或属性
- 作为参数传递
- 作为函数返回值结果
- 赋值给属性和方法
回调函数是被代码在未知情况回来调用的函数
函数具有属性,而且这些属性能够被存储任何信息,可以利用这个特性存储函数,或者缓存结果减少不必要的计算
有很多不同类型的函数:函数声明、函数表达式、箭头函数、函数生成器
函数声明和函数表达式是两种最主要的函数类型。函数声明必须具有函数名,在代码中也必须作为一个独立的语句存在。函数表达式可以不必有函数名,但此时它就i必须作为其他语句的一部分。
箭头函数是JavaScript新增特性,可以使用更简洁的方式定义函数
形参试函数定义时列出的变量,实参试函数调用时传递的值
形参列表和实参列表长度可以不同
未赋值的形参求值为undefined
传入的额外的实参不会赋值给任何一个命名形参
剩余参数和默认参数是JavaScript的新特性
剩余参数-不与任何形参名相匹配的额外实参可以通过剩余参数来引用
默认参数-函数调用时,若没传入参数,默认参数可以给函数提供缺省的参数值