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: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(), 事实上还可以产生更大的可能性. 比如我们可以将服务器端的逻辑传递到客户端, 而不仅仅只是传递数据.
Function.prototype.toString 的使用技巧的更多相关文章
- esnext:Function.prototype.toString 终于有规范了
从 ES1 到 ES5 的这 14 年时间里,Function.prototype.toString 的规范一字未变: An implementation-dependent representati ...
- Function.prototype.toString
语法:fn.toString(indentation) 改方法返回当前函数源代码的字符串,而且还可对此字符串进行操作,比如: function num(){ }; var str = num.toSt ...
- Object.prototype和Function.prototype一些常用方法
Object.prototype 方法: hasOwnProperty 概念:用来判断一个对象中的某一个属性是否是自己提供的(主要是判断属性是原型继承还是自己提供的) 语法:对象.hasOwnProp ...
- 利用Object.prototype.toString方法,实现比typeof更准确的type校验
Object.prototype.toString方法返回对象的类型字符串,因此可以用来判断一个值的类型. 调用方法: Object.prototype.toString.call(value) 不同 ...
- Object.prototype 与 Function.prototype 与 instanceof 运算符
方法: hasOwnProperty isPrototypeOf propertyIsEnumerable hasOwnProperty 该方法用来判断一个对象中的某一个属性是否是自己提供的( 住要用 ...
- instanceof, typeof, & Object.prototype.toString
/** * * @authors Your Name (you@example.org) * @date 2016-11-18 09:31:23 * @version $Id$ */instanceo ...
- 判断一个变量的类型Object.prototype.toString.call
var num = 1;alert(Object.prototype.toString.call(num)); // [object Number]var str = 'hudidit.com';al ...
- Object.prototype.toString.call() 区分对象类型
判断一个对象的类型: /** * 判断对象是否为数组 * @param {Object} source 待判断的对象 * @return {Boolean} true|false */ Object. ...
- toStirng()与Object.prototype.toString.call()方法浅谈
一.toString()是一个怎样的方法?它是能将某一个值转化为字符串的方法.然而它是如何将一个值从一种类型转化为字符串类型的呢? 通过下面几个例子,我们便能获得答案: 1.将boolean类型的值转 ...
随机推荐
- Asp.net Core中使用Session
前言 2017年就这么悄无声息的开始了,2017年对我来说又是特别重要的一年. 元旦放假在家写了个Asp.net Core验证码登录, 做demo的过程中遇到两个小问题,第一是在Asp.net Cor ...
- ABP入门系列(2)——通过模板创建MAP版本项目
一.从官网创建模板项目 进入官网下载模板项目 依次按下图选择: 输入验证码开始下载 下载提示: 二.启动项目 使用VS2015打开项目,还原Nuget包: 设置以Web结尾的项目,设置为启动项目: 打 ...
- Connect() 2016 大会的主题 ---微软大法好
文章首发于微信公众号"dotnet跨平台",欢迎关注,可以扫页面左面的二维码. 今年 Connect 大会的主题是 Big possibilities. Bold technolo ...
- .NET Core RC2/RTM 明确了时间表
.NET Core 经过了将近2年的开发,去年12月份发布的RC1版本,明确来说那只是一个beta版本,自从RC1发布以来,看到github里的RC2分支,整个工具链都发生了很大的变化,大家都在焦急的 ...
- 采用MiniProfiler监控EF与.NET MVC项目(Entity Framework 延伸系列1)
前言 Entity Framework 延伸系列目录 今天来说说EF与MVC项目的性能检测和监控 首先,先介绍一下今天我们使用的工具吧. MiniProfiler~ 这个东西的介绍如下: MVC Mi ...
- Eclipse中启动tomcat报错java.lang.OutOfMemoryError: PermGen space的解决方法
有的项目引用了太多的jar包,或者反射生成了太多的类,异或有太多的常量池,就有可能会报java.lang.OutOfMemoryError: PermGen space的错误, 我们知道可以通过jvm ...
- Node.js:Buffer浅谈
Javascript在客户端对于unicode编码的数据操作支持非常友好,但是对二进制数据的处理就不尽人意.Node.js为了能够处理二进制数据或非unicode编码的数据,便设计了Buffer类,该 ...
- 破解SQLServer for Linux预览版的3.5GB内存限制 (UBUNTU篇)
在上一篇中我提到了如何破解RHEL上SQLServer的内存大小限制,但是Ubuntu上还有一道检查 这篇我将会讲解如何在3.5GB以下内存的Ubuntu中安装和运行SQLServer for Lin ...
- 缓存、队列(Memcached、redis、RabbitMQ)
本章内容: Memcached 简介.安装.使用 Python 操作 Memcached 天生支持集群 redis 简介.安装.使用.实例 Python 操作 Redis String.Hash.Li ...
- PHP设计模式(三)抽象工厂模式(Abstract Factory For PHP)
一.什么是抽象工厂模式 抽象工厂模式的用意为:给客户端提供一个接口,可以创建多个产品族中的产品对象 ,而且使用抽象工厂模式还要满足以下条件: 系统中有多个产品族,而系统一次只可能消费其中一族产品. 同 ...