第3章_函数

作者: 往事随风_0817 | 来源:发表于2019-01-08 21:26 被阅读9次

    绝大部分语言都有自己专门的面向对象的语法,而 JavaScript 没有:它是通过函数来实现面向对象特性的。

    定义和使用函数

    什么是函数

    所谓函数,本质上是一种代码的分组形式。我们可以通过这种形式赋予某组代码一个名字,以便于之后的调用。

    //函数的声明
    function sum(a, b) {
        var c = a + b;
        return c;
    }
    

    函数声明通常由以下几部分组成

    • 关键词 [function]

    • 函数名称,即这里的 sum

    • 函数所需的参数,即这里的 a、 b。一个函数通常都具有 0 个或多个参数。参数之间用逗号分隔

    • 函数所要执行的代码块,我们称之为函数体

    • return 子句。函数通常都会有返回值,如果某个函数没有显式的返回值,我们就会默认它的返回值为 undefined。

    调用函数

    调用的方式很简单,只需在函数名后面加一对用以传递参数的括号即可

    var result = sum(1, 2);
    
    
    result;
    
    3
    

    参数

    但如果您设定了,而又在调用时忘了传递相关的参数值, JavaScript 引擎就会自动将其设定为 undefined。例如在下面这个调用中,函数返回的是 NaN,因为这里试图将 1 与 undefined 相加。

    sum(1);
    NaN
    
    • 形式参数 形参是指定义函数时所用的那些参数

    • 实际参数 实参则指的是在调用函数时所传递的那些参数

    function sum(a, b){
        return a + b;
    }
    
    sum(1, 2);
    

    对于那些已经传递进来的参数, JavaScript 是来者不拒的。但是,即便我们向 sum()传递再多的参数,多余的那部分也只会被默默地忽略掉

    arguments 变量

    该变量为内建变量,每个函数中都能调用。它能返回函数所接收的所有参数

    function args() {
    
        return arguments;
    }
    args();
    
    []
    
    args( 1, 2, 3, 4, true, 'ninja');
    
    [1, 2, 3, 4, true, "ninja"]
    

    arguments 实际上不是一个数组(虽然它有很多数组的特性),而是一 个类似数组的对象。

    预定义函数

    JavaScript 引擎中有一组可供随时调用的内建函数

    • parseInt()

    parseInt()会试图将其收到的任何输入值(通常是字符串)转换成整数类型输出。如果转换失败就返回 NaN

    该函数还有个可选的第二参数:基数(radix),它负责设定函数所期望的数字类型—十进制、十六进制、二进制等

    如果我们在调用 parseInt()时没有指定第二参数,函数就会将其默认为十进制,但有两种情况例外。

    1. 如果首参数字符串是 0x 开头,第二参数就会被默认指定为 16(也就是默认其为十六进制数)。

    2. 如果首参数以 0 开头, 第二参数就会被默认指定为 8(也就是默认其为八进制数)。

    • parseFloat()

    parseFloat()的功能与 parseInt()基本相同,只不过它仅支持将输入值转换为十进制数。因此,该函数只有一个参数。

    此外, parseFloat()还可以接受指数形式的数据(这点与 parseInt()不同)

    • isNaN() 通过 isNaN(),我们可以确定某个输入值是否是一个可以参与算术运算的数字

    • isFinite() isFinite()可以用来检查输入是否是一个既非 Infinity 也非 NaN 的数字。

    转义特殊字符

    • encodeURI()

    • decodeURI()

    分别都有各自对应的反编码函数

    • encodeURIComponent()

    • decodeURIComponent()

    • eval() eval()会将其输入的字符串当做 JavaScript 代码来执行

    变量的作用域

    即在 JavaScript 中,变量的定义并不是以代码块作为作用域的,而是以函数作为作用域

    var global = 1;
    function f() {
        var local = 2;
        global++;
        return global;
    }
    
    > f();
    2
    > f();
    3
    > local;
    ReferenceError: local is not defined
    

    这里还有一点很重要,如果我们声明一个变量时没有使用 var 语句,该变量就会被默认为全局变量。

    在该函数被调用之前,这个变量是不存在的。该变量会在函数首次被调用时创建,并被赋予全局作用域。这使得我们可以在该函数以外的地方访问它。

    变量提升

    var a = 123;
    
    function f() {
    
        alert(a);
    
        var a = 1;
    
        alert(a);
    
    }
    
    
    f();
    

    但事实并非如此,第一个 alert()实际上显示的是undefined这是因为函数域始终优先于全局域所以局部变量 a 会覆盖掉所有与它同名的全局变量,尽管在 alert()第一次被调用时, a 还没有被正式定义(即该值为undefined),但该变量本身已经存在于本地空间了。这种特殊的现象叫做提升(hoisting)。

    函数也是数据

    在 JavaScript 中,函数实际上也是一种数据

    var f = function() {
    
        return 1;
    
    };
    

    上面这种定义方式通常被叫做函数标识记法(function literal notation)。

    function(){ return 1;}是一个函数表达式。函数表达式可以被命名,称为命名函数表达式(named function expression, NFE)。所以以下这种情况也是合法的,虽然我们不常常用到(在这里, myFunc 是函数的名字,而不是变量; IE 会错误地创建 f 和 myFunc两个变量)

    var f = function myFunc() {
    
        return 1;
    
    };
    

    这样看起来,似乎命名函数表达式与函数声明没有什么区别。但它们其实是不同的。两者的差别表现于它们所在的上下文。

    如果我们对函数变量调用 typeof,操作符返回的字符串将会是"function"。所以, JavaScript 中的函数也是一种数据,只不过这种特殊的数据类型有两个重要的特性

    • 它们所包含的是代码

    • 它们是可执行的(或者说是可调用的)

    将函数拷贝给不同的变量

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

    匿名函数

    定义一个函数:

    var f = function(a){
    
        return a;
    
    };
    

    通过这种方式定义的函数常被称为匿名函数(即没有名字的函数)

    • 您可以将匿名函数作为参数传递给其他函数,这样,接收方函数就能利用我们所传递的函数来完成某些事情

    • 您可以定义某个匿名函数来执行某些一次性任务

    回调函数

    既然函数与任何可以被赋值给变量的数据是相同的,那么它当然可以像其他数据那样 被定义、删除、拷贝,以及当成参数传递给其他函数。

    function invokeAdd(a, b){
    
        return a() + b();
    
    }
    
    function one() {
    
        return 1;
    
    }
    
    function two() {
    
        return 2;
    
    }
    
    > invokeAdd(one, two);
    
    3
    
    //另外可以直接使用匿名函数
    > invokeAdd(
    
        function () { return 1; },
    
        function () { return 2; }
    
    );
    
    3
    

    什么时候使用回调函数呢?回调函数的优势,包括:

    • 它可以让我们在不做命名的情况下传递函数(这意味着可以节省变量名的使用)

    • 我们可以将一个函数调用操作委托给另一个函数(这意味着可以节省一些代码编写工作)

    • 它们也有助于提升性能

    回调示例

    //三个参数分别乘以 2
    function multiplyByTwo(a, b, c) {
    
        var i, ar = [];
    
        for(i = 0; i < 3; i++) {
    
            ar[i] = arguments[i] * 2;
    
        }
    
        return ar;
    
    }
    
    //传参+1
    function addOne(a) {
    
        return a + 1;
    
    }
    
    > var myarr = [];
    
    > myarr = multiplyByTwo(10, 20, 30);
    
    [20, 40, 60]
    
    
    //然后,用循环遍历每个元素,并将它们分别传递给 addOne()。
    
    > for (var i = 0; i < 3; i++) {
    
        myarr[i] = addOne(myarr[i]);
    
    }
    
    
    > myarr;
    
    [21, 41, 61]
    
    
    //改善方案
    function multiplyByTwo(a, b, c, callback) {
    
        var i, ar = [];
    
        for(i = 0; i < 3; i++) {
    
            ar[i] = callback(arguments[i] * 2);
    
        }
    
        return ar;
    
    }
    
    > myarr = multiplyByTwo(1, 2, 3, addOne);
    
    [3, 5, 7]
    
    //同样, 我们还可以用匿名函数来代替 addOne()
    > multiplyByTwo(1, 2, 3, function (a){
    
        return a + 1;
      });
    
    [3, 5, 7]
    

    即时函数

    这种函数可以在定义后立即调用

    //方式一
    (
    
        function(){
    
            alert('boo');
    
        }
    
    )();
    
    //方式二
    (
    
        function(name){
    
            alert('Hello ' + name + '!');
    
        }
    
    )('dude');
    
    另外,您也可以将第一对括号闭合于第二对括号之后。这两种做法都有效。
    
    (function () {
    
        // …
    
    } () );
    
    
    // vs.
    
    (functioin () {
    
        // …
    
    })();
    

    使用即时(自调)匿名函数的好处是不会产生任何全局变量。

    缺点在于这样的函数是无法重复执行的(除非您将它放在某个循环或其他函数中)。即时函数非常适合于执行一些一次性的或初始化的任务

    内部(私有)函数

    一个函数内部定义另一个函数

    function outer(param) {
    
        function inner(theinput) {
    
        return theinput * 2;
    
        }
    
        return 'The result is ' + inner(param);
    
    }
    
    //我们也可以改用函数标识记法来写这段代码:
    var outer = function (param) {
    
        var inner = function (theinput) {
    
        return theinput * 2;
        };
    
        return 'The result is ' + inner(param);
    
    };
    

    当我们调用全局函数 outer()时,本地函数 inner()也会在其内部被调用。

    使用私有函数的好处主要有以下几点:

    • 有助于我们确保全局名字空间的纯净性(这意味着命名冲突的机会很小)。

    • 确保私有性—这使我们可以选择只将一些必要的函数暴露给“外部世界”,而保留属于自己的函数,使它们不为该应用程序的其他部分所用

    返回函数的函数

    函数始终都会有一个返回值,即便不是显式返回,它也会隐式返回一个 undefined。既然函数能返回一个唯一值,那么这个值就也有可能是另一个函数。

    function a() {
    
        alert('A!');
    
        return function(){
    
        alert('B!');
    
        };
    
    }
    

    在这个例子中,函数 a()会在执行它的工作(弹出'A!')之后返回另一个函数。而所返回的函数又会去执行另外一些事情(弹出'B!')。我们只需将该返回值赋值给某个变量,然后就可以像使用一般函数那样调用它了。

    > var newFunc = a();
    
    > newFunc();
    
    //第一行执行的是 alert('A!'),第二行才是 alert ('B!')
    

    如果您想让返回的函数立即执行,也可以不用将它赋值给变量,直接在该调用后面再加一对括号即可,效果是一样的

    > a()();
    

    能重写自己的函数

    由于一个函数可以返回另一个函数,因此我们可以用新的函数来覆盖旧的。例如在之前的例子中,我们也可以通过 a()的返回值来重写 a()函数自己:

    > a = a();
    

    外面来重定义该函数的—即我们将函数返回值赋值给函数本身。但我们也可以让函数从内部重写自己。例如:

    function a() {
    
        alert('A!');
    
        a = function(){
    
        alert('B!');
    
        };
    
    }
    

    这样一来,当我们第一次调用该函数时会有如下情况发生

    • alert ('A!')将会被执行(可以视之为一次性的准备操作)。

    • 全局变量 a 将会被重定义,并被赋予新的函数。

    而如果该函数再被调用的话,被执行的就将是 alert ('B!')了

    var a = (function () {
    
        function someSetup () {
            var setup = 'done';
    
        }
    
        function actualWork() {
    
            alert('Worky-worky');
    
        }
    
        someSetup();
    
        return actualWork;
    
    }() );
    

    闭包

    作用域链

    尽管 JavaScript 中不存在大括号级的作用域,但它有函数作用域,也就是说,在某函数内定义的所有变量在该函数外是不可见的。但如果该变量是在某代码块中被定义的(如在某个 if 或 for 语句中),那它在代码块外是可见的。

    > var a = 1;
    
    > function f() {
    
        var b = 1;
    
        return a;
    
    }
    
    > f();
    
    1
    
    > b;
    
    ReferenceError: b is not defined
    

    在这里,变量 a 是属于全局域的,而变量 b 的作用域就在函数 f()内了。所以:

    • 在 f()内, a 和 b 都是可见的;

    • 在 f()外, a 是可见的, b 则不可见。

    利用闭包突破作用域链

    var a = "global variable";
    
    var F = function () {
    
        var b = "local variable";
    
        var N = function () {
    
            var c = "inner local";
    
        };
    
    };
    

    究竟是如何突破作用域链的呢?我们只需要将它们升级为全局变量(不使用var 语句)或通过 F 传递(或返回)给全局空间即可

    闭包#1

    首先,我们先来看一个函数。这个函数与之前所描述的一样,只不过在 F 中多了返回N,而在函数 N 中多了返回变量 b, N 和 b 都可通过作用域链进行访问。

    var a = "global variable";
    
    var F = function () {
    
        var b = "local variable";
    
        var N = function () {
    
            var c = "inner local";
    
            return b;
    
        };
    
        return N;
    
    };
    
    //函数 F 中包含了局部变量 b,因此后者在全局空间里是不可见的。
    
    > b;
    
    ReferenceError: b is not defined
    

    函数 N 有自己的私有空间,同时也可以访问 f()的空间和全局空间,所以 b 对它来说是可见的。因为 F()是可以在全局空间中被调用的(它是一个全局函数),所以我们可以将它的返回值赋值给另一个全局变量,从而生成一个可以访问 F()私有空间的新全局函数。

    > var inner = F();
    
    > inner();
    
    "local variable"
    

    闭包#2

    下面这个例子的最终结果与之前相同,但在实现方法上存在一些细微的不同。在这里 F()不再返回函数了,而是直接在函数体内创建一个新的全局函数 inner()

    var inner; // placeholder
    
    var F = function (){
    
        var b = "local variable";
    
        var N = function () {
    
            return b;
    
        };
    
        inner = N;
    
    };
    
    //现在,请读者自行尝试, F()被调用时会发生什么:
    > F();
    
    > inner();
    
    "local variable".
    

    我们在 F()中定义了一个新的函数 N(),并且将它赋值给了全局变量 inner。由于N()是在 F()内部定义的,它可以访问 F()的作用域,所以即使该函数后来升级成了全局函数,但它依然可以保留对 F()作用域的访问权。

    相关定义与闭包#3

    事实上,每个函数都可以被认为是一个闭包。因为每个函数都在其所在域(即该函数的作用域)中维护了某种私有联系。但在大多数时候,该作用域在函数体执行完之后就自行销毁了—除非发生一些有趣的事(比如像上一小节所述的那样),导致作用域被保持。但其实每个函数本身就是一个闭包,因为每个函数至少都有访问全局作用域的权限,而全局作用域是不会被破坏的。

    //子函数返回的则是其父函数的参数
    function F(param) {
    
        var N = function(){
    
            return param;
    
        };
        param++;
    
        return N;
    
    }
    //然后我们可以这样调用它:
    > var inner = F(123);
    
    > inner();
    
    124
    

    请注意,当我们的返回函数被调用时②, param++已经执行过一次递增操作了。所以inner()返回的是更新后的值。由此我们可以看出,函数所绑定的是作用域本身,而不是在函数定义时该作用域中的变量或变量当前所返回的值。

    循环中的闭包

    //该新函数会被添加到一个数组中,并最终返回
    function F() {
    
        var arr = [], i;
    
        for (i = 0; i < 3; i++) {
    
            arr[i] = function () {
    
                return i;
    
            };
    
        }
    
        return arr;
    
    }
    
    //下面,我们来运行一下函数,并将结果赋值给数组 arr。
    
    > var arr = F();
    
    //按通常的估计,它们应该会依照循环顺序分别输出 0、 1 和 2,下面就让我们来试试:
    > arr[0]();
    
    3
    
    > arr[1]();
    
    3
    
    > arr[2]();
    
    3
    

    显然,这并不是我们想要的结果。究竟是怎么回事呢?原来我们在这里创建了三个闭包,而它们都指向了一个共同的局部变量 i。但是,闭包并不会记录它们的值,它们所拥有的只是相关域在创建时的一个连接(即引用)。在这个例子中,变量 i 恰巧存在于定义这三个函数域中。对这三个函数中的任何一个而言,当它要去获取某个变量时,它会从其所在的域开始逐级寻找那个距离最近的 i 值。由于循环结束时 i 的值为 3,所以这三个函数都指向了这一共同值

    //那么,应该如何纠正这种行为呢?答案是换一种闭包形式:
    function F() {
    
        var arr = [], i;
    
        for(i = 0; i < 3; i++) {
    
            arr[i] = (function (x){
    
                return function () {
    
                return x;
    
            }
    
        }(i));
    
    }
    
    return arr;
    
    }
    
    //这样就能获得我们预期的结果了:
    > var arr = F();
    
    > arr[0]();
    
    0
    
    > arr[1]();
    
    1
    
    > arr[2]();
    
    2
    

    在这里,我们不再直接创建一个返回 i 的函数了,而是将 i 传递给了另一个即时函数。在该函数中, i 就被赋值给了局部变量 x,这样一来,每次迭代中的 x 就会拥有各自不同的值了。或者,我们也可以定义一个“正常点的”内部函数(不使用即时函数)来实现相同的 功能。要点是在每次迭代操作中,我们要在中间函数内将 i 的值“本地化

    function F() {
    
        function binder(x) {
    
            return function(){
    
                return x;
    
            };
    
        }
    
        var arr = [], i;
        for(i = 0; i < 3; i++) {
    
            arr[i] = binder(i);
    
        }
    
        return arr;
    
    }
    

    getter 与 setter

    var getValue, setValue;
    
    (function() {
    
        var secret = 0;
    
        getValue = function(){
    
            return secret;
    
        };
    
        setValue = function (v) {
    
            if (typeof v === "number") {
    
            secret = v;
    
            }
    
        };
    
    }());
    

    在这里,所有一切都是通过一个即时函数来实现的,我们在其中定义了全局函数setValue()和 getValue(),并以此来确保局部变量 secret 的不可直接访问性。

    > getValue();
    
    0
    
    > setValue(123);
    
    > getValue();
    
    123
    
    
    > setValue(false);
    
    > getValue();
    
    123
    

    迭代器

    将一些“谁是下一个”的复杂逻辑封装成易于使用的 next()函数,然后,我们只需要简单地调用 next()就能实现对于相关的遍历操作了

    function setup(x) {
    
        var i = 0;
    
        return function(){
    
            return x[i++];
    
        };
    
    }
    
    > next();
    
    "a"
    
    > next();
    
    "b"
    > next();
    
    "c"
    

    本章小结

    • 定义和调用函数的基础知识—您既可以使用函数声明语法,也可以使用函数表达式

    • 函数的参数及其灵活性

    • 内置函数—包括 parseInt()、 parseFloat()、 isNaN()、 isFinite()、 eval() 以及对 URL 执行编码、反编码操作的四个相关函数。

    • JavaScript 变量的作用域—尽管这些变量没有大括号级作用域,但它有函数作用域以及相关的作用域链。

    • 函数也是一种数据—即函数可以跟其他数据一样被赋值给一个变量,我们可以据此 实现大量有趣的应用。例如:

      • 私有函数和私有变量

      • 匿名函数

      • 回调函数。

      • 即时函数

      • 能重写自身的函数。

    • 闭包

    相关文章

      网友评论

        本文标题:第3章_函数

        本文链接:https://www.haomeiwen.com/subject/dbqirqtx.html