Javascript指南-笔记

第3章_函数

2019-01-08  本文已影响9人  往事随风_0817

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

定义和使用函数

什么是函数

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

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

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

调用函数

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

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

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

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

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

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

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

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

转义特殊字符

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

变量的作用域

即在 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 ('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()内了。所以:

利用闭包突破作用域链

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"

本章小结

上一篇下一篇

猜你喜欢

热点阅读