关于clone Function遇到的知识点
偶然在搜索如何实现js的拷贝时,搜到一篇文章:手写js之浅克隆,里面的克隆函数的方法让我耳目一新,其中有一些知识点是可以去稍稍了解一下的。
1 | const reg = /function\((.*)\).*\{([^\}]*)\}/; |
Function.prototype.toString
首先是function的toString我是没想过没用过的。这个方法可以输出函数源码的字符串,基本上就是与源码一模一样,
上面的方法是把方法源码通过toString来转成字符串,然后通过正则匹配的方式提取出参数、函数体。然后再用Function构造函数新建一个方法。
但是有一个问题,上面的正则只能匹配匿名普通函数比如:function(a, b) {return a + b}
,其他函数如箭头函数、具名函数就无法匹配了。各种不同的函数toString的效果可以看MDN。要实现完美可能得准备好几份正则去匹配不同类型的函数。
但是从mdn上看,此方法有兼容性问题,最好不要太依赖此方法
String.prototype.match
以前都是用RegExt.prototype.test来匹配,从来没有注意过这个方法。现在看来还是非常有用的。
此方法可以返回所有匹配上的字符串,以数组的形式返回。就如开头的例子
1 | const reg = /function\((.*)\).*\{([^\}]*)\}/; |
其中index、input、groups、length都是不可枚举的固定的值,可以不用管。
前面三个字符串就是正则匹配到的三种情况。其中第二个'a, b'
与'return a + b'
就是函数入参与函数体。
拿到这两个,就可以用Function构造函数去创建新函数了。
正则里的()
这里能匹配三个值,都是小括号()
的功劳。找到一个文档的解释是:标记一个子表达式的开始和结束位置
,说实话不太理解,没有找到更合适的解释。后面补充。
但从行为上看,小括号包裹的正则,匹配上的内容会添加到match返回值里。
1 | const reg1 = /function\((.*)\).*\{([^\}]*)\}/; |
上面例子,reg2比reg1少了个小括号,则match的结果少解析出入参。
reg3没有小括号,则只解析出整段文本,函数体也没有解析出来。
Function
Function构造函数返回一个方法,Function构造函数的使用方法可以用new 也可以不用new,完全一样。
Function构造函数传的参数以最后一个参数作为函数体,前面其他参数作为函数入参
1 | new Function(functionBody) |
- Function构造函数创建的函数不会创建当前环境的闭包,他们总是被创建于全局环境。因此在运行时他们只能访问全局变量和自己的局部变量,不能访问他们被Function构造函数创建时所在的作用域的变量。这样可能会带来问题。所以一般情况下不能这么使用
1 | const x = 1; |
- 如上例,引用到全局变量的例子,在node环境下运行是无效的。会报错:找不到变量x的ReferenceError。这是因为在Node中顶级作用域不是全局作用域,而x其实是在当前模块的作用域之中。
所以结论是:不要轻易使用Function构造函数。
for in与hasOwnProperty
有一个小知识点之前是没有注意的。
for in会遍历所有属性,包含原型链上的。再clone方法上,我们肯定不希望把原型链上的东西都clone过来,所以需要用hasOwnProperty来判断是否是对象自己的属性
1 |
|
实际情况下的克隆函数
既然上面的方法实现起来有问题,那实际情况是怎样的。
答案出乎意料实际上是不用处理的。。。lodash里遇到函数就直接返回了。这篇掘金里提到,克隆函数是没有实际场景的。两个对象使用同一个内存当中的函数是没有任何问题的。
weakmap解决循环引用
一种小情况,如果要拷贝的对象发生了循环引用,那么clone时就会栈溢出,需要特殊处理。处理方式很简单,用一个map来缓存以及遍历过的对象即可。
1 | var input = { |
具体怎么缓存就不写了,可以看看掘金的文章。
一个小知识点是,可以用weakmap来做一个小小的优化。
首先,map数据结构是一个键名必须是对象(未来可能还会加上Symbol)所以可以用map.set(o)来缓存对象。这时会有一个小小的问题。这时这个map会对对象有一个引用关系。这个引用关系如果不取消,是会影响垃圾回收。这时需要手动的删除map中的对象才会释放。
weakmap的签名与map一模一样,只是引用的对象不会产生引用,就不会影响引用对象的垃圾回收,也就减少一步手动删除缓存的操作了。
constructor
还有一个小小的知识点。再赋值不可遍历的类型的数据时,如:Date、RegExp、可以统一用obj.constructor来找到对应的构造函数。
1 |
|
那么克隆这些对象直接可以:
1 | const Ctor = obj.constructor; |
最后放上最终代码:
1 |
|
总结
以上代码肯定是还有很多没有考虑的,包括前面提到的循环引用。如果真的要实现一个完美的clone,是一件非常庞大的工作量。这里就不再展开讲了。