重学 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关键字,也是用函数来实现的“类”

你可以在

https://www.babeljs.cn/

来尝试 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()