重学 JavaScript[2] 函数

语法

JavaScript 中函数大致有三种写法

普通的函数写法:

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

声明一个匿名函数并赋值给 fn:

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

箭头函数:

let fn = (a, b) => {
    return a + b
}
// 或
let fn = (a, b) => a + b

另外,你可以直接写出“立即执行的函数”

;(function fn() {
    console.log('fn')
})()

请注意:**箭头函数与“function”写法的函数不能完全等价!**其原因会在下一篇解释

高阶函数

高阶函数(HoC)可以接受函数为参数,在很多框架中都有类似这样的使用:

doSomeThingAsync(params, function(result) {
    console.log(result)
})

一些操作数组的函数也有类似的调用方法,比如:

;[1, 2, 3].forEach(function(e) {
    console.log(e)
})

闭包

现象

高阶函数还可以接受函数作为返回值,比如:

function fn() {
    let count = 0
    return function() {
        count++
        console.log(count)
    }
}

你可以这样调用它:

let counter = fn()
counter()
counter()
counter()

//依次输出1 2 3

结果好像令人费解,fn()调用后返回了一个函数,并赋值给 counter

counter 函数对 fn 内部的变量count进行了自增并打印,但每次执行 counter()后,打印出来的结果,就好像变量count一直“维持”在 fn 函数内部

如果在代码后再追加一行,变成这样:

function fn() {
    let count = 0
    return function() {
        count++
        console.log(count)
    }
}

let counter = fn()
counter()
counter()
counter()
console.log(count)

// 依次输出1 2 3后,ReferenceError报错

这个结果说明变量count并不在全局作用域内,而是在 fn()形成的闭包内

使用闭包

闭包使得函数在执行后有了“自己的”作用域,且会将一些变量和值一直保存在内存中

在 Java 和 C++等语言里,函数执行过后,内部的变量就会被销毁,在这里,JavaScript 显得很“违反直觉”

但请不要认为 JavaScript 函数只是简单的“函数”

避免污染作用域

我们可以使用上面提到的“立即执行函数”来避免一些变量污染作用域,但又不影响去使用这个变量

这段代码避免了直接声明 num 带来的“污染”,并通过 add 和 getNum 函数对 num 变量进行保护

;(function() {
    this.num = 0
    this.add = function() {
        this.num++
    }
    this.getNum = function() {
        return this.num
    }
})()

add()
console.log(getNum())

// 1

缓存

对于需要反复调用的函数,可以使用闭包来存放缓存,以提升性能

考虑以下计算斐波那契数列的代码:

function fib(n) {
    if (n < 2) {
        return n
    } else {
        return fib(n - 1) + fib(n - 2)
    }
}
// 有很多值被重复计算

使用闭包存放缓存:

let fib = (function() {
    let cache = [0, 1]
    return function(n) {
        let result = cache[n]
        if (!result) {
            result = fib(n - 1) + fib(n - 2)
            cache[n] = result
        }
        return result
    }
})()

在 Java 类语言里,如果要持久的保存变量,应该写到类的静态或非静态变量里,然而 ES5 时代,JavaScript 并没有类的概念,甚至于 ES6 引入的class关键字,也是用函数来实现的“类”

你可以在Babel 官网来尝试 ES6 中的类编译为 ES5 后的代码

ES6:

class Person {
    constructor() {
        this.name = '王昭君'
        this.age = 21
    }

    introduce() {
        console.log(this.name, this.age)
    }
}

ES5:

'use strict'

function _instanceof(left, right) {
    if (
        right != null &&
        typeof Symbol !== 'undefined' &&
        right[Symbol.hasInstance]
    ) {
        return !!right[Symbol.hasInstance](left)
    } else {
        return left instanceof right
    }
}

function _classCallCheck(instance, Constructor) {
    if (!_instanceof(instance, Constructor)) {
        throw new TypeError('Cannot call a class as a function')
    }
}

function _defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
        var descriptor = props[i]
        descriptor.enumerable = descriptor.enumerable || false
        descriptor.configurable = true
        if ('value' in descriptor) descriptor.writable = true
        Object.defineProperty(target, descriptor.key, descriptor)
    }
}

function _createClass(Constructor, protoProps, staticProps) {
    if (protoProps) _defineProperties(Constructor.prototype, protoProps)
    if (staticProps) _defineProperties(Constructor, staticProps)
    return Constructor
}

var Person = /*#__PURE__*/ (function() {
    function Person() {
        _classCallCheck(this, Person)

        this.name = '王昭君'
        this.age = 21
    }

    _createClass(Person, [
        {
            key: 'introduce',
            value: function introduce() {
                console.log(this.name, this.age)
            },
        },
    ])

    return Person
})()

当然,在 ES6 之前,JavaScript 程序员会使用一种“构造器函数”来充当类的作用,比如:

function Person() {
    this.name = '王昭君'
    this.age = 21
}

const person = new Person()
console.log(person.name, person.age)

// 王昭君 21

这类函数通常遵循 Java 中类的命名方式(首字母大写),在调用时需要使用new关键字(当然如果你不用new会得到 undefined)

通常情况下,如果不是一定要写 ES5 语法,个人建议不要使用这样的方法(直接用 class 他不香么?还不容易出错)

函数的函数

如果你声明了一个函数,你会发现 IDE 有自动提示的函数的函数:
16109473914710.jpg

我们主要关注这几个函数:

  • call()
  • apply()
  • bind()

考虑以下代码:

this.name = 'Global'

let obj = {
    name: '王昭君',
    getName() {
        console.log(this.name)
    },
}

function getName() {
    console.log(this.name)
}

getName()
obj.getName()

// Global
// 王昭君

对象内部的函数,其this指向对象本身(有关 this 指向问题,会在下一篇具体描述)

而使用callapplybind可以改变函数中 this 的指向,比如:

this.name = 'Global'

let obj = {
    name: '王昭君',
}

function getName() {
    console.log(this.name)
}

getName()
getName.call(obj)

三者的区别

call的定义:

(method) Function.call(this: Function, thisArg: any, ...argArray: any[]): any

Calls a method of an object, substituting another object for the current object.

@param thisArg — The object to be used as the current object.

@param argArray — A list of arguments to be passed to the method.

apply的定义:

(method) Function.apply(this: Function, thisArg: any, argArray?: any): any

Calls the function, substituting the specified object for the this value of the function, and the specified array for the arguments of the function.

@param thisArg — The object to be used as the this object.

@param argArray — A set of arguments to be passed to the function.

bind的定义:

(method) Function.bind(this: Function, thisArg: any, ...argArray: any[]): any

For a given function, creates a bound function that has the same body as the original function. The this object of the bound function is associated with the specified object, and has the specified initial parameters.

@param thisArg — An object to which the this keyword can refer inside the new function.

@param argArray — A list of arguments to be passed to the new function.

可以看出,callapply只有传参方式的区别

function fn() {}

fn.call(this, arg1, arg2, ...)
fn.apply(this, [arg1, arg2, ...])

bind会返回一个“绑定”了传入的 this 的函数

function fn() {}

let fn2 = fn.bind(this, arg1, arg2, ...)
fn2()