JS数据类型之函数

定义

函数可以理解为一个特定的代码块容器,它可以完成特定的需求,并且可以重复使用。

函数声明和调用

函数默认不会主动执行,必须通过函数名()调用才会执行。

函数声明

JavaScript 有三种声明函数的方法。

function 命令

function 命令声明的代码区块,就是一个函数。function命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。

function print(s) {
    console.log(s);
}

上面的代码命名了一个 print 函数,以后使用 print 这种形式,就可以调用相应的代码。这叫做函数的声明。

函数表达式

可以采取变量赋值的方法命名函数

var print = function(s) {
  console.log(s);
};

上面代码是将一个匿名函数赋值给变量。此时,这个匿名函数又称函数表达式,因为赋值语句的右侧只能放表达式。

需要注意的是:

  • function 命令后面不带有函数名;
  • 函数的表达式需要在末尾加上分号,表示语句结束。

Function 构造函数

使用 Function 构造函数,这种声明函数的方式非常不直观,几乎无人使用。

var add = new Function(
    'x',
    'y',
    'return x + y'
);

// 等同于
function add(x, y) {
  return x + y;
}

上面代码中,Function 构造函数接受了三个参数,除了最后一个函数是 add 函数的函数体,其他都是 add 函数的参数。

Function 构造函数可以不使用 new 命令,返回结果完全一样。

函数的重复声明

如果同一个函数被重复声明,后面的声明就会覆盖前面的声明。

function f() {
    console.log(1)
}
f(); // 2

function f() {
    console.log(2)
}
f(); // 2

上面代码中,后一次的函数声明覆盖了前面一次。而且,由于函数名的提升,前一次的声明在任何时候都是无效的

函数名的提升

JavaScript 引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。

f();
function f(){}

上面代码中看起来像在声明之前就调用了函数 f。但是实际上,由于“变量提升”,函数 f 被提升到了代码头部,也就是在调用之前就已经声明了。

函数参数

函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结构,这种外部数据就叫做参数。

参数的省略

function f(a, b) {
    return a;
}

f(1, 2, 3); // 1
f(1); // 1
f(); // undefined

console.log(f.length); // 2

上面代码的函数 f 定义了两个参数,但是运行时无论提供多少个参数(或者不提供参数),JavaScript都不会报错。省略的参数的值就变为 undefined 。需要注意的是,函数的 length 属性与实际传入的参数个数无关,只反映函数预期传入的参数个数。

但是,没有办法只省略靠前的参数,而保留靠后的参数,如果一定要省略靠前的参数,只有显示传入 undefined

传递方式

函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递。这意味着,在函数体内修改函数值,不会影响到函数外部。

var p = 2;

function f(p) {
    p = 3;
}

f(p);

console.log(p) // 2

上面代码中,变量 p 是一个原始类型的值,传入函数 f 是传值传递。因此,在函数内部,p 的值是原始值的拷贝,无论怎么修改,都不会影响到原始值。

但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。

var obj = { p: 1 };
function f(o) {
    o.p = 2;
}
f(obj);

console.log(obj.p); // 2

上面代码中,传入函数 f 的是参数对象 obj 的地址。因此,在函数内部修改 obj 的属性 p,会影响到原始值。

注意,如果函数内部修改的不是参数的某个属性,而是替换掉整个参数,此时不会影响到原始值。(这是因为,形参的值是实参的地址,重新对行参赋值导致行参指向另一个地址,保存在原地址上的值当然不受影响)

arguments 对象

由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内读取所有参数。这就是 arguments 对象的由来。

arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。

需要注意的是,虽然 arguments 很像数组,但它是一个对象。数组专有的方法(比如 sliceforEach ),不能在 arguments 对象上使用。

函数的返回值

将值返回给调用函数的地方,使用return表示。

function doSomething() {
    // doing
    return result;
}

let res = doSomething(); // 可得到函数doSomething的结果

函数体内部的return语句,表示返回。JavaScript 引擎遇到return语句,就直接返回return后面的那个表达式的值,后面即使还有语句,也不会得到执行。return语句不是必需的,如果没有的话,该函数就不返回任何值,或者说返回undefined

闭包

函数内部可以直接读取全局变量。

var n = 999;
function f1() {
    console.log(n)
}
f1(); // 999

但是,正常情况下, 函数外部无法读取函数内部的变量。

function f1() {
    var n = 999;
}
console.log(n);
// Uncaught ReferenceError: n is not defined(

如果处于种种原因,需要得到函数内部的变量。正常情况下,这是办不到的。只有通过在函数的内部,再定义一个函数。

function f1() {
    var n = 999;
    function f2() {
        console.log(n)
    }
}

上面代码中,函数 f2 就在函数 f1 内部,此时 f1 内部的所有局部变量,对 f2 都是可见的。但是反过来则不行。这就是 JavaScript 语言特有的“链式作用域”结构,子对象会一级一级地向上寻找所有父对象的变量。

既然 f2 可以读取 f1 的局部变量,那么只要把 f2 作为返回值,我们便可在 f1 的外部读取它的内部变量了。

function f1() {
    var n = 999;
    function f2() {
        console.log(n);
    }
    return f2;
}

var result = f1();
console.log(result()); // 999

上面代码中,函数 f1 的返回值就是函数 f2,由于 f2 可以读取 f1 的内部变量,所有就可以在外部获得 f1 的内部变量了。

闭包就是函数 f2,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解为“定义在一个函数内部的函数”。闭包最大的特点,就是它可以记住诞生的环境,比如 f2 就记住了它诞生的环境 f1,所以从 f2 可以得到 f1 的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

闭包的最大用处有两个,一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保存在内存中,即闭包可以使得它诞生的环境一直存在。

function createIncrementor(start) {
    return function() {
        return start++;
    }
}
var inc = createIncrementor(5);

inc(); // 5
inc(); // 6
inc(); // 7

上面代码中,start是函数createIncrementor的内部变量。通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc使得函数createIncrementor的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。

注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。

这些信息可能会帮助到你: 关于我们 | 红包返钱 | 捐赠支持

文章名称:JS数据类型之函数
文章链接:https://www.bysjb.cn/js-function.html
THE END
分享
二维码
打赏
< <上一篇
下一篇>>