• 热门专题

JavaScript学习总结(二)闭包 IIFE apply 函数与对象

作者:张果  发布日期:2016-12-22 20:36:22
Tag标签:闭包  函数  对象  
  • 一、闭包(Closure)

    1.1、闭包相关的问题

    请在页面中放10个div,每个div中放入字母a-j,当点击每一个div时显示索引号,如第1个div显示0,第10个显示9;方法:找到所有的div,for循环绑定事件。

    示例代码:

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset='UTF-8'>
            <title>闭包</title>
            <style type='text/css'>
                div {
                    width: 100px;
                    height: 100px;
                    background: lightgreen;
                    float: left;
                    margin: 20px;
                    font: 30px/100px 'microsoft yahei';
                    text-align: center;
                }
            </style>
        </head>
        <body>
            <div>a</div>
            <div>b</div>
            <div>c</div>
            <div>d</div>
            <div>e</div>
            <div>f</div>
            <div>g</div>
            <div>h</div>
            <div>i</div>
            <div>j</div>
            <script type='text/javascript'>
                var divs=document.getElementsByTagName('div');
                for (var i=0;i<divs.length;i++) {
                    divs[i].onclick=function(){
                        alert(i);
                    }
                }
            </script>
        </body>
    </html>

    运行结果:

    因为点击事件的函数内部使用外部的变量i一直在变化,当我们指定click事件时并没有保存i的副本,这样做也是为了提高性能,但达不到我们的目的,我们要让他执行的上下文保存i的副本,这种机制就是闭包。

    修改后的代码:

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset='UTF-8'>
            <title>闭包</title>
            <style type='text/css'>
                div {
                    width: 100px;
                    height: 100px;
                    background: lightgreen;
                    float: left;
                    margin: 20px;
                    font: 30px/100px 'microsoft yahei';
                    text-align: center;
                }
            </style>
        </head>
        <body>
            <div>a</div>
            <div>b</div>
            <div>c</div>
            <div>d</div>
            <div>e</div>
            <div>f</div>
            <div>g</div>
            <div>h</div>
            <div>i</div>
            <div>j</div>
            <script type='text/javascript'>
                var divs=document.getElementsByTagName('div');
                for (var i=0;i<divs.length;i++) {
                    divs[i].onclick=(function(n){
                        return function(){
                            alert(n);    
                        }
                    })(i);
                }
            </script>
        </body>
    </html>

    运行结果:

    n是外部函数的值,但是内部函数(点击事件)需要使用,返回函数前的n被临时驻留在内存中给点击事件使用,简单说就是函数的执行上下文被保存起来,i生成了多个副本。

    1.2、理解闭包

    闭包概念:当一个内部函数被调用,就会形成闭包,闭包就是能够读取其他函数内部变量的函数,定义在一个函数内部的函,创建一个闭包环境,让返回的这个子程序抓住i,以便在后续执行时可以保持对这个i的引用。内部函数比外部函数有更长的生命周期;函数可以访问它被创建时所处的上下文环境。
     

    Javascript语言特有的'链式作用域'结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量

    二、对象

    对象就是“键/值”对的集合并拥有一个连接到原型(prototype)对隐藏连接。

    2.1、对象常量(字面量)

    一个对象字面量就是包含在一对花括号中的零个或多个“键/值”对。对象字面量可以出现在任何允许表达式出现的地方。

    对象的定义:

            //空对象
            var obj1={};
            
            //对象中的属性
            var obj2={name:'foo',age:19};
            var obj3={'nick name':'dog'};
            
            //对象中的方法
            var obj4={
                price:99,
                inc:function(){
                    this.price+=1;
                }
            }

    对象中可包含的内容:

    对象常量可以出现在任何允许表达式出现的地方,对象、数组、函数可以相互间嵌套,形式可以多种多样。对象的值可以是:数组,函数,对象,基本数据类型等。

                //对象中可包含的内容
                var obj5 = [{
                    name: 'jack'
                }, {
                    name: 'lucy',  //常量
                    hobby:['读书','上网','代码'],  //数组
                    friend:{name:'mark',height:198,friend:{}},  //对象
                    show:function(){  //函数
                        console.log('大家好,我是'+this.name);
                    }
                }];
                //对象中的this是动态的,指向的是:调用者
                obj5[1].show();

    输出:大家好,我是lucy

    2.2、取值

    方法一:直接使用点号运算

                //3取值
                var obj6={'nick name':'pig',realname:'Rose'};
                console.log(obj6.realname);
                //console.log(obj6.nick name);  错误

    方法二:使用索引器,当对象中的key有空格是

                //3取值
                var obj6={'nick name':'pig',realname:'Rose'};
                
                console.log(obj6['realname']);
                console.log(obj6['nick name']);

    2.3、枚举(遍历)

    方法一:

                var obj7={weight:'55Kg','nick name':'pig',realname:'Rose'};
                
                for (var key in obj7) {
                    console.log(key+':'+obj7[key]);
                }

    运行结果:

    输出顺序是不能保证的。

    2.4、更新与添加

    如果对象中存在属性就修改对应值,如果不存在就添加。对象通过引用传递,它们永远不会被复制

                var obj8={realname:'King'};
                obj8.realname='Queen';  //修改
                obj8.weight=1000;  //添加属性
                obj8.show=function()  //添加方法
                {
                    console.log(this.realname+','+this.weight);
                }
                obj8.show();

    输出:

    Queen,1000

                var obj8={realname:'King'};
                obj8.realname='Queen';  //修改
                obj8.weight=1000;  //添加属性
                obj8.show=function()  //添加方法
                {
                    console.log(this.realname+','+this.weight);
                }
                obj8.show();
                
                //引用
                var obj9=obj8;   //obj9指向obj8的引用
                obj9.realname='Jack';
                obj8.show();

    输出:

    2.5、对象的原型

    javascript是一种动态语言,与C#和Java这样的静态语言是不一样的;javascript并没有严格的类型,可以简单认为javascript是由对象组成的,对象间连接到原型(prototype)实现功能的扩展与继承。每个对象都链接到一个原型对象,并且可以从中继承属性,所有通过常量(字面量)创建的对象都连接到Object.prototype,它是JavaScript中的顶级(标配)对象,类似高级语言中的根类。

    现在我们修改系统中的Object对象,添加一个创建方法,指定要创建对象的原型,实现类似继承功能:

            <script type='text/javascript'>
                if(typeof Object.beget !== 'function')
                {
                    Object.create = function(o) {
                        //构造函数,用于创建对象
                        var F = function() {};
                        //指定由构造函数创建的对象的原型
                        F.prototype = o;
                        //调用构造方法创建新对象
                        return new F();
                    }
                }
                
                var rose={
                    name:'rose',
                    show:function(){
                        console.log('姓名:'+this.name);
                    }
                };
                
                rose.show();  //输出
                
                var lucy=Object.create(rose);  //简单认为是:创建一个对象且继承rose
                lucy.name='lucy';  //重写
                lucy.show();
            </script>

    运行结果:

    原型关系是一种动态关系,如果修改原型,该原型创建的对象会受到影响。

                var lucy=Object.create(rose);  //简单认为是:创建一个对象且继承rose
                lucy.name='lucy';  //重写
                
                var jack=Object.create(rose);
                jack.name='jack';
                
                //修改原型中的方法
                rose.show=function(){
                    console.log('姓名->'+this.name);
                }
                
                lucy.show();
                jack.show();

    结果:

    关于原型在函数中会再讲到。

    2.6、删除

                //删除属性
                delete mark.name;   
                //调用方法,输出:姓名:undefined
                mark.show(); 
                
                //删除函数
                delete mark.show;  
                //错误,mark.show is not a function
                mark.show();

    删除不用的属性是一个好习惯,在某些情况下可能引发内存泄漏。

    2.7、封装

    使用对象封装的好处是可以减少全局变量的污染机会,将属性,函数都隶属一个对象。

    封装前:

    var name='foo';   //name是全局的,被暴露
                i=1;  //全局的,没有var关键字声明的变量是全局的,与位置关系不大
                function show(){  //show 是全局的,被暴露
                    console.log('name->'+name);
                    console.log(++i);
                }
                
                //i是全局的 2
                show();  
                //3
                show();

    封装后:

    //对外只暴露bar,使用闭包封装
                var bar=function(){
                    var i=1;
                    return{
                        name:'bar',
                        show:function(){
                            console.log('name->'+this.name);
                            console.log(++i);
                        }
                    };
                };
                
                var bar1=bar();
                //2
                bar1.show();
                //3
                bar1.show();
                
                var bar2=bar();
                //2,因为被封装,且闭包,i是局部私有的
                bar2.show();

    运行结果:

    三、函数

    javascript中的函数就是对象,对象就是“键/值”对的集合并拥有一个连接到原型对隐藏连接。

    3.1、参数对象 (arguments)

    第一个函数中有一个默认对象叫arguments,类似数组,但不是数组,该对象是传递给函数的参数。

            <script type='text/javascript'>
                function counter(){
                    var sum=0;
                    for(var i=0;i<arguments.length;i++){
                        sum+=arguments[i];
                    }
                    return sum;
                }
                
                console.log(counter(199,991,1,2,3,4,5));
                console.log(counter());
            </script>

    运行结果:

    1205

    这里的arguments是一个隐式对象,不声明也在函数中,内部函数可以访问外部函数的任意内容,但是不能直接访问外部函数的arguments与this对象。

                function f1()
                {
                    console.log(arguments.length);
                    f2=function()
                    {
                        console.log(arguments.length);
                    }
                    return f2;
                }
                
                var f=f1(1,2,3);
                f();

    运行结果:

    3

    0

    3.2、构造函数

    在javascript中对象构造函数可以创建一个对象。

               <script type='text/javascript'>
               /*构造函数*/
              //可以简单的认为是一个类型的定义
               function Student(name,age){
                     this.name=name;
                     this.age=age;
                     this.show=function(){
                         console.log(this.name+','+this.age);
                     }
               }
               
               //通过new关键字调用构造函数,创建一个对象tom
               var rose=new Student('rose',18);
               var jack=new Student('jack',20);
               
               rose.show();
               jack.show();
            </script>

    3.3、函数调用

    3.3.1、call

    调用一个对象的一个方法,以另一个对象替换当前对象

    call([thisObj[,args])

    hisObj 可选项。将被用作当前对象的对象。args 将被传递方法参数序列。
    call 方法可以用来代替另一个对象调用一个方法。call 方法可将一个函数的对象上下文从初始的上下文改变为由 thisObj 指定的新对象。

    示例:

               /*构造函数*/
               function Student(name,age){
                     this.name=name;
                     this.age=age;
               }
               
                show=function(add){
                         console.log(add+':'+this.name+','+this.age);
                   }
               
               //通过new关键字调用构造函数,创建一个对象tom
               var rose=new Student('rose',18);
               var jack=new Student('jack',20);
              
              //调用show方法,指定上下文,指定调用对象,this指向rose,“大家好是参数”
              show.call(rose,'大家好');
              show.call(jack,'Hello');

    运行结果:

    call方法中的参数都可以省去,第1个参数表示在哪个对象上调用该方法,或this指向谁,如果不指定则会指向window对象。

    示例:

              var name='无名';
              var age=18;
              show.call();

    结果:

    undefined:无名,18

    3.3.2、apply

    apply([thisObj[,argArray]])
    应用某一对象的一个方法,用另一个对象替换当前对象,与call类似。
    如果 argArray 不是一个有效的数组或者不是arguments对象,那么将导致一个 TypeError。
    如果没有提供 argArray 和 thisObj 任何一个参数,那么 Global 对象将被用作 thisObj, 并且无法被传递任何参数。
    对于第一个参数意义都一样,但对第二个参数:
    apply传入的是一个参数数组,也就是将多个参数组合成为一个数组传入,而call则作为call的参数传入(从第二个参数开始)。
    如 func.call(func1,var1,var2,var3)对应的apply写法为:func.apply(func1,[var1,var2,var3])
    同时使用apply的好处是可以直接将当前函数的arguments对象作为apply的第二个参数传入

    示例代码:

               /*构造函数*/
               function Student(name,age){
                     this.name=name;
                     this.age=age;
               }
               
                show=function(greeting,height){
                         console.log(greeting+':'+this.name+','+this.age+','+height);
                   }
               
               //通过new关键字调用构造函数,创建一个对象tom
               var rose=new Student('rose',18);
               var jack=new Student('jack',20);
              
              //调用show方法,指定上下文,指定调用对象,this指向rose,“大家好是参数”
              show.apply(rose,['大家好','178cm']);
              show.apply(jack,['Hello','188cm']);

    运行结果:

     

    从上面的示例中可以发现apply的第2个参数是一个数组,数组中的内容将映射到被调用方法的参数中,如果单这样看发现不如call方便,其实如果直接取方法的参数arguments则apply要方便一些。通过简单的变化就可以替代call。

              function display(){
                 show.apply(jack,arguments);
              }
              display('hi','224cm');

    结果:

    hi:jack,20,224cm

    javascript里call和apply操作符可以随意改变this指向
    如果在javascript语言里没有通过new(包括对象字面量定义)、call和apply改变函数的this指针,函数的this指针都是指向window的。
    关于this指针,我的总结是:是谁调用的函数,那么这个函数中的this指针就是它;如果没有明确看出是谁调用的,那么应该就是window调用的,那么this指针就是window。

    3.3.3、caller

    在一个函数调用另一个函数时,被调用函数会自动生成一个caller属性,指向调用它的函数对象。如果该函数当前未被调用,或并非被其他函数调用,则caller为null。
    在JavaScript的早期版本中,Function对象的caller属性是对调用当前函数的函数的引用

            function add()
            {
                console.log('add被调用');
                //add方法的调用函数,如果调用add方法的不是函数则为null
                console.log(add.caller);
            }
            
            function calc(){
                add();
            }
            
            //直接调用add方法
            add(); 
            //间接通过calc方法调用
            calc();

    运行结果:

    caller与this还是有区别的,this是指调用方法的对象,而caller是指调用函数的函数。

            <script type='text/javascript'>
            function add(n)
            {
                console.log('add被调用');
                if(n<=2){
                    return 1;
                }
                return add.caller(n-1)+add.caller(n-2);
            }
            
            function calc(n){
                console.log('calc被调用');
                return add(n);
            }
            
            //1 1 2
            console.log(calc(3));
            </script>

    结果:

    3.3.4、Callee

    当函数被调用时,它的arguments.callee对象就会指向自身,也就是一个对自己的引用

               function add(n1,n2){
                      console.log(n1+n2);
                      //arguments.callee(n1,n2);  //指向add方法
                      return arguments.callee;
               }
               
               add(1,2)(3,4)(5,6)(7,8)(8,9);

    运行结果:

    当第1次调用add方法时输入3,立即将函数返回再次调用,每次调用后又返回自己,这样可以实现链式编程。

    3.5、立即执行函数表达式 (IIFE)

    IIFE即Immediately-Invoked Function Expression,立即执行函数表达式

    3.5.1、匿名函数与匿名对象

    匿名函数就是没有名称的函数,javascript中经常会使用匿名函数实现事件绑定,回调,实现函数级的私有作用域,如下所示:

            function(){
                console.log('这是一个匿名函数');
            };

    匿名对象:

            {
                name:'foo',
                show:function(){
                    console.log(this.name);
                }
            }

    没有名称的匿名函数也叫函数表达式,它们间是有区别的。

    3.5.2、函数与函数表达式

    下面是关于函数与函数表达式定义时的区别

    a)、函数定义(Function Declaration)

    function Identifier ( Parameters ){ FunctionBody }

    function 函数名称(参数){函数主体}

    在函数定义中,参数(Parameters)标识符(Identifier )是必不可少的。如果遗漏,会报提示错误:

    代码:

            function(){
                console.log('这是一个匿名函数');
            };

    结果:

    b)、函数表达式(Function Expression)

    function Identifier(Parameters){ FunctionBody }
    函数表达式中,参数和标识符都是可选的,与函数定义的区别是标识符可省去。

    其实,'function Identifier(Parameters){ FunctionBody }'并不是一个完整的函数表达式,完整的函数的表达式,需要一个赋值操作。
    比如: var name=function Identifier(Parameters){ FunctionBody }

    3.5.3、立即执行函数表达式与匿名对象

                //1 正常定义函数
                function f1(){
                    console.log('正常定义f1函数');
                };
                
                //2 被误解的函数表达式
                function(){
                    console.log('报错Unexpected token (');
                }();
                
                //3 IIFE,括号中的内容被解释成函数表达式
                (function(){
                    console.log('IIFE,正常执行');
                })();
                
                //4 函数表达式
                var f2=function(){
                    console.log('这也被视为函数表达式');
                };

    第3种写法为什么这样就能立即执行并且不报错呢?因为在javascript里,括号内部不能包含语句,当解析器对代码进行解释的时候,先碰到了(),然后碰到function关键字就会自动将()里面的代码识别为函数表达式而不是函数声明。

    如果需要将函数表达式或匿名对象立即执行,可以使用如下方法:

    <!DOCTYPE html>
    <html>
    
        <head>
            <meta charset='UTF-8'>
            <title>IIFE</title>
        </head>
    
        <body>
            <script type='text/javascript'>
                //调用匿名函数
                (function() {
                    console.log('这是一个函数表达式');
                })();
    
                //调用匿名对象
                ({
                    name: 'foo',
                    show: function() {
                        console.log(this.name);
                    }
                }).show();
    
                console.log({
                    a: 1
                }.a);
    
                console.log({
                    a: function() {}
                }.a());
            </script>
        </body>
    
    </html>

    运行结果:

    3.5.4、各种IIFE的写法

    //最常用的两种写法
    (function(){ /* code */ }()); // 老师推荐写法
    (function(){ /* code */ })(); // 当然这种也可以
    
    // 括号和JS的一些操作符(如 = && || ,等)可以在函数表达式和函数声明上消除歧义
    // 如下代码中,解析器已经知道一个是表达式了,于是也会把另一个默认为表达式
    // 但是两者交换则会报错
    var i = function(){ return 10; }();
    true && function(){ /* code */ }();
    0, function(){ /* code */ }();
    
    // 如果你不怕代码晦涩难读,也可以选择一元运算符
    !function(){ /* code */ }();
    ~function(){ /* code */ }();
    -function(){ /* code */ }();
    +function(){ /* code */ }();
    
    // 你也可以这样
    new function(){ /* code */ }
    new function(){ /* code */ }() // 带参

    如果是函数表达式,可直接在其后加'()'立即执行。

    如果是函数声明,可以通过'()'、'+'、'-'、'void'、'new'等运算符将其转换为函数表达式,然后再加'()'立即执行。

    3.5.5、参数

    函数表达式也是函数的一种表达形式,同样可以像函数一样使用参数,如下所示:

                (function (n){
                    console.log(n);
                })(100);

    输出:100 

    其实通过IIFE还能形成一个类似的块级作用域,当块内的程序在使用外部对象时将优先查找块内的对象,再查找块外的对象,依次向上。

                (function(win,undfd){
                    win.console.log('Hello'==undfd);
                })(window,undefined);

    3.5.6、添加分号

    为了避免与其它的javascript代码产生影响后报错,常常会在IIFE前增加一个分号,表示前面所有的语句都结束了,开始新的一语句。

                var k=100
                (function (n){
                    console.log(n);
                })(k);

    上面的脚本会报错,因为javascript解释器会认为100是函数名。

                var k=100
                ;(function (n){
                    console.log(n);
                })(k);

    这样就正确了,在javascript中一行语句的结束可以使用分号,也可以不使用分号,因为一般的自定义插件会使用IIFE,这是一段独立的代码,在应用过程中不能保证用户会加上分号,所以建议在IIFE前加上分号。

    3.5.7、IIFE的作用

    1)、提高性能

    减少作用域查找时间。使用IIFE的一个微小的性能优势是通过匿名函数的参数传递常用全局对象window、document、jQuery,在作用域内引用这些全局对象。JavaScript解释器首先在作用域内查找属性,然后一直沿着链向上查找,直到全局范围。将全局对象放在IIFE作用域内提升js解释器的查找速度和性能。

    function(window, document, $) {
    
    }(window, document, window.jQuery); 

    2)、压缩空间

    通过参数传递全局对象,压缩时可以将这些全局对象匿名为一个更加精简的变量名

    function(w, d, $) {  
      
    }(window, document, window.jQuery);

    3)、避免冲突

     匿名函数内部可以形成一个块级的私有作用域。

    4)、依赖加载

    可以灵活的加载第三方插件,当然使用模块化加载更好(AMD,CMD),示例如下。

    A.html与B.html文件同时引用公用的common.js文件,但是只有A.html需要使用到StuObj对象,B.html不需要,但使用其它方法。

    Student.js

    var StuObj = {
        getStu: function(name) {
            return new Student(name);
        }
    }
    
    /*构造函数*/
    function Student(name) {
        this.name = name;
        this.show = function() {
            console.log('Hello,' + this.name);
        }
    }

    Common.js

    function other1() {}
    
    function other2() {}
    
    (function($) {
        if($) {
            $.getStu('Tom').show();
        }
    })(typeof StuObj=='undefined'?false:StuObj);

    A.HTML

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset='UTF-8'>
            <title>A</title>
        </head>
        <body>
            <script src='js/Student.js' type='text/javascript' charset='utf-8'></script>
            <script src='js/common.js' type='text/javascript' charset='utf-8'></script>
        </body>
    </html>

    B.HTML

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset='UTF-8'>
            <title></title>
        </head>
        <body>
            <script src='js/common.js' type='text/javascript' charset='utf-8'></script>
            <script type='text/javascript'>
                other1();
            </script>
        </body>
    </html>

    3.5.8、IIFE的变形

    也许有人会说IIFE将参数放在最后,需要移动到文档的末尾才能看到参数,比较麻烦,那么可以将IIFE变形为如下形式:

            (function(n){
                console.log(n);
                
                
                
                
                
                
                
                //认为这里有30000代码
                
                
                
                
                
                
                
            }(100));

    如果中间有很长的代码,参数100只有到文档的末尾才可以看得到,变形后的结果:

            (function(exp){
                exp(100);
            }(function(n){
                console.log(n);
                //认为这里有30000代码
            }));

    修改后的代码中有两个函数表达式,一个作为参数,就是我们主要要完成的功能向控制台输出数字,另一个作来IIFE立即执行的函数,主要的功能函数变成的IIFE的参数了。

                (function(win, doc, $) {
    
                }(window, document, jQuery));
    
                (
                    function(library) {
                        library(window, document, window.jQuery);
                    }
                    (function(window, document, $) {
    
                    })
                );

    bootstrap的写法:

                +function(yourcode) {
    
                    yourcode(window.jQuery, window, document);
    
                }(function($, window, document) {
                        $(function() {});  //jQueryDOM加载完成事件
                  });

    结合call或apply的写法:

                  (function(x){console.log(x)}).call(window,888);
                  (function(x){console.log(x)}).apply(window,[999]);

    输出:888 999

    四、示例下载

    https://github.com/zhangguo5/javascript003.git

About IT165 - 广告服务 - 隐私声明 - 版权申明 - 免责条款 - 网站地图 - 网友投稿 - 联系方式
本站内容来自于互联网,仅供用于网络技术学习,学习中请遵循相关法律法规