JavaScript函數式編程入門-計算器應用

目錄

預備知識

函數式編程

模塊化

計算器示例

功能分析

建立index.html

計算過程顯示欄實現

建立計算過程顯示欄

輸出欄組件實現

建立輸出欄組件

鍵盤組件實現

建立鍵盤組件

完成計算器

函數式編程

概念

在軟件開發中有一句名言:共享的可變狀態是萬惡之源,而函數式編程正是可以完全解決共享狀態。函數式編程本質上也是一種編程範式(Programming Paradigm ),其表明了一系列用於構建軟件系統的基本定義準則;它強調避免使用共享狀態(Shared State )、可變狀態(Mutable Data )以及反作用(Side Effects ),整個軟件系統由數據驅動,應用的狀態在不一樣純函數之間流動。與偏向命令式編程的面向對象編程而言,函數式編程其更偏向於聲明式編程,代碼更加簡潔明瞭、更可預測,而且可測試性也更好;典型的函數式編程語言有 Scala、Haskell 等,而其編程思想在 Go、Swift 、 JavaScript、Python 乃至於 Java 中都有着普遍而深遠的實踐應用。javascript

共享狀態(Shared State )能夠是存在於共享做用域(全局做用域與閉包做用域)或者做爲傳遞到不一樣做用域的對象屬性的任何變量、對象或者內存空間。在面向對象編程中,咱們經常是經過添加屬性到其餘對象的方式共享某個對象。共享狀態問題在於,若是開發者想要理解某個函數的做用,必須去詳細瞭解該函數可能對於每一個共享變量形成的影響。每每多個併發請求會致使的數據一致性錯亂也就是觸發所謂的競態條件(Race Condition ),而不一樣的調用順序可能會觸發未知的錯誤,這是由於對於共享狀態的操做每每是時序依賴的。css

純函數指那些僅根據輸入參數決定輸出而且不會產生任何反作用的函數。純函數最優秀的特性之一在於其結果的可預測性:html

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 )則是可以接受函數做爲參數,可以返回某個函數做爲返回值的函數。java

const add10 = value => value + 10;
const mult5 = value => value * 5;
const mult5AfterAdd10 = value => 5 * (value + 10);
複製代碼

引用自:數據流驅動的界面git

模塊化

ES2015 Modules

JavaScript 模塊規範領域羣雄逐鹿,各領風騷,做爲 ECMAScript 標準的起草者 TC39 委員會天然也不能置身事外。ES2015 Modules 規範始於 2010 年,主要由 Dave Herman 主導;隨後的五年中 David 還參與了 asm.js,emscription,servo,等多個重大的開源項目,也使得 ES2015 Modules 的設計可以從多方面進行考慮與權衡。而最後的模塊化規範定義於 2015 年正式發佈,也就是被命名爲 ES2015 Modules。咱們上述的例子改寫爲 ES2015 Modules 規範以下所示:github

// 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) => ` <div class="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, // 把點擊事件往下傳遞,傳給keyboardItemComponent onClick: 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 = []; // 輸出欄顯示的值,默認值爲0 let 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. 怎麼控制反作用
相關文章
相關標籤/搜索