认真学JavaScript系列之:函数

616 阅读21分钟

前言

函数在很多编程语言都能见到,我记得大一学C语言的时候,老师说了一句我至今难忘的话:"不要把函数想的多么难,它就是一个工厂,原材料丢进去,加工后,再把产品送出来"

确实,简单明了,函数本身就是数据的加工厂,输入数据,处理完后并返回

1 函数

相比其他语言,JavaScript中的函数最有意思的一部分,因为函数本身就是对象,每个函数都是Function的实例,所以函数本身也有自己的属性和方法。

在JavaScript中,函数是一等公民,与其他数据类型一样,可以赋值给其他变量,也可以作为参数传入另一个函数,或者作为别的函数的返回值。


1.1 JavaScript定义函数的方式

在我们日常写代码时,几乎到处都是函数,对于不同的场景,函数的定义方式也不相同

1.1.1 函数声明

函数声明 是定义最常见的方式,也是大多数人学习js时接触过的头一种方式,比如:

function add (num1, num2) {
    return num1 + num2
}

上面使用函数声明的方式定义了一个函数,用于求2数相加的和,其结构很简单:

function 函数名 (参数1, 参数2, ...) {
    函数体
}
1.1.1.1 函数提升

不同于其他的函数声明方式,函数声明存在着“提升”,所谓提升,意思就是代码的执行顺序提升排到最前面。类似于使用var声明的变量提升一样。

console.log(func0); //>> func0() {return 0}
console.log(func1); //>> undefined
//函数的声明形态
function func0() {
  return 0;
}
//函数的表达式形态
var func1 = function() {
  return 1;
};

上面的代码func0函数在声明之前就可以调用console.log(func0)被打印输出,是因为JS引擎把声明形态的函数提前处理,相当于提升了处理优先级。上面代码等同于:

var func1;
//函数的声明形态
function func0() {
  return 0;
}
console.log(func0); //>> func0() {return 0}
console.log(func1); //>> undefined
//函数的表达式形态
func1 = function() {
  return 1;
};

1.1.2 函数表达式

函数表达式定义的函数与函数声明是等价的,区别在于,函数表达式用一个变量来保存函数,如下:

let add = function (num1, num2) {
    return num1 + num2
}

上面同样定义了一个求和函数,用一个变量 add保存起来。注意function关键字后面没有名称,这个函数可以直接通过变量add来调用

//调用add函数
add(1,2)

⚠ 注意 用一个变量 add保存起来这句话 ,这里的保存是保存函数的引用地址的意思,即:变量add指向堆内存中的函数。具体可以参考我之前写的一篇文章《变量和内存》

函数表达式看起来就像一个普通的变量定义和赋值。即创建一个函数再把它赋值给一个变量functionName。这样创建的函数叫做匿名函数(anoymous function)。因为function关键字后面没有函数名。匿名函数有时候也叫兰姆达函数。未赋值给其他变量的匿名函数的name属性是空字符串。

此外,使用函数表达式定义的函数,没有提升的概念,需要先赋值再使用,下面的代码会导致错误:

hello() //>> Error ! function doesn't exist yet

let hello = function() {
    console.log('你好')
}

1.1.3 箭头函数定义

箭头函数(arrow function)是ES6引出的一种新的定义函数的方式,与函数表达式很像,如下所示:

let add = (num1, num2) => {
    return num1 + num2
}

(参数1, 参数2, ...) => { }

这种方式省略了function关键字,并使用“胖箭头”指向函数体。关于箭头函数的用法以及与普通函数的区别,在后面的文章会详细展开,这里只需先了解其是定义函数的一种方式即可。

1.1.4 使用Function构造函数

正如之前所说,所有函数都是Function构造函数的实例,无论用哪种方式定义的函数。 既然这样,那么定义函数也可以使用 new Function()的方式进行实例化

let add = new Function("num1", "num2", "return num1 + num2")

不过,大多时候是不推荐这种做法的,首先其可读性很差,当函数体过长的时候写起来很费劲,其次其性能不好,因为参数和函数体都作为字符串用引号包裹,js要先解析字符串,再去定义函数体


1.2 函数的返回值

开头说过,函数是个工厂,处理数据并返回结果


function addNum(num) {
    return num + 10
}

let a = 10

let result = addNum(a)
console.log(result) //>> 20

上面定义了一个函数,将传入的数据加上10,再返回。

函数使用return关键字返回处理结果,return后面的内容,就是函数返回的结果。 函数可以返回任何值,当然也可以不返回值,或者返回空值,具体要看我们的需求

// 返回一个对象
function createObj() {
    return new Object()
}

// 返回一个函数
function createObj() {
    return function() {
        console.log('hello world')
    }
}

//无返回值
function hello() {
    console.log('hello world')
}

//返回空值
function nothing() {
    return
}

此外,在return下面的代码都不会被执行,因为函数碰到return就意味着执行结束了。

function say() {
    consoe.log('我在return前,可以执行')
    return console.log('我下面的代码不会被执行')
    consoe.log('我在return后,不会执行')
}
        //>> 我下面的代码不会被执行
say()  // >> 我在return前,可以执行

另外,我个人比较喜欢将返回值用在if else的判断中,用于终止多层次的嵌套。以一个请求服务数据为例:

function getListData() {
  fetch('http://example.com/movies.json')
  .then(function(response) {
    return response.json();
  })
  .then(function(res) {
     if(res.code === 200) {
        if(res.data.length > 0) {
            // do something
        }else {
            return Message('暂无新的值')
        }
     }else {
         return Message('服务出错')
     }
  });
}

上面的代码可以使用return提前终止,因为return后的代码不会执行,所以当服务异常或者数据没有更新时,下面的 do something就不会执行。这样当嵌套代码特别多的时候,就特别好用。

function getListData() {
  fetch('http://example.com/movies.json')
  .then(function(response) {
    return response.json();
  })
  .then(function(res) {
    if(res.code !== 200) return  Message('服务出错')
    if(res.data.length = 0) return Message('暂无新的值')
     //此处省略10000行
     //do something
  });
}

1.3 函数名

1.3.1 函数名即指针

在看该章节之前,假设你已经看了我之前写的《变量和内存》,或者你对js的变量类型已经有了充足的了解。

函数名,作为基本类型的变量,保存在栈内存中,其值就是指向函数的指针,所以函数名和存储其他引用类型的变量具有类似的行为,即:按地址引用。这意味一个函数可以有多个名称

function add (a, b) {
    return a + b
}

console.log(add(1, 2)) // >> 3

let newAdd = add
console.log(newAdd(1, 2)) // >> 3

上面的代码定义了一个名为add()的函数,用于求2数的和。

然后又声明了一个变量newAdd,并将它的值设置为add。此时sum和newAddd都指向同一个求和函数。

这里就相当于对象的浅拷贝

image.png


这样一来,add和newAdd指向的都是同一个函数对象,那么修改其中一个对象,另一个也会随之变化

function add(a, b) {
    return a + b + 1
}

let newAdd = add

console.log(newAdd(1, 2)) // >> 4
console.log(add(1, 2)) // >> 4

上面的代码,我们把add函数进行了修改,随之newAdd的调用结果也会发生同样的变化。


此外,要注意,上面说的对add函数进行修改,仅仅是修改函数体,而不是覆盖,如果add或newAdd被覆盖,二者就断了联系,请看如下代码:

function add(a, b) {
    return a + b + 1
}

let newAdd = add

// 这里相当于将add指向了一个新的求和函数,即覆盖
add = function (a, b) {
    return a + b + 3
}

// add 和 newAdd断开了联系,输出结果就不一样了
console.log(newAdd(1, 2)) // >> 4
console.log(add(1, 2)) // >> 6

上面对add函数的修改,实质上就是完全重写了add函数,此时add指向新创建的求和函数,而newAdd还是指向原有的求和函数,如下图所示:

image.png

这样一来,二者就完全断开了联系,因为各自指向了新的不同对象。

1.3.2 函数的name属性

在ES6中,所有的函数对象都会暴露一个只读的name属性,其中包含了关于函数的信息

let foo = () => { }

let bar = function () {}

function baz() {}

console.log(foo.name) // >> foo
console.log(bar.name) // >> bar
console.log(baz.name) // >> baz
console.log( (() => {}).name ) // >> (空字符串)
console.log( ( new Function () ).name ) //>> anonymous

可以看出,这个name属性保存的就是一个函数的标识符,或者说是一个字符串化的变量名。

即使函数没有名称,也会如实显示为一个空字符串。如果是使用Function构造函数创建的,则会被标识为anonymous

如果是一个get、set函数,或者使用bind()实例化,那么标识符前面会加上一个前缀:

function foo () {}
console.log(foo.bind(null).name) //>> bound foo

let dog = {
    yaers: 1,
    get age() {
        return this.yaers
    }
    set age(newAge) {
        this.yaers = newAge
    }
}

let propertyDescriptor = Object.getOwnPropertyDescriptor(dog, 'age')
console.log(propertyDescriptor.get.name) // >> get age
console.log(propertyDescriptor.set.name) // >> set age

延伸:Object.getOwnPropertyDescriptor用于获取指定对象上一个自有属性对应的属性描述符

1.4 理解函数的参数

在之前的《变量和内存》一文里,我们讲到了函数参数的复制,其实里面涉及到了诸如形参、实参和arguments对象没有详细展开,这里会做一个详细的介绍。

JavaScript中,对于函数的参数个数没有严格的校验,也就是说,函数定义时有参数,但是调用时可以不传、也可以多传同样的,函数定义时没有参数,你也可以传递参数,只不过多余的参数会被忽略


function add(a, b) {
    return a + b
}

//调用时不传
console.log(add()) //>> NAN

//调用时多传,多余的参数会被忽略
console.log(add(4, 5, 6)) //>> 9

function say() {
    console.log('hello')
}

say('你好', '世界') // >> hello

一开始看到这里,满头问号,第一感觉就是觉得,这也太不严谨了,岂不是乱来吗?

image.png

确实,谁让JavaScript是一门松散类型的语言呢。不过这样有好处,也有坏处,继续往下看

1.4.1 形参

形参,函数定义时规定要传的参数

function add(a, b) {
    return a + b
}

上面我们定义了一个函数,该函数接收2个参数ab,此时的a,b就是形参,即形式参数。

何为形式参数?打个比方,老板让你找2小伙子人去接项目,就说了是2人,至于姓什么,叫什么,多高,多大他不关心,就只要2人就行了,这里的2人就是形参

1.4.2 函数的length属性

了解完形参之后,来看一下函数的length属性,函数默认有一个length属性,是个数值,这个属性表示函数定义时形参的个数

function add(a, b) {
    return a + b
}
console.log(add.length) //>> 2

function say() {
  console.log('hello')
}
console.log(say.length) //>> 0

一般我们很少用length属性,多数时候可以用它来看一个未知函数的形参个数

import {unknowFunction} from 'other.js'
console.log(unknowFunction.length)

1.4.3 实参

实参,函数调用时实际传递的参数。

function add(a, b) {
    return a + b
}

let x = 4
let y = 5

add(x, y)

上面同样定义了一个函数,然后定义了变量x,y,调用函数,将x,y传递进去,此时的x,y就是实参。

打个比方,老板让你找2小伙子人去接项目,就说了是2人,至于姓什么,叫什么,多高,多大他不关心,就只要2人就行了。接着,你把人找来了,一个叫卧龙,一个叫凤雏,卧龙4岁,凤雏5岁,这里的2人就是实参,就是你实际找过来的人

image.png

1.4.4 函数的 arguments对象

前面说的形参、实参、length属性有什么用呢?这就不得不说arguments对象了

函数内部(箭头函数除外),都有一个arguments对象,这个对象是个类数组,存储着函数调用时的实参

function add (a, b) {
 console.log(arguments)
}
             //>> 2
add(4 ,5, 6) //>> [0: 4, 1: 5, 2: 6]

在浏览器控制台打印是这样的: image.png

上面在定义函数add时,明确接收2个形参,但调用时传了3个实参,可以看到,实参都保存在arguments对象里, arguments[0] = 4 , arguments[1] = 5 , arguments[2] = 6 接着我们再看一下函数的length属性和argumentslength属性

function add (a, b) {
 console.log(add.length)
 console.log(arguments.length)
}
             //>> 2
add(4 ,5, 6) //>> 3

分别输出23。到这里相信大家都看出来了,函数调用时多余的参数都在arguments里存着

所以,这里小结一下:

  • 函数定义时的参数是形参
  • 函数调用时的参数是实参
  • 形参和实参的个数不必相同,多余的实参会被忽略
  • 函数的length属性表示定义函数时的形参个数
  • 函数的arguments对象保存着函数调用时的实参
  • 函数的arguments对象的的length属性表示函数调用时的实参个数

1.4.4.1 arguments的作用

但知道这些这又有什么用呢? 但知道这些这又有什么用呢? 但知道这些这又有什么用呢?

继续看那个求和函数,目前只能接收固定个数的参数,假如某天需求突然变了:“无论我传递多少参数过去,都得求出相加的和”

function add(a,b){
    return a + b
}

add(1, 2, 3, 4, 5, 6, 7)

这时,arguments对象就派上用场了, 因为auguments是个类数组,可以通过下标取值,我们遍历将所有实参相加,这样,无论传递多少参数,都可以实现求和。

function add(){
   let sum = 0
   for(let i = 0; i < arguments.length; i++) {
       sum = sum + arguments[i]
   }
   return sum
}

console.log(add(1, 2, 3, 4, 5, 6, 7)) //>> 28
console.log(add(1,1,1)) //>> 3
console.log(add()) //>> 0

此外,arguments在实现call、bind、apply以及设计模式中还有着更加巧妙的作用,后面会单独讲解。

1.4.4.2 实参(arguments)与形参混用

参考JavaScript高级程序设计(第四版)第10章

什么是与形参混用?看下面的代码

function add(num1, num2) {
    if(arguments.length === 1) {
        console.log(num1 + 10)
    }else if(arguments.length === 2){
        console.log(arguments[0] + num2)
    }
}

上面的函数中,使用了2个形参和arguments对象。形参num1保存着和arguments[0]一样的值,因此使用谁都一样。同样arguments[1]也保存着与num2一样的值。

此外,arguments另一个有意思的地方是,它的值始终与对应的形参保持同步。看下面的例子:

function add(num1, num2) {
    arguments[1] = 10
    console.log(arguments[0] + num2)
}

上面的函数,把第二个实参的值重写为10。因为arguments对象的值会自动同步到对应的命名参数。所以修改arguments[1]的值也会修改num2的值,因此二者都是10。但要注意,这不意味着它们共享同一个栈内存地址,它们在内存中还是分开的,只不过会保持同步而已

另外切记:如果只传递了一个参数,然后把arguments[1]设置为某个值,那么这个值并不会反映到第二个形参上。这是因为,我们前面说过,arguments的值是函数调用时传递的实参个数确定的,而非定义时给出的形参个数确定的,如下代码所示:

function add(num1, num2){
    arguments[1] = 10
    console.log(arguments[0] + num2) //num2不会被设置为10,因为num2在下面调用时没有传递
}

add(6) //>> NaN

1.4.5 默认参数

在ES6没出来之前,实现函数的默认参数,需要先判断某个参数是否等于undefined,如果是,则意味着没有传递这个参数,那就赋给它一个值

function showName(name){
    name = (typeof name === 'undefined') ? '皮卡丘' : name
    return console.log('my name is' + name)
}

showName() //>> my name is 皮卡丘
showName('卧龙') // my name is 卧龙

ES6之后就不用那么麻烦了,因为它支持了默认参数。以下的写法与上面的代码等价。只要在函数定义的参数后面用=号赋值,就可以设置默认参数

function showName(name = '皮卡丘'){
    return console.log('my name is' + name)
}

showName() //>> my name is 皮卡丘
showName('卧龙') // my name is 卧龙

而且,给参数传递undefined相当于没有传值,这样函数还是会使用默然参数的值

function getName(firstName = '诸葛', lastName = '孔明') {
    return console.log(`${firstName}·${lastName}`)
}

getName()//>> 诸葛·孔明
getName('姑苏') //>> 姑苏·孔明
getName(undefined, '亮') //>> 诸葛·亮  传undefined时还会使用默认值

此外,默认参数也可以使用函数的返回值

function getYear() {
    return new Date().getFullYear()
}

function print(msg = 'hello', year = getYear()) {
    return `${msg},今年是${year}`
}

不过,默认参数只会在函数调用时才会求值,上面的计算默认参数的getYear函数,只会在print调用时,且没有传递year时才会调用

1.4.5.1 默认参数的作用域与顺序问题

因为在给默认参数赋值时,可以定义对象,也可以动态调用函数,所以函数参数肯定是在某个作用域中求值的。

给多个参数定义默认值,实际上跟let关键字挨个声明变量一样,是有顺序的,看下面例子。

function add(x = 12, y = 24) {
    return x + y
}

这里的默认参数x,y会按照它们定义的顺序依次初始化。可以想想如下过程(下面的代码事实上并不存在,只是为了演示)

function add() {
    let x = 12
    let y = 24
}

因为是按照顺序初始化的,所以后定义的默认参数可以使用先定义的默认参数,如下:

function add(x = 12, y = x + 2) {
    return x + y
}

console.log(add(2)) //>> 6

但是前面定义的参数不能使用后面的,let声明的变量没有变量提升,下面的代码将会抛出错误:

// 调用时不传递x会报错
function add(x = y-1, y = 4) {
    return x + y
}

此外,参数也存在于自己的作用域中,它不能引用函数体里面的变量:

// 调用时不传递y会报错,因为默认参数y引用了函数作用域的变量
function add(x = 2, y = z + 3) {
    let z = 2
    return x + y
}

1.5 箭头函数

ES6新增的箭头函数,很大程度上和函数表达式的作用是一样的,任何可以使用函数表达式的地方,都可以使用箭头函数:

let add = function (a, b) {
    return a + b
}

let arrowAdd = (a, b) => {
    return a + b
}

console.log(add(1, 2)) // >> 3
console.log(arrowAdd(1, 2)) // >> 3

箭头函数的语法很简洁,非常适用于函数嵌入、匿名函数


//map遍历数组

let nums = [1, 2, 3, 4]

nums.map(function(n) { return n + 1 })
nums.map((n) => {  return n + 1 } )

//定时器匿名函数

let count = 0

setlnterval( function() {
    count++ 
}, 2000)

setlnterval(() => { count++ }, 2000)

如果只有一个参数,那也可以不用括号

//下面2种方式写法都可以
let double = (x) => { return x*2 }
let reduce = x => { return x - 1 }

在没有参数,或者有多个参数的时候才需要写括号

//多个参数
let sum = (a, b, c) => { return a + b + c }

//没有参数
let getDate = () => { return new Date() }

//错误的写法
let add = a, b => { return a + b }

此外,箭头函数也可以省略箭头后面的大括号,此时箭头后面就只能有一行代码,比如一个赋值操作,或者一个表达式,而且会返回这行代码执行的值


//使用一行表达式
let double = (x) => { return x*2  }
let reduce = (x) => x - 1

//使用赋值
let person = {}
let setAge = (obj) => obj.age = 26
setAge(person)
console.log(person.name) // >> 26

//错误的写法
let add = (a, b) => return a + b

1.5.1 箭头函数没有arguments对象

️箭头函数语法虽然简单,但又很多场景不适用。比如箭头函数没有arguments、super、new.target,也不能用作构造函数。此外,箭头函数也没有prototype属性。

参考 developer.mozilla.org/zh-CN/docs/…

const add = () => {
    console.log(arguments)
}

//没有argument
add(1,2,3) //>> Uncaught ReferenceError: arguments is not defined

//没有prototype
console.log(add.prototype) //undefined

//不能作为构造函
const newAdd = new add() //>> Uncaught TypeError: add is not a constructor

那如果,我即想用箭头函数,又想使用arguments对象该咋办呢?可以包装一下,使用普通函数包裹一层。

function outer(){
    let inner = () => {
        console.log(arguments[0])
    }
    inner()
}

outer(5)

1.6 arguments.callee与递归函数

递归函数通常的形式是一个函数通过名称调用自己,如下面的例子:

function factorial (num) {
    if(num < 1){
        return 1
    }else {
        return num * factorial(num - 1)
    }
}

上面是经典的阶乘递归函数。虽然这样写是可以的,但是如果要把这个函数赋值给其他变量,就会出问题:

let newFactorial = factorial
factorial = null
console.log(newFactorial(4)) //>> 报错

我们之前在说过,变量保存的是对原函数的引用,即变量newFactorial指向factorial函数,后面将factorial设置为null后,即为重新赋值。原函数不存在了,调用就会报错。

如何避免这个问题?

我们前面说过的arguments参数,里面有个属性叫callee,这个属性指向正在执行的函数的地址

arguments.callee === 函数本身

function bar() {
    console.log(arguments.callee == bar)
}
bar() //>> true

既然这样,我们可以把递归函数改一改,如下:

function factorial (num) {
    if(num < 1){
        return 1
    }else {
        return num * arguments.callee(num - 1) //将函数换成arguments.callee
    }
}

let newFactorial = factorial

factorial = null
console.log(newFactorial) 
console.log(newFactorial(4)) //>> 24


像上面一样,把函数名换成arguments.callee,可以确保无论通过什么变量调用这个函数都不会出问题。因此在编写递归函数时,arguments.callee是引用当前函数的首选。

1.7 IIFE 立即调用函数表达式

还有一种叫 IIFE(Immediately-Invoked Function Expression,立即执行函数形式的函数调用方式,非常适合匿名函数调用,特征是在关键字function左侧有一个括号“(”,右侧的闭括号“)”则有两种放置方式,一种是放在紧挨调用括号对的左侧,一种是放在紧挨调用括号对的右侧。如下代码示例,这两种写法是等效的,都可以。

(function(){
    console.log("我是立即运行的匿名函数");
})();

(function(){
    console.log("我也是立即运行的匿名函数");
}());

1.7.1 使用IIFE模拟块级作用域

使用IIFE可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。这样位于函数作用域的变量属于局部变量,在函数内部形成了块级作用域。在ES6之前,JavaScript没有块级作用域的概念。使用IIFE模拟块级作用域是很常见的:

(function(){
    for(var i = 0; i < count; i++) {
        console.log(i)
    }
})();

console.log(i) // 报错,i未定义,因为i属于函数作用域

在ES之后,使用新增了let和块的概念,实现块级作用域就比较简单了,下面是块级作用域的2种形式:

//内嵌作用域
{
    let i;
    for( i = 0; i < count; i++) {
        console.log(i)
    }
}
console.log(i) //抛出错误,i未定义
//循环的块级作用域
for( let i = 0; i < count; i++) {
    console.log(i)
}
console.log(i) //抛出错误,i未定义

上面的块级作用域在之前的《变量和内存》里已经说过了

1.7.2 使用IIFE锁定参数值

学过前端的大多碰到过这个经典问题:


<body>
    <div>click me</div>
    <div>click me</div>
    <div>click me</div>
    <div>click me</div>
    <div>click me</div>
</body>


let divs = document.querySelectorAll('div')
for( var i = 0; i < divs.length; i++) {
    divs[i].addEventListener('click', function(){
        console.log(i) //4
    })
}

上面的会输出4,这并不奇怪,因为在页面加载完成后,循环结束的值是最终的值,即元素个数。而且变量i存在于循环体内部,随时可以访问。

在ES6之前,为了实现点击第几个div就显示对应的索引,需要借助一个IIFE来执行一个函数表达式,传入每次循环的当前索引,从而锁定点击时应该该显示的索引值:


let divs = document.querySelectorAll('div')
for( var i = 0; i < divs.length; i++) {
    divs[i].addEventListener('click', (function (frozenCounter){
        return function() {
            console.log(frozenCounter) // 0 1 2 3 4
        }
    })(i));
}

不过在ES6中引入了块级作用域变量,就不用那么费劲了:


let divs = document.querySelectorAll('div')
for( let i = 0; i < divs.length; i++) {
    divs[i].addEventListener('click', function(){
        console.log(i) // 0 1 2 3 4
    })
}

1.8 高阶函数

高阶函数的概念是:函数可以作为其他函数的参数,同时函数可以做其他函数的返回值

因为函数可以作为变量保存起来,所以函数就可以作为参数和返回值

1.8.1 回调函数

回调函数是经典的函数作为参数的应用案例,比较常见的就是平时开发时候的ajax请求。 因为ajax请求是异步的,不知道什么时候结束,最常见的解决方案就是把callback回调函数当做参数,传入发起ajax请求的函数中,待请求完成后执行callback函数

var getData = function(id, callback) {
    ajax('http://xx.com/getData?' + id, function(data){
        if(data.code === 200) {
            callback(data)
        }
    })
}

getData('36', function(data){
    console.log(data)
})

此外,在一些JavaScript中原生的方法中,也有着一些案例,比如数组的原生方法Array.prototype.sort()

let arr = [1,2,3]

//正序排列,从小到大
arr.sort(function(a,b){
    return a - b
})

//倒序排列,大小到小
arr.sort(function(a,b){
    return b - a
})

sort方法接收一个函数当参数,参数里封装了数字的排序规则,这些规则是可以变化的,正序还是倒序排列,都掌握在我们手中,此外,一些其他方法,比如map、filter、some都会接收一个匿名函数当参数:

let arr = [1,2,3]
arr.map(function(item, index){
    return item + 1
})

arr.filer(function(item, index){
    return item > 1
})

1.8.2 函数作为返回值

函数作为返回值是用的最多的场景,也是很多框架里用的比较多的。让函数返回一个可以执行的函数,可以延长变量的声明周期(这一点在后面的闭包和作用域链中会有更详细的介绍)。

我在之前做过的一个三维地图有关的项目,有这样一个操作地图相机场景:

  • 根据相机模块的配置数据,动态生成一系列按钮
  • 点击按钮执行对应的方法
  • 每个按钮的方法和参数都不一样

简化后的配置数据大致是这样的:


export const cameraConfig = [
 {
    buttonName: '获取相机参数',
    func: () => {
      return (msg) => {
        mapmostUE.getCameraParameters(function (response) {
          Message({
            ...msg
          })
        })
      }
    },
    code: `
      mapmostUE.getCameraParameters(function(response){
          let {location, rotation} = response;
          console.log({response})
          Message({
            message: '获取成功,请打卡控制台查看参数',
            type: 'success'
          })
        });

    `
  },

  {
    buttonName: '无人机模式',
    func: () => {
      return () => {
        mapmostUE.setInteractionMode('Drone')
      }
    },
    code: `
      mapmostUE.setInteractionMode("Drone");

    `
  },
 //此处省略1000行...
]


注意上面每个对象的func属性,它的值是一个箭头函数,这个箭头函数的返回值,又是一个箭头函数 ,返回的箭头函数接受了一个msg作为外部传入的参数。其在组件中的用法是这样的:

// camera.vue
<template>
  <div class="global-menu">
    <button
      class="global-menu-btn"
      v-for="(config, index) in hooks"
      :key="index"
      @click="handleClick(config)"
    >
      {{ hook.name }}
    </button>
  </div>
</template>

<script>
import { cameraConfig } from './../config.js'
export default {
 methods: {
    //处理按钮点击
    handleClick(hook) {
      const { func, code } = handleClick
      this.$store.commit('app/SET_CODE', code)
      let resultFunc = func()
      if(this.$store.state.code.length > 0) {
          resultFunc({
            message: '获取成功,请打卡控制台查看参数',
            type: 'success'
          })
      }
    }
  },
}
</script>

不必关心具体的功能,我们只看里面的数据格式的使用,这里使用了vue框架,根据配置数据动态生成按钮,点击按钮时,做一些业务处理,重点下面这几行代码

let resultFunc = func()
if(this.$store.state.code.length > 0) {
  resultFunc({
    message: '获取成功,请打卡控制台查看参数',
    type: 'success'
  })
}

上面用一个变量接受了func属性的返回值,因为其返回值是一个箭头函数,所以变量resultFunc的值就指向返回的箭头函数。接着,在适当的时机,去执行resultFunc,并传入参数

上面就是函数作为返回值的最常见的用法,其形式一般是这样的:

//定义一个函数,处理一些事情,返回一个函数,接收外界参数
function creator() {
    // do something
    return function(param) {
        // do somthing
    }
}


//用变量接收函数
let res = creator ()

//在适当的时候执行函数
if(some condition) {
    res(param)
}

这样一来,原函数的生命周期就会被延长。当然,函数作为返回值的用法远不止这些,在闭包中和设计模式中,这种用法很常见。

1.9 一些没有细说的函数知识点

除了上面的内容,其实函数还有一些知识点没有说到,比如:

  • arguments.callee.caller
  • 参数收集与扩展
  • this
  • 作用域链
  • 闭包和内存泄漏
  • 原型
  • new.target
  • 私有变量
  • call、bind、apply

之所以没有写出来,是因为上面的内容有很强的关性的,而且每个知识点都很重要,涉及到的内容比较多,后面有时间会继续把每个知识点单独写一篇文章。

1.10 参考