函数
在数学中,函数是一种映射,其功能是将自变量的值 (输入)映射到一个函数值(输出)。例如实数x对应到其平方x2的关系就是一个函数,若以3作为此函数的输入值,所得的输出值便是9。
编程语言中的函数是一段程序代码,其功能是根据输入(参数)进行计算,并产生输出(返回值)。
function square(x) { // 参数就是输入
return x * x; // 返回值就是输出
}
函数声明
声明函数的存在,包括函数的名字、函数类型以及形参类型。并把这些信息通知编译系统,以便在调用该函数时进行对照检查(例如,函数名是否正确,实参与形参的类型和个数是否一致),它不包括函数体。这种情况下函数和函数定义分开。
JS中不存在函数声明。 因为JS是弱类型语言,声明变量的时候并不能确定变量的类型。
函数定义
对函数功能的确立,包括指定函数名,函数类型(返回值)、形参类型(参数)、函数体等,它是一个完整的、独立的函数单位。
- 声明式定义
使用关键字function定义函数。
function func() {
// body
}
- 函数表达式定义
在一个表达式中定义一个函数,可以指定函数名也可以不指定。可以理解为定义一个变量,这个变量是函数对象。函数表达式定义的函数在代码执行的时候才会真正创建。
const func = function [name]([param1[, param2[, ..., paramN]]]) { statements };
const func = function() {
// code
};
const func = function func() {
// code
}
- Function定义
JS支持使用Function / GeneratorFunction动态创建一个新的Function对象,也就是函数对象。
const func = new Function('a', 'b', 'return a + b');
console.log(func(1, 2); // 3
使用Function构造器生成的Function对象是在函数创建时解析的。这比你使用函数声明或者函数表达式(function)并在你的代码中调用更为低效,因为使用后者创建的函数是跟其他代码一起解析的。所以一般情况下不推荐使用这种方式。
虽然性能存在一些问题,但是动态生成函数也有它的优势,可以把复杂的需要动态处理的逻辑动态生成函数使用,比如AJV使用校验规则动态生成具有校验功能的函数对JSON进行校验。
方法
当函数作为对象的属性的时候,我们把函数叫做方法。
class User {
static staticMethod() {
console.log('This is a static method');
}
instanceMethod() {
console.log('This is a instance method');
}
}
const obj = {
name: 'test',
method: () {
console.log('This is a method');
}
}
为什么要使用函数
- 减少重复代码
- 改善程序结构
- 增强程序的通用性
JS中的函数
JS 所有的函数都是Function()的实例对象(参考JS prototype)。
- Function:普通函数,使用function定义的函数。
- Arrow Function:箭头函数,使用=>定义的函数。
- Generator Function:使用function*定义的函数。
- Async Function:使用async function定义的函数。
Function
普通函数,也就是我们常说的函数。
Arrow Function
箭头函数表达式的语法比函数表达式更短,简洁。箭头函数更适用于那些本来需要匿名函数的地方。
箭头函数的特点:
- 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
- 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
- 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
适用场景
- 简化回调函数
const arr = ['a', ' b', ' c '];
// 正常函数写法
arr.map(function (str) {
return str.trim();
});
// 箭头函数写法
arr.map(str => str.trim())
- 预期的this
class UserController {
async create(user) {
await model.save(user);
}
async batchCreate(users) {
await Promise.all(users.map(user => this.create(user)));
}
}
const ctrl = new UserController();
await batchCreate([{ name: '1' }, { name: '2' }]);
不适用场景
- 定义函数的方法,且该方法内部包括this
const cat = {
lives: 9,
jumps: () => { this.lives--; }
};
cat.jumps()方法是一个箭头函数,这是错误的。调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。
- 需要动态this
const button = document.getElementById('press'); button.addEventListener('click', () => { this.classList.toggle('on'); });
点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。
Generator Function
generator函数用来返回generator对象,并且它符合可迭代协议和迭代器协议。
在没有async函数之前,generator函数主要用来异步编程。阮一峰老师的ES6入门对generator有详细的有详细的介绍,这里就不再啰嗦。
Async Function
Async函数用来处理异步操作,避免了回调黑洞,让异步代码看起来更human readable。
Async函数的特点
- 调用async函数的时候会异步执行。
- 在async函数中使用await的时候,会执行完当前代码才会往下继续执行其他代码,实现按照指定顺序执行异步操作。
- async函数会返回一个Promise对象。
异常处理
一般async函数配合await实现异步操作的顺序执行。但async函数返回Promise实例对象,所以async函数可以像Promise一样使用。所以有两种处理方式处理async函数异常。
- try catch
async function fun() {
await Promise.reject('error test')
}
async function run() {
try {
await fun();
} catch(e) {
console.log(e); // catch error test
}
}
run()
- catch()
async function fun() {
await Promise.reject('error test')
}
fun()
.then(v => console.log('success'))
.catch(e => console.log('catch', e)); // catch error test
参数
参数类型
- 形参
形式参数,是在声明或定义函数的时候在()中使用的参数,目的是用来接收调用该函数时传递的参数。可以把形参理解为一种变量。
形参只有在函数被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只在函数内部有效。函数调用结束返回主调用函数后则不能再使用该形参变量。
- 实参
实际参数,是在调用函数时传递的参数,即传递给被调用函数的值。
参数传递
- 值传递:对于基本数据类型来说,形参会拷贝实参的值(参考JS Data & Type基本类型)。
- 引用传递:对于引用类型来说,形参拷贝实参的内存地址(参考JS Data & Type引用类型)。
静态化
JS Data & Type中提过动态类型静态化这个概念,在这里也提出参数静态化:1.参数类型静态化;2.参数个数静态化。
- 参数类型静态化
也就是参数的类型需要动态类型静态化。JS Data & Type函数参数部分。
- 参数个数静态化
JS函数支持不写形参,在调用函数的时候可以传递任意的实参,在函数体中直接arguments这样的变量就可以获得传进来的参数。这样容易造成阅读和使用上的误解,需要在函数体代码中寻找理解。所以尽量把参数全部写到形参列表中,明确函数都有哪些形参。
函数调用
JS有三种函数调用方式:直接调用、apply调用和call调用。
不过个人不太推荐apply和call这两种方式,因为这两种方式有时候让代码理解起来困惑。所以为了保持代码的简洁性和清晰性,除非是不得不用,否则使用直接调用的方式。
直接调用
funcName(arg1, arg2, ..) 函数名 + 参数。
Apply
通过apply方法可以调用它所属的函数(对象)。可以给函数提供运行时的this值,以及使用一个数组(或类似数组对象)作为参数。
func.apply(thisArg, [argsArray])
- thisArg:可选。在 func 函数运行时使用的 this 值。
- argsArray:可选。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 null 或 undefined,则表示不需要传入任何参数。
const obj = {
name : 'apply'
}
function func(firstName, lastName){
console.log(`${firstName} ${this.name} {lastName}`);
}
func.apply(obj, ['weber', 'yang']); // weber apply yan
Call
通过call方法可以调用它所属的函数(对象)。可以给函数提供运行时的this值,以及逐个提供各个参数(参数列表)。
fun.call(thisArg, arg1, arg2, ...)
- thisArg:在fun函数运行时指定的this值。
- arg1, arg2, ...:参数列表
const obj = {
name: 'call'
}
function func(firstName, lastName) {
console.log(`${firstName} ${this.name} {lastName}`);
}
func.call(obj, 'weber', 'yang'); // weber apply yan
Apply & Call
apply和call都是函数对象的方法,两者都可以改变函数运行时的this,这个是apply和call的主要使用的功能。
apply和call不同在于,提供的参数格式不一样:apply需要的是一个参数数组;call需要的是参数列表。
co是一个generator流程控制器。在有async函数之前,co和generator是绝佳搭配。co代码小巧精悍,值得学习其中的JS技巧。
co.wrap用来封装generator成Promise对象,使得generator可以像Promise对象一样使用,同时也依然保持generator要完成的功能,也就是generator会依然正常执行。
co.wrap = function (fn) {
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1);
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
...
});
}
co.wrap(function* (arg) {
})('arg value').then(function (value) {
})
wrap返回了闭包,闭包包含了对fn封装的代码。通过往闭包传入参数使得fn封装后依然可以接收参数进行调用。通过co.call注入了this,把fn封装到了Promise对象。通过fn.apply把this注入到fn;使用fn.apply而不是fn.call的原因是,传入的参数是不确定的,arguments类数组可以拿到传入的参数列表,所以用apply。
gen.apply(ctx, args)使用apply的原因是参数不确定,所以使用了slice.call(arguments, 1);通过ctx保持原来的this不变。
this 指向问题
Bind
bind方法创建并返回一个新的函数,当这个绑定函数被调用时this值为其提供的值,其参数列表前几项值为创建绑定函数时指定的参数序列。
fun.bind(thisArg[, arg1[, arg2[, ...]]])
- thisArg:调用绑定函数时作为this参数传递给目标函数的值。 如果使用new运算符构造绑定函数,则忽略该值。
- arg1, arg2, ...:当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。
bind与apply和call不同的是:apply和call是在每次调用的时候动态指定被调用函数的this和实参,apply和call自动帮我们对目标函数进行调用;而bind是创建一个新的封装绑定函数,这个绑定函数固定了目标函数的this值,和部分实参。
为了使得代码逻辑更加的清晰,controller层会使用class面向对象组织代码。一般把controller通用的代码逻辑放在Base Controller中,其他Controller通过继承Base,在Controller实例中调用this,就可以访问这些方法了。但是问题来了,this是动态确定的,当Controller实例的方法和router绑定后,由router调用Controller实例的方法的时候,this就改变了,这样就无法访问其他实例方法了。这个时候可以考虑使用bind绑定Controller实例,这样Controller实例是方法就可以通过this访问其他实例方法了。
// /controllers/base
class Base {
// 权限校验
assertScope(ctx, role = 'user') {
ctx.assert(ctx.state.me && ctx.state.me.role === role, 401);
}
}
module.exports = Base;
// /controllers/order
class Order extends Base {
async find() {
this.assertScope(ctx);
// other code
}
}
module.exports = new Order();
// /routers/
const KoaRouter = require('koa-router');
const ctrl = require('../controllers/order');
const router = KoaRouter({ prefix: '/api' });
router.post('order.find', '/orders', ctrl.find.bind(ctr
IIFE
立即调用函数表达式,是一个在定义时就会立即执行的 JS 函数。
// 支持所有类型的函数
(function () {
// body
})();
第一部分是包围在圆括号运算符() 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。 第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。
const result = (function () {
return 'return value';
})();
// IIFE 执行后返回的结果 'return value'
函数原型链
JS prototype
闭包
JS Scope & Closure
函数上下文
JS Execution Context
函数式编程
函数式编程是编程范式中的一种,是一种典型的编程思想和方法。其他的编程范式还包括面向对象编程,逻辑编程等。
编程范式的意义在于它提供了模块化代码的各种思想和方法。
- 模块化使得开发更快、维护更容易
- 模块可以复用
- 模块化便于单元测试和debug
由此可以理解为:函数式编程是以函数为核心来组织模块的一套编程方法。(不是写了函数就是函数式编程)
高阶函数 Higher-order function
满足一个或多个条件的函数属于高阶函数:
- 接受一个或多个函数作为输入
- 输出一个函数
函数参数
Array.prototype.sort
// 从小到大排列
[1, 3, 2].sort((a, b) => a - b);
//从大到小排列
[1, 3, 2].sort((a, b) => b - a);
函数返回值
函数作为返回值一个典型的情况就是闭包。比如之前提到的co.wrap使用了闭包。
// koa-bodyparser
co.wrap = function (fn) {
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};
纯函数
符合以下要求的函数就是纯函数:
- 相同输入总是会返回相同的输出。返回的结果只依赖于输入的参数且与外部系统状态无关。
- 没有副作用。不会影响该函数作用域以外的外部状态(比如全局变量、参数)。
优点:
- 更加容易被测试,因为它们唯一的职责就是根据输入计算输出。
- 结果可以被缓存,因为相同的输入总会获得相同的输出。
- 自我文档化,因为函数的依赖关系很清晰。
- 更容易被调用,因为你不用担心函数会有什么副作用。
柯里化
把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回一个新的函数的技术,新函数接受余下参数并返回运算结果。
参考
- 程序设计思想与方法
- ECMAScript6入门
- 6 ways to declare JavaScript functions
- Exploring ES6
- JS 函数式编程指南
- JavaScript专题之函数柯里化