什么是函数
Function
: [countable](computing) part of a program
, etc. that performs a basic operation
program
:a set of instructions
in code that control the operations or functions
of a computer
instructions
:[countable] a piece of information
that tells a computer to perform a particular operation
如上可得出,函数
为程序的一部分,程序由一系列指令
组成,指令
用于完成某种操作
。
那么,是否可以认为,函数封装了某种操作?
操作
描述的是一个过程
,大部分我们期望的是如下的一个过程:
输入 -> 逻辑 -> 预期结果
那么,⭐️为何要封装操作呢?这貌似也回答了为什么要提炼函数?
良好的代码组织
逻辑复用
那么,应该以怎样的方式进行组织呢?
给这段操作起一个名称,通过调用这个名称,来达到组织代码的目的。同时,如果重复调用该名称,是否能重复执行里面的操作呢?
于是,通过调用相同的函数名称
,可以实现逻辑复用
既然是逻辑复用,那么通过函数与函数调用,能否执行某种循环操作呢?答案为是,如后面的【循环替换】
单个函数操作的数据范围是有限,于是,通过函数参数,使其能与外界进行交互。
Extract Funtion
extract - 牛津高阶词典
extract (from something) a short passage from a book, piece of music, etc. that gives you an idea of what the whole thing is like
可以看出,你对extract
的part
有着初步的认识(an idea)
Motivation 动机
why do we extract function?
代码组件及逻辑的复用
如果提炼出来的part
只在一处地方使用,还有必要吗?
视情况,如果函数意图与实现相差太大,可以考虑提炼。
函数提炼的原因,另一个合理的观点是:将意图与实现分离(separation between
intention
and
implementation
)intention:意图,通常指你内心的想法。(我只需要了解它能干什么,而不关心是如何实现的)
一旦接受这个原则,你甚至会写出仅仅只有一行的代码
比如:在一些黑白计算机的系统中,调用
reverse 函数
使得某些文字或图片高亮显示,我将其封装到highlight函数
中,可以看到,highlight
竟比reverse
还长,但是,这并不重要。因为,reverse
的的意图(intention)与实现之间有着很大的差距(big
distance)。
提炼一定是好的吗?
不是,不合理的组织代码,会使得代码变得非常乱。所以需要⭐️视情况进行合理的提炼。
Mechanics 做法
1️⃣ look at a fragment of code
, understand what it is doing, then extract it into its own function
named after its purpose
.
purpose: 事情应该被这样实现,一种规范
查看某个代码片段,理解其行为,然后将其提炼到一个独立的函数,并以这段代码的用途为该函数命名。
函数名称的作用:1️⃣ 将 fragment of code
fit together - effectively 2️⃣ 了解这些 fragment of code
的 intention
关于函数命名的技巧:在函数上方写一段注释,描述这个函数的用途,再将这段注释变为函数的名字。
❗️ 如果想不出一个好的名字,可能你不应该提炼这块代码
函数
,通常表示一种行为,其前缀最好为动词;变量
,通常表示一种状态,其前缀最好为形容词。
2️⃣ 查看extracted code
中是否存在访问不到的变量,如果是,在 source function
通过参数传递
如果变量只在
extracted code
中使用,就把变量的的声明也搬到extracted code
中
Demo 例子
提取重复代码
同一个类
的两个函数
有着相同的表达式
,通过extract function
提炼出重复的代码,并且 invoke the code
from both places
class SimpleCalcultor {
logNumberAddOne(aNumber) {
const manyDashes = '--------------'
console.log(manyDashes)
console.log(aNumber + 1)
console.log(manyDashes)
}
logNumberAddTen(aNumber) {
const manyDashes = '--------------'
console.log(manyDashes)
console.log(aNumber + 10)
console.log(manyDashes)
}
}
after extract function
class SimpleCalcultor {
logNumberAddOne(aNumber) {
this.logNumber(aNumber + 1)
}
logNumberAddTen(aNumber) {
this.logNumber(aNumber + 10)
}
logNumber(aNumber) {
const manyDashes = '--------------' // <------ 该变量只在extracted code中使用,就把该变量的的声明也搬到extracted code中
console.log(manyDashes)
console.log(aNumber)
console.log(manyDashes)
}
}
将返回值赋值给局部变量
如果在extracted code
中修改了source code
的变量,则在extracted code
中将修改的值返回出去即可
function foo(arr) {
let sum = 123 // <------ 在 extracted code 中修改了 source code 的变量
arr.forEach(e => sum += e)
console.log('do some other things--------')
}
foo([1, 2, 3, 4, 5])
after extract function
function foo(arr) {
let sum = getSum()
console.log(sum)
console.log('do some other things--------')
function getSum() {
let res = 123
arr.forEach(e => res += e)
return res // <------ 将修改的值返回出去
}
}
如果自身语言支持嵌套函数,应首先
extract
成嵌套函数,以防止函数名重名等问题
个人风格:将函数的返回值命名为 res,这样一眼就可以知道它的作用
函数式语言
《JavaScript 语言精髓与编程实战》(第三版)第五章:JavaScript 的函数式语言特性
根据冯诺依曼体系:运算数
要先放到寄存器
中,然后再参与CPU运算
,对存储的理解
规范了我们的代码风格
如果说运算式语言
形式类似如下
const a = 1
const b = 1
const c = 1
const res1 = a + b
const res2 = res1 + c
console.log(res1)
console.log(res2)
运算的目的是为了“产生值”
而函数式语言——为了自身的某些特性
,而产生了一种复杂的代码风格(在 JavaScript 中,函数可被当作参数
及返回值
使用)
函数式语言
形式类似如下:以连续运算为核心特征
const a = 1
const b = 1
const c = 1
function sumAndLog(a, b) {
const res = a + b // <------ 根据输入,得到某个结果产生值
console.log(res) // <------ 执行某种副作用
return res // <------ 返回可供后续运算的值
}
sumAndLog(sumAndLog(a, b), c) // <------ 通过连续的表达式运算, 得到最终的结果
可以看到,函数式语言
舍弃了保留中间结果
的过程,而是直接将输出的结果
当作下一个运算的输入
你可以将函数当成一个运算符,如“+”
1 + 1 + 1
sum(sum(1, 1), 1)
逗号运算符与连续运算
逗号运算符表明从左至右
计算两个操作数,并返回右操作的值
function foo() {
console.log('1')
// 左边可以进行某些操作, 如打印
return console.log('2'), 3
}
console.log(foo()) // 依次输出 1 2 3
分支、循环替换
一般编程语言会有:顺序,分支,循环三种基本逻辑结构。它即可用于组织语句
,又可用于陈述逻辑
然而,分支
和循环
是可以被三元表达式
和函数递归
替代的。
分支替换
const a = 123
if (a > 0) {
console.log('a > 0')
} else if (a == 0) {
console.log('a = 0')
} else {
console.log('a < 0')
}
// 替代如下
(a > 0) ? console.log('a > 0') // <----- 第三个操作数:为下一行运算的返回结果
: (a == 0) ? console.log('a = 0')
: console.log('a < 0')
在连续运算过程
中,将输出的结果
当作下一个运算的输入
,因此,无需中间变量
来“寄存”,只需要值声明
循环替换
let res = 0
for (let i = 0; i <= 10; i++) {
res += i
}
console.log(res)
// 替换如下
function getSum(i) {
return (i > 0) ? i + getSum(--i) : 0
}
console.log(getSum(10))
函数模拟循环会产生“栈溢出”的问题
而 JavaScript 引擎会进行尾递归优化,当满足:外部函数的返回值是内部函数的返回值时,如
function outerFunction() {
return innerFunction() // <----- innerFunction 的返回值也是 outerFunction 的返回值
}