Function.prototype.toString这个原型方法可以帮助你获得函数的源代码, 比如:

  1. function hello ( msg ){
  2. console.log("hello")
  3. }
  4.  
  5. console.log( hello.toString() );

输出:

  1. 'function hello( msg ){ \
  2. console.log("hello") \
  3. }'

这个方法真是碉堡了…, 通过合适的正则, 我们可以从中提取出丰富的信息.

  • 函数名
  • 函数形参列表
  • 函数源代码

这些信息提供了javascript意想不到的灵活性, 我们来看看野生的例子吧.

提取AMD模块定义里的依赖列表.

熟悉AMD或者被CMD科普过的同学应该知道,AMD中是这样定义模块的.

  1. // 模块c的定义
  2. define( ['a', 'b'] ,function ( a, b ) {
  3. return {
  4. action: function(){
  5. return a.key + b.key;
  6. }
  7. }
  8. });

当此模块加载完成的同时define函数将被运行,传入依赖列表的'b''a'指导模块加载器需要先获得他们的模块定义, 并以参数形式注入到c模块的factory函数. 所以明确声明的['a', 'b']依赖列表至关重要,它指导模块下一步的策略.

事实上,AMD规范中也定义了一种叫simplified commonjs wrapping的写法, 可以以类commonjs的写法来定义一个模块.

  1. define(function (require, exports, module) {
  2. var a = require('a'),
  3. b = require('b');
  4.  
  5. exports.action = function () {
  6. return a.key + b.key;
  7. };
  8. });

依赖变成了【使用注入到模块的require函数引入】(如require('a')), 但是这就带来了一个问题, 如何获得此模块的依赖列表?

答案当然是使用function.toString.

  1. var rRequire = /\brequire\(["'](\w+)["']\)/g;
  2.  
  3. function getDependencies( fn ){
  4. var map = {};
  5. fn.toString().replace(rRequire, function(all, dep){
  6. map[dep] = 1;
  7. })
  8. return Object.keys(map);
  9. }
  10.  
  11. getDependencies(function(require, exports){
  12.  
  13. var a = require("a");
  14. var b = require("b");
  15.  
  16. exports.c = require("a").key + b.key;
  17. })
  18.  
  19. // => ["a", "b"]

输出["a", "b"], 我们成功获得依赖列表.

当然,这里的正则是简化版的,实际要处理的情况要复杂的多,比如你至少要过滤掉注释里的信息.

多行字符串

关注ES6的同学应该知道, 在ES6中新增一个特性叫Template String, 除了支持插值可以获得微弱的模板能力之外,它还有一个能力就是支持多行字符串的定义

这个在你定义多行模板字符串的时候非常有用, 可以避免不直观的字符串拼接操作.

  1. var template = `
  2. <div>
  3. <h2>{blog.title}</h2>
  4. <div class='content'>{blog.content}</div>
  5. </div>
  6. `

这个等同于

  1. var template = "<div>" +
  2. "<h2>{blog.title}</h2>" +
  3. "<div class='content'>{blog.content}</div>"+
  4. "</div>"

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

  1. var rComment = /\/\*([\s\S]*?)\*\//;
  2. // multiply string
  3. function ms(fn){
  4. return fn.toString().match(rComment)[1]
  5. };
  6.  
  7. ms(function(){/*
  8. <div>
  9. <h2>{blog.title}</h2>
  10. <div class='content'>{blog.content}</div>
  11. </div>
  12. */})

将会输出下面这段字符串

  1. <div>
  2. <h2>{blog.title}</h2>
  3. <div class='content'>{blog.content}</div>
  4. </div>

因为在通过fn.toString()的时候, 同时会保留函数中的注释,但是注释是不会被执行的,所以我们可以安全的在注释中写一些非js语句,就比如html.

基于形参约定的依赖注入

Angular里有个很大的噱头就是它的依赖注入。

假设现在有如下一段Angularjs的代码,它定义了2个factory:greeterrunner, 以及controllerMyController.

  1. angular.module('myApp', [])
  2. .factory('greeter', function() {
  3. return {
  4. greet: function(msg) { alert(msg); }
  5. }
  6. })
  7. .factory('runner', function() {
  8. return {
  9. run: function() { }
  10. }
  11. })
  12. .controller('MyController', function($scope, greeter) {
  13. $scope.sayHello = function() {
  14. greeter.greet("Hello!");
  15. };
  16. });

注意这个controller会在angular内部compile遇到节点上的某个指令比如<div ng-controller="MyController">时被调用.

现在问题来了, angular如何知道要传入什么参数呢? 比如上例中的controller其实是需要两个参数的.

答案是基于形参名的推测

你可以先简单理解为在每次调用factory等函数时, 对应的定义会缓存起来,例如

  1. var cache = {
  2. greeter: function(){
  3.  
  4. },
  5. runner: function(){
  6.  
  7. }
  8. }

既然如此,现在要做的就是获得依赖, function.toString可以帮助我们从形参中获得这些信息

  1. var rArgs = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
  2. function getParamNames( fn ){
  3. var argStr = fn.toString().match(rArgs)[1].trim();
  4. return argStr? argStr.split(/\s*,\s*/): [];
  5. }
  6.  
  7. getParamNames(function( $scope, greeter ){})
  8.  
  9. // ["$scope", "greeter"]

输出["$scope", "greeter"], 也就意味着我们获得了依赖列表, 这样我们就可以从cache中获得对应的定义了.

继承中的super()实现.

我们先来看下教科书版本的js继承的实现

  1. // 基类
  2. function Mesh(){}
  3.  
  4. function SkinnedMesh( geometry, materials ){
  5. Mesh.call( this, geometry, materials )
  6. // blablabla...
  7. }
  8. // 避免new Mesh,带来的两次构造函数调用
  9. SkinnedMesh.prototye = Object.create(Mesh.prototype)
  10. SkinnedMesh.prototye.constructor = Mesh;
  11.  
  12. // other
  13.  
  14. SkinnedMesh.prototype.update = function(camera){
  15. Mesh.prototype.update.call(this, camera);
  16. }

这种继承方式足够用,但是有几个问题.

  • 调用父类函数真的足够繁琐
  • 一旦父类发生改变,所有对父类的调用都要改写
  • 从编程逻辑上看, 这种类式继承不够直观

如果是下面这种方式呢?

  1. var SkinnedMesh = Mesh.extend({
  2. // 履行构造函数职责
  3. init: function( geometry, materials ){
  4. // 由于super是关键字,修改为supr
  5. this.supr( geometry, materials ); // 调用父类同名方法
  6. },
  7. update: function( camera ){
  8. this.supr() // 调用Mesh.prototype.update
  9. }
  10. })

是不是直观了很多, 已经非常接近与有关键字支持的语言了. 但相信不少人还是会疑惑, 为什么在initupdate中调用this.supr()为什么可以准确定位到父类不同的方法?

其实,在extend的同时就已经在查找规则封装好了, 让我们将这个问题简化为两个对象间的继承。

  1. function extend(child, parent){
  2. for (var i in child ) if (child.hasOwnProperty(i) ){
  3. wrap(i, child, parent)
  4. }
  5. return child;
  6. }
  7.  
  8. var rSupr = /\bsupr\b/
  9. function wrap(name, child, parent){
  10. var method = child[name],
  11. superMethod = parent[name];
  12.  
  13. // 我们通过fn.toString() 打印出方法体,并确保它使用的this.supr()
  14. if( rSupr.test( method.toString() ) && superMethod) {
  15. superMethod = superMethod.bind(child);
  16. child[name] = function(arguments){
  17. // 保证嵌套函数调用时候正确
  18. var preSuper = child.supr;
  19. child.supr = superMethod;
  20. method.apply(this, arguments);
  21. child.supr = preSuper
  22. }
  23. }
  24. }
  25.  
  26. var mesh = {
  27.  
  28. init: function(){
  29. console.log( "mesh init ");
  30. },
  31. update: function(){
  32. console.log(" mesh update");
  33. }
  34. }
  35.  
  36. var skinnedmesh = extend({
  37.  
  38. init: function(){
  39. this.supr()
  40. console.log( "skinnedmesh init ");
  41. },
  42. update: function(){
  43. this.supr()
  44. console.log(" skinnedmesh update");
  45. }
  46. }, mesh)
  47.  
  48. skinnedmesh.init();
  49. skinnedmesh.update();

输出

  1. mesh init
  2. skinnedmesh init
  3. mesh update
  4. skinnedmesh update

其中, fn.toString()输出方法源码, 并通过正则判断是否源码中调用了supr(). 如果是就包一层函数用来动态的制定this.supr对应的方法。

是不是挺奇妙的构想?事实上由于方法的包裹是发生在extend时,在方法运行时,是没有查找开销的,所以很多框架都使用这个技巧来实现一个简化的继承模型.

在ES6规范中,已经引入了语言级别的class支持

  1. class SkinnedMesh extends Mesh {
  2. constructor(geometry, materials) {
  3. super(geometry, materials);
  4.  
  5. //...
  6. }
  7. update( camera ) {
  8. //...
  9. super.update( camera );
  10. }
  11. }

注意构造函数里的super和update里的super()以及super.update()分别用来调用父类的构造函数和实例方法, 相当于

  1. Mesh.call(this, geometry, materials)
  2.  
  3. Mesh.prototype.update.call(this)

序列化函数

什么是函数序列化,即将函数序列话成字符串这种通用数据格式 这样可以实现程序逻辑在不同的runtime之间传递

我们这里点一个应用场景: 不依赖外部js文件时仍能使用webworker帮助我们进行并行计算

什么是webworker

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

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

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

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

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

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

  1. <div>
  2. <input type="text" id="num" placeholder="请输入你要计算的数值" value=40>
  3. <button onclick="compute()">使用worker计算</button>
  4. <button onclick="compute(1)">不使用worker</button>
  5. </div>
  6. <hr/>
  7. 结果: <output id="result"></output>
  8.  
  9. <button>点我看看UI线程阻塞了吗</button>
  10.  
  11. <script>
  12. var worker = new Worker('mytask.js');
  13. var vnode = document.getElementById("num");
  14. var rnode = document.getElementById('result');
  15.  
  16. function compute(noWorker) {
  17. var value = parseInt(vnode.value || 0, 10) ;
  18.  
  19. if(noWorker){
  20. console.time("fibonacci-noworker")
  21. rnode.textContent = fibonacci( value );
  22. return console.timeEnd("fibonacci-noworker")
  23. }
  24.  
  25. console.time("fibonacci-worker")
  26. worker.postMessage( value );
  27. }
  28.  
  29. worker.onmessage= function(e) {
  30. rnode.textContent = e.data;
  31. console.timeEnd("fibonacci-worker");
  32. }
  33.  
  34. function fibonacci(n) {
  35. if(n < 2) return n;
  36. return fibonacci( n - 1 ) + fibonacci(n - 2);
  37. }
  38.  
  39. </script>

对应的mytask.js,如下

  1. onmessage = function( ev ){
  2. self.postMessage( fibonacci( ev.data ) );
  3. }
  4.  
  5. function fibonacci(n) {
  6. if(n < 2) return n;
  7. return fibonacci( n - 1 ) + fibonacci(n - 2);
  8. }

mytask.js与worker.html的文件结构如下.

  1. └── folder
  2. ├── mytask.js
  3. └── worker.html

打开worker.html, 分别点击两个按钮, 你会发现控制台输出结果是这样的.

  1. fibonacci-worker: 1299.735ms
  2. fibonacci-noworker: 5198.129ms

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

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

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

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

我们对worker.html做以下调整

  1. <div>
  2. <input type="text" id="num" placeholder="请输入你要计算的数值" value=40>
  3. <button onclick="compute()">使用内联式的worker计算</button>
  4. </div>
  5. <hr/>
  6. 结果: <output id="result"></output>
  7.  
  8. <button>点我看看UI线程阻塞了吗</button>
  9.  
  10. <script>
  11. function workerify(fn) {
  12. var worker = new Worker(
  13. URL.createObjectURL(new Blob([
  14. 'self.onmessage = ' + fn.toString()], {
  15. type: 'application/javascript'
  16. })
  17. ));
  18. return worker
  19. }
  20.  
  21. function compute(noWorker) {
  22. var value = parseInt(vnode.value || 0, 10) ;
  23. worker.postMessage( value );
  24. }
  25.  
  26. var worker = workerify(function(e){
  27. function fibonacci(n) {
  28. if(n < 2) return n;
  29. return fibonacci( n - 1 ) + fibonacci(n - 2);
  30. }
  31. return self.postMessage( fibonacci(e.data) )
  32. })
  33.  
  34. var vnode = document.getElementById("num");
  35. var rnode = document.getElementById('result');
  36.  
  37. worker.onmessage = function(e){
  38. rnode.textContent = e.data;
  39. }
  40.  
  41. </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. 【原创分享·支付宝支付】HBuilder打包APP调用支付宝客户端支付

    前言 最近有点空余时间,所以,就研究了一下APP支付.前面很早就搞完APP的微信支付了,但是由于时间上和应用上的情况,支付宝一直没空去研究.然后等我空了的时候,发现支付宝居然升级了支付逻辑,虽然目前还 ...

  2. 在Ubuntu下搭建ASP.NET 5开发环境

    在Ubuntu下搭建ASP.NET 5开发环境 0x00 写在前面的废话 年底这段时间实在太忙了,各种事情都凑在这个时候,没时间去学习自己感兴趣的东西,所以博客也好就没写了.最近工作上有个小功能要做成 ...

  3. SQLServer事务同步下如何收缩日志

    事务同步是SQLServer做读写分离的一种常用的方式. 随着业务数据的不断增长,数据库积攒了大量的日志,为了腾出硬盘空间,需要对数据库日志进行清理 订阅数据库的日志清理 因为订阅数据库所有的数据都来 ...

  4. MVC Core 网站开发(Ninesky) 1、创建项目

    又要开一个新项目了!说来惭愧,以前的东西每次都没写完,不是不想写完,主要是我每次看到新技术出来我都想尝试一下,看到.Net Core 手又痒了,开始学MVC Core. MVC Core最吸引我的有三 ...

  5. netty5 HTTP协议栈浅析与实践

      一.说在前面的话 前段时间,工作上需要做一个针对视频质量的统计分析系统,各端(PC端.移动端和 WEB端)将视频质量数据放在一个 HTTP 请求中上报到服务器,服务器对数据进行解析.分拣后从不同的 ...

  6. PHP之使用网络函数和协议函数

    使用其他Web站点的数据 <html> <head> <title> Stock Quote From NASDAQ </title> </hea ...

  7. ubuntu 下安装scrapy

    1.把Scrapy签名的GPG密钥添加到APT的钥匙环中: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 6272 ...

  8. CSS中强悍的相对单位之em(em-and-elastic-layouts)学习小记

    使用相对单位em注意点 1.浏览器默认字体是16px,即1em = 16px,根元素设置如下 html{ font-size: 100%; /* WinIE text resize correctio ...

  9. MongoDB备份(mongodump)和恢复(mongorestore)

    MongoDB提供了备份和恢复的功能,分别是MongoDB下载目录下的mongodump.exe和mongorestore.exe文件 1.备份数据使用下面的命令: >mongodump -h ...

  10. linux下 lvm 磁盘扩容

    打算给系统装一个oracle,发现磁盘空间不足.在安装系统的时候我选择的是自动分区,系统就会自动以LVM的方式分区.为了保证系统后期的可用性,建议所有新系统安装都采用LVM,之后生产上的设备我也打算这 ...