目錄
看本篇文章需要知道的一些知識點
什麼是柯里化?
為什麼要對函數進行柯里化?
如何對函數進行柯里化?
柯里化的一些应用场景
关于柯里化的性能
关于本章一些知识点的解释
一些你可能会喜欢的JS库
首頁 web前端 js教程 帶你了解並掌握JavaScript函數的柯里化

帶你了解並掌握JavaScript函數的柯里化

Jun 29, 2020 am 09:57 AM
javascript 柯里化

Haskell和scala都支援函數的柯里化,JavaScript函數的柯里化也與JavaScript的函數程式設計有很大的連結,如果你有興趣的話,可以在這些方面多下功夫了解,相信收穫一定很多.

看本篇文章需要知道的一些知識點

  • 函數部分的call/apply/arguments
  • 閉包
  • 高階函數
  • #不完全函數

文章後面有對這些知識的簡單解釋,大家可以看看.

什麼是柯里化?

我們先來看看維基百科中是如何定義的:在計算機科學中,柯里化(英語:Currying),又譯為卡瑞化或加里化,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受餘下的參數而且傳回結果的新函數的技術。

我們可以舉個簡單的例子,如下函數add是一般的一個函數,就是將傳進來的參數ab相加;函數curryingAdd就是對函數add進行柯里化的函數;
這樣一來,原來我們需要直接傳進去兩個參數來運算的函數,現在需要分別傳入參數ab,函數如下:

function add(a, b) {
    return a + b;
}

function curryingAdd(a) {
    return function(b) {
        return a + b;
    }
}

add(1, 2); // 3
curryingAdd(1)(2); // 3
登入後複製

看到這裡你可能會想,這樣做有什麼用?為什麼要這樣做?這樣做能夠為我們的應用帶來什麼樣的好處?先別著急,我們接著往下看.

為什麼要對函數進行柯里化?

  • 可以使用一些小技巧(見下文)
  • 提前綁定好函數裡面的某些參數,達到參數復用的效果,提高了適用性.
  • 固定易變因素
  • 延遲計算

總之,函數的柯里化能夠讓你重新組合你的應用,把你的複雜功能拆分成一個一個的小部分,每一個小的部分都是簡單的,便於理解的,而且是容易測試的;

如何對函數進行柯里化?

在這一部分裡,我們由淺入深的一步步來告訴大家如何對一個多參數的函數進行柯里化.其中用到的知識有閉包,高階函數,不完全函數等等.

  • I 開胃菜

    假如我們要實現一個功能,就是輸出語句name喜歡song,其中namesong都是可變參數;那麼一般情況下我們會這樣寫:

    function printInfo(name, song) {
        console.log(name + '喜欢的歌曲是: ' + song);
    }
    printInfo('Tom', '七里香');
    printInfo('Jerry', '雅俗共赏');
    登入後複製

    對上面的函數進行柯里化之後,我們可以這樣寫:

    function curryingPrintInfo(name) {
        return function(song) {
            console.log(name + '喜欢的歌曲是: ' + song);
        }
    }
    var tomLike = curryingPrintInfo('Tom');
    tomLike('七里香');
    var jerryLike = curryingPrintInfo('Jerry');
    jerryLike('雅俗共赏');
    登入後複製
  • II 小雞燉蘑菇

    #上面我們雖然對對函數printInfo進行了柯里化,但是我們可不想在需要柯里化的時候,都像上面那樣不斷地進行函數的嵌套,那簡直是噩夢;
    所以我們要創造一些幫助其它函數進行柯里化的函數,我們暫且叫它為curryingHelper吧,一個簡單的curryingHelper函數如下:

    function curryingHelper(fn) {
        var _args = Array.prototype.slice.call(arguments, 1);
        return function() {
            var _newArgs = Array.prototype.slice.call(arguments);
            var _totalArgs = _args.concat(_newArgs);
            return fn.apply(this, _totalArgs);
        }
    }
    登入後複製

    這裡解釋一點東西,首先函數的arguments表示的是傳遞到函數中的參數物件,它不是一個陣列,它是一個類別數組物件;
    所以我們可以使用函數的Array.prototype.slice方法,然後使用 .call方法來取得arguments裡面的內容.
    我們使用fn.apply(this, _totalArgs)來給函數fn#傳遞正確的參數.

    接下來我們來寫一個簡單的函數驗證上面的輔助柯里化函數的正確性, 程式碼部分如下:

    function showMsg(name, age, fruit) {
        console.log('My name is ' + name + ', I\'m ' + age + ' years old, ' + ' and I like eat ' + fruit);
    }
    
    var curryingShowMsg1 = curryingHelper(showMsg, 'dreamapple');
    curryingShowMsg1(22, 'apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    
    var curryingShowMsg2 = curryingHelper(showMsg, 'dreamapple', 20);
    curryingShowMsg2('watermelon'); // My name is dreamapple, I'm 20 years old,  and I like eat watermelon
    登入後複製

    上面的結果表示,我們的這個柯裡化的函數是正確的.上面的curryingHelper就是一個高階函數,關於高階函數的解釋可以參考下文.

  • III 牛肉火鍋

    上面的柯里化幫助函數確實已經能夠達到我們的一般性需求了,但是它還不夠好,我們希望那些經過柯里化後的函數可以每次只傳遞進去一個參數,
    然後可以進行多次參數的傳遞,那麼應該怎麼做呢?我們可以再花費一些腦筋,寫出一個betterCurryingHelper函數,實現我們上面說的那些
    功能.代碼如下:

    function betterCurryingHelper(fn, len) {
        var length = len || fn.length;
        return function () {
            var allArgsFulfilled = (arguments.length >= length);
    
            // 如果参数全部满足,就可以终止递归调用
            if (allArgsFulfilled) {
                return fn.apply(this, arguments);
            }
            else {
                var argsNeedFulfilled = [fn].concat(Array.prototype.slice.call(arguments));
                return betterCurryingHelper(curryingHelper.apply(this, argsNeedFulfilled), length - arguments.length);
            }
        };
    }
    登入後複製

    其中curryingHelper就是上面II 小雞燉蘑菇中提及的那個函數.需要注意的是fn.length表示的是這個函數的參數長度.
    接下來我們來檢定這個函數的正確性:

    var betterShowMsg = betterCurryingHelper(showMsg);
    betterShowMsg('dreamapple', 22, 'apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    betterShowMsg('dreamapple', 22)('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    betterShowMsg('dreamapple')(22, 'apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    betterShowMsg('dreamapple')(22)('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    登入後複製

    其中showMsg就是II 小雞燉蘑菇部分提及的那個函數.
    我們可以看出來,這個betterCurryingHelper確實實現了我們想要的那個功能.並且我們也可以像使用原來的那個函數一樣使用柯里化後的函數.

  • IV 泡椒鳳爪

    我们已经能够写出很好的柯里化辅助函数了,但是这还不算是最刺激的,如果我们在传递参数的时候可以不按照顺来那一定很酷;当然我们也可以写出这样的函数来,
    这个crazyCurryingHelper函数如下所示:

    var _ = {};
    function crazyCurryingHelper(fn, length, args, holes) {
        length = length || fn.length;
        args   = args   || [];
        holes  = holes  || [];
    
        return function() {
            var _args       = args.slice(),
                _holes      = holes.slice();
    
            // 存储接收到的args和holes的长度
            var argLength   = _args.length,
                holeLength  = _holes.length;
    
            var allArgumentsSpecified = false;
    
            // 循环
            var arg     = null,
                i       = 0,
                aLength = arguments.length;
    
            for(; i < aLength; i++) {
                arg = arguments[i];
    
                if(arg === _ && holeLength) {
                    // 循环holes的位置
                    holeLength--;
                    _holes.push(_holes.shift());
                } else if (arg === _) {
                    // 存储hole就是_的位置
                    _holes.push(argLength + i);
                } else if (holeLength) {
                    // 是否还有没有填补的hole
                    // 在参数列表指定hole的地方插入当前参数
                    holeLength--;
                    _args.splice(_holes.shift(), 0, arg);
                } else {
                    // 不需要填补hole,直接添加到参数列表里面
                    _args.push(arg);
                }
            }
    
            // 判断是否所有的参数都已满足
            allArgumentsSpecified = (_args.length >= length);
            if(allArgumentsSpecified) {
                return fn.apply(this, _args);
            }
    
            // 递归的进行柯里化
            return crazyCurryingHelper.call(this, fn, length, _args, _holes);
        };
    }
    登入後複製

    一些解释,我们使用_来表示参数中的那些缺失的参数,如果你使用了lodash的话,会有冲突的;那么你可以使用别的符号替代.
    按照一贯的尿性,我们还是要验证一下这个crazyCurryingHelper是不是实现了我们所说的哪些功能,代码如下:

    var crazyShowMsg = crazyCurryingHelper(showMsg);
    crazyShowMsg(_, 22)('dreamapple')('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    crazyShowMsg( _, 22, 'apple')('dreamapple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    crazyShowMsg( _, 22, _)('dreamapple', _, 'apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    crazyShowMsg( 'dreamapple', _, _)(22)('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    crazyShowMsg('dreamapple')(22)('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    登入後複製

    结果显示,我们这个函数也实现了我们所说的那些功能.

柯里化的一些应用场景

说了那么多,其实这部分才是最重要的部分;学习某个知识要一定可以用得到,不然学习它干嘛.

  • 关于函数柯里化的一些小技巧

    • setTimeout传递地进来的函数添加参数

      一般情况下,我们如果想给一个setTimeout传递进来的函数添加参数的话,一般会使用之种方法:

      function hello(name) {
          console.log('Hello, ' + name);
      }
      setTimeout(hello('dreamapple'), 3600); //立即执行,不会在3.6s后执行
      setTimeout(function() {
          hello('dreamapple');
      }, 3600); // 3.6s 后执行
      登入後複製

      我们使用了一个新的匿名函数包裹我们要执行的函数,然后在函数体里面给那个函数传递参数值.

      当然,在ES5里面,我们也可以使用函数的bind方法,如下所示:

      setTimeout(hello.bind(this, 'dreamapple'), 3600); // 3.6s 之后执行函数
      登入後複製

      这样也是非常的方便快捷,并且可以绑定函数执行的上下文.

      我们本篇文章是讨论函数的柯里化,当然我们这里也可以使用函数的柯里化来达到这个效果:

      setTimeout(curryingHelper(hello, 'dreamapple'), 3600); // 其中curryingHelper是上面已经提及过的
      登入後複製

      这样也是可以的,是不是很酷.其实函数的bind方法也是使用函数的柯里化来完成的,详情可以看这里Function.prototype.bind().

    • 写出这样一个函数multiply(1)(2)(3) == 6结果为true,multiply(1)(2)(3)(...)(n) == (1)*(2)*(3)*(...)*(n)结果为true

      这个题目不知道大家碰到过没有,不过通过函数的柯里化,也是有办法解决的,看下面的代码:

      function multiply(x) {
          var y = function(x) {
              return multiply(x * y);
          };
          y.toString = y.valueOf = function() {
              return x;
          };
          return y;
      }
      
      console.log(multiply(1)(2)(3) == 6); // true
      console.log(multiply(1)(2)(3)(4)(5) == 120); // true
      登入後複製

      因为multiply(1)(2)(3)的直接结果并不是6,而是一个函数对象{ [Number: 6] valueOf: [Function], toString: [Function] },我们
      之后使用了==会将左边这个函数对象转换成为一个数字,所以就达到了我们想要的结果.还有关于为什么使用toStringvalueOf方法
      可以看看这里的解释Number.prototype.valueOf(),Function.prototype.toString().

    • 上面的那个函数不够纯粹,我们也可以实现一个更纯粹的函数,但是可以会不太符合题目的要求.
      我们可以这样做,先把函数的参数存储,然后再对这些参数做处理,一旦有了这个思路,我们就不难写出些面的代码:

      function add() {
          var args = Array.prototype.slice.call(arguments);
          var _that = this;
          return function() {
              var newArgs = Array.prototype.slice.call(arguments);
              var total = args.concat(newArgs);
              if(!arguments.length) {
                  var result = 1;
                  for(var i = 0; i < total.length; i++) {
                      result *= total[i];
                  }
                  return result;
              }
              else {
                  return add.apply(_that, total);
              }
          }
      }
      add(1)(2)(3)(); // 6
      add(1, 2, 3)(); // 6
      登入後複製
    • 当我们的需要兼容IE9之前版本的IE浏览器的话,我们可能需要写出一些兼容的方案 ,比如事件监听;一般情况下我们应该会这样写:

      var addEvent = function (el, type, fn, capture) {
              if (window.addEventListener) {
                  el.addEventListener(type, fn, capture);
              }
              else {
                  el.attachEvent('on' + type, fn);
              }
          };
      登入後複製

      这也写也是可以的,但是性能上会差一点,因为如果是在低版本的IE浏览器上每一次都会运行if()语句,产生了不必要的性能开销.
      我们也可以这样写:

      var addEvent = (function () {
              if (window.addEventListener) {
                  return function (el, type, fn, capture) {
                      el.addEventListener(type, fn, capture);
                  }
              }
              else {
                  return function (el, type, fn) {
                      var IEtype = 'on' + type;
                      el.attachEvent(IEtype, fn);
                  }
              }
          })();
      登入後複製

      这样就减少了不必要的开支,整个函数运行一次就可以了.

  • 延迟计算

    上面的那两个函数multiply()add()实际上就是延迟计算的例子.

  • 提前绑定好函数里面的某些参数,达到参数复用的效果,提高了适用性.

    我们的I 开胃菜部分的tomLikejerryLike其实就是属于这种的,绑定好函数里面的第一个参数,然后后面根据情况分别使用不同的函数.

  • 固定易变因素

    我们经常使用的函数的bind方法就是一个固定易变因素的很好的例子.

关于柯里化的性能

当然,使用柯里化意味着有一些额外的开销;这些开销一般涉及到这些方面,首先是关于函数参数的调用,操作arguments对象通常会比操作命名的参数要慢一点;
还有,在一些老的版本的浏览器中arguments.length的实现是很慢的;直接调用函数fn要比使用fn.apply()或者fn.call()要快一点;产生大量的嵌套
作用域还有闭包会带来一些性能还有速度的降低.但是,大多数的web应用的性能瓶颈时发生在操作DOM上的,所以上面的那些开销比起DOM操作的开销还是比较小的.

关于本章一些知识点的解释

  • 琐碎的知识点

    fn.length: 表示的是这个函数中参数的个数.

    arguments.callee: 指向的是当前运行的函数.calleearguments对象的属性。
    在该函数的函数体内,它可以指向当前正在执行的函数.当函数是匿名函数时,这是很有用的,比如没有名字的函数表达式(也被叫做"匿名函数").
    详细解释可以看这里arguments.callee.我们可以看一下下面的例子:

    function hello() {
        return function() {
            console.log('hello');
            if(!arguments.length) {
                console.log('from a anonymous function.');
                return arguments.callee;
            }
        }
    }
    
    hello()(1); // hello
    
    /*
     * hello
     * from a anonymous function.
     * hello
     * from a anonymous function.
     */
    hello()()();
    登入後複製

    fn.caller: 返回调用指定函数的函数.详细的解释可以看这里Function.caller,下面是示例代码:

    function hello() {
        console.log('hello');
        console.log(hello.caller);
    }
    
    function callHello(fn) {
        return fn();
    }
    
    callHello(hello); // hello [Function: callHello]
    登入後複製
  • 高阶函数(high-order function)

    高阶函数就是操作函数的函数,它接受一个或多个函数作为参数,并返回一个新的函数.
    我们来看一个例子,来帮助我们理解这个概念.就举一个我们高中经常遇到的场景,如下:

    f1(x, y) = x + y;
    f2(x) = x * x;
    f3 = f2(f3(x, y));
    登入後複製

    我们来实现f3函数,看看应该如何实现,具体的代码如下所示:

    function f1(x, y) {
        return x + y;
    }
    
    function f2(x) {
        return x * x;
    }
    
    function func3(func1, func2) {
        return function() {
            return func2.call(this, func1.apply(this, arguments));
        }
    }
    
    var f3 = func3(f1, f2);
    console.log(f3(2, 3)); // 25
    登入後複製

    我们通过函数func3将函数f1,f2结合到了一起,然后返回了一个新的函数f3;这个函数就是我们期望的那个函数.

  • 不完全函数(partial function)

    什么是不完全函数呢?所谓的不完全函数和我们上面所说的柯里化基本差不多;所谓的不完全函数,就是给你想要运行的那个函数绑定一个固定的参数值;
    然后后面的运行或者说传递参数都是在前面的基础上进行运行的.看下面的例子:

    // 一个将函数的arguments对象变成一个数组的方法
    function array(a, n) {
        return Array.prototype.slice.call(a, n || 0);
    }
    // 我们要运行的函数
    function showMsg(a, b, c){
        return a * (b - c);
    }
    function partialLeft(f) {
        var args = arguments;
        return function() {
            var a = array(args, 1);
            a = a.concat(array(arguments));
            console.log(a); // 打印实际传递到函数中的参数列表
            return f.apply(this, a);
        }
    }
    function partialRight(f) {
        var args = arguments;
        return function() {
            var a = array(arguments);
            a = a.concat(array(args, 1));
            console.log(a); // 打印实际传递到函数中的参数列表
            return f.apply(this, a);
        }
    }
    function partial(f) {
        var args = arguments;
        return function() {
            var a = array(args, 1);
            var i = 0; j = 0;
            for(; i < a.length; i++) {
                if(a[i] === undefined) {
                    a[i] = arguments[j++];
                }
            }
            a = a.concat(array(arguments, j));
            console.log(a); // 打印实际传递到函数中的参数列表
            return f.apply(this, a);
        }
    }
    partialLeft(showMsg, 1)(2, 3); // 实际参数列表: [1, 2, 3] 所以结果是 1 * (2 - 3) = -1
    partialRight(showMsg, 1)(2, 3); // 实际参数列表: [2, 3, 1] 所以结果是 2 * (3 - 1) = 4
    partial(showMsg, undefined, 1)(2, 3); // 实际参数列表: [2, 1, 3] 所以结果是 2 * (1 - 3) = -4
    登入後複製

一些你可能会喜欢的JS库

JavaScript的柯里化与JavaScript的函数式编程密不可分,下面列举了一些关于JavaScript函数式编程的库,大家可以看一下:

  • underscore
  • lodash
  • ramda
  • bacon.js
  • fn.js
  • functional-js

推荐教程:《JS教程

以上是帶你了解並掌握JavaScript函數的柯里化的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

如何使用WebSocket和JavaScript實現線上語音辨識系統 如何使用WebSocket和JavaScript實現線上語音辨識系統 Dec 17, 2023 pm 02:54 PM

如何使用WebSocket和JavaScript實現線上語音辨識系統引言:隨著科技的不斷發展,語音辨識技術已成為了人工智慧領域的重要組成部分。而基於WebSocket和JavaScript實現的線上語音辨識系統,具備了低延遲、即時性和跨平台的特點,成為了廣泛應用的解決方案。本文將介紹如何使用WebSocket和JavaScript來實現線上語音辨識系

WebSocket與JavaScript:實現即時監控系統的關鍵技術 WebSocket與JavaScript:實現即時監控系統的關鍵技術 Dec 17, 2023 pm 05:30 PM

WebSocket與JavaScript:實現即時監控系統的關鍵技術引言:隨著互聯網技術的快速發展,即時監控系統在各個領域中得到了廣泛的應用。而實現即時監控的關鍵技術之一就是WebSocket與JavaScript的結合使用。本文將介紹WebSocket與JavaScript在即時監控系統中的應用,並給出程式碼範例,詳細解釋其實作原理。一、WebSocket技

如何利用JavaScript和WebSocket實現即時線上點餐系統 如何利用JavaScript和WebSocket實現即時線上點餐系統 Dec 17, 2023 pm 12:09 PM

如何利用JavaScript和WebSocket實現即時線上點餐系統介紹:隨著網路的普及和技術的進步,越來越多的餐廳開始提供線上點餐服務。為了實現即時線上點餐系統,我們可以利用JavaScript和WebSocket技術。 WebSocket是一種基於TCP協定的全雙工通訊協議,可實現客戶端與伺服器的即時雙向通訊。在即時線上點餐系統中,當使用者選擇菜餚並下訂單

如何使用WebSocket和JavaScript實現線上預約系統 如何使用WebSocket和JavaScript實現線上預約系統 Dec 17, 2023 am 09:39 AM

如何使用WebSocket和JavaScript實現線上預約系統在當今數位化的時代,越來越多的業務和服務都需要提供線上預約功能。而實現一個高效、即時的線上預約系統是至關重要的。本文將介紹如何使用WebSocket和JavaScript來實作一個線上預約系統,並提供具體的程式碼範例。一、什麼是WebSocketWebSocket是一種在單一TCP連線上進行全雙工

JavaScript與WebSocket:打造高效率的即時天氣預報系統 JavaScript與WebSocket:打造高效率的即時天氣預報系統 Dec 17, 2023 pm 05:13 PM

JavaScript和WebSocket:打造高效的即時天氣預報系統引言:如今,天氣預報的準確性對於日常生活以及決策制定具有重要意義。隨著技術的發展,我們可以透過即時獲取天氣數據來提供更準確可靠的天氣預報。在本文中,我們將學習如何使用JavaScript和WebSocket技術,來建立一個高效的即時天氣預報系統。本文將透過具體的程式碼範例來展示實現的過程。 We

簡易JavaScript教學:取得HTTP狀態碼的方法 簡易JavaScript教學:取得HTTP狀態碼的方法 Jan 05, 2024 pm 06:08 PM

JavaScript教學:如何取得HTTP狀態碼,需要具體程式碼範例前言:在Web開發中,經常會涉及到與伺服器進行資料互動的場景。在與伺服器進行通訊時,我們經常需要取得傳回的HTTP狀態碼來判斷操作是否成功,並根據不同的狀態碼來進行對應的處理。本篇文章將教你如何使用JavaScript來取得HTTP狀態碼,並提供一些實用的程式碼範例。使用XMLHttpRequest

javascript如何使用insertBefore javascript如何使用insertBefore Nov 24, 2023 am 11:56 AM

用法:在JavaScript中,insertBefore()方法用於在DOM樹中插入一個新的節點。這個方法需要兩個參數:要插入的新節點和參考節點(即新節點將要插入的位置的節點)。

JavaScript與WebSocket:打造高效率的即時影像處理系統 JavaScript與WebSocket:打造高效率的即時影像處理系統 Dec 17, 2023 am 08:41 AM

JavaScript是一種廣泛應用於Web開發的程式語言,而WebSocket則是一種用於即時通訊的網路協定。結合二者的強大功能,我們可以打造一個高效率的即時影像處理系統。本文將介紹如何利用JavaScript和WebSocket來實作這個系統,並提供具體的程式碼範例。首先,我們需要明確指出即時影像處理系統的需求和目標。假設我們有一個攝影機設備,可以擷取即時的影像數

See all articles