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(), 事实上还可以产生更大的可能性. 比如我们可以将服务器端的逻辑传递到客户端, 而不仅仅只是传递数据.

Function.prototype.toString 的使用技巧的更多相关文章

  1. esnext:Function.prototype.toString 终于有规范了

    从 ES1 到 ES5 的这 14 年时间里,Function.prototype.toString 的规范一字未变: An implementation-dependent representati ...

  2. Function.prototype.toString

    语法:fn.toString(indentation) 改方法返回当前函数源代码的字符串,而且还可对此字符串进行操作,比如: function num(){ }; var str = num.toSt ...

  3. Object.prototype和Function.prototype一些常用方法

    Object.prototype 方法: hasOwnProperty 概念:用来判断一个对象中的某一个属性是否是自己提供的(主要是判断属性是原型继承还是自己提供的) 语法:对象.hasOwnProp ...

  4. 利用Object.prototype.toString方法,实现比typeof更准确的type校验

    Object.prototype.toString方法返回对象的类型字符串,因此可以用来判断一个值的类型. 调用方法: Object.prototype.toString.call(value) 不同 ...

  5. Object.prototype 与 Function.prototype 与 instanceof 运算符

    方法: hasOwnProperty isPrototypeOf propertyIsEnumerable hasOwnProperty 该方法用来判断一个对象中的某一个属性是否是自己提供的( 住要用 ...

  6. instanceof, typeof, & Object.prototype.toString

    /** * * @authors Your Name (you@example.org) * @date 2016-11-18 09:31:23 * @version $Id$ */instanceo ...

  7. 判断一个变量的类型Object.prototype.toString.call

    var num = 1;alert(Object.prototype.toString.call(num)); // [object Number]var str = 'hudidit.com';al ...

  8. Object.prototype.toString.call() 区分对象类型

    判断一个对象的类型: /** * 判断对象是否为数组 * @param {Object} source 待判断的对象 * @return {Boolean} true|false */ Object. ...

  9. toStirng()与Object.prototype.toString.call()方法浅谈

    一.toString()是一个怎样的方法?它是能将某一个值转化为字符串的方法.然而它是如何将一个值从一种类型转化为字符串类型的呢? 通过下面几个例子,我们便能获得答案: 1.将boolean类型的值转 ...

随机推荐

  1. 用dubbo时遇到的一个序列化的坑

    首先,这是标题党,问题并不是出现在序列化上,这是报错的一部分: Caused by: com.alibaba.dubbo.remoting.RemotingException: Failed to s ...

  2. [linux]阿里云主机的免登陆安全SSH配置与思考

    公司服务器使用的第三方云端服务,即阿里云,而本地需要经常去登录到服务器做相应的配置工作,鉴于此,每次登录都要使用密码是比较烦躁的,本着极速思想,我们需要配置我们的免登陆. 一 理论概述 SSH介绍 S ...

  3. [Java 缓存] Java Cache之 DCache的简单应用.

    前言 上次总结了下本地缓存Guava Cache的简单应用, 这次来继续说下项目中使用的DCache的简单使用. 这里分为几部分进行总结, 1)DCache介绍; 2)DCache配置及使用; 3)使 ...

  4. 关于面试题 Array.indexof() 方法的实现及思考

    这是我在面试大公司时碰到的一个笔试题,当时自己云里雾里的胡写了一番,回头也曾思考过,最终没实现也就不了了之了. 昨天看到有网友说面试中也碰到过这个问题,我就重新思考了这个问题的实现方法. 对于想进大公 ...

  5. Coroutine in Java - Quasar Fiber实现--转载

    转自 https://segmentfault.com/a/1190000006079389?from=groupmessage&isappinstalled=0 简介 说到协程(Corout ...

  6. required

    required,这是HTML5中的一个新属性:这是HTML5中input元素中的一个属性. required译为必须的,在input元素中应用这一属性,就表示这一input元素节点是必填的或者必选的 ...

  7. 基于Vue2.0的单页面开发方案

    2016的最后一天,多多少少都应该总结一下这一年的得失,哪里做的好,哪里需要改进,记一笔,或许将来会用到呢. 毕业差不多半年了,一直是一个人在负责公司项目的前端开发与维护,当时公司希望前后端分离,提高 ...

  8. 【JQ基础】数组

    each() 方法规定为每个匹配元素规定运行的函数.

  9. SSIS 包部署 Package Store 后,在 IS 中可以执行,AGENT 执行却报错

    可以执行 SSIS Package ,证明用 SSIS Package 的账户是可以执行成功的.SQL Server Agent 默认指定账号是 Network Service. 那么可以尝试一下将 ...

  10. Linux程序包管理之rpm

    rpm简介 rpm( Red Hat Package Manager )是一个开放的软件包管理系统.它工作于Red Hat Linux及其他Linux系统,成为Linux中公认的软件包管理标准. rp ...