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

  • 相关阅读:
    SwitchHosts——一个快速切换host的小工具
    Electron——node_modulesffi-napiuildReleaseffi_bindings.node is not a valid Win32 application.
    NPM——Electron failed to install correctly, please delete node_modules/electron and try
    alpakka-kafka(8)-kafka数据消费模式实现
    alpakka-kafka(7)-kafka应用案例,消费模式
    alpakka-kafka(6)-kafka应用案例,用户接口
    如何在Mac中卸载openjdk15
    我的一个切面
    定义一个通用的切面
    Oracle创建用户,创建表空间
  • 原文地址:https://www.cnblogs.com/wengxuesong/p/5577683.html
Copyright © 2011-2022 走看看