Function.prototype.toString 的使用技巧

来源:http://www.cnblogs.com/peakleo/archive/2017/01/04/6249657.html
-Advertisement-
Play Games

Function.prototype.toString這個原型方法可以幫助你獲得函數的源代碼, 比如: 輸出: 這個方法真是碉堡了…, 通過合適的正則, 我們可以從中提取出豐富的信息. 函數名 函數形參列表 函數源代碼 這些信息提供了javascript意想不到的靈活性, 我們來看看野生的例子吧. ...


Function.prototype.toString這個原型方法可以幫助你獲得函數的源代碼, 比如:

function hello ( msg ){
  console.log("hello")
}

console.log( hello.toString() );

輸出:

'function hello( msg ){ \
  console.log("hello") \
}'

這個方法真是碉堡了…, 通過合適的正則, 我們可以從中提取出豐富的信息.

  • 函數名
  • 函數形參列表
  • 函數源代碼

這些信息提供了javascript意想不到的靈活性, 我們來看看野生的例子吧.

提取AMD模塊定義里的依賴列表.

熟悉AMD或者被CMD科普過的同學應該知道,AMD中是這樣定義模塊的.

// 模塊c的定義
define( ['a', 'b'] ,function ( a, b ) {
  return {
    action: function(){
        return a.key + b.key;
    }
  }
});

當此模塊載入完成的同時define函數將被運行,傳入依賴列表的'b''a'指導模塊載入器需要先獲得他們的模塊定義, 並以參數形式註入到c模塊的factory函數. 所以明確聲明的['a', 'b']依賴列表至關重要,它指導模塊下一步的策略.

事實上,AMD規範中也定義了一種叫simplified commonjs wrapping的寫法, 可以以類commonjs的寫法來定義一個模塊.

define(function (require, exports, module) {
  var a = require('a'),
      b = require('b');

  exports.action = function () {
    return a.key + b.key;
  };
});

依賴變成了【使用註入到模塊的require函數引入】(如require('a')), 但是這就帶來了一個問題, 如何獲得此模塊的依賴列表?

答案當然是使用function.toString.

var rRequire = /\brequire\(["'](\w+)["']\)/g;

function getDependencies( fn ){
  var map = {};
  fn.toString().replace(rRequire, function(all, dep){
    map[dep] = 1;
  })
  return Object.keys(map);
}


getDependencies(function(require, exports){

    var a = require("a");
    var b = require("b");

    exports.c = require("a").key + b.key;
})

// => ["a", "b"]

輸出["a", "b"], 我們成功獲得依賴列表.

當然,這裡的正則是簡化版的,實際要處理的情況要複雜的多,比如你至少要過濾掉註釋里的信息.

多行字元串

關註ES6的同學應該知道, 在ES6中新增一個特性叫Template String, 除了支持插值可以獲得微弱的模板能力之外,它還有一個能力就是支持多行字元串的定義

這個在你定義多行模板字元串的時候非常有用, 可以避免不直觀的字元串拼接操作.

var template = `
<div>
   <h2>{blog.title}</h2>
   <div class='content'>{blog.content}</div>
</div>
`

這個等同於

var template = "<div>" +
   "<h2>{blog.title}</h2>" +
   "<div class='content'>{blog.content}</div>"+
"</div>"

Duang~ function.toString又閃亮登場, 一解我們青黃不接時的尷尬.

var rComment = /\/\*([\s\S]*?)\*\//;
// multiply string
function ms(fn){
  return fn.toString().match(rComment)[1]
};

ms(function(){/*
  <div>
    <h2>{blog.title}</h2>
    <div class='content'>{blog.content}</div>
  </div>
*/})

將會輸出下麵這段字元串

<div>
   <h2>{blog.title}</h2>
   <div class='content'>{blog.content}</div>
</div>

因為在通過fn.toString()的時候, 同時會保留函數中的註釋,但是註釋是不會被執行的,所以我們可以安全的在註釋中寫一些非js語句,就比如html.

基於形參約定的依賴註入

Angular里有個很大的噱頭就是它的依賴註入。

假設現在有如下一段Angularjs的代碼,它定義了2個factory:greeterrunner, 以及controllerMyController.

angular.module('myApp', [])
.factory('greeter', function() {
  return {
    greet: function(msg) { alert(msg); }
  }
})
.factory('runner', function() {
  return {
    run: function() {  }
  }
})
.controller('MyController', function($scope, greeter) {
  $scope.sayHello = function() {
    greeter.greet("Hello!");
  };
});

註意這個controller會在angular內部compile遇到節點上的某個指令比如<div ng-controller="MyController">時被調用.

現在問題來了, angular如何知道要傳入什麼參數呢? 比如上例中的controller其實是需要兩個參數的.

答案是基於形參名的推測

你可以先簡單理解為在每次調用factory等函數時, 對應的定義會緩存起來,例如

var cache = {
  greeter: function(){

  },
  runner: function(){

  }
}

既然如此,現在要做的就是獲得依賴, function.toString可以幫助我們從形參中獲得這些信息

var rArgs = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
function getParamNames( fn ){
  var argStr = fn.toString().match(rArgs)[1].trim();
  return argStr? argStr.split(/\s*,\s*/): [];
}

getParamNames(function( $scope, greeter ){})

// ["$scope", "greeter"]

輸出["$scope", "greeter"], 也就意味著我們獲得了依賴列表, 這樣我們就可以從cache中獲得對應的定義了.

繼承中的super()實現.

我們先來看下教科書版本的js繼承的實現

// 基類
function Mesh(){}

function SkinnedMesh( geometry, materials ){
  Mesh.call( this, geometry, materials )
  // blablabla...
}
// 避免new Mesh,帶來的兩次構造函數調用
SkinnedMesh.prototye = Object.create(Mesh.prototype)
SkinnedMesh.prototye.constructor = Mesh;


// other

SkinnedMesh.prototype.update = function(camera){
  Mesh.prototype.update.call(this, camera);
}

這種繼承方式足夠用,但是有幾個問題.

  • 調用父類函數真的足夠繁瑣
  • 一旦父類發生改變,所有對父類的調用都要改寫
  • 從編程邏輯上看, 這種類式繼承不夠直觀

如果是下麵這種方式呢?

var SkinnedMesh = Mesh.extend({
  // 履行構造函數職責
  init: function( geometry, materials ){
    // 由於super是關鍵字,修改為supr
    this.supr( geometry, materials ); // 調用父類同名方法
  },
  update: function( camera ){
    this.supr() // 調用Mesh.prototype.update
  }
})

是不是直觀了很多, 已經非常接近與有關鍵字支持的語言了. 但相信不少人還是會疑惑, 為什麼在initupdate中調用this.supr()為什麼可以準確定位到父類不同的方法?

其實,在extend的同時就已經在查找規則封裝好了, 讓我們將這個問題簡化為兩個對象間的繼承。

function extend(child, parent){
    for (var i in child ) if (child.hasOwnProperty(i) ){
      wrap(i, child, parent)
    }
    return child;
}

var rSupr = /\bsupr\b/
function wrap(name, child, parent){
  var method = child[name],
    superMethod = parent[name];

  // 我們通過fn.toString() 列印出方法體,並確保它使用的this.supr()
  if( rSupr.test( method.toString() ) && superMethod) {
    superMethod = superMethod.bind(child);
    child[name] = function(arguments){
        // 保證嵌套函數調用時候正確
        var preSuper = child.supr;
        child.supr = superMethod;
        method.apply(this, arguments);
        child.supr = preSuper
    }
  }
}

var mesh = {

  init: function(){
    console.log( "mesh init ");
  },
  update: function(){
    console.log(" mesh update");
  }
}

var skinnedmesh = extend({

  init: function(){
    this.supr()
    console.log( "skinnedmesh init ");
  },
  update: function(){
    this.supr()
    console.log(" skinnedmesh update");
  }
}, mesh)

skinnedmesh.init();
skinnedmesh.update();

輸出

mesh init
skinnedmesh init
mesh update
skinnedmesh update

其中, fn.toString()輸出方法源碼, 並通過正則判斷是否源碼中調用了supr(). 如果是就包一層函數用來動態的制定this.supr對應的方法。

是不是挺奇妙的構想?事實上由於方法的包裹是發生在extend時,在方法運行時,是沒有查找開銷的,所以很多框架都使用這個技巧來實現一個簡化的繼承模型.

在ES6規範中,已經引入了語言級別的class支持

class SkinnedMesh extends Mesh {
  constructor(geometry, materials) {
    super(geometry, materials);

    //...
  }
  update( camera ) {
    //...
    super.update( camera );
  }
}

註意構造函數里的super和update里的super()以及super.update()分別用來調用父類的構造函數和實例方法, 相當於

Mesh.call(this, geometry, materials)

Mesh.prototype.update.call(this)

序列化函數

什麼是函數序列化,即將函數序列話成字元串這種通用數據格式 這樣可以實現程式邏輯在不同的runtime之間傳遞

我們這裡點一個應用場景: 不依賴外部js文件時仍能使用webworker幫助我們進行並行計算

什麼是webworker

在瀏覽器中, js的執行與UI更新是公用一個進程, 這導致它們會互相阻塞, 用戶直接的感受就是, 在長時間的腳本執行中,界面會“卡住”.

特別在很多處理大列表的場景中,熟練的程式員會通過(setTimeout/setInterval/requestAnimationFrame)等方法來模擬任務分片,好為UI線程騰出時間, 這樣用戶的體驗就是按鈕可以點了,但總的完成時間其實是增加了

有沒有一種一勞永逸的方法呢? webworker

即我們可以將耗時的計算任務放置在後臺運行, 完成之後通過事件來通知主線程, 註意它會真正生成系統級別的線程,而不是模擬出來的。

事實上,worker分為專用worker和共用worker,我們只會涉及到前者

我們來個耗時的例子,第一個映入我腦簾的就是計算斐波那契數列, 足夠簡單但是足夠耗時, 就它了。

<div>
  <input type="text" id="num" placeholder="請輸入你要計算的數值" value=40>
  <button onclick="compute()">使用worker計算</button>
  <button onclick="compute(1)">不使用worker</button>
</div>
<hr/>
結果: <output id="result"></output>

<button>點我看看UI線程阻塞了嗎</button>

<script>
  var worker = new Worker('mytask.js');
  var vnode = document.getElementById("num");
  var rnode = document.getElementById('result');

  function compute(noWorker) {
    var value = parseInt(vnode.value || 0, 10) ;

    if(noWorker){
      console.time("fibonacci-noworker")
      rnode.textContent = fibonacci( value );
      return console.timeEnd("fibonacci-noworker")
    }

    console.time("fibonacci-worker")
    worker.postMessage( value );
  }


  worker.onmessage= function(e) {
    rnode.textContent = e.data;
    console.timeEnd("fibonacci-worker");
  }

  function fibonacci(n) {
    if(n < 2) return n;
    return fibonacci( n - 1 ) + fibonacci(n - 2);
  }

</script>

對應的mytask.js,如下

onmessage = function( ev ){
  self.postMessage( fibonacci( ev.data ) );
}

function fibonacci(n) {
  if(n < 2) return n;
  return fibonacci( n - 1 ) + fibonacci(n - 2);
}

mytask.js與worker.html的文件結構如下.

└── folder
  ├── mytask.js
  └── worker.html

打開worker.html, 分別點擊兩個按鈕, 你會發現控制台輸出結果是這樣的.

fibonacci-worker: 1299.735ms
fibonacci-noworker: 5198.129ms

使用worker的版本速度會更高一些, 當然更關鍵的問題是 noworker版本阻塞的UI線程,使得button等控制項都沒有反應了.

使用function.toString實現單文件的Webworker運算

但是, 非worker版本有個好處就是邏輯定義都在一個文件里, 而不用分散計算邏輯到子文件, 有沒有兩全的方案呢?

答案是 使用function.toString() 和 URL.createObjectURL 方法來動態創建腳本文件內容.

我們對worker.html做以下調整

<div>
  <input type="text" id="num" placeholder="請輸入你要計算的數值" value=40>
  <button onclick="compute()">使用內聯式的worker計算</button>
</div>
<hr/>
結果: <output id="result"></output>

<button>點我看看UI線程阻塞了嗎</button>

<script>
  function workerify(fn) {
    var worker = new Worker(
        URL.createObjectURL(new Blob([
           'self.onmessage = ' + fn.toString()], {
           type: 'application/javascript'
        })
    ));
    return worker
  }

  function compute(noWorker) {
    var value = parseInt(vnode.value || 0, 10) ;
    worker.postMessage( value );
  }

  var worker = workerify(function(e){
    function fibonacci(n) {
      if(n < 2) return n;
      return fibonacci( n - 1 ) + fibonacci(n - 2);
    }
    return self.postMessage( fibonacci(e.data) )
  })


  var vnode = document.getElementById("num");
  var rnode = document.getElementById('result');

  worker.onmessage = function(e){
    rnode.textContent = e.data;
  }

</script>

這一次,我們不再需要mytask.js了,因為這個文件內容其實已經通過 URL.createObjectURL 和 Blob創建出來了.

總結

其實fn.toString()所有的能力都歸結為它可以得到函數源碼,配合new Function(), 事實上還可以產生更大的可能性. 比如我們可以將伺服器端的邏輯傳遞到客戶端, 而不僅僅只是傳遞數據.


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 封裝就是將相關的方法或者屬性抽象成為一個對象。 封裝的意義: 當一個類的屬性類型為集合,或者方法返回類型為集合時,如果符合以下條件,我們就可以考慮將集合進行封裝: 返回的數據僅用於展示 當集合的Add,Remove方法包含其它業務邏輯 向類的調用者隱藏類中的完整集合有如下幾個好處: 保證返回的集合數 ...
  • 在上一篇博文結尾中,提到了存在的問題,那麼我們通過策略模式與簡單工廠結合的方式來解決上篇結尾中提到的問題。 方法很簡單,我們將CashContext簡單的改造一下即可 哈哈,是不是很像一個工廠? 那麼,客戶端調用起來就非常非常簡單了。 嗯,是不是很簡潔明瞭? 很多剛開始接觸設計模式的小伙伴有這樣一個 ...
  • 公司這幾天在準備新版本的上線,今天才忙裡偷閑來寫這篇博客。接著上一篇的“H5坦克大戰之【玩家控制坦克移動2】”(http://www.cnblogs.com/zhouhuan/p/H5_tankgame3.html),這一篇來寫怎麼建造敵人的坦克。 我的想法是,基於可擴展性和性能等方面的考慮,用構造 ...
  • 前言:家裡的樹莓派吃灰很久,於是拿出來做個室內溫度展示也不錯。 板子是model b型。 使用Python開發,web框架是flask,溫度感測器是ds18b20 1 硬體連接 ds18b20的vcc連接樹莓派的vcc , gnd連接gnd,DS連接GPIO4 2 ssh登錄樹莓派查看ds18b20 ...
  • 前段時間在使用Compass時遇到了其為難處理的一個坑,現記錄到博客希望能幫助到各位。 一、問題: 利用Koala或者是gulp編譯提示如下,截圖為koala編譯提示錯誤: 二、解決辦法 從問題截圖上看提示是找不到.sass-cache文件夾下麵的一個文件夾(父級目錄名稱中帶有特殊字元很容易重現), ...
  • 我們都知道在javascript里是沒有塊級作用域的,而ES6添加了塊級作用域,塊級作用域能帶來什麼好處呢?為什麼會添加這個功能呢?那就得瞭解ES5沒有塊級作用域時出現了哪些問題。 ES5在沒有塊級作用域的情況下出現的問題: 一。在if或者for迴圈中聲明的變數會泄露成全局變數 二。內層變數可能會覆 ...
  • 實例 讓第二個元素收縮到其他元素的三分之一: 效果預覽 div:nth-of-type(2){flex-shrink:3;} 瀏覽器支持 表格中的數字表示支持該屬性的第一個瀏覽器的版本號。 緊跟在 -webkit-, -ms- 或 -moz- 後的數字為支持該首碼屬性的第一個版本。 屬性 flex- ...
  • 要求: 確保字元串的每個單詞首字母都大寫,其餘部分小寫。 這裡我自己寫了兩種方法,或者說是一種方法,另一個是該方法的變種。 第一種: 第一種方法我認為比較好理解一點。 第二種(這是基於第一種方法上的改動): 第二種方法減少了轉換對象,原理還是一樣的。 兩種方法的結果都是: ps:如有不足或錯誤請指出 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...