[Effective JavaScript 笔记]第3章:使用函数--个人总结
前言
这一章把平时会用到,但不会深究的知识点,分开细化地讲解了。里面很多内容在高3等基础内容里,也有很多讲到。但由于本身书籍的篇幅较大,很容易忽视对应的小知识点。这章里的许多小提示都很有帮助,特别是在看对应内容的时候,把高3等工具书籍放在一边,边查边看收获很大。这章重点围绕函数的相关属性,方法,参数,关键字,命名,柯里化,高阶,闭包等内容作了各种提示。下面只是个人对于各条内容的一些总结,知识面有限,如有不对请大家一定指出。真心希望大家可以给点指导,个人写博客,感觉一直没人交流很没有动力的。
第18条:理解函数调用、方法调用及构造函数调用之间的不同
个人总结
这节里主要讲了函数在js中存在的3种形式,也是在不同的环境中的叫法不同而已。
函数调用
纯函数
就是一组输入产生一组输出,之间不对上下文环境中的其它变量产生副作用(这个词也是别处学到的,其实就是上下文中,有这个函数和没有这个函数对其它变量不会产生影响)。只是一个功能性的存在,类似于工具函数。
不纯的函数
对应的是对上下文环境会产生影响,并要去修改和访问上下文环境中变量的函数。这种函数也产生了很大的复杂性,对于没有块级作用域的js语言来说。这其中也就包含了一些对于闭包,作用域的技巧。对于初学者,这也会产生一些困扰。可参见之前《[Effective JavaScript 笔记] 第11条:熟练掌握闭包》。
方法调用
方法调用其实里面如果没有和this关键相关的操作,就和函数调用一样一样的。当包含this时,这个调用就相对复杂了,是方法就一定有对应的对象,这个对象就是this的指向值。难点主要是判断,什么时候this对应的对象是什么。这里也会牵扯到函数在运行时,各变量值的查找和访问的内容,也就是常听到的作用域链,而这个this也是在这个过程中才被确定的。
一组相关功能的函数放到一起,组成一个对象,这个对象只是一个简单的工具组合,里面的方法调用就可以等同于函数,对象只是用于组织而已,并不起到什么实质的作用,如Math对象,里面的方法都是一些数学函数。
构造函数
这个是开启js面向对象可能的一种应用形式,构造函数不同于函数调用和方法调用,它的调用过程需要一个关键字new,如果没有这个那就是函数调用,不会产生对象。使用new后一切都变了,new在调用构造函数,会产生一个新对象。调用几次产生几个,这些个对象就是函数中this的对应指向。这里先不说原型对象什么的,这个应该在下章会有。
提示
-
方法调用将被查找方法属性的对象作为调用对象
-
函数调用将全局对象(处于严格模式下则为undefined)作为其接收者。一般很少使用函数调用语法来调用方法
-
构造函数需要通过new运算符调用,并产生一个新的对象作为其接收者。
第19条:熟练掌握高阶函数
个人总结
高阶函数,就是对函数的一种多重嵌套,可以把函数体作为参数或返回值。再结合闭包的相关特性,实现各种有用的功能。
作为返回值
今天看到一个试题,实现如下语法的功能:
var a = add(2)(3)(4); //9
这个就是一个高阶函数的应用,
分析:add(2)会返回一个函数,
add(2)(3)也会返回一个函数,
最后add(2)(3)(4)返回一个数值。
实现:
function add(num1){ return function(num2){ return function(num3){ return num1+num2+num3; } } } add(2)(3)(4);//9
这个没有错的,可以完美解决问题。
优化:这里只讨论关于高阶函数的部分,对于更好的解决方案,可以实现无限这种调用,代码如下:
//方法一 function add(a) { var temp = function(b) { return add(a + b); } temp.valueOf = temp.toString = function() { return a; }; return temp; } add(2)(3)(4)(5);//14 //方法二、另看到一种很飘逸的写法(来自Gaubee): function add(num){ num += ~~add; add.num = num; return add; } add.valueOf = add.toString = function(){return add.num}; var a= add(3)(4)(5)(6); // 18 //方法二注释:其实就相当于,只不过对函数应用了自定义属性,用于存储值。 ;(function(){ var sum=0; function add(num){ sum+=num; return add; } add.valueOf=add.toString=function(){return sum;} window.add=add; })() var a= add(3)(4)(5)(6); // 18
以上结合了,类型的隐式转换,可以查看《[Effective JavaScript笔记]第3条:当心隐式的强制转换》
作为参数
函数作为参数的高阶运用,在js的日常编程中可以说随处可见。
在DOM编程中,使用事件的时候。
window.addEventListener('click',function(){},false);
在使用jquery的事件的时候
$(function(){ })
在使用动画函数的时候
$().animate({},function(){});
在使用数组方法的时候
var a=[12,24,56,7,68,9,1]; a.sort(function(a,b){ return a-b; })
上面的这些常用的场景,都是把函数作为参数的形式,传递给别一个函数。而这里的另一个函数,可以完成大部分的抽象工作,把常用的功能抽象出来为工具函数,把个性化的处理放到回调函数中。
提示
-
高阶函数是那些将函数作为参数或返回值的函数
-
熟悉掌握现有库中的高阶函数
-
学会发现可以被高阶函数所取代的常见的编码模式
第20条:使用call方法自定义接收者来调用方法
个人总结
这个call方法和apply方法,作用是把函数绑定到相应的对象,也就是接收者。简单明了的说,就是定义函数中this的指向问题。它们接收的第一个参数,即为要绑定的对象;它们主要区别在第二个参数,call是一个个参数,apply是一个参数组成的数组。这种方法的优点之处,对方法或函数的重用,比如借用已经在其它对象上实现的方法。
function a(name){ this.name=name; } var obj={ info:function(name,age){ a.call(this,name);//函数的复用 this.age=age; } } var obj2={} obj.info.call(obj2,'cedrusweng',30);//方法重用 obj2.name;//"cedrusweng" obj2.age;//30
提示
-
使用call方法自定义接收者来调用函数
-
使用call方法可以调用在给定的对象中不存在的方法
-
使用call方法定义高阶函数允许使用者给回调函数指定接收者
第21条:使用apply方法通过不同数量的参数调用函数
个人总结
apply方法,第一个参数是函数的绑定接收者,如果没有可以传入null。第二个参数是参数数组。
没有什么其它特别的使用方法,对应这个可以用call方法进行替换。
应用:给可变参数的函数传值
function applyFn(){ for(var i=0,len=arguments.length;i<len;i++){ //somecode } } applyFn.apply(null,[1,2,3,3,54,'6']);
提示
-
使用apply方法指定一个可计算的参数数组来调用可变参数的函数
-
使用apply方法的第一个参数给可变参数的方法提供一个接收者
第22条:使用arguments创建可变参数的函数
个人总结
arguments对象是一个类数组的对象。
类数组
就是有"0","1"等代表索引的键值,并含有length属性的对象。这里用类数组,意思是像数组但没有对应的数组方法,但可以通过转换复制出一个真正的数组。这里可以通过一些返回数组的方法,来对类数组进行复制。其中经常使用的代码如下(使用call方法)
var likeArr={ '0':'dddd', '1':10, a:1000, b:400, length:2 } var slice=[].slice; var actualArr=slice.call(likeArr); actualArr;//['dddd',10]
使用上面的方法,可以把arguments对象处理成真正的数组,这是一个副本,对arguments对象的修改并不会反应到这个画本上,所以可以防止一些arguments对象被修改造成的问题。而且可以使用数组对象提供的高阶函数方法。
提示
-
使用隐式的arguments对象实现可变参数的函数
-
考虑对可变参数的函数提供一个额外的固定元素的版本,从而使使用者无需借助apply方法。
第23条:永远不要修改arguments对象
个人总结
arguments对象中的参数,是和形参一一对应的,对于arguments对象的修改会影响到实参的值。最终,函数的实际功能完全无法预料。在严格模式下,对arguments对象进行修改会直接导致错误。这个可以通过上条讲的,使用apply方法把arguments对象转化成一个真正的数组副本。从而避免对象修改对实际功能产生影响。下面是一个函数的应用
//实际目的是去除arguments的前两个参数,结果是obj,method两个形参数的值也被修改 function callMethod(obj,method){ var shift=[].shift; shift.call(arguments); shift.call(arguments); return obj[method].apply(obj,arguments); } //复制arguments的一份副本,然后去除参数对象数组中的obj,method的值,实参并没有改变,只是改变了数组副本中的值 function callMethod(obj,method){ var args=[].slice.call(arguments); args.shift(); args.shift(); return obj[method].apply(obj,args); } //通过slice方法直接去除前两个值,简化版 function callMethod(obj,method){ var args=[].slice.call(arguments,2); return obj[method].apply(obj,args); }
提示
-
永远不要修改arguments对象
-
使用[].slice.call(arguments)将arugments对象复制到一个真正的数组中再进行修改
第24条:使用变量保存arguments的引用
个人总结
arguments对象,只是保存着当前函数的参数信息。如果出现多层函数,内层函数是无法访问外层的arguments对象的,因为在它内部的arguments对象是它自身的参数信息。如果想访问到外部的arguments对象的相关信息,就要借助于一个新的变量,这个变量只是用于对象的一个引用,方便内部函数的访问。如下代码
function outFn(){ function innerFn(){ return arguments[0];//希望访问外部函数的第一个参数 } return innerFn; } var fn=outFn(100); fn();//undefined function outFn(){ var outArgs=arguments; function innerFn(){ return outArgs[0];//希望访问外部函数的第一个参数 } return innerFn; } var fn=outFn(100); fn();//100
上面代码就是一个注意的例子,我平时很少直接使用arguments对象,都是给予对应的形参,方便配置说明。
提示
-
当引用arguments时当心函数嵌套层级
-
绑定一个明确作用域的引用到arguments变量,从而可以在嵌套的函数中引用它。
第25条:使用bind方法提取具有确定接收者的方法
个人总结
bind方法是ES5才提供给函数对象的。其目的是把函数中this的指向明确化,防止this不明不白产生错误。这个方法会返回一个绑定了接收者的新函数。
//bind方法示例 function a(){ this.name='lizi'; } var obj={}; var b=a.bind(obj); b(); obj.name;//'lizi' //不使用bind方法 function a(){ this.name='lizi2'; } var obj={}; var b=function(){return a.call(obj)}; b(); obj.name;//'lizi2'
我觉得这个只是一种简化,其实像上面这种也是可以实现的。当你使用的js环境支持bind方法,那首先使用bind方法。
对于不支持bind方法的,可以使用下面的版本兼容方法
if (!Function.prototype.bind) { Function.prototype.bind = function(oThis) { if (typeof this !== 'function') { throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); } var aArgs = [].slice.call(arguments, 1),//获取绑定对象oThis后的预置参数 fToBind = this,//原函数体 fNOP = function() {},//空构造函数 fBound = function() { return fToBind.apply(this instanceof fNOP? this: oThis, aArgs.concat([].slice.call(arguments)));//返回的新函数,接收的参数 }; if (this.prototype) { // Function.prototype doesn't have a prototype property fNOP.prototype = this.prototype; } fBound.prototype = new fNOP();//接上原型链 return fBound; }; }
提示
-
要注意,提取一个方法将方法的接收者绑定到该方法的对象上
-
当给高阶函数传递对象方法时,使用匿名函数在适当的接收者上调用该方法
-
使用bind方法创建绑定到适当接收者的函数
第26条:使用bind方法实现函数柯里化
个人总结
从上一条中bind方法的兼容实现就可以看出,其中可以先预置一些参数。只处理新函数传入的参数,产生的新函数,对参数进行了简化。这里看可以看出使用bind方法对函数进行柯里化很方便快捷。
1、可以直接看书上的例子
function simpleURL(protocol,domain,path){ return protocol+'://'+domain+'/'+path; } var paths=['wengxuesong/p/5545281.html#wxs-h-11','wengxuesong/p/5545281.html#wxs-h-10','wengxuesong/p/5545281.html#wxs-h-9']; var urls=paths.map(function(path){ return simpleURL('http','cnblogs.com',path); });
2、对函数进行柯里化
function simpleURL(protocol,domain,path){ return protocol+'://'+domain+'/'+path; } function pathURL(path){//对simpleURL进行了柯里化 return simpleURL('http','cnblogs.com',path); } var paths=['wengxuesong/p/5545281.html#wxs-h-11','wengxuesong/p/5545281.html#wxs-h-10','wengxuesong/p/5545281.html#wxs-h-9']; var urls=paths.map(pathURL);
3、使用bind方法进行柯里化
function simpleURL(protocol,domain,path){ return protocol+'://'+domain+'/'+path; } var paths=['wengxuesong/p/5545281.html#wxs-h-11','wengxuesong/p/5545281.html#wxs-h-10','wengxuesong/p/5545281.html#wxs-h-9']; var urls=paths.map(simpleURL.bind(null,'http','cnblogs.com'));
从上面代码可以看出,使用bind方法是这里处理最简洁的,对于2里的方法并没有什么错,使用原生支持的bind方法更快捷。(环境支持的情况下)
提示
-
使用bind方法实现函数柯里化,即创建一个固定需求参数子集的委托函数
-
传入null或undefined作为接收者的参数来实现函数的柯里化,从而忽略其接收者
第27条:使用闭包而不是字符串来封装代码
个人总结
使用字符串封装代码,这个可能也就是json数据的传输过程会用到。其它情况从来没有使用过,但使用闭包来对代码进行封装经常使用。首先,可以产生安全的作用域,可以避免命名冲突,可以对代码进行功能分块。字符串封装,相当于还需要解析的一个过程,在这个过程中,解析后的代码都是执行在全局作用域的,无法访问到闭包中的变量值。对字符串中的代码也不能有效的优化,编译无法一开始就对字符串中的代码进行优化。
提示
-
当将字符串传递给eval函数以执行它们的API时,绝不要在字符串中包含局部变量引用
-
接受函数调用的API优于使用eval函数执行字符串的API
第28条:不要信赖函数对象的toString方法
个人总结
1、toString方法功能很强大,函数对象的toString方法会返回函数体。
2、标准库中对于函数的toString方法没做任何规定,这意味着在不同的环境中可能产生不能的结果。
3、宿主环境提供的内置库提供的函数,无法使用toString获取函数体。
4、无法获取源代码中访问闭包中的值。
5、提取js函数源代码,可以借助于其它js解释器和处理库。
提示
-
当调用函数的toString方法时,并没有要求js引擎能够精确地获取到函数的源代码
-
由于不同的引擎下调用toString方法的结果可能不同,所以不要依赖于函数源代码的细节
-
toString方法的执行结果并不会暴露存储在闭包中的局部变量值
-
通常情况下,应该避免使用函数对象的toString方法
第29条:避免使用非标准的栈检查属性
个人总结
这一条里对应的也就是arguments对象的两个属性:callee,caller。函数的一个函数caller。其中arugments.caller和function.caller是一个意思。arguments.caller已经不能使用,大多数浏览器厂商实现了function.caller,用于指向其调用函数。当使用严格模式时,这条讲的都不能使用,都会报错。所以能不用就别用,看到别人的代码里有这个就要提高警惕。然后看看为什么要用。
提示
-
避免使用非标准的arguments.callee和arguments.caller属性,因为它们不具备良好的移植性
-
避免使用非标准的函数对象caller属性,因为在包含全部栈信息方面,它是不可靠的
总结
这已经是第3章了,共29条,每天都想写2条相关的提示信息。这些内容看起来真的可以一眼带过,但当真的想把记录下来,并试着推导作者的代码及结果产生过程。这个就不是一下就可以完成的,有些知识和查一下资料,一些得翻翻书,一些得查查百度,这个过程很不错。一个知识点带起了很多内容,这些内容使我对这个知识点了解的更加深入。写博客不是一个非要展示给别人看的过程,而是自我知识的一下梳理过程。从开始写到现在很少有人和我交流相关内容。可能是知识太过浅显,不过于我却受益无穷。这个过程还会继续,坚持一下,终归要有始有终吧。
翻译的文章,版权归原作者所有,只用于交流与学习的目的。
原创文章,版权归作者所有,非商业转载请注明出处,并保留原文的完整链接。