JavaScript面向对象(2)——函数与闭包(函数、对象、闭包)

       很多同学甚至在相当长的时间里,都忽略了JavaScript也可以进行面向对象编程这个事实。一方面是因为,在入门阶段我们所实现的各种页面交互功能,都非常顺理成章地使用过程式程序设计解决了,我们只需要写一些方法,然后将事件绑定在页面中的DOM节点上便可以完成。尤其像我这类一开始C++这类语言没好好学,第一门主力语言就是JavaScript的同学来说,过程化程序设计的思维似乎更加根深蒂固。另一方面,就算是对于Java、C++等语言的程序员来说,JavaScript的面向对象也是一个异类:JavaScript中没有class的概念(在ES5及之前版本中没有,ES6会单独介绍),其基于prototype的继承模式也与传统面向对象语言不同,而JavaScript的弱类型特性更会令这里面的很多人抓狂。当然,在熟悉了之后,这种灵活性也会带来很多好处。总之,封装、继承、多态、聚合这些面向对象的基本特性JavaScript都有其自己的实现方式,这些知识的学习是从入门级JS程序员进阶的必经之路。

JavaScript面向对象(1)——谈谈对象

JavaScript面向对象(2)——谈谈函数(函数、对象、闭包)

JavaScript面向对象(3)——原型与基于构造函数的继承模式(原型链)

JavaScript面向对象(4)——最佳继承模式(深拷贝、多重继承、构造器借用、组合寄生式继承)



        前面提到,要理解JavaScript中的面向对象编程,就要对JavaScript中一些独特的语言特性进行了解。在JavaScript中,函数也是对象,这就赋予了JavaScript函数很大的灵活性。函数就像JavaScript中普通的值一样进行各种各样的值操作;而函数的定义也可以在其他函数中进行,这就使得JavaScript函数可以构成闭包,能够完成更多功能。我们照例还是从创建开始,看看JS中函数的各种性质。


一、函数的定义

/*通过函数声明语句定义函数*/
function sum(a,b){
	return a + b;
}

/*通过函数定义表达式定义函数*/
var sum = function(a,b){
	return a + b;
}

        和大多数编程语言类似,Js的函数也包含函数名称标识符、参数表和函数体。在函数定义表达式中,我们声明了一个变量,并把函数对象赋值给了这个变量。这种情况下,函数名称标识符是可选的,并且在大多数情况下不需要定义函数名称标识符,这个时候函数的名称会默认和变量名相同;假如函数定义表达式包含了函数名称,则将会在函数的局部作用域包含一个绑定到函数对象的名称(在函数作用域内可以直接用该函数名称标识符调用),而在外部作用域中无法使用这个名称调用函数,而是只能使用定义表达式声明的变量后加上参数表进行调用:

/*通过函数声明语句定义函数 不包含函数名称标识符*/
var a = function(){
	console.log(Object.getOwnPropertyNames(a))
	console.log(a.name);
}
a();// ["length", "name", "arguments", "caller", "prototype"]


/*通过函数声明语句定义函数 包含函数名称标识符*/
var a = function aa(){
	console.log(a.name);
	console.log(aa);
}
a();//aa
    //function aa(){...}
aa();//Uncaught ReferenceError: aa is not defined

        关于两种声明方式,还有值得一提的一点是:在js脚本执行的时候,解释器会先遍历整个程序,将使用函数声明语句声明的函数进行声明。也就是说,函数声明语句会被提前到所在作用域的顶部执行,在程序中这种函数的调用语句可以出现在声明语句之前。而使用函数定义表达式方式定义的函数则不拥有这种特性。

aa();//"AAA"
function aa(){console.log("AAA")};

bb(); //Uncaught ReferenceError: bb is not defined
var bb = function(){console.log("BBB")};
bb(); //"BBB"

var cc = function(){
	dd();
	function dd(){
		console.log("DDD");
	}
}
cc(); //"DDD"
dd(); //Uncaught ReferenceError: dd is not defined

        函数声明语句并非真正的语句,它只被ECMAScript规范允许出现在全局代码或其他函数中,不能在循环、条件判断,或try/chtch等语句中。

        并不是所有函数都包含return语句,未包含return语句的函数会返回undefined。


        另外,创建一个函数还可以通过调用Function()构造函数的方法,不过没人会这么去用。Function对象构造函数会在最后讲述

二、函数的调用

        一般来说有四种方法:函数调用、方法调用、作为构造函数调用、使用call()\apply()方法间接调用。

        1、函数调用与方法调用

             使用函数表达式可以进行函数调用与方法调用,其方法是用函数对象(函数声明语句的使用函数名、函数定义表达式的使用所定义的变量)后跟参数表(括号内加上用,分隔的参数)进行调用。

             函数调用是最直接的调用,在声明语句所在作用域范围内直接使用函数表达式进行调用。根据ECMA的规定,函数调用的时候调用上下文(即在函数体中使用this访问到的对象)是全局对象;而在严格模式下,调用上下文是undefined。可以通过(function() { return this; }())的值来判断当前脚本是否运行在严格模式下。

            方法是保存在对象属性里的函数,对这类函数的调用则是用通过对象属性访问到函数对象,在其后加上参数表进行调用,即方法调用。

/*函数调用与方法调用*/
function sayHello(){
    return "Hello";
}
sayHello(); //"Hello"

var obj = new Object();
obj.say = sayHello;
obj.say(); //"Hello"

          个人感觉,其实在万物皆对象的JS中,因为全局对象的存在,函数调用和方法调用其实是相同的。就如同全局变量一样,我们使用函数声明语句定义的函数,其实是以全局对象属性的形式存在的,属性名和该函数的函数名一致。我们可以很轻易的体会到这一特点。

        2、构造函数调用

             在函数或方法调用前带有关键词new便构成了构造函数调用。在函数的调用的特点上,它与之前两种有一些不同:

                    在参数处理上,如果需要传入参数的话,其处理没什么特殊之处;如果不需要传入参数的话,调用构造函数时可以省略参数表(省略圆括号)。构造函数用于创建一个新的对象,其调用上下文是这个新创建的对象,即使构造函数是以对象方法的形式出现也依然如此。在返回值方面,构造函数中一般不使用return关键字,返回所创建的新对象;假如使用了return语句但返回了原始值或没有指定返回值,则该次调用依然会返回新创建的对象;只有在使用了return语句并返回一个对象时,构造函数调用才会按照return语句返回值。

function CreateObj1(){
	this.name = "obj";
}
console.log(new CreateObj); //CreateObj {name: "obj"}


function CreateObj2(){
	this.name = "obj";
	return "This string will not be returned";
}
console.log(new CreateObj2()); //CreateObj2 {name: "obj"}

function CreateObj3(){
	this.name = "obj";
	return {text: "This object will be returned"};
}
console.log(new CreateObj3()); //Object {text: "This object will be returned"}

        3、间接调用

              JavaScript中的Function对象提供了call()、apply()这样的方法来间接调用函数,它们都允许显式地指定调用上下文以及实参,而返回值则没什么特殊之处。


四种调用的总结:

 实参处理调用上下文返回值
函数调用先计算参数表达式的值再传入

全局对象

(严格模式下为 undefined)

根据return语句返回

若不存在return语句 返回undefined


方法调用
调用函数的对象

构造函数调用
若无参数可省略括号新创建的对象

除非使用return语句显示规定了返回的对象,此时返回所指定的对象

否则一律返回新创建的对象


间接调用
按照方法显示指定在方法中显式指定

根据return语句返回

若不存在return语句 返回undefined

关于调用上下文的总结:JavaScript中this的指向 


三、关于函数的形参与实参

        JavaScript中,函数的参数也有许多特点:在函数定义时不用指定形参的类型,在调用时也不对实参做任何检查,包括类型与数量。这就带来了一些有趣的现象:

            1、实参个数少于形参:当实参个数少于形参时,剩下的形参会自动设置为undefined。这使得我们在定义函数的时候可以设置一些可选参数,这时可选参数要放在后面。同时为了避免参数省略时传入undefined可能造成的程序错误,我们可以使用条件判断语句或者||运算符给可选参数赋个合适的值。

function addString(a,/* optional */ b){
	b = b || " b is undefined";
	return a + b;
}
console.log(addString("111")); // "111 b is undefined"

            2、实参个数多于形参:当传入实参多于形参时,多出来的参数是未命名的,无法被直接引用。在函数中包含一个名为arguments的类数组对象,它里面包含了所有传入的实参,通过数字下标即可访问。实参对象arguments使得JavaScript函数可以操作任意数量的实参,下面给出了权威指南中的一段代码。另外,JavaScript中函数重载的实现也是使用了实参对象来模拟的:

function getMax(/* ... */){
	var max = Number.NEGATIVE_INFINITY;
	for(var i = 0;i < argumengs.length; i++)
		if(argumengs[i] > max) max = argumengs[i];
	return max;
}
function funcOverloading(a, b /*,...*/ ){
	var argumentsLength = argumengs.length;
	if(argumentsLength == 0){
		//do something
	}else if(argumentsLength == 1){
		//do something
	}//以此类推。。。
}


          在arguments对象中,除了存储了各个实参的属性以及length属性之外,实参对象中还定义了callee和caller属性,它们只能在非严格模式中使用,在严格模式中会产生类型错误。callee指代当前正在执行的函数,而caller指代调用当前正在执行函数的函数。为了说明它们的用处,这里再次搬运一段权威指南中的代码:通过callee属性调用自身实现递归调用匿名函数:

var factorial = function(x){
	if(x <= 1) return 1;
	return x * arguments.callee(x-1);
}


        因为这些情况,JavaScript函数的参数是很灵活的,也会有很多具有很多参数的函数,这个时候记住参数的顺序就很让人头疼。为了应对这种情况,我们可以将参数以键值对的形式封装在对象中将对象作为参数传入,这个时候顺序就变的无关紧要。这种方式我们在JQuery等方法库中一定使用过。


四、函数是对象

       这是JavaScript函数最大的特色了。函数是对象,即是值。这里列举一下由此带来的特性:

  • 可以将函数赋值给变量
  • 函数可以作为对象的属性值
  • 函数可以作为实参被传入函数
  • 函数自身也拥有属性,并且支持自定义
五、闭包
       说起闭包,一定要先说说作用域。关于作用域、作用域链的概念: 简述JavaScript作用域与作用域链
       JavaScript的变量作用域规则:不在任何函数体中声明的变量是全局变量;在函数中声明的变量只在该函数体内可见。在JS中变量的作用域只以函数体来界定,有时我们为了避免污染全局命名空间,就会定义一个简单的函数用作临时命名空间。下面这种常见的写法就是出于这样的使用:最外侧的括号保证了解释器将这个语句以函数定义表达式来解析,而不是当作函数声明语句。函数体之后紧接的括号表示立即调用。
(function() {
	// do something
}());
       
        闭包是一个很古老的计算机术语,它指函数内部的变量可以被隐藏于作用域链之内,就像是函数将变量包裹了起来保存。在JavaScript中,狭义的闭包是指这样的情形:在嵌套函数的情况下,外部函数将嵌套的函数对象作为返回值返回。这个时候,由于外部仍然存在着对嵌套函数对象的引用,其作用域链并未被销毁,故其内部状态得以保存(局部变量),并且只有被返回的那个函数对象可以访问。我们通过代码分析一下:

function testScope(){
	var test = "local";
	function localFun(){
		return test;
	}
	return localFun;
}

testScope()();//返回值在最右 先自己分析一下                                                                                                        "local"
console.log(test); // 返回值在最右 先自己分析一下                                                             Uncaught ReferenceError:test is not defined                      
        在这段代码中,我们定义了函数testScope,并在其中定义了局部变量test和嵌套函数localFun,并将嵌套函数对象作为返回值返回。之后调用testScope函数并立即调用所返回的localFun函数。  按照我们通常的理解,这里的testScope函数返回了之后,其中的局部变量就应该被销毁了;并且返回的函数对象是在顶层调用的,应当在顶层访问test变量,因为顶层未定义此变量从而返回undefined。   当然,这只是我们通常的、错误的理解。   
        要理解这个问题,首先就必须懂得作用域链的规则,理解 简述JavaScript作用域与作用域链中,描述嵌套函数中局部变量与全局变量重名时的处理过程那段代码。由于作用域链是在函数声明的过程中就生成的,所以函数对象localFun中访问的test无疑是局部变量。另一方面,testScope返回之后,其局部变量本来应随着其声明上下文对象的销毁得以销毁,然而因为返回了嵌套函数localFun,使得顶层空间保留了一个对localFun函数对象的引用,使得这一条作用域链(localFun的声明上下文对象 --> testScope的声明上下文对象 --> 全局对象)得以保存,未被销毁,在testScope函数的声明上下文对象中储存的局部变量test也就保存了下来。 并且只有返回的函数对象localFun可以访问到这个局部变量,在顶层当然是无法访问的。
        闭包主要有两个用途: 1、让局部变量可以在函数外访问。比如在上面的testScope函数中不使用var关键字声明一个函数对test进行操作,之后就可以直接在顶层通过这个函数操作局部变量test了;   2、让局部变量的值在内存中持久化,不会因为函数的返回而销毁。     闭包就是JavaScript中访问控制的实现手段了。面向对象四大特征里封装性的要求便得以实现。下面抄了几段帮助理解闭包的代码:
//在函数外访问局部变量
function testScope(){
    var test = "local";
    addStr = function(){
        test = test + ">_<";
    }
    function localFun(){
        return test;
    }
    return localFun;
}

var f = testScope();
console.log(f()); //"test"
addStr();
console.log(f()); // "test>_<"

//理解JavaScript作用域与闭包程序一例
var name = "The Window",
    object1 = {   
      name : "My Object",   
     getNameFunc : function(){   
       return function(){   
         return this.name;   
         };   
      }   
    },
    object2 = {
    name : "My Object",
      getNameFunc : function(){
        var that = this;
        return function(){
          return that.name;
        };
      }
    };
console.log(object1.getNameFunc()());  //"The Window"
console.log(object2.getNameFunc()());  //"My Object"

//多个嵌套函数的情况
function counter(){
    var n = 0;
    return {
        count: function(){return n++;},
        reset: function(){n = 0;}
    };
}
var a = counter,b = counter;
c.count(); //0
d.count(); //0
c.reset(); 
c.count(); //0
d.count(); //1


六、Function对象的属性、方法以及构造函数
       Function对象是所有JavaScript函数对象的原型对象,它赋予了JavaScript函数自己的属性与方法。简单整理一下:
       1、属性
属性名简介
arguments类数组对象arguments一个类数组对象,以数字为键储存传入函数的所有实参
length定义的形参个数只读
name函数名称标识符 
caller调用当前函数的对象若在顶层调用为null
prototype该函数对象的原型对象 
        2、方法
            1).间接调用函数: call() apply() bind()。这三个方法可以对函数进行间接调用,并手动设置调用上下文对象和传入实参。详细介绍: call()、apply()与bind()

            2).toString():一般的函数返回函数源码,内置函数返回'[native code]"。
        
        3、Function构造函数:
              前面提到,构造函数除了可以通过函数声明语句和函数定义表达式之外,还可以使用Function()构造函数直接生成一个函数对象。构造函数接收任意多个类型为字符串的参数,其中最后一个字符串为函数体语句,其余的字符串都为形参名。例如 var a = function(x,y){return x+y;}这个函数定义表达式便可以这么写:
var a = new Funcion("x","y","return x+y;");
              使用Function构造函数可以在程序运行时动态地创建并编译函数,不过使用它的情况非常之少。一方面,每次调用Function()就会解析函数体,创建新的函数对象。在循环中或者重复执行的情况下,其执行效率比函数声明语句与函数定义表达式要低得多。另一方面,使用Function()创建的函数不使用词法作用域,无论在哪里执行Function()构造函数,它创建的函数都在顶层执行,不拥有局部作用域。比如:
var test = "global";
function constructFun(){
    var test = "local";
    return new Function("return scope;");
}
constructFun()(); //"global"





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值