この記事では、javascript に関する関連知識を提供します。主に関数型プログラミングに関連する問題を紹介します。関数型プログラミングは、関数を主なキャリアとして使用するプログラミング手法として理解できます。関数を使用して逆アセンブルおよび抽象化します。一般的な表現ですので、皆様のお役に立てれば幸いです。
関連する推奨事項: JavaScript 学習チュートリアル
関数型プログラミングに関する多くの説明を見てきましたが、そのほとんどは「概要」にとどまっています。理論レベルでは、Haskell などの純粋な関数型プログラミング言語のみに対応したものもあります。この記事の目的は、私の目から見た JavaScript での関数型プログラミングの具体的な実践について話すことです。「私の目から見て」という理由は、私が述べていることは私の個人的な意見を表しているだけであり、厳密な概念と矛盾する可能性があることを意味します。
この記事では、正式な概念の紹介の多くを省略し、JavaScript の関数コードとは何か、関数コードと一般的な記述の違いは何なのか、関数コードがどのような利点をもたらすのかを示すことに重点を置きます。一般的な機能モデルにはどのようなものがありますか?
関数型プログラミングについての私の理解
関数型プログラミングは、関数をメインキャリアとして使用し、関数を使用して一般的な式を逆アセンブルおよび抽象化するプログラミング手法として理解できると思います。
命令型と比較してこれを行う利点は何ですか?主なポイントは次のとおりです。
より明確なセマンティクス
より高い再利用性
より優れた保守性
限定された範囲、より少ない副作用
基本関数 数式プログラミング
次の例は、特定の機能の実施例です。
Javascript コード
// 数组中每个单词,首字母大写 // 一般写法 const arr = ['apple', 'pen', 'apple-pen']; for(const i in arr){ const c = arr[i][0]; arr[i] = c.toUpperCase() + arr[i].slice(1); } console.log(arr); // 函数式写法一 function upperFirst(word) { return word[0].toUpperCase() + word.slice(1); } function wordToUpperCase(arr) { return arr.map(upperFirst); } console.log(wordToUpperCase(['apple', 'pen', 'apple-pen'])); // 函数式写法二 console.log(arr.map(['apple', 'pen', 'apple-pen'], word => word[0].toUpperCase() + word.slice(1)));
状況がより複雑になると、式の書き方でいくつかの問題が発生します。 :
意味は異なります。明白であり、徐々に保守が困難になります
再利用性が悪く、より多くのコードが生成されます
中間変数が大量に生成されます
関数型プログラミングは、上記の問題を非常にうまく解決します。まず、関数の書き方 1 を参照してください。関数のカプセル化を利用して関数を分解し (粒度は一意ではありません)、それらを異なる関数にカプセル化し、呼び出しを組み合わせて目的を達成します。これにより、式が明確になり、保守、再利用、拡張が容易になります。次に、高階関数を使用して、Array.map は配列トラバーサルの for...of を置き換え、中間の変数と演算を減らします。
関数記述法 1 と関数記述法 2 の主な違いは、関数がその後再利用される可能性があるかどうかを考慮できることであり、そうでない場合は後者の方が優れています。
上記の関数記述方法 2 から、関数コードの記述プロセス中に水平拡張が発生しやすいことがわかります。複数レベルのネストを使用して、以下のより極端な例を見てみましょう。
Javascript コード
// 计算数字之和 // 一般写法 console.log(1 + 2 + 3 - 4) // 函数式写法 function sum(a, b) { return a + b; } function sub(a, b) { return a - b; } console.log(sub(sum(sum(1, 2), 3), 4); 本例仅为展示 横向延展 的比较极端的情况,随着函数的嵌套层数不断增多,导致代码的可读性大幅下降,还很容易产生错误。 在这种情况下,我们可以考虑多种优化方式,比如下面的 链式优化 。 // 优化写法 (嗯,你没看错,这就是 lodash 的链式写法) Javascript代码 const utils = { chain(a) { this._temp = a; return this; }, sum(b) { this._temp += b; return this; }, sub(b) { this._temp -= b; return this; }, value() { const _temp = this._temp; this._temp = undefined; return _temp; } }; console.log(utils.chain(1).sum(2).sum(3).sub(4).value());
このように書き換えると、全体の構造が明確になり、チェーンの各リンクが何をしているのかを簡単に表示できるようになります。関数のネストとチェーンを比較するもう 1 つの良い例は、コールバック関数と Promise パターンです。
Javascript コード
// 顺序请求两个接口 // 回调函数 import $ from 'jquery'; $.post('a/url/to/target', (rs) => { if(rs){ $.post('a/url/to/another/target', (rs2) => { if(rs2){ $.post('a/url/to/third/target'); } }); } }); // Promise import request from 'catta'; // catta 是一个轻量级请求工具,支持 fetch,jsonp,ajax,无依赖 request('a/url/to/target') .then(rs => rs ? $.post('a/url/to/another/target') : Promise.reject()) .then(rs2 => rs2 ? $.post('a/url/to/third/target') : Promise.reject());
コールバック関数の入れ子レベルと単一レイヤーの複雑さが増加すると、肥大化して保守が困難になり、Promise のチェーン構造は非常に複雑になります。 、引き続き垂直方向に拡張でき、階層の分離は非常に明確です。
共通関数型プログラミング モデル
ローカル変数を保持し解放できないコード ブロックはクロージャと呼ばれます
クロージャの概念誰もが多かれ少なかれこの機能を知っており、使用していると思います
それでは、クロージャーが私たちにどのようなメリットをもたらすのでしょうか?
まずクロージャの作成方法を見てみましょう:
Javascript コード
// 创建一个闭包 function makeCounter() { let k = 0; return function() { return ++k; }; } const counter = makeCounter(); console.log(counter()); // 1 console.log(counter()); // 2
makeCounter この関数のコード ブロックは、返された関数内でローカル変数 k を実行します。参照が欠落しているため、関数の実行完了後にシステムがローカル変数をリサイクルできなくなり、クロージャが発生します。このクロージャの機能は、内部関数が呼び出されたときに変数を再利用できるようにローカル変数を「保持」することです。グローバル変数とは異なり、この変数は関数内でのみ参照できます。
言い換えれば、クロージャは実際には、関数にとってプライベートないくつかの「永続変数」を作成します。
したがって、この例から、クロージャを作成するための条件は次のように結論付けることができます:
関数には内部と外部の 2 つの層があります。
内部関数はローカル変数を変更します。 Quote
クロージャの目的
クロージャの主な目的は、スコープが制限された永続変数を定義することです。これらの変数は、キャッシュや中間計算などに使用できます。
Javascript コード
// 简单的缓存工具 // 匿名函数创造了一个闭包 const cache = (function() { const store = {}; return { get(key) { return store[key]; }, set(key, val) { store[key] = val; } } }()); cache.set('a', 1); cache.get('a'); // 1
上記の例は、単純なキャッシュ ツールの実装です。匿名関数は、ストア オブジェクトが常に参照でき、リサイクルされないようにクロージャを作成します。
クロージャの欠点
永続変数は通常は解放されず、メモリ領域を占有し続けるため、メモリの無駄が発生しやすいため、通常は追加の手動クリーンアップ メカニズムが必要です。
関数を受け取ったり返したりする関数を高階関数といいます
听上去很高冷的一个词汇,但是其实我们经常用到,只是原来不知道他们的名字而已。JavaScript 语言是原生支持高阶函数的,因为 JavaScript 的函数是一等公民,它既可以作为参数又可以作为另一个函数的返回值使用。
我们经常可以在 JavaScript 中见到许多原生的高阶函数,例如 Array.map , Array.reduce , Array.filter
下面以 map 为例,我们看看他是如何使用的
映射是对集合而言的,即把集合的每一项都做相同的变换,产生一个新的集合
map 作为一个高阶函数,他接受一个函数参数作为映射的逻辑
Javascript代码
// 数组中每一项加一,组成一个新数组 // 一般写法 const arr = [1,2,3]; const rs = []; for(const n of arr){ rs.push(++n); } console.log(rs) // map改写 const arr = [1,2,3]; const rs = arr.map(n => ++n);
上面一般写法,利用 for…of 循环的方式遍历数组会产生额外的操作,而且有改变原数组的风险
而 map 函数封装了必要的操作,使我们仅需要关心映射逻辑的函数实现即可,减少了代码量,也降低了副作用产生的风险。
给定一个函数的部分参数,生成一个接受其他参数的新函数
可能不常听到这个名词,但是用过 undescore 或 lodash 的人都见过他。
有一个神奇的 _.partial 函数,它就是柯里化的实现
Javascript代码
// 获取目标文件对基础路径的相对路径 // 一般写法 const BASE = '/path/to/base'; const relativePath = path.relative(BASE, '/some/path'); // _.parical 改写 const BASE = '/path/to/base'; const relativeFromBase = _.partial(path.relative, BASE); const relativePath = relativeFromBase('/some/path');
通过 _.partial ,我们得到了新的函数 relativeFromBase ,这个函数在调用时就相当于调用 path.relative ,并默认将第一个参数传入 BASE ,后续传入的参数顺序后置。
本例中,我们真正想完成的操作是每次获得相对于 BASE 的路径,而非相对于任何路径。柯里化可以使我们只关心函数的部分参数,使函数的用途更加清晰,调用更加简单。
将多个函数的能力合并,创造一个新的函数
同样你第一次见到他可能还是在 lodash 中,compose 方法(现在叫 flow)
Javascript代码
// 数组中每个单词大写,做 Base64 // 一般写法 (其中一种) const arr = ['pen', 'apple', 'applypen']; const rs = []; for(const w of arr){ rs.push(btoa(w.toUpperCase())); } console.log(rs); // _.flow 改写 const arr = ['pen', 'apple', 'applypen']; const upperAndBase64 = _.partialRight(_.map, _.flow(_.upperCase, btoa)); console.log(upperAndBase64(arr));
_.flow 将转大写和转 Base64 的函数的能力合并,生成一个新的函数。方便作为参数函数或后续复用。
自己的观点
我理解的 JavaScript 函数式编程,可能和许多传统概念不同。我并不只认为 高阶函数 算函数式编程,其他的诸如普通函数结合调用、链式结构等,我都认为属于函数式编程的范畴,只要他们是以函数作为主要载体的。
而我认为函数式编程并不是必须的,它也不应该是一个强制的规定或要求。与面向对象或其他思想一样,它也是其中一种方式。我们更多情况下,应该是几者的结合,而不是局限于概念。
相关推荐:javascript教程
以上がJavaScript 関数プログラミングについて話しましょうの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。