博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Function.prototype.toString 的使用技巧
阅读量:4320 次
发布时间:2019-06-06

本文共 14391 字,大约阅读时间需要 47 分钟。

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 = `

{blog.title}

{blog.content}
`

这个等同于

var template = "
" + "

{blog.title}

" + "
{blog.content}
"+"
"

Duang~ function.toString又闪亮登场, 一解我们青黄不接时的尴尬.

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

{blog.title}

{blog.content}
*/})

将会输出下面这段字符串

{blog.title}

{blog.content}

因为在通过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;// otherSkinnedMesh.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 initskinnedmesh initmesh updateskinnedmesh 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帮助我们进行并行计算

在浏览器中, js的执行与UI更新是公用一个进程, 这导致它们会互相阻塞, 用户直接的感受就是, 在长时间的脚本执行中,界面会“卡住”.

特别在很多处理大列表的场景中,熟练的程序员会通过(setTimeout/setInterval/requestAnimationFrame)等方法来模拟任务分片,好为UI线程腾出时间, 这样用户的体验就是按钮可以点了,但总的完成时间其实是增加了

有没有一种一劳永逸的方法呢? webworker

即我们可以将耗时的计算任务放置在后台运行, 完成之后通过事件来通知主线程, 注意它会真正生成系统级别的线程,而不是模拟出来的。

事实上,worker分为专用worker和共享worker,我们只会涉及到前者

我们来个耗时的例子,第一个映入我脑帘的就是计算斐波那契数列, 足够简单但是足够耗时, 就它了。


结果:

对应的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.735msfibonacci-noworker: 5198.129ms

使用worker的版本速度会更高一些, 当然更关键的问题是 noworker版本阻塞的UI线程,使得button等控件都没有反应了.

使用function.toString实现单文件的Webworker运算

但是, 非worker版本有个好处就是逻辑定义都在一个文件里, 而不用分散计算逻辑到子文件, 有没有两全的方案呢?

答案是 使用function.toString() 和 URL.createObjectURL 方法来动态创建脚本文件内容.

我们对worker.html做以下调整


结果:

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 = `

{blog.title}

{blog.content}
`

这个等同于

var template = "
" + "

{blog.title}

" + "
{blog.content}
"+"
"

Duang~ function.toString又闪亮登场, 一解我们青黄不接时的尴尬.

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

{blog.title}

{blog.content}
*/})

将会输出下面这段字符串

{blog.title}

{blog.content}

因为在通过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;// otherSkinnedMesh.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 initskinnedmesh initmesh updateskinnedmesh 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帮助我们进行并行计算

在浏览器中, js的执行与UI更新是公用一个进程, 这导致它们会互相阻塞, 用户直接的感受就是, 在长时间的脚本执行中,界面会“卡住”.

特别在很多处理大列表的场景中,熟练的程序员会通过(setTimeout/setInterval/requestAnimationFrame)等方法来模拟任务分片,好为UI线程腾出时间, 这样用户的体验就是按钮可以点了,但总的完成时间其实是增加了

有没有一种一劳永逸的方法呢? webworker

即我们可以将耗时的计算任务放置在后台运行, 完成之后通过事件来通知主线程, 注意它会真正生成系统级别的线程,而不是模拟出来的。

事实上,worker分为专用worker和共享worker,我们只会涉及到前者

我们来个耗时的例子,第一个映入我脑帘的就是计算斐波那契数列, 足够简单但是足够耗时, 就它了。


结果:

对应的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.735msfibonacci-noworker: 5198.129ms

使用worker的版本速度会更高一些, 当然更关键的问题是 noworker版本阻塞的UI线程,使得button等控件都没有反应了.

使用function.toString实现单文件的Webworker运算

但是, 非worker版本有个好处就是逻辑定义都在一个文件里, 而不用分散计算逻辑到子文件, 有没有两全的方案呢?

答案是 使用function.toString() 和 URL.createObjectURL 方法来动态创建脚本文件内容.

我们对worker.html做以下调整


结果:

这一次,我们不再需要mytask.js了,因为这个文件内容其实已经通过 URL.createObjectURL 和 Blob创建出来了. 

总结

其实fn.toString()所有的能力都归结为它可以得到函数源码,配合new Function(), 事实上还可以产生更大的可能性. 比如我们可以将服务器端的逻辑传递到客户端, 而不仅仅只是传递数据.

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;  }

这一次,我们不再需要mytask.js了,因为这个文件内容其实已经通过 URL.createObjectURL 和 Blob创建出来了. 

总结

其实fn.toString()所有的能力都归结为它可以得到函数源码,配合new Function(), 事实上还可以产生更大的可能性. 比如我们可以将服务器端的逻辑传递到客户端, 而不仅仅只是传递数据.

转载于:https://www.cnblogs.com/libin-1/p/6250800.html

你可能感兴趣的文章
dell support
查看>>
转:Maven项目编译后classes文件中没有dao的xml文件以及没有resources中的配置文件的问题解决...
查看>>
MTK android 设置里 "关于手机" 信息参数修改
查看>>
单变量微积分笔记6——线性近似和二阶近似
查看>>
补几天前的读书笔记
查看>>
HDU 1829/POJ 2492 A Bug's Life
查看>>
CKplayer:视频推荐和分享插件设置
查看>>
CentOS系统将UTC时间修改为CST时间
查看>>
redis常见面试题
查看>>
导航控制器的出栈
查看>>
玩转CSS3,嗨翻WEB前端,CSS3伪类元素详解/深入浅出[原创][5+3时代]
查看>>
iOS 9音频应用播放音频之播放控制暂停停止前进后退的设置
查看>>
Delphi消息小记
查看>>
HNOI2016
查看>>
JVM介绍
查看>>
将PHP数组输出为HTML表格
查看>>
Java中的线程Thread方法之---suspend()和resume() 分类: ...
查看>>
经典排序算法回顾:选择排序,快速排序
查看>>
BZOJ2213 [Poi2011]Difference 【乱搞】
查看>>
c# 对加密的MP4文件进行解密
查看>>