# 函数的定义与参数 🔥

先讲函数是为了明确编写 JS 时需要有一颗函数式编程的心,而不是对象。当然也有缺点。

JavaScript中最关键的概念是:函数是第一类对象(first-class objects),或者说它们被称作一等公民(first-class citizens)。函数与对象共存,函数也可以被视为其他任意类型的JavaScript对象。

  • 能以字面量形式声明
  • 能被变量引用
  • 甚至能被作为函数参数进行传递

# 对象的功能—引入

  • 对象可通过字面量来创建{}

  • 对象可以赋值给变量、数组项,或其他对象的属性

    // 为变量赋值一个新对象
    var ninja = {};
    // 向数组增加一个新对象
    ninjaArray.push({});
    // 给某个对象的属性赋值一个新对象
    ninja.data = {};
    
  • 对象可以作为参数传递给函数

    function hide(ninja){
        ninja.visibility = false;
    }
    hide({});
    
  • 对象可以作为函数的返回值

    function returnNewNinja(){
        return {};
    }
    
  • 对象能够具有动态创建和分配的属性

    var ninja = {};
    ninja.name = "conanan";
    

在JavaScript中,我们几乎能够用函数来实现同样的事

# 函数是第一类对象 🔥

JavaScript中函数拥有对象的所有能力,也因此函数可被作为任意其他类型对象来对待。当我们说函数是第一类对象的时候,就是说函数也能够实现以下功能。

  • 通过字面量创建

    function ninjaFunction(){}
    
  • 赋值给变量,数组项或其他对象的属性

    // 为变量赋值一个新函数
    var ninja = function(){};
    // 向数组增加一个新函数
    ninjaArray.push(function(){});
    // 给某个对象的属性赋值一个新函数
    ninja.data = function(){};
    
  • 作为函数的参数来传递

    function call(ninjaFunction){
        ninjaFunction();
    }
    call(function(){});
    
  • 作为函数的返回值

    function returnNewNinjaFunction(){
        return function(){};
    }
    
  • 具有动态创建和分配的属性。这简直令人惊讶!!!🔥

    var ninjaFunction = function(){};
    ninjaFunction.ninja = 'conanan';
    // 上面两个的确实可以打印出正确的结果(注意不要使用name属性,它是函数的名称)
    
    function useless(ninjaCallback){
        return ninjaCallback();
    }
    

对象能做的任何一件事,函数也都能做。函数也是对象,唯一的特殊之处在于它是可调用的(invokable),即函数会被调用以便执行某项动作。

# 回调函数 🔥

函数是第一类对象,它可以作为函数的参数来传递,这也表明传入函数会在应用程序执行的未来某个时间点才执行即回调函数

在执行过程中,我们建立的函数会被其他函数(无论是在事件处理阶段通过浏览器(事件)还是通过自己写的其他代码)在稍后的某个合适时间点“再回来调用”。实际中使用的地方很多,如:单击一次按钮、从服务端接收数据,还是UI动画的一部分,都是回调函数!

可以在表达式出现的任意位置创建函数,除此之外这种方式能使代码更紧凑和易于理解(把函数定义放在函数使用处附近)。当一个函数不会在代码的多处位置被调用时,该特性可以避免用非必须的名字污染全局命名空间。

# 函数动态创建和分配属性的应用 🔥

函数具有动态创建和分配的属性,利用此可以解决很多问题

# 存储函数

在集合中存储函数使我们轻易管理相关联的函数。例如,某些特定情况下必须调用的回调函数。不能重复!

const 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.')

这个方法也不是特别好,后续可以使用 ES6 的 Set 来改进

# 自记忆函数

记忆让函数能记住上次计算得到的值,从而提高后续调用的性能。对于动画中的计算、搜索不经常变化的数据或任何耗时的数学计算来说,记忆化这种方式是十分有用的。如下面的素数:

function isPrimer(value){
    if(!isPrimer.answers){
        // 构建一个结果缓存,它会保存函数每次计算得到的结果
        isPrimer.answers = {}
    }
    
    // 检查缓存的值
    if(isPrimer.answers[value] !== undefined){
        return isPrimer.answers[value]
    }
    
    // 计算素数
    let prime = value !== 0 && value !== 1 // 0 和 1 都不是素数
    for(let i = 2; i < value; i++){
        if(value % i === 0){
            prime = false
            break
        }
    }
    
    // 记录结果缓存,并返回 prime 的值,只是在这之前多个赋值操作
    return isPrimer.answers[value] = prime
}

优点:

  • 由于函数调用时会寻找之前调用所得到的值,所以会有性能收益
  • 它几乎是无缝地发生在后台,最终用户和页面作者都不需要执行任何特殊请求,也不需要做任何额外初始化

缺点:

  • 任何类型的缓存都必然会为性能牺牲内存
  • 缓存逻辑不应该和业务逻辑混合,函数或方法只需要把一件事做好。后续改进
  • 对于这类问题很难做负载测试或估计算法复杂度,因为结果依赖于函数之前的输入

# 函数定义

# 函数声明 & 函数表达式 🔥

函数定义/声明(function declarations)和函数表达式(function expressions)最常用,在定义函数上却有微妙不同的的两种方式

对于函数声明来说,函数名是强制性的,而对于函数表达式来说,函数名则完全是可选的

function myFun(){
    return 1
}

const myFun = function(){
    return 1
}

函数声明是独立的,是独立的JavaScript代码块它可以被包含在其他函数中

让函数包含在另一个函数中可能会因为忽略作用域的标识符解析而引发一些有趣的问题

function ninja(){
    function hiddenNinja(){
        return 'ninja here'
    }
    
    return hiddenNinja()
}

函数表达式 🔥

JavaScript函数通常由函数字面量(function literal)来创建函数值,就像数字字面量创建一个数字值一样,function(){}就是字面量。它通常作为其他语句的一部分

const a = 3
myFunc(3)

const a = function(){}
myFunc(function(){})

# 立即调用函数表达式 IIFE 🔥

也称为立即函数。这一特性能够模拟JavaScript中的模块化,故可以说它是JavaScript开发中的重要理念

(function(num){ 
    console.log(num)
})(10)

为什么**函数表达式**被包裹在一对括号内

其原因是纯语法层面的。JavaScript解析器必须能够轻易区分函数声明和函数表达式之间的区别。如果去掉包裹函数表达式的括号,把立即调用作为一个独立语句function() {}(3), JavaScript开始解析时便会结束,因为这个独立语句以function开头,那么解析器就会认为它在处理一个函数声明。每个函数声明必须有一个名字(然而这里并没有指定名字),所以程序执行到这里会报错。为了避免错误,函数表达式要放在括号内,为JavaScript解析器指明它正在处理一个函数表达式而不是语句。

还有一种相对简单的替代方案(function(){}(3))也能达到相同目标(然而这种方案有些奇怪,故不常使用)。把立即函数的定义和调用都放在括号内,同样可以为JavaScript解析器指明它正在处理函数表达式。

立即调用函数表达式主题的4个不同版本

+function(){}()
-function(){}()
!function(){}()
~function(){}()

这种做法也是用于向JavaScript引擎指明它处理的是表达式,而不是语句。从计算机的角度来讲,注意应用一元操作符得到的结果没有存储到任何地方并不重要,只有调用IIFE才重要

# 箭头函数 🔥

箭头函数(通常被叫做lambda函数),ES6 新增。由于JavaScript中会使用大量函数,增加简化创建函数方式的语法十分有意义

let arr = [3,1,5,2,9,6]
arr.sort((value1, value2) => value1 - value2)
  • 箭头函数的定义以一串可选参数名列表开头,参数名以逗号分隔

    对于0个或1个以上的参数,括号为必选项;否则括号不是必须的

  • 必选的=>

  • 如果箭头函数的函数体是一个表达式,则该箭头函数的返回值就是表达式的值

  • 如果箭头函数的函数体是一个代码块,则该箭头函数的返回值与普通函数一样(没有 return 语句则是 undefined)

它能帮助我们规避一些在很多标准函数中可能遇到的难以捉摸的缺陷

# 函数构造函数(略过)

与构造函数不同!!!

一种不常使用的函数定义方式,能让我们以字符串形式动态构造一个函数,这样得到的函数是动态生成的

new Function('a', 'b', 'return a + b')

# 生成器函数

ES6新增功能,能让我们创建不同于普通函数的函数,在应用程序执行过程中,这种函数能够退出再重新进入,在这些再进入之间保留函数内变量的值。我们可以定义生成器版本的函数声明、函数表达式、函数构造函数。

function* myGen(){
    yield 1
}

函数创建的方式很大程度地影响了函数可被调用的时间、函数的行为以及函数可以在哪个对象上被调用

# 函数的参数

# 函数的形参和实参

  • 形参是我们定义函数时所列举的变量。所有类型的函数都能有形参(函数声明、函数表达式、箭头函数)
  • 实参是我们调用函数时所传递给函数的值

注意

  • 实参的数量大于形参时不会报错,额外的实参不会赋值给任何形参。尽管有些实参没有被分配给某个形参名,但依然有一种获取它们的方式(后续补充)
  • 如果形参的数量大于实参,那么那些没有对应实参的形参则会被设为 undefined

# 剩余参数—ES6

类似 Java 可变参数。剩余参数是真正的Array实例,arguments不是!

function multiMax(first, ...remainingNumbers){
    const sortedArr = remainingNumbers.sort((value1, value2) => value2 - value1)
    return first * sortedArr[0]
}

multiMax(10,2,6,3,8,3)// 80

只有函数的最后一个参数才能是剩余参数。否则报错 SyntaxError: parameter after rest parameter

# 默认参数—ES6

许多网页的UI组件(尤其是jQuery插件)都能被配置。例如,如果正在开发一个轮播组件,我们可能会给用户提供一个选项,用于指定某个项目多久会被另一个项目替代,以及一段在变化发生时间段内的动画。与此同时,可能某些用户并不关心这些问题,而且无论我们提供什么选项他们都乐于使用。对于这类场景,默认参数是完美选择。

一个简单的例子:大部分“忍者”常常是偷偷摸摸地潜行(skulking),但Yagyu只喜欢简简单单地潜行(sneaking)

function performAction(ninja, action){
    return ninja + " " + action
}

performAction('conan','skulking')
performAction('conanan','skulking')
performAction('zhangsan','sneaking')

每次重复相同的参数skulking是不是看起来相当无聊。在其他编程语言中,这个问题最常用的解决方式是函数重载(再定义一个名字相同但参数不同的函数)。但是 JavaScript不支持函数重载,所以当在过去面临这个问题的时候,开发者通常采用如下方法:

function performAction(ninja, action){
    action = typeof action === 'undefined' ? 'skulking' : 'sneaking'
    return ninja + " " + action
}

performAction('conan')
performAction('conanan')
performAction('zhangsan','sneaking')

ES6 的默认参数解决:

function performAction(ninja, action = 'skulking'){
    return ninja + " " + action
}

performAction('conan')
performAction('conanan')
performAction('zhangsan','sneaking')

可以为默认参数赋任何值,它既可以是数字或者字符串这样的原始类型,也可以是对象、数组,甚至函数这样的复杂类型。每次函数调用时都会从左到右求得参数的值,并且当对后面的默认参数赋值时可以引用前面的默认参数

// 甚至可以引用前面的参数,不推荐这样写
function performAction(ninja, action = 'skulking', message = ninja + " " + action){
    return message
}

# 练习

# 1

var samurai = (()=> 'tomoe')()
console.log(samurai)// tomoe

var samurai2 = (()=> {'tomoe'})()
console.log(samurai2)// undefined