目录

预备知识

函数式编程

模块化

计算器示例

功能分析

创建index.html

计算过程显示栏实现

创建计算过程显示栏

输出栏组件实现

创建输出栏组件

键盘组件实现

创建键盘组件

完成计算器

函数式编程

概念

在软件开发中有一句名言:共享的可变状态是万恶之源,而函数式编程正是能够彻底解决共享状态。函数式编程本质上也是一种编程范式(Programming Paradigm ),其代表了一系列用于构建软件系统的基本定义准则;它强调避免使用共享状态(Shared State )、可变状态(Mutable Data )以及副作用(Side Effects ),整个软件系统由数据驱动,应用的状态在不同纯函数之间流动。与偏向命令式编程的面向对象编程而言,函数式编程其更偏向于声明式编程,代码更加简洁明了、更可预测,并且可测试性也更好;典型的函数式编程语言有 Scala、Haskell 等,而其编程思想在 Go、Swift 、 JavaScript、Python 乃至于 Java 中都有着广泛而深远的实践应用。

共享状态(Shared State )可以是存在于共享作用域(全局作用域与闭包作用域)或者作为传递到不同作用域的对象属性的任何变量、对象或者内存空间。在面向对象编程中,我们常常是通过添加属性到其他对象的方式共享某个对象。共享状态问题在于,如果开发者想要理解某个函数的作用,必须去详细了解该函数可能对于每个共享变量造成的影响。往往多个并发请求会导致的数据一致性错乱也就是触发所谓的竞态条件(Race Condition ),而不同的调用顺序可能会触发未知的错误,这是因为对于共享状态的操作往往是时序依赖的。

纯函数指那些仅根据输入参数决定输出并且不会产生任何副作用的函数。纯函数最优秀的特性之一在于其结果的可预测性:

var z = 10;
function add(x, y) {return x + y;
}
console.log(add(1, 2)); // prints 3
console.log(add(1, 2)); // still prints 3
console.log(add(1, 2)); // WILL ALWAYS print 3
复制代码

副作用指那些在函数调用过程中没有通过返回值表现的任何可观测的应用状态变化,常见的副作用包括但不限于修改任何外部变量或者外部对象属性、在控制台中输出日志、写入文件、发起网络通信、触发任何外部进程事件、调用任何其他具有副作用的函数等。在函数式编程中我们会尽可能地规避副作用,保证程序更易于理解与测试。Haskell 或者其他函数式编程语言通常会使用 Monads来隔离与封装副作用。在绝大部分真实的应用场景进行编程开始时,我们不可能保证系统中的全部函数都是纯函数,但是我们应该尽可能地增加纯函数的数目并且将有副作用的部分与纯函数剥离开来,特别是将业务逻辑抽象为纯函数,来保证软件更易于扩展、重构、调试、测试与维护。这也是很多前端框架鼓励开发者将用户的状态管理与组件渲染相隔离,构建松耦合模块的原因。不可变对象(Immutable Object )指那些创建之后无法再被修改的对象,与之相对的可变对象(Mutable Object )指那些创建之后仍然可以被修改的对象。

const a = Object.freeze({foo: "Hello",bar: "world",baz: "!"
});a.foo = "Goodbye";// Error: Cannot assign to read only property 'foo' of object Object
复制代码

函数式编程倾向于重用一系列公共的纯函数来处理数据,而面向对象编程则是将方法与数据封装到对象内。这些被封装起来的方法复用性不强,只能作用于某些类型的数据,往往只能处理所属对象的实例这种数据类型。而函数式编程中,任何类型的数据则是被一视同仁,譬如map()函数允许开发者传入函数参数,保证其能够作用于对象、字符串、数字,以及任何其他类型。JavaScript 中函数同样是一等公民,即我们可以像其他类型一样处理函数,将其赋予变量、传递给其他函数或者作为函数返回值。而高阶函数(Higher Order Function )则是能够接受函数作为参数,能够返回某个函数作为返回值的函数。

const add10 = value => value + 10;
const mult5 = value => value * 5;
const mult5AfterAdd10 = value => 5 * (value + 10);
复制代码

引用自:数据流驱动的界面

模块化

ES2015 Modules

JavaScript 模块规范领域群雄逐鹿,各领风骚,作为 ECMAScript 标准的起草者 TC39 委员会自然也不能置身事外。ES2015 Modules 规范始于 2010 年,主要由 Dave Herman 主导;随后的五年中 David 还参与了 asm.js,emscription,servo,等多个重大的开源项目,也使得 ES2015 Modules 的设计能够从多方面进行考虑与权衡。而最后的模块化规范定义于 2015 年正式发布,也就是被命名为 ES2015 Modules。我们上述的例子改写为 ES2015 Modules 规范如下所示:

// file lib/greeting.js
const helloInLang = {en: "Hello world!",es: "¡Hola mundo!",ru: "Привет мир!"
};export const greeting = {sayHello: function(lang) {return helloInLang[lang];}
};// file hello.js
import { greeting } from "./lib/greeting";
const phrase = greeting.sayHello("en");document.write(phrase);
复制代码

ES2015 Modules 中主要的关键字就是 importexport,前者负责导入模块而后者负责导出模块。完整的导出语法如下所示:

// default exports
export default 42;
export default {};
export default [];
export default foo;
export default function () {}
export default class {}
export default function foo () {}
export default class foo {}// variables exports
export var foo = 1;
export var foo = function () {};
export var bar; // lazy initialization
export let foo = 2;
export let bar; // lazy initialization
export const foo = 3;
export function foo () {}
export class foo {}// named exports
export { foo };
export { foo, bar };
export { foo as bar };
export { foo as default };
export { foo as default, bar};// exports from
export * from "foo";
export { foo } from "foo";
export { foo, bar} from "foo";
export { foo as bar } from "foo";
export { foo as default } from "foo";
export { foo as default, bar } from "foo";
export { default } from "foo";
export { default as foo } from "foo";
复制代码

相对应的完整的支持的导入方式如下所示:

// default imports
import foo from "foo";
import { default as foo } from "foo";// named imports
import { bar } from "foo";
import { bar, baz } from "foo";
import { bar as baz } from "foo";
import { bar as baz, xyz} from "foo";// glob imports
import * as foo from "foo";// mixing imports
import foo, { baz as xyz } from "foo";
import * as bar, { baz as xyz } from "foo";
import foo, * as bar, { baz as xyz } from "foo";
复制代码

ES2015 Modules 作为 JavaScript 官方标准,日渐成为了开发者的主流选择。虽然我们目前还不能直接保证在所有环境(特别是旧版本浏览器)中使用该规范,但是通过 Babel 等转化工具能帮我们自动处理向下兼容。此外 ES2015 Modules 还是有些许被诟病的地方,譬如导入语句只能作为模块顶层的语句出现,不能出现在 function 里面或是 if 里面:

if (Math.random() > 0.5) {import './module1.js'; // SyntaxError: Unexpected keyword 'import'
}
const import2 = (import './main2.js'); // SyntaxError
try {import './module3.js'; // SyntaxError: Unexpected keyword 'import'
} catch(err) {console.error(err);
}
const moduleNumber = 4;import module4 from `module${moduleNumber}`; // SyntaxError: Unexpected token
复制代码

并且 import 语句会被提升到文件顶部执行,也就是说在模块初始化的时候所有的 import 都必须已经导入完成:

import './module1.js';alert('code1');import module2 from './module2.js';alert('code2');import module3 from './module3.js';// 执行结果
module1
module2
module3
code1
code2
复制代码

并且 import 的模块名只能是字符串常量,导入的值也是不可变对象;比如说你不能 import { a } from './a' 然后给 a 赋值个其他什么东西。这些设计虽然使得灵活性不如 CommonJS 的 require,但却保证了 ES6 Modules 的依赖关系是确定(Deterministic)的,和运行时的状态无关,从而也就保证了 ES6 Modules 是可以进行可靠的静态分析的。对于主要在服务端运行的 Node 来说,所有的代码都在本地,按需动态 require 即可,但对于要下发到客户端的 Web 代码而言,要做到高效的按需使用,不能等到代码执行了才知道模块的依赖,必须要从模块的静态分析入手。这是 ES6 Modules 在设计时的一个重要考量,也是为什么没有直接采用 CommonJS。此外我们还需要关注下的是 ES2015 Modules 在浏览器内的原生支持情况,尽管我们可以通过 Webpack 等打包工具将应用打包为单个包文件。

引用JavaScript 模块演化简史

功能分析

目标效果

在线示例

点击查看

功能描述

  1. 点击数字后,顶部会有一栏显示待计算的数字。
  2. 点击加减乘除后,顶部会有一排小字,用来显示计算过程。
  3. 点击等于后,会清空计算过程并且顶部会显示计算结果。
  4. 点击后退后,会从右往左删除一位待计算的数字。
  5. 点击清除后,会初始化输出栏和计算过程。

模块分析

计算过程

会显示已输入的数字和运算符

输出栏

数字键盘

0 - 9 包括 .

功能键盘

加减乘除 清除 等于 后退

编程思路

计算过程分析

  1. 这其实是一个历史记录的功能,只是没有撤销重做
  2. 历史记录是一个栈结构的数据,这里可以用数组来表示。
  3. push时机:两次点击操作类型不同时,比如上一次点击的是数字,下次点击的是加号。
  4. 运算符覆盖:修改数组最后一位的值。

输出栏分析

  1. 纯展示功能,给什么数据就显示什么。

数字键盘分析

  1. 数据的输入点,会影响输出栏模块。
  2. 创建一个变量用来储存临时输入的数据
  3. 这是一个有副作用的功能,要小心对待

功能键盘分析

  1. 计算之前必须要先有数字
  2. 计算之后在点击数字按钮会重置待计算数字的显示
  3. 这是一个有副作用的功能,要小心对待

创建index.html

index.html

在项目目录下创建index.html,添加下面的内容

<!DOCTYPE html>
<html lang="zh-cn"><head><title>计算器</title><meta charset="UTF-8"><!-- 用于在手机上显示,具体可百度viewport --><meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"><style>/* 重置默认样式 */* {padding: 0;margin: 0;box-sizing: border-box;}body {/* 主体的背景色 */background: #e6e6e6;}</style></head><body><!-- 先把之前分析的模块用div标签来占位 --><!-- 算术过程模块 --><div id="process"></div><!-- 输出栏模块 --><div id="output"></div><!-- 键盘模块 --><div id="keyboard"></div></body>
</html>
复制代码

计算过程显示栏实现

说明

本教程将使用组件化的思维来实现示例,传统的实现方式不再赘述。此说明只在这里说明一次,后面不会再提。

模块效果

创建文件夹

在项目目录下创建一个名为process的文件夹

index.css

process文件夹下创建一个index.css文件

.process {/* 高度 */height: 40px;/* 用于文本居中 */line-height: 40px;/* 字体大小 */font-size: 12px;/* 文字颜色 */color: #666;/* 文本向右对齐 */text-align: right;
}
复制代码

index.js

process文件夹下创建一个index.js文件

/*** 计算过程显示栏组件* @param text 显示的文本*/
const processComponent = (text) => `<div class="process">${text}</div>
`;function renderProcessComponent(text) {// 先获取计算过程的占位元素const elem = document.getElementById('process');// 将计算过程的HTML填充到占位元素中elem.innerHTML = processComponent(text);console.log('渲染计算过程组件完成,输出的值是', text);
}
复制代码

创建计算过程显示栏

更新index.html

</head>标签前添加

<!-- 引入计算过程组件的样式 -->
<link rel="stylesheet" href="./process/index.css">
复制代码

</body>标签前添加

<!-- 引入计算过程组件的js -->
<script src="./process/index.js"></script>
复制代码

接着上一行代码,换行添加

<script>// 初始化应用的函数function init() {// 渲染计算过程,初始化为空字符串renderProcessComponent('');}//调用初始化函数init();
</script>
复制代码

最终index.html

<html lang="zh-cn"><head><title>计算器</title><meta charset="UTF-8"><!-- 用于在手机上显示,具体可百度viewport --><meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"><style>/* 重置默认样式 */* {padding: 0;margin: 0;box-sizing: border-box;}</style><!-- 引入计算过程组件的样式 --><link rel="stylesheet" href="./process/index.css"></head><body><!-- 先把之前分析的模块用div标签来占位 --><!-- 算术过程模块 --><div id="process"></div><!-- 待计算数字或计算结果模块 --><div id="display"></div><!-- 键盘模块 --><div id="keyboard"></div><!-- 引入计算过程组件的js --><script src="./process/index.js"></script><script>// 初始化应用的函数function init() {// 渲染计算过程,初始化为空字符串renderProcessComponent('');}//调用初始化函数init();</script></body>
</html>
复制代码

输出栏组件实现

模块效果

创建文件夹

在项目目录下创建一个名为output的文件夹

index.css

output文件夹下创建一个index.css文件

.output {/* 上边距0,右边距16px,下边距60px,左边距0(最后一个为0的话可以省略不写) */padding: 0 16px 60px;/* 字体大小 */font-size: 33px;/* 文字颜色 */color: #000;/* 文本向右对齐 */text-align: right;
}
复制代码

index.js

output文件夹下创建一个index.js文件

/*** 输出栏组件* @param text 显示的文本*/
const outputComponent = (text) => `<div class="output">${text}</div>
`;function renderOutputComponent(text) {// 先获取输出栏的占位元素const elem = document.getElementById('output');// 将输出栏的HTML填充到占位元素中elem.innerHTML = outputComponent(text);console.log('渲染输出栏组件完成,输出的值是', text);
}
复制代码

创建输出栏组件

更新index.html

</head>标签前添加

<!-- 引入输出栏组件的样式 -->
<link rel="stylesheet" href="./output/index.css">
复制代码

<script src="./process/index.js"></script>代码后面添加

<!-- 引入输出栏组件的js -->
<script src="./output/index.js"></script>
复制代码

renderProcessComponent('');后面添加

// 渲染输出栏,初始化为'0',这里不初始化为数字0的原因后面再提
renderOutputComponent('0');
复制代码

最终index.html

<html lang="zh-cn"><head><title>计算器</title><meta charset="UTF-8"><!-- 用于在手机上显示,具体可百度viewport --><meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"><style>/* 重置默认样式 */* {padding: 0;margin: 0;box-sizing: border-box;}body {/* 主体的背景色 */background: #e6e6e6;}</style><!-- 引入计算过程组件的样式 --><link rel="stylesheet" href="./process/index.css"><!-- 引入输出栏组件的样式 --><link rel="stylesheet" href="./output/index.css"></head><body><!-- 先把之前分析的模块用div标签来占位 --><!-- 算术过程模块 --><div id="process"></div><!-- 输出栏模块 --><div id="output"></div><!-- 键盘模块 --><div id="keyboard"></div><!-- 引入计算过程组件的js --><script src="./process/index.js"></script><!-- 引入输出栏组件的js --><script src="./output/index.js"></script><script>// 初始化应用的函数function init() {// 渲染计算过程,初始化为空字符串renderProcessComponent('');// 渲染输出栏,初始化为'0',这里不初始化为数字0的原因后面再提renderOutputComponent('0');}//调用初始化函数init();</script></body>
</html>
复制代码

键盘组件实现

模块效果

这里我们把数字键盘和功能键盘合在一起做

创建文件夹

在项目目录下创建一个名为keyboard的文件夹

index.css

keyboard文件夹下创建一个index.css文件

#keyboard {/* 包裹键盘组件的容器,内边距3px */padding: 3px;
}.keyboard-row {/* 显示模式为弹性布局 */display: flex;/* 竖轴方向上居中 */align-items: center;/* 横轴方向两端对齐 */justify-content: space-between;/* 上下内边距3px */padding: 3px 0;
}.keyboard-button {/* 每个按钮平分横轴上的宽度 */flex: 1;/* 文字水平居中 */text-align: center;/* 文字大小 */font-size: 18px;/* 文字颜色 */color: #000;/* 上下内边距20px,相当于使文字垂直居中 */padding: 20px 0;/* 这里可以起到一个左内边距为3px的作用,因为边框颜色和主体背景相同,所以看上去是一个间距 */border-left: 3px solid #e6e6e6;/* 这里可以起到一个右内边距为3px的作用,因为边框颜色和主体背景相同,所以看上去是一个间距 */border-right: 3px solid #e6e6e6;/* 这里为了简化实现,把所有按钮都设置为了白色 */background: #fff;/* 将鼠标手势设置为手指,表示这是一个可点击的按钮 */cursor: pointer;
}
复制代码

index.js

keyboard文件夹下创建一个index.js文件

/*** 键盘的按键映射,这里用了一个二维数组来实现* 空白按钮使用了一个全角的空格,目的是为了让div元素拥有高度*/
const keyMap = [['&emsp;', '清除', '后退', '/'],['7', '8', '9', '*'],['4', '5', '6', '-'],['1', '2', '3', '+'],['&emsp;', '0', '.', '=']
];/*** 键盘按钮组件* onclick事件传递了一个this,表示执行事件的元素* @param props {*   // 按钮的文本*   label: string;*   // 按钮点击事件方法名*   onClick: string* }*/
const keyboardButtonComponent = (props) => `<divclass="keyboard-button"onclick="${props.onClick}(this)">${props.label}</div>
`;/*** 键盘组件* 代码中的join是因为keyMap返回的是一个数组,然后渲染组件需要的是一个字符串拼接的html* 如果不做join(' ')的操作,会自动使用join(),join的默认参数是','* @param props {*   // 按钮点击事件方法名*   onClick: string* }*/
const keyboardComponent = (props) => keyMap.map((row) => `<div class="keyboard-row">${row.map((label) => keyboardButtonComponent({label: label,// 把点击事件往下传递,传给keyboardItemComponentonClick: props.onClick})).join(' ')}</div>
`).join(' ');/*** 键盘组件的渲染* @param props {*   // 按钮点击事件方法名*   onClick: string* }*/
function renderkeyboardComponent(props) {// 先获取键盘组件的占位元素const elem = document.getElementById('keyboard');// 将键盘组件的HTML填充到占位元素中elem.innerHTML = keyboardComponent(props);console.log('渲染键盘组件完成');
}
复制代码

创建键盘组件

更新index.html

</head>标签前添加

<!-- 引入键盘组件的样式 -->
<link rel="stylesheet" href="./keyboard/index.css">
复制代码

<script src="./output/index.js"></script>代码后面添加

<!-- 引入键盘组件的js -->
<script src="./keyboard/index.js"></script>
复制代码

renderOutputComponent('0');后面添加

// 渲染键盘组件,传递点击键盘按钮的事件
renderkeyboardComponent({onClick: 'onKeyboardClick'
});
复制代码

init();前面添加一个空的函数

function onKeyboardClick(e) {}
复制代码

最终index.html

<html lang="zh-cn"><head><title>计算器</title><meta charset="UTF-8"><!-- 用于在手机上显示,具体可百度viewport --><meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"><style>/* 重置默认样式 */* {padding: 0;margin: 0;box-sizing: border-box;}body {/* 主体的背景色 */background: #e6e6e6;}</style><!-- 引入计算过程组件的样式 --><link rel="stylesheet" href="./process/index.css"><!-- 引入输出栏组件的样式 --><link rel="stylesheet" href="./output/index.css"><!-- 引入键盘组件的样式 --><link rel="stylesheet" href="./keyboard/index.css"></head><body><!-- 先把之前分析的模块用div标签来占位 --><!-- 算术过程模块 --><div id="process"></div><!-- 输出栏模块 --><div id="output"></div><!-- 键盘模块 --><div id="keyboard"></div><!-- 引入计算过程组件的js --><script src="./process/index.js"></script><!-- 引入输出栏组件的js --><script src="./output/index.js"></script><!-- 引入键盘组件的js --><script src="./keyboard/index.js"></script><script>// 初始化应用的函数function init() {// 渲染计算过程组件,初始化为空字符串renderProcessComponent('');// 渲染输出栏组件,初始化为'0',这里不初始化为数字0的原因后面再提renderOutputComponent('0');// 渲染键盘组件,传递点击键盘按钮的事件renderkeyboardComponent({onClick: 'onKeyboardClick'});}function onKeyboardClick(e) {}//调用初始化函数init();</script></body>
</html>
复制代码

小结

至此计算器的UI算是完成了,剩下的工作就是添加按钮事件了。

完成计算器

最终index.html

<html lang="zh-cn"><head><title>计算器</title><meta charset="UTF-8"><!-- 用于在手机上显示,具体可百度viewport --><meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"><style>/* 重置默认样式 */* {padding: 0;margin: 0;box-sizing: border-box;}body {/* 主体的背景色 */background: #e6e6e6;}</style><!-- 引入计算过程组件的样式 --><link rel="stylesheet" href="./process/index.css"><!-- 引入输出栏组件的样式 --><link rel="stylesheet" href="./output/index.css"><!-- 引入键盘组件的样式 --><link rel="stylesheet" href="./keyboard/index.css"></head><body><!-- 先把之前分析的模块用div标签来占位 --><!-- 算术过程模块 --><div id="process"></div><!-- 输出栏模块 --><div id="output"></div><!-- 键盘模块 --><div id="keyboard"></div><!-- 引入计算过程组件的js --><script src="./process/index.js"></script><!-- 引入输出栏组件的js --><script src="./output/index.js"></script><!-- 引入键盘组件的js --><script src="./keyboard/index.js"></script><script>// 计算过程let process = [];// 输出栏显示的值,默认值为0let output = '0';// 初始化应用的函数function init() {// 渲染计算过程组件,初始化为空字符串renderProcessComponent('');// 渲染输出栏组件,初始化为'0',这里不初始化为数字0的原因后面再提renderOutputComponent('0');// 渲染键盘组件,传递点击键盘按钮的事件renderkeyboardComponent({onClick: 'onKeyboardClick'});}// 对计算过程求和function processSum() {// 计算过程至少有3次操作才能求和if (process.length > 2) {// slice方法获取数组的副本,不对原数组进行更改, process.length - 1是为了过滤最后一位的运算符// reduce因为乘法和除法对运算有优先级,所以要按照数组顺序依次求和const result = process.slice(0, process.length - 1).reduce((prev, cur) => {// 这里相当于是对cur做一个是否是数字的判断try {return eval(prev + cur);} catch (err) {return prev + cur;}});return result;} else {// 对第一次操作就是等号时做处理return output;}}// 键盘点击事件function onKeyboardClick(e) {// 获取按钮的值const label = e.innerText;// 计算结果, 默认为输出栏的输出let result = output;switch (label) {case '&emsp;':// 过滤空白的按钮,不做任何操作break;case '.':// 判断有没有重复点击小数点按钮,有小数点存在就不再拼接output += output.indexOf('.') >= 0 ? '' : '.';break;case '+':case '-':case '*':case '/':// 把待计算的数字加入计算过程process.push(output);// 把运算符加入计算过程process.push(label);// 将输出栏的内存记录状态初始化为'0'output = '0';// 重新渲染输出栏组件,把计算结果作为显示的值renderOutputComponent(processSum());// 重新渲染计算过程组件renderProcessComponent(process.join(' '));break;case '=':// 至少要有2个操作存在的情况下才能使用等号求值if (process.length >= 2) {process.push(output);process.push(label);result = processSum();}// 因为返回结果是一个数字,而indexOf需要字符串output = result.toString();// 重置计算过程process = [];renderOutputComponent(result);renderProcessComponent('');break;case '后退':// 从末端减少一位,如果越界了就初始化为'0'output = output.substr(0, output.length - 1) || '0';renderOutputComponent(output);break;case '清除':output = '0';process = [];renderOutputComponent(output);renderProcessComponent('');break;default:// 最后一种情况就是点击的是数字// 如果输出栏上第一位是0,那么就覆盖掉,否则就拼接output = output.indexOf('0') === 0 ? label : output + label;// 重新渲染输出栏组件renderOutputComponent(output);break;}}//调用初始化函数init();</script></body>
</html>
复制代码

更新内容

2个控制变量

// 计算过程
let process = [];
// 输出栏显示的值,默认值为0
let output = '0';
复制代码

2个函数

// 对计算过程求和
function processSum() {// 计算过程至少有3次操作才能求和if (process.length > 2) {// slice方法获取数组的副本,不对原数组进行更改, process.length - 1是为了过滤最后一位的运算符// reduce因为乘法和除法对运算有优先级,所以要按照数组顺序依次求和const result = process.slice(0, process.length - 1).reduce((prev, cur) => {// 这里相当于是对cur做一个是否是数字的判断try {return eval(prev + cur);} catch (err) {return prev + cur;}});return result;} else {// 对第一次操作就是等号时做处理return output;}
}// 键盘点击事件
function onKeyboardClick(e) {// 获取按钮的值const label = e.innerText;// 计算结果, 默认为输出栏的输出let result = output;switch (label) {case '&emsp;':// 过滤空白的按钮,不做任何操作break;case '.':// 判断有没有重复点击小数点按钮,有小数点存在就不再拼接output += output.indexOf('.') >= 0 ? '' : '.';break;case '+':case '-':case '*':case '/':// 把待计算的数字加入计算过程process.push(output);// 把运算符加入计算过程process.push(label);// 将输出栏的内存记录状态初始化为'0'output = '0';// 重新渲染输出栏组件,把计算结果作为显示的值renderOutputComponent(processSum());// 重新渲染计算过程组件renderProcessComponent(process.join(' '));break;case '=':// 至少要有2个操作存在的情况下才能使用等号求值if (process.length >= 2) {process.push(output);process.push(label);result = processSum();}// 因为返回结果是一个数字,而indexOf需要字符串output = result.toString();// 重置计算过程process = [];renderOutputComponent(result);renderProcessComponent('');break;case '后退':// 从末端减少一位,如果越界了就初始化为'0'output = output.substr(0, output.length - 1) || '0';renderOutputComponent(output);break;case '清除':output = '0';process = [];renderOutputComponent(output);renderProcessComponent('');break;default:// 最后一种情况就是点击的是数字// 如果输出栏上第一位是0,那么就覆盖掉,否则就拼接output = output.indexOf('0') === 0 ? label : output + label;// 重新渲染输出栏组件renderOutputComponent(output);break;}
}
复制代码

总结

计算器的求值算法不强求理解,这个示例主要展示了:

  1. 怎么编写一个纯组件
  2. 怎么让组件工作
  3. 怎么让多个组件拼装在一起
  4. 数据驱动的简单展示
  5. 怎么控制副作用

JavaScript函数式编程入门-计算器应用相关推荐

  1. JavaScript函数式编程入门经典

    一个持续更新的github笔记,链接地址:Front-End-Basics,可以watch,也可以star. 此篇文章的地址:JavaScript函数式编程入门经典 正文开始 什么是函数式编程?为何它 ...

  2. JavaScript 函数式编程——入门指南

    JavaScript 函数式编程 一.什么是函数式编程 **定义:**函数式编程是一种编程范式,将整个程序都由函数调用以及函数组合构成. 可以看成一条流水线,数据可以不断地从一个函数的输出流入另一个函 ...

  3. JavaScript 函数式编程(一)

    零.前言 说到函数式编程,想必各位或多或少都有所耳闻,然而对于函数式的内涵和本质可能又有些说不清楚. 所以本文希望针对工程师,从应用(而非学术)的角度将函数式编程相关思想和实践(以 JavaScrip ...

  4. JavaScript 函数式编程(三)

    slide 地址 四.Talk is cheap!Show me the ... MONEY! 以下内容主要参考自 Professor Frisby Introduces Composable Fun ...

  5. 专访《Haskell函数式编程入门》作者张淞:浅谈Haskell的优点与启发

    张淞,Haskell语言爱好者,著有<Haskell函数式编程入门>一书.目前就职于网易杭州研究院.在10月15日~17日的QCon上海2015上,他将分享<Haskell中的函数与 ...

  6. 学会JavaScript函数式编程(第1部分)

    2019独角兽企业重金招聘Python工程师标准>>> 摘要: JS函数式编程入门. 原文:学会使用函数式编程的程序员(第1部分) 作者:前端小智 Fundebug经授权转载,版权归 ...

  7. Java 函数式编程入门

    Java 函数式编程入门 函数式编程实战 改进 完整代码   像 JavaScript 这种语言很早就支持闭包了,虽然 C++ 很早就有了函数指针,Java 也很早就提供了反射中的 Method 类, ...

  8. SegmentFault 技术周刊 Vol.16 - 浅入浅出 JavaScript 函数式编程

    函数式编程(Functional Programming),一看这个词,简直就是学院派的典范. 以至于从 Lisp 的创世,到 Scheme.Haskell.Clean.Erlang.Miranda. ...

  9. 编程范式 —— 函数式编程入门

    该系列会有 3 篇文章,分别介绍什么是函数式编程.剖析函数式编程库.以及函数式编程在 React 中的应用,欢迎关注我的 blog 命令式编程和声明式编程 拿泡茶这个事例进行区分命令式编程和声明式编程 ...

最新文章

  1. 算法结构2.希尔排序
  2. clone git 修改保存路径_Git和Github详细入门教程(别再跟我说你不会Git和Github)
  3. python tk下拉列表的state_Python tkinter之ComboBox(下拉框)的使用简介
  4. Android ImageLoader(Android-Universal-Image-Loader)【1】概述及使用简单介绍
  5. 如何快速重置打印队列
  6. Memcached安装和基本使用
  7. [reference]-ARM缩写
  8. Python基础知识细节点总结,零基础一分钟也能掌握
  9. leetcode79. 24 点游戏
  10. vs2008智能提示jQuery的设置注意事项
  11. Ubuntu查看CPU和MEM的使用率
  12. (转)SpringMVC学习(十一)——SpringMVC实现Resultful服务
  13. 斐波那契数列的非递归实现
  14. 对列表中k之前和之后的元素分别进行逆序
  15. strstr的实现 和key=value字符串的实现
  16. easyUI跨Tab操作datagrid
  17. RabbitMQ项目实战——商户管理系统
  18. DIY多快充协议太阳能充电器!----锂电池充电电路
  19. html页面 js注释,html、css和js注释的规范用法
  20. python box2d模拟平抛运动_[HTML5]使用Box2dWeb模拟飞行箭矢

热门文章

  1. 多线程采集表情包,下一届斗图王者属于你
  2. Allegro中 板框 尺寸标注
  3. 图像处理之基础---去污算法
  4. 截断二进制指数退避算法c++实现
  5. java Excel导出功能之 固定列表格
  6. CES Asia展华为秀肌肉,布局智能互联生态
  7. 圆梦微软 — 旅游和入职体验
  8. 系统检测到您疑似使用网页抓取工具访问本_12款最常使用的网络爬虫工具推荐...
  9. JAVA工具_PinyinConv
  10. MIT CMS.300 Session 5 – UNWRITTEN RULES 游戏的潜在规则