函数的定义和参数
结构
- 函数式的不同点到底是什么?
- 函数作为对象的乐趣
- 函数定义
- 函数的实参和形参
- 小结
- 练习
函数式的不同点到底是什么?
函数式概念之所以如此重要, 原因之一在于函数是执行过程中的主要模块单元.
既然大多数代码都以函数作为模块单元, 那么就应该尽可能少的限制函数的使用方式, 从而引出函数是一等公民的概念. 函数拥有对象的所有能力. 从一等公民的概念上看, 函数应该具有如下能力:
// 使用字面量创建一个函数
function f() {}
// 将函数赋值给变量
var v = f
// 将函数插入到数组中
var arr = []
arr.push(f)
// 将函数作为对象的属性
var obj = {}
obj.f = f
// 将函数作为参数传递给另外一个函数
function call(func) {
func()
}
call(f)
// 将函数作为一个值从另一个函数返回
function creator() {
return function() {}
}
// 奇特的是, 函数其实是对象, 给一个函数添加属性
// 函数和其他非函数的对象的主要区别在于函数是可调用的, 而其它对象不是
f.status = "done"
函数式编程是一种编程风格, 或称为范式. 函数式编程不是唯一的编程范式, 对于同一个问题, 可以使用函数式风格解决, 也可以通过其他编程风格解决. 其他编程风格还有命令式(C语言), 面向对象式(Java). 函数式编程本身是一个较大的问题, 一般来说, 使用函数式编程的主要优势在于其代码易于测试, 扩展和模块化.
回调函数
高效使用Javascript编程的关键在于使用回调函数. 基于Javascript中函数作为一等对象且函数可以定义在任何表达式出现的位置, 也就可以方便的将函数做为回调函数传递给另一个函数完成调用. 这样的调用通常是异步的.
let arr = []
for(let i = 0; i < 20; i++) {
arr.push(Number.parseInt(Math.random()*100))
}
// 将比较函数作为回调函数传递给排序函数, 该函数定义在参数列表中
arr.sort(function(v1, v2) {
return v1 - v2
})
函数作为对象的乐趣
在Javascript中一个让人感觉惊讶的地方是函数是对象. 和其他所有对象一样可以赋予属性, 属性可以是任意的值. 如此可以产生一些特别的应用, 如赋值给函数一个id来管理函数的集合, 或者将函数每次计算得到的值缓存在其自身的属性中, 如此可以在后续调用时首先根据入参查找是否已经缓存过结果, 从而提高运算性能.
管理函数集合
var store = {
nextId: 1,
cache: {},
add: function(fn) {
if(!fn.id) {
fn.id = this.nextId++
this.cache[fn.id] = fn
return true
}
}
}
store对象的作用是存储一个函数的集合用于如某个事件触发之后调用其中存储的所有回调函数. 也可以简单的使用数组来存储这样一个集合, 这样就需要一个方式确认向集合中添加函数时不会添加了重复的函数. 使用数组来存储这个集合也就意味着每次添加函数时都需要遍历一遍集合以确保集合中不会保存重复的函数. 而使用store对象则是更简洁的方法, 添加函数进入集合时不需要每次都遍历一遍集合, 只需简单的在函数上设置一个标志位即可.
自记忆函数
function isPrime(value) {
// 首先创建缓存区
if(!isPrime.cache) {
isPrime.cache = {}
}
// 在缓存区查找缓存值
if(isPrime.cache[value] !== undefined) {
return isPrime.cache[value]
}
// 没有缓存值的情况下才执行运算
let prime = value !== 0 && value !== 1
for(let i = 2; i < value; i++) {
if(value % i === 0) {
prime = false
break
}
}
// 将执行运算的结果存入缓存区
return isPrime.cache[value] = prime
}
以上代码利用了函数是对象的特性创建了一个可以缓存计算结果的函数, 此函数将这个缓存操作封装在其内部, 外部调用者无需以任何特殊方式取使用它就可以获得性能的提升.
函数定义
函数通常使用函数字面量来创建函数值, 然而作为第一类对象, 函数可以使用程序中的值定义的, 如字符串或变量中的值. 一共有4类函数定义方式:
- 函数定义和函数表达式
function func() { return 1 }
var func = function() { return 1 }
- 箭头函数
param => param * 2
- 函数构造函数
new Function('a', 'b', 'return a + b')
- 生成器函数
function* gen() { yield 1 }
函数创建的方式影响了函数可被调用的时间, 函数的行为及函数可以在哪个对象上被调用.
函数声明和函数表达式
首先是函数声明, 其定义语法是: function func(a, b) { return a + b }
, 它和函数表达式看起来类似, 其特点是函数声明是独立的Javascript代码块, 作为一个单独的Javascript语句.
接着是函数表达式, 其定义语法是: function(a, b) { return a + b }
, 可以看到它就像一个没有名字的函数声明. 实际上函数表达式几乎总是其他表达式的一部分, 例如可以出现在赋值表达式的右值, 或出现在函数调用的参数列表中. 简单来说它就是一个表达式.
最后是立即函数, 对于基本的函数调用而言首先求值得到函数的标识符作为左值, 接着使用函数调用运算符(即一对括号)调用这个函数. 函数表达式的定义本身就可以作为函数标识符的左值, 因此可以使用一对括号立即调用这个新定义的函数, 这就是立即调用函数表达式(IIFE).
// 从语法层面依然需要一对括号括起函数表达式的定义
// 如果使用函数表达式定义函数, 而该定义独立的出现在程序代码中
// 而非作为其他表达式的一部分, 解释器将会报错, 因为其认为这是一个
// 忘记写函数名的函数声明, 因此需要让函数定义表达式出现在另一个表达式中,
// 如使用运算符通知解释器这里是一个表达式
;(function(){
console.log('immediately')
})()
;(function() {
console.log('expression')
}())
// 使用4个一元运算符依然可以通知解释器接下来的函数表达式是一个表达式而非语句
;+function(){
console.log('+++')
}()
;-function(){
console.log('---')
}()
;!function(){
console.log('!!!')
}()
;~function(){
console.log('~~~')
}()
箭头函数
Javascript中会大量使用函数, 因此ES6标准中出于简化创建函数方式的目的新增了箭头函数, 一般来说箭头函数就是函数表达式的简化版. 箭头函数有两种可选方式:
// 函数体就是一个表达式时省略了return, 该表达式的求值结果就是函数的值
var greet = name => 'Greetings ' + name
// 和函数表达式一样, 使用大括号包含了整个函数体
var greet = name => {
var helloString = 'Greetings '
return helloString + name
}
函数的实参和形参
- 形参是定义函数时所列举的变量
- 实参是调用函数时所传递给函数的值
当调用一个函数时所列举的实参会按照顺序赋值给函数内的形参. 当实参多余形参或形参多余实参时都不会抛出错误. 多出的实参被简单的丢弃, 而多出的形参则被赋值为undefined.
剩余参数
在形参列表中的最后一个形参可以使用剩余参数, 使用...
标识这个形参是剩余参数, 它将接收所有还未被之前形参所接收的实参的值, 并将这些值放到一个数组中. 如果没有任何可以接收的实参那么剩余参数将被赋值为空数组而非undefined
.
默认参数
默认参数是ES6的新特性, 其目的依然是书写简洁的代码, 看下面的例子中的ES6之前的默认参数处理方式和ES6之后的默认参数处理方式就可以看出其区别:
function sum(a, b) {
// ES6之前处理默认参数需要写出冗长的代码
a = typeof a === 'undefined' ? 0 : a
b = typeof b === 'undefined' ? 0 : b
return a + b
}
// ES6之后简单的在参数列表中指定其默认值
function sum(a=0, b=0) {
return a + b
}
// 默认参数可以引用参数列表中之前的参数的值
// 现在如果只向sum传递一个参数那么它的功能就变成了double
function sum(a=0, b=a) {
return a + b
}