你的位置:首页 > Java教程

[Java教程][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();//undefinedfunction 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条相关的提示信息。这些内容看起来真的可以一眼带过,但当真的想把记录下来,并试着推导作者的代码及结果产生过程。这个就不是一下就可以完成的,有些知识和查一下资料,一些得翻翻书,一些得查查百度,这个过程很不错。一个知识点带起了很多内容,这些内容使我对这个知识点了解的更加深入。写博客不是一个非要展示给别人看的过程,而是自我知识的一下梳理过程。从开始写到现在很少有人和我交流相关内容。可能是知识太过浅显,不过于我却受益无穷。这个过程还会继续,坚持一下,终归要有始有终吧。