zoukankan      html  css  js  c++  java
  • 第五章 引用类型 Function 类型

    函数实际上是对象。每个函数都是Function 类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。

    函数通常是使用函数声明语法定义的,如下面的例子所示。

    function sum (num1, num2) {
        return num1 + num2;
    }

    这与下面使用函数表达式定义函数的方式几乎相差无几。

    var sum = function(num1, num2){
        return num1 + num2;
    };

    定义函数的最后一种方式是使用Function 构造函数。Function 构造函数可以接收任意数量的参数,但最后一个参数始终都被看成是函数体,而前面的参数则枚举出了新函数的参数。来看下面的例子:

    var sum = new Function("num1", "num2", "return num1 + num2"); // 不推荐
    从技术角度讲,这是一个函数表达式。但是,我们不推荐读者使用这种方法定义函数,因为这种语法会导致解析两次代码(第一次是解析常规ECMAScript 代码,第二次是解析传入构造函数中的字符串),
    从而影响性能。不过,这种语法对于理解“函数是对象,函数名是指针”的概念倒是非常直观的。
    由于函数名仅仅是指向函数的指针,因此函数名与包含对象指针的其他变量没有什么不同。换句话说,一个函数可能会有多个名字,如下面的例子所示。

    function sum(num1, num2){
        return num1 + num2;
    }
    alert(sum(10,10)); //20
    var anotherSum = sum;
    alert(anotherSum(10,10)); //20
    sum = null;
    alert(anotherSum(10,10)); //20

    使用不带圆括号的函数名是访问函数指针,而非调用函数。

    5.1 没有重载(深入理解)

    将函数名想象为指针,也有助于理解为什么ECMAScript 中没有函数重载的概念。

    function addSomeNumber(num){
        return num + 100;
    }
    function addSomeNumber(num) {
        return num + 200;
    }

    var result = addSomeNumber(100); //300

    显然,这个例子中声明了两个同名函数,而结果则是后面的函数覆盖了前面的函数。以上代码实际上与下面的代码没有什么区别。

    var addSomeNumber = function (num){
        return num + 100;
    };
    addSomeNumber = function (num) {
        return num + 200;
    };
    var result = addSomeNumber(100); //300

    通过观察重写之后的代码,很容易看清楚到底是怎么回事儿——在创建第二个函数时,实际上覆盖了引用第一个函数的变量addSomeNumber。

    5.2 函数声明和函数表达式

    解析器对函数声明和函数表达式不一样。解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。

    alert(sum(10,10));
    function sum(num1, num2){
        return num1 + num2;
    }

    以上代码完全可以正常运行。因为在代码开始执行之前,解析器就已经通过一个名为函数声明提升(function declaration hoisting)的过程,读取并将函数声明添加到执行环境中。对代码求值时,JavaScript引擎在第一遍会声明函数并将它们放到源代码树的顶部。所以,即使声明函数的代码在调用它的代码后面,JavaScript 引擎也能把函数声明提升到顶部。

    如果像下面例子所示的,把上面的函数声明改为等价的函数表达式,就会在执行期间导致错误。

    alert(sum(10,10));
    var sum = function(num1, num2){
        return num1 + num2;
    };

    以上代码之所以会在运行期间产生错误,原因在于函数位于一个初始化语句中,而不是一个函数声明。换句话说,在执行到函数所在的语句之前,变量sum 中不会保存有对函数的引用;而且,由于第一行代码就会导致“unexpected identifier”(意外标识符)错误,实际上也不会执行到下一行。

    除了什么时候可以通过变量访问函数这一点区别之外,函数声明与函数表达式的语法其实是等价的。

    5.3 作为值的函数

    因为ECMAScript 中的函数名本身就是变量,所以函数也可以作为值来使用。也就是说,不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。来看一看下面的函数。
    function callSomeFunction(someFunction, someArgument){
    return someFunction(someArgument);
    }
    这个函数接受两个参数。第一个参数应该是一个函数,第二个参数应该是要传递给该函数的一个值。然后,就可以像下面的例子一样传递函数了。

    function add10(num){
        return num + 10;
    }
    var result1 = callSomeFunction(add10, 10);
    alert(result1); //20
    function getGreeting(name){
        return "Hello, " + name;
    }
    var result2 = callSomeFunction(getGreeting, "Nicholas");
    alert(result2); //"Hello, Nicholas"

    从一个函数中返回另一个函数,这也是极为有用的一种技术。 

    例如,假设有一个对象数组,我们想要根据某个对象属性对数组进行排序。而传递给数组sort()方法的比较函数要接收两个参数,即要比较的值。可是,我们需要一种方式来指明按照哪个属性来排序。要解决这个问题,可以定义一个函数,它接收一个属性名,然后根据这个属性名来创建一个比较函数,下面就是这个函数的定义。

    function createComparisonFunction(propertyName) {
        return function(object1, object2){
            var value1 = object1[propertyName];
            var value2 = object2[propertyName];
            if (value1 < value2){
                return -1;
            } else if (value1 > value2){
                return 1;
            } else {
                return 0;
            }
        };
    }    

    这个函数定义看起来有点复杂,但实际上无非就是在一个函数中嵌套了另一个函数,而且内部函数前面加了一个return 操作符。在内部函数接收到propertyName 参数后,它会使用方括号表示法来取得给定属性的值。取得了想要的属性值之后,定义比较函数就非常简单了。上面这个函数可以像在下面例子中这样使用。

    var data = [{name: "Zachary", age: 28}, {name: "Nicholas", age: 29}];
    data.sort(createComparisonFunction("name"));
    alert(data[0].name); //Nicholas
    data.sort(createComparisonFunction("age"));
    alert(data[0].name); //Zachary

    5.4 函数内部属性

    在函数内部,有两个特殊的对象:arguments 和this。其中,arguments 是一个类数组对象,包含着传入函数中的所有参数。虽然arguments 的主要用途是保存函数参数,但这个对象还有一个名叫callee 的属性,该属性是一个指针,指向拥有这个arguments 对象的函数。
    请看下面这个非常经典的阶乘函数。

    function factorial(num){
        if (num <=1) {
            return 1;
        } else {
            return num * factorial(num-1)
        }
    }        

    可以像下面这样使用arguments.callee。

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

    在这个重写后的factorial()函数的函数体内,没有再引用函数名factorial。这样,无论引用函数时使用的是什么名字,都可以保证正常完成递归调用。例如:

    var trueFactorial = factorial;
        factorial = function(){
        return 0;
    };
    alert(trueFactorial(5)); //120
    aler t(factorial(5)); //0

    在此,变量trueFactorial 获得了factorial 的值,实际上是在另一个位置上保存了一个函数的指针。然后,我们又将一个简单地返回0 的函数赋值给factorial 变量。如果像原来的factorial()那样不使用arguments.callee,调用trueFactorial()就会返回0。可是,在解除了函数体内的代码与函数名的耦合状态之后,trueFactorial()仍然能够正常地计算阶乘;至于factorial(),它现在只是一个返回0 的函数。

    函数内部的另一个特殊对象是this,this引用的是函数据以执行的环境对象——或者也可以说是this 值(当在网页的全局作用域中调用函数时,this 对象引用的就是window)。来看下面的例子。

    window.color = "red";
    var o = { color: "blue" };
    function sayColor(){
        alert(this.color);
    }
    sayColor(); //"red"
    o.sayColor = sayColor;
    o.sayColor(); //"blue"

    对this.color 求值会转换成对window.color 求值,于是结果就返回了"red"。而当把这个函数赋给对象o 并调用o.sayColor()时,this 引用的是对象o,因此对this.color 求值会转换成对o.color 求值,结果就返回了"blue"。

    函数的名字仅仅是一个包含指针的变量而已。因此,即使是在不同的环境中执行,全局的sayColor()函数与o.sayColor()指向的仍然是同一个函数。

    ECMAScript 5 也规范化了另一个函数对象的属性:caller。除了Opera 的早期版本不支持,其他浏览器都支持这个ECMAScript 3 并没有定义的属性。这个属性中保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,它的值为null。例如:

    function outer(){
        inner();
    }
    function inner(){
        alert(inner.caller);
    }
    outer();

    以上代码会导致警告框中显示outer()函数的源代码。因为outer()调用了inter(),所以inner.caller 就指向outer()。为了实现更松散的耦合,也可以通过arguments.callee.caller来访问相同的信息。

    function outer(){
        inner();
    }
    function inner(){
        alert(arguments.callee.caller);
    }
    outer();

    当函数在严格模式下运行时,访问arguments.callee 会导致错误。ECMAScript 5 还定义了arguments.caller 属性,但在严格模式下访问它也会导致错误,而在非严格模式下这个属性始终是undefined。定义这个属性是为了分清arguments.caller 和函数的caller 属性。以上变化都是为了加强这门语言的安全性,这样第三方代码就不能在相同的环境里窥视其他代码了。严格模式还有一个限制:不能为函数的caller 属性赋值,否则会导致错误。

    5.5 函数属性和方法

    ECMAScript 中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性:length 和prototype。

    1、length属性

    其中,length 属性表示函数希望接收的命名参数的个数,如下面的例子所示。

    function sayName(name){
        alert(name);
    }
    function sum(num1, num2){
        return num1 + num2;
    }
    function sayHi(){
        alert("hi");
    }
    alert(sayName.length); //1
    alert(sum.length); //2
    alert(sayHi.length); //0

    以上代码定义了3 个函数,但每个函数接收的命名参数个数不同。首先,sayName()函数定义了一个参数,因此其length 属性的值为1。类似地,sum()函数定义了两个参数,结果其length 属性中保存的值为2。而sayHi()没有命名参数,所以其length 值为0。

    2、prototype属性

    对于ECMAScript 中的引用类型而言,prototype 是保存它们所有实例方法的真正所在。换句话说,诸如toString()和valueOf()等方法实际上都保存在prototype 名下,只不过是通过各自对象的实例访问罢了。在创建自定义引用类型以及实现继承时,prototype 属性的作用是极为重要的。

    在ECMAScript 5 中,prototype 属性是不可枚举的,因此使用for-in 无法发现。每个函数都包含两个非继承而来的方法:apply()和call()。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this 对象的值。

    (1)apply()方法

    首先,apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是Array 的实例,也可以是arguments 对象。

    例如:

    function sum(num1, num2){
        return num1 + num2;
    }
    function callSum1(num1, num2){
        return sum.apply(this, arguments); // 传入arguments 对象
    }
    function callSum2(num1, num2){
        return sum.apply(this, [num1, num2]); // 传入数组
    }
    alert(callSum1(10,10)); //20
    alert(callSum2(10,10)); //20

    在上面这个例子中,callSum1()在执行sum()函数时传入了this 作为this 值(因为是在全局作用域中调用的,所以传入的就是window 对象)和arguments 对象。而callSum2 同样也调用了sum()函数,但它传入的则是this 和一个参数数组。这两个函数都会正常执行并返回正确的结果。

    (2)call()方法

    call()方法与apply()方法的作用相同,它们的区别仅在于接收参数的方式不同。对于call()方法而言,第一个参数是this 值没有变化,变化的是其余参数都直接传递给函数。换句话说,在使用call()方法时,传递给函数的参数必须逐个列举出来,如下面的例子所示。

    function sum(num1, num2){
        return num1 + num2;
    }
    function callSum(num1, num2){
        return sum.call(this, num1, num2);
    }
    alert(callSum(10,10)); //20

    在使用call()方法的情况下,callSum()必须明确地传入每一个参数。结果与使用apply()没有什么不同。至于是使用apply()还是call(),完全取决于你采取哪种给函数传递参数的方式最方便。如果你打算直接传入arguments 对象,或者包含函数中先接收到的也是一个数组,那么使用apply()肯定更方便;否则,选择call()可能更合适。(在不给函数传递参数的情况下,使用哪个方法都无所谓。)

    事实上,传递参数并非apply()和call()真正的用武之地;它们真正强大的地方是能够扩充函数赖以运行的作用域。下面来看一个例子。

    window.color = "red";
    var o = { color: "blue" };
    function sayColor(){
        alert(this.color);
    }
    sayColor(); //red
    sayColor.call(this); //red
    sayColor.call(window); //red
    sayColor.call(o); //blue

    sayColor()也是作为全局函数定义的,而且当在全局作用域中调用它时,它确实会显示"red"——因为对this.color 的求值会转换成对window.color 的求值。而sayColor.call(this)和sayColor.call(window),则是两种显式地在全局作用域中调用函数的方式,结果当然都会显示"red"。但是,当运行sayColor.call(o)时,函数的执行环境就不一样了,因为此时函数体内的this 对象指向了o,于是结果显示的是"blue"。

    使用call()(或apply())来扩充作用域的最大好处,就是对象不需要与方法有任何耦合关系。在前面例子的第一个版本中,我们是先将sayColor()函数放到了对象o 中,然后再通过o 来调用它的;而在这里重写的例子中,就不需要先前那个多余的步骤了。

    (3)bind()方法
    ECMAScript 5 还定义了一个方法:bind()。这个方法会创建一个函数的实例,其this 值会被绑定到传给bind()函数的值。例如:

    window.color = "red";
    var o = { color: "blue" };
    function sayColor(){
        alert(this.color);
    }
    var objectSayColor = sayColor.bind(o);
    objectSayColor(); //blue

    在这里,sayColor()调用bind()并传入对象o,创建了objectSayColor()函数。objectSayColor()函数的this 值等于o,因此即使是在全局作用域中调用这个函数,也会看到"blue"。

    支持bind()方法的浏览器有IE9+、Firefox 4+、Safari 5.1+、Opera 12+和Chrome。

    每个函数继承的toLocaleString()和toString()方法始终都返回函数的代码。返回代码的格式则因浏览器而异——有的返回的代码与源代码中的函数代码一样,而有的则返回函数代码的内部表示,即由解析器删除了注释并对某些代码作了改动后的代码。由于存在这些差异,我们无法根据这两个方法返回的结果来实现任何重要功能;不过,这些信息在调试代码时倒是很有用。另外一个继承的valueOf()方法同样也只返回函数代码。

  • 相关阅读:
    简明Python3教程 12.问题解决
    简明Python3教程 11.数据结构
    【SPOJ 694】Distinct Substrings
    【codeforces Manthan, Codefest 17 C】Helga Hufflepuff's Cup
    【CF Manthan, Codefest 17 B】Marvolo Gaunt's Ring
    【CF Manthan, Codefest 17 A】Tom Riddle's Diary
    【SPOJ 220】 PHRASES
    【POJ 3261】Milk Patterns
    【POJ 3294】Life Forms
    【POJ 1226】Substrings
  • 原文地址:https://www.cnblogs.com/aimee2004/p/6171300.html
Copyright © 2011-2022 走看看