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:greeter
和runner
, 以及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
}
})
是不是直觀了很多, 已經非常接近與有關鍵字支持的語言了. 但相信不少人還是會疑惑, 為什麼在init
和update
中調用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(), 事實上還可以產生更大的可能性. 比如我們可以將伺服器端的邏輯傳遞到客戶端, 而不僅僅只是傳遞數據.