zoukankan      html  css  js  c++  java
  • JavaScript模式(2):函数

    本篇中,将学习到以多种不同的方式来定义JavaScript中的函数,你会学习到函数表达式和函数声明,并且还会看到局部作用域和变量声明提升的工作原理。

    性质

    JavaScript中的一切都是对象,函数也不例外。因此函数也是对象,并且具有所有对象的特性,其表现如下:

    • 函数可以在运行时动态创建,还可以在程序执行过程中创建。
    • 函数可以分配给变量,可以将他们的引用复制到其他变量,可以被扩展。此外,除少数特殊情况外,函数还可以被删除。
    • 可以作为参数传递给其他函数,并且还可以由其他函数返回。
    • 函数可以有自己的属性和方法。

    作用域

    JavaScript中的作用域并不像其他编程语言那样复杂,在JavaScript中仅存在函数作用域。在函数内以var关键字定义的任何变量都是局部变量,对于函数外部是不可见的。

    不同于其他编程语言通过大括号{}划分作用域,如果在if语句或在for以及while循环中使用var关键字定义一个变量,这并不意味着该变量对于if或for来说是局部变量。相反,它仅对于包装该函数来说是局部变量,并且如果没有包装函数,它将会成为一个全局变量。例如:

    function foo() {
        for (var i = 0; i < 5; i++) {
        }
        console.log(i);  // 输出5
    }

    函数表达式与函数声明

    正如C++中有函数指针和函数,C#中有委托和方法一样,JavaScript中所对应的是函数表达式和函数声明。

    函数表达式

    正如本篇开始时提到的,函数也是对象,因此可以把一个函数赋值给一个变量,这就是函数表达式。例如:

    var add = function add(a, b) {
        return a + b;
    };
    var sum = add(1, 2);  // 3

    注意最后的大括号后有一个分号,说明这是一条赋值语句。同时也要注意函数的调用,即add(1, 2),此处的add代表的是var关键字后的add,而非function后的add。

    大多数浏览器会为函数创建一个名为name的属性(函数也是对象,因此函数也可以有属性),属性值为function关键字后的函数名,因此可以将function后的add替换成其他名称,而调用该函数时依旧要使用var关键字声明的变量名,例如:

    var add = function plus(a, b) {
        return a + b;
    };
    var sum = add(1, 2);  // 3

    如果function关键字后不声明方法名,将会得到一个未命名函数表达式,或称为匿名函数,例如:

    var add = function (a, b) {
        return a + b;
    };
    var sum = add(1, 2);  // 3

    函数表达式实际上就是传递函数对象的指针,通过该指针执行该函数,函数名反而变得可有可无,请读者和函数指针或者委托做类比。

    函数声明

    函数声明和其他编程语言中定义一个函数类似,通过函数名来执行该函数。

    函数声明只能出现在“程序代码”中,这表示它仅能在其他函数体内部或全局空间中。它们的定义不能分配给变量或属性,也不能以参数形式出现在函数调用中。例如:

    function add(a, b) {
        return a + b;
    }
    var sum = add(1, 2);  // 3

    注意函数定义的大括号最后没有分号。

    函数的提升

    由于函数表达式的核心是变量,而函数声明的核心是函数,因此二者在声明提升上具有不同的行为。

    变量提升

    对于所有的变量,无论在函数体的何处进行声明,都会在后台被提升到函数的顶部。

    函数提升

    与变量提升类似,唯一不同的地方在于被提升的不仅是函数声明,函数的定义也被提升至函数顶部。请参考如下代码,观察变量提升和函数提升的不同之处:

    // 全局函数
    function foo() {
        console.log("global foo");
    }
    function bar() {
        console.log("global bar");
    }
    
    function hoistMe() {
        console.log(typeof foo);  // 输出function
        console.log(typeof bar);  // 输出undefined
    
        foo();  // 输出local foo
        bar();  // TypeError: undefined is not a function
    
        // 函数声明
        // foo和实现都被提升
        function foo() {
            console.log("local foo");
        }
    
        // 函数表达式
        // 仅bar被提升,实现未被提升
        var bar = function () {
            console.log("local bar");
        }
    }
    hoistMe();

    可以发现,函数创建了作用域,因此hoistMe中的foo和bar覆盖了同名全局函数。

    同时,我们也可以发现,foo作为函数声明,其具体实现也被提升至函数顶部,因此调用foo输出了local foo;相反,bar作为函数表达式,本质上是一个变量,仅bar的声明被提升到函数顶部,具体实现并未被提升,因此bar覆盖了全局同名函数,但并未被赋值。

    回调模式

    函数都是对象,这表示它们也可以作为参数传递给其他函数,这种模式也称为回调模式。例如:

    function fromOneToTen(callback) {
        var result = 1;
        for (var i = 1; i < 11; i++) {
            result = callback(result, i);
        }
        return result;
    }
    function add(a, b) {
        return a + b;
    }
    function multipy(a, b) {
        return a * b;
    }
    console.log(fromOneToTen(add));  // 输出56
    console.log(fromOneToTen(multipy));  // 输出3628800

    示例中主函数fromOneToTen从1迭代到10,迭代方式通过回调函数传入,分别为叠加和叠乘。

    回调的作用域

    既然函数也是对象,那么函数也存在于某个作用域下。想象一下如果回调函数内使用了this对象,this应该指向哪个对象?我们可以做如下实验:

    var myapp = {
        color: "green",
        paint: function () {
            console.log(this.color);
        }
    };
    
    var master = {
        color: "red"
    };
    
    myapp.paint();  // 输出green
    
    master.callback = myapp.paint;
    master.callback();  // 输出red

    可见,this所指代的对象严格遵从于作用域的限制,只要充分理解了作用域的范围,就不难理解回调的作用域。

    自定义上下文

    我们也可手动指定函数执行的上下文。下例展示了两种方式,均通过函数的call函数传入所执行的上下文环境,第二种方式是第一种的变种,仅需传入上下文对象和函数名:

    var myapp = {
        color: "green",
        paint: function () {
            console.log(this.color);
        }
    };
    
    var master = {
        color: "red"
    };
    
    myapp.paint();  // 输出green
    
    master.invokeCallback = function (sender, callback) {
        callback.call(sender);
    };
    master.invokeCallback(myapp, myapp.paint);  // 输出green
    
    master.invokeCallback2 = function (sender, func) {
        sender[func].call(sender);
    };
    master.invokeCallback2(myapp, "paint"); // 输出green

    返回函数

    本篇已经多次提到函数是对象,因此一个函数也可以返回另一个函数,例如:

    var setup = function () {
        var count = 0;
        return function () {
            return count++;
        }
    }
    var next = setup();
    console.log(next());  // 输出0
    console.log(next());  // 输出1
    console.log(next());  // 输出2

    返回函数的好处在于,返回的函数仅暴露出原函数的一个子集,示例中的count变量对外是不可见的。setup包装了返回函数,它创建了一个闭包,可以使用这个闭包存储一些私有数据,而这些数据仅可被该返回函数访问,但对外部代码却无法访问。

    即时函数

    即时函数模式是一种可以支持在定义函数后立即执行该函数的语法。例如:

    (function () {
        console.log("Hello Immediate Function!");
    }());  // 输出 Hello Immediate Function!

    这种模式本质上只是一个函数表达式,该函数会在创建后立即执行。该模式由以下几部分组成:

    • 可以使用函数表达式定义一个函数(函数声明则无法达到这个效果);
    • 在末尾添加一组括号,这将导致该函数立即执行;
    • 将整个函数包装在括号中(只有不将该函数分配给变量才需要这样做)。

    这种模式非常有用,如果某个函数仅会被执行一次,同时又需要一些临时变量,那么我们没有必要将函数赋值给某个变量(函数表达式)或给函数定义函数名(函数声明)来调用。考虑下面这样一个例子,该函数输出当天的日期,函数内用到了一些局部变量:

    (function () {
        var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
            today = new Date(),
            msg = "Today is " + days[today.getDay()] + ", " + today.getDate();
        console.log(msg);
    }());  // 输出 Today is Thu, 2

    如果该段代码没有包装到即时函数中,那么days、today、msg变量将会成为全局变量。也就是说即时函数为代码的执行提供了一个沙箱。

    即时函数的参数

    也可以将参数传递到即时函数中,例如:

    (function (god) {
        console.log("Hello " + god);
    }("Tale Xu"));  // 输出 Hello Tale Xu

    即时函数的返回值

    正如任何其他函数一样,即时函数可以返回值,并且这些返回值也可以分配给变量,例如:

    var date = (function () {
        today = new Date();
        return today.getDate();
    }());
    console.log(date);  // 输出 2

    即时函数和返回函数结合可以用于给即时函数添加局部变量:

    var sayHello = (function () {
        var god = "Tale Xu";
        return function () {
            console.log("Hello " + god);
        };
    }());
    console.log(typeof sayHello.god);  // 输出 undefined
    sayHello();  // 输出 Hello Tale Xu
    示例中的变量god只对即时函数所返回的匿名函数可见,对外不可见。

    即时对象初始化

    如果即时函数涉及到非常复杂的逻辑,仅凭即时函数和局部变量无法很好地组织代码时,我们可以将即时函数和对象字面量相结合,该模式称为即时对象初始化。例如:

    ({
        max 600,
        maxheight: 400,
    
        gimmeMax: function () {
            return this.maxwidth + "*" + this.maxheight;
        },
    
        init: function () {
            console.log(this.gimmeMax());
        }
    }).init();  // 输出 600*400

    就语法而言,对待这种模式就像在使用对象字面量创建一个普通的对象。这种模式的优点与即时函数的优点是相同的:可以在执行一次性的初始化任务时保护全局命名空间。与仅仅将一堆代码包装到匿名函数的方法比,这种模式看起来涉及更多的语法特征,但是如果初始化任务更复杂,它会使整个初始化过程显得更有结构化。比如,私有帮助函数是非常难清晰可辨的,因为它们是临时对象的属性,而在即时函数模式中,它们就很可能只是分散在各处的函数而已。

    需要注意的是,这种模式主要适用于一次性的任务,而且在init()完毕后也没有对该对象的访问。但是如果想在init()完毕后保存对该对象的一个引用,可以通过在init()尾部添加"return this;"语句实现该功能。

    函数属性——备忘模式

    函数是对象,因此函数具有属性,可以在任何时候将自定义属性添加到你的函数中。自定义属性的一个应用是缓存函数结果,在下一次调用该函数时就不用重做计算,这个模式在一些算法中对于提升性能尤为有用。缓存函数结果也被称为备忘模式。

    在下面的例子中,函数myFunc创建了一个属性cache,该属性可以通过myFunc.cache像通常那样进行访问。cache属性是一个对象(即哈希对象),其中使用传递给函数的参数param作为键,而计算结果作为值。计算结果可以是需要的任意复杂数据结构:

    var myFunc = function (param) {
        if (!myFunc.cache[param]) {
            var result = {};
            // ...开销很大的操作...
            myFunc.cache[param] = result;
        }
        return myFunc.cache[param];
    };
    myFunc.cache = {};

    Curry

    本篇剩下的部分主要讨论有关Curry化和部分函数应用的主题。但是在深入讨论该主题之前,让我们首先看看函数应用准确的含义。

    函数应用

    我们很熟悉如何执行某个函数,即function(param),事实上还存在另一种执行方式,即function.apply(param)。下面我们通过一个示例说明:

    var sayHello = function (god) {
        return "Hello" + (god ? ", " + god : "") + "!";
    };
    console.log(sayHello());  // 输出 Hello!
    console.log(sayHello("Tale Xu"));  // 输出 Hello, Tale Xu!
    
    console.log(sayHello.apply(null, ["Tale Xu"]));  // 输出 Hello, Tale Xu!

    正如从上面的例子中所看到的,调用函数和应用函数可以得到完全相同的结果。apply()带有两个参数:第一个参数为将要绑定到该函数内部this的一个对象(或称为上下文对象),而第二个参数是一个数组或多个参数变量,这些参数将变成可用于该函数内部的类似数组arguments对象。如果第一个参数为null,那么this将指向全局对象,此时得到的结果就恰好如同当调用一个非指定对象时的方法。

    需要注意的是,除了apply()外,还有一个类似的call()方法,这个方法我们之前用到过。实际上call只是建立在apply上的语法糖而已。当函数仅带有一个参数时,可以根据实际情况避免创建只有一个元素的数组。例如:

    var sayHello = function (god) {
        return "Hello" + (god ? ", " + god : "") + "!";
    };
    
    console.log(sayHello.apply(null, ["Tale Xu"]));  // 输出 Hello, Tale Xu!
    console.log(sayHello.call(null, "Tale Xu"));  // 输出 Hello, Tale Xu!

    部分应用

    在其他编程语言中,我们可以为函数的参数设置一个默认值,在执行该函数时可以不传入具备默认值的参数。在javaScript中,我们可以实现类似的功能,并且由于javaScript中的函数也是对象,所以实现起来更加灵活。这种模式也称为部分应用,即我们仅应用了第一个参数。当执行部分应用时,并不会得到结果,相反,会获得另一个函数。使函数理解并处理部分应用的过程就称为Curry过程(Curring)。

    Curry化

    Curry和印度的咖喱没有任何关系,它来源于数学家Haskell Curry的名字。Curry化是一个转换过程,即我们执行函数转换的过程。

    那么如何Curry化一个函数?让我们看下边的例子:

    function add(x, y) {
        if (typeof y === "undefined") {
            return function (y) {
                return x + y;
            }
        }
        return x + y;
    }
    console.log(typeof add(5));  // 输出 function
    console.log(add(3)(4));  // 输出 7
    var add2000 = add(2000);
    console.log(add2000(14));  // 输出 2014

    在上面的代码段中,如果调用add时只传入了一个参数,那么返回的结果是一个新的函数,这个新函数将调用add时传入的参数作为add第一个参数的值。

    上述例子只能Curry化特定函数,下面介绍一种更通用的方法:

    function schonfinkelize(fn) {
        var slice = Array.prototype.slice,
            stored_args = slice.call(arguments, 1);
        return function () {
            var new_args = slice.call(arguments),
                args = stored_args.concat(new_args);
            return fn.apply(null, args);
        };
    }
    
    function add(x, y) {
        return x + y;
    }
    
    var newadd = schonfinkelize(add, 5);
    console.log(newadd(4));  // 输出 9
    
    // 一些灵活用法
    console.log(schonfinkelize(add, 5)(4));  // 输出 9
    
    function add2(a, b, c, d, e) {
        return a + b + c + d + e;
    }
    
    console.log(schonfinkelize(add2, 1, 2, 3)(5, 5));  // 输出 16
    
    var addOne = schonfinkelize(add2, 1);
    console.log(addOne(10, 10, 10, 10));  // 输出 41
    var addSix = schonfinkelize(addOne, 2, 3);
    console.log(addSix(5, 5));  // 输出 16

    示例中,schonfinkerlize为通用的Curry化函数,传入参数为要被Curry化的函数以及一些参数,返回值为Curry化后的函数。实际上该函数创建了一个闭包,缓存原函数和参数。

    何时使用Curry化

    当发现正在调用同一个函数,并且传递的参数绝大多数都是相同的,那么该函数可能是用于Curry化的一个很好的候选参数。可以通过将一个函数集合部分应用到函数中,从而动态创建一个新函数。这个新函数将会保存重复的参数,并且还会使用预填充原始函数所期望的完整参数列表。

    小节

    在javaScript中,有关函数的知识以及函数的正确用法是至关重要的。本篇讨论了有关函数的背景和术语。学习了JavaScript中函数的两个重要特性,即:

    • 函数是第一类对象(first-class object),可以作为带有属性和方法的值以及参数进行传递。
    • 函数提供了局部作用域,而其他大括号并不能提供这种局部作用域。此外还需要记住的是,声明的局部变量可被提升到局部作用域的顶部。

    创建函数的语法包括:

    • 命名函数表达式。
    • 函数表达式(与上面的相同,但缺少一个名字),通常也称为匿名函数。
    • 函数声明,与其他编程语言中的函数语法类似。

    在涵盖了函数的背景和语法之后,将学习到一些有用的模式,可以将它们分为以下几类:

    1. API模式,它们可以帮助您为函数提供更好且更整洁的接口。这些模式包括以下几个:
      • 回调模式:将函数作为参数进行传递。
      • 配置对象: 有助于保持受到控制的函数的参数数量。
      • 返回函数:当一个函数的返回值是另一个函数时。
      • Curry化:当新函数是基于现有函数,并加上部分参数列表创建时。
    2. 初始化模式,它们可以帮助您在不污染全局命名空间的情况下,使用临时变量以一种更加整洁、结构化的方式执行初始化以及设置任务。这些模式包括:
      • 即时函数:只要定义之后就立即执行。
      • 即时对象初始化:匿名对象组织了初始化任务,提供了可被立即调用的方法。
      • 初始化时分支:帮助分支代码在初始化代码执行过程中仅检测一次,这与以后在程序生命周期内多次检测相反。
    3. 性能模式,可以帮助加速代码运行,这些模式包括:
      • 备忘模式:使用函数属性以便使得计算过的值无须再次计算。
      • 自定义模式:以新的主体重写自身,以使得在第二次或以后调用时仅需执行更少的工作。
  • 相关阅读:
    机器学习(01)——机器学习简介
    Harbor本地镜像库安装与使用
    大数据高可用集群环境安装与配置(10)——安装Kafka高可用集群
    大数据高可用集群环境安装与配置(09)——安装Spark高可用集群
    大数据高可用集群环境安装与配置(08)——安装Ganglia监控集群
    大数据高可用集群环境安装与配置(07)——安装HBase高可用集群
    大数据高可用集群环境安装与配置(06)——安装Hadoop高可用集群
    大数据高可用集群环境安装与配置(05)——安装zookeeper集群
    大数据高可用集群环境安装与配置(04)——安装JAVA运行环境
    大数据高可用集群环境安装与配置(03)——设置SSH免密登录
  • 原文地址:https://www.cnblogs.com/talexu/p/3499685.html
Copyright © 2011-2022 走看看