Function.prototype.toString 的使用技巧


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(), 事實上還可以產生更大的可能性. 比如我們可以將服務器端的邏輯傳遞到客戶端, 而不僅僅只是傳遞數據.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM