小白科普之JavaScript的函数

时间:2023-03-09 17:59:39
小白科普之JavaScript的函数

一 概述

1.1 函数声明

(1)function命令

  函数就是使用function命令命名的代码区块,便于反复调用。这种声明方式叫做函数的声明(Function Declaration)。

function print(){
// ...
}

(2)函数表达式

  除了用function命令声明函数,还可以采用变量赋值的写法。

var print = function (){
// ...
};

  这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。采用函数表达式声明函数时,function命令后面不带有函数名。如果加上函数名,该函数名将成为函数内部的一个局部变量,只在函数体内部有效,在函数体外部无效。

var print = function x(){
console.log(typeof x);
}; x // ReferenceError: x is not defined print() // function

(3)Function构造函数

  还有第三种声明函数的方式:通过Function构造函数声明。如果只有一个参数,该参数就是函数体。

var add = new Function("x","y","return (x+y)");
// 相当于定义了如下函数
// function add(x, y) {
// return (x+y);
// }

1.2 return语句

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

1.3 第一等公民

  JavaScript的函数与其他数据类型处于同等地位,可以使用其他数据类型的地方就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。这表示函数与其他数据类型的地方是平等,所以又称函数为第一等公民。

function add(x,y){
return x+y;
} // 将函数赋值给一个变量
var operator = add; // 将函数作为参数和返回值
function a(op){
return op;
}
a(add)(1,1) //

1.4 函数名的提升

  JavaScript引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会被提升到代码头部。所以,下面的代码不会报错:

f();
function f(){}

  表面上,上面代码好像在声明之前就调用了函数f。但是实际上,由于“变量提升”,函数f被提升到了代码头部,也就是在调用之前已经声明了。但是,如果采用赋值语句定义函数,JavaScript就会报错:

f();
var f = function (){}; // TypeError: undefined is not a function

上面的代码等同于:

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

  当调用f的时候,f只是被声明,还没有被赋值,等于undefined,所以会报错。因此,如果同时采用function命令和赋值语句声明同一个函数,最后总是采用赋值语句的定义。

// 由于不带var的function声明被提前解析,被带var的函数覆盖
var f = function() {
console.log ('1');
} function f() {
console.log('2');
} f() //

1.5 不能声明函数的情况

  函数声明语句并非真正的语句,ECMAScript规范只是允许他们作为*语句。它们可以出现在全局代码里,或者内嵌在其他函数里,但它们不能出现在循环、条件判断、或者try/catch/finally以及with语句中。

1.6 name属性

  大多数JavaScript引擎支持非标准的name属性。该属性返回紧跟在function关键字之后的那个函数名。

function f1() {}
f1.name // 'f1' var f2 = function () {};
f2.name // '' var f3 = function myName() {};
f3.name // 'myName'

  上面代码中,函数的name属性总是返回紧跟在function关键字之后的那个函数名。对于f2来说,返回空字符串,对于f3来说,返回函数表达式的名字(真正的函数名还是f3,myName这个名字只在函数体内部可用)。

2. 函数的实参和形参

2.1 length属性

  所有函数都有一个length属性,返回函数定义中参数的个数。length属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的”方法重载“(overload)。

function f(a,b) {}

f.length //

  上面代码定义了空函数f,它的length属性就是定义时参数的个数。不管调用时输入了多少个参数,length属性始终等于2。

2.2 参数的省略

  参数不是必需的,Javascript语言允许省略参数。

function f(a,b){
return a;
} f(1,2,3) //
f(1) //
f() // undefined f.length //

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

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

2.3 传参方式

  JavaScript的函数参数传递方式是传值传递(passes by value),这意味着,在函数体内修改参数值,不会影响到函数外部。

// 修改原始类型的参数值
var p = 2; function f(p){
p = 3;
} f(p);
p // // 修改复合类型的参数值
var o = [1,2,3]; function f(o){
o = [2,3,4];
} f(o);
o // [1, 2, 3]

  上面代码分成两段,分别修改原始类型的参数值和复合类型的参数值。两种情况下,函数内部修改参数值,都不会影响到函数外部。

  需要十分注意的是,虽然参数本身是传值传递,但是对于复合类型的变量来说,属性值是传址传递(pass by reference),也就是说,属性值是通过地址读取的。所以在函数体内修改复合类型变量的属性值,会影响到函数外部。

// 修改对象的属性值
var o = { p:1 }; function f(obj){
obj.p = 2;
} f(o);
o.p // // 修改数组的属性值
var a = [1,2,3]; function f(a){
a[0]=4;
} f(a);
a // [4,2,3]

  上面代码在函数体内,分别修改对象和数组的属性值,结果都影响到了函数外部,这证明复合类型变量的属性值是传址传递。

  某些情况下,如果需要对某个变量达到传址传递的效果,可以将它写成全局对象的属性。

var a = 1;

function f(p){
window[p]=2;
} f('a'); //变量a本来是传值传递,但是写成window对象的属性,就达到了传址传递的效果。 a //

2.4 可变长的实参列表arguments对象

  ECMAScript函数不介意传递进来多少个参数,也不在乎传进来的参数是什么类型的,原因是在ECMAScript中的参数是用一个数组表示的,这个数字就是arguments。arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,依次类推。这个对象只有在函数体内部,才可以使用。

var f = function(one) {
console.log(arguments[0]);
console.log(arguments[1]);
console.log(arguments[2]);
} f(1, 2, 3)
//
//
//

  arguments对象除了可以读取参数,还可以为参数赋值(严格模式不允许这种用法)。可以通过arguments对象的length属性,判断函数调用时到底带几个参数。

(1)与数组的关系

  需要注意的是,虽然arguments很像数组,但它是一个对象。某些用于数组的方法(比如slice和forEach方法),不能在arguments对象上使用。但是,有时arguments可以像数组一样,用在某些只用于数组的方法。比如,用在apply方法中,或使用concat方法完成数组合并。

// 用于apply方法
myfunction.apply(obj, arguments). // 使用与另一个数组合并
Array.prototype.concat.apply([1,2,3], arguments)

(2)caller和callee

  callee属性指代当前正在执行的函数,caller指代调用当前正在执行的函数的函数。用caller属性可以访问调用栈。

3 函数调用

3.1 作为函数调用

var total = distance(0,0,2,1) + distance(2,1,3,5);
var probability = factorial(5) / factorial(13);

3.2 作为方法调用

  一个方法就是保存在一个对象的属性里的JavaScript函数。例如:

o.m = f; //给o定义一个名为m()的方法
o.m() //调用m()函数的方法
o.m(x.y) // m()函数包含两个参数

  方法调用和函数调用有一个重要的区别,即:调用上下文。属性访问表达式由两部分组成:一个对象(例如o)和属性名称(m)。在像这样的方法调用表达式里,对象o成为调用上下文,函数体可以使用关键字this引用该对象。

3.3 作为构造函数

var o = new Object();

  如果函数或方法调用之前带有关键字new,它就构成了构造函数调用。构造函数调用会试图创建一个新的空对象,这个对象继承自构造函数的prototype属性。

3.4 间接调用

  JavaScript中的函数也是对象,和其他JavaScript对象没有两样,函数对象也可以包含方法。其中call()和apply()可以用来间接的调用函数。两个方法都允许显式指定调用所需的this值,也就是说,任何函数可以作为任何对象的方法来调用,哪怕这个函数不是这个对象的方法。

  call()和apply()看做某个对象的方法,通过调用方法的形式来间接调用函数。call()和apply()的第一个参数是要调用函数的母对象,它是调用上下文(运行函数的作用域),在函数体内通过this来获得对它的引用。

// 以对象o的方法来调用函数f()
f.call(o);
f.apply(o);

  对于call()来说,第一个调用上下文参数之后的所有实参就是要传入待调用函数的值。

f.call(o,1,2); //以对象o的形式调用函数f(),并传入两个参数

  apply()方法和call()方法类似,但传入实参的形式和call()有所不同,它的实参都放入一个数组中。传入apply()的参数数组可以是类数组对象也可以是真实数组。

f.apply(o, [1,2]);

4 作为命名空间的函数

  由于在函数中声明的变量在整个函数体内都是可见的,在函数的外部是不可见的。而不在任何函数中声明的变量是全局变量,为了不污染全局命名空间,可以声明一个函数用作临时的命名空间。

// 将代码放入一个函数内,然后调用这个函数
(function(){
// 代码模块
}());

  function之前的左圆括号是必需的,因为如果不写这个左圆括号,JavaScript解释器会试图将关键字function解析为函数声明语句。使用圆括号JavaScript解释器才会正确地将其解析为函数定义表达式。

var students = (function() {
// 这里定义了很多类如课程类/成绩类, 使用局部变量和函数
function Subject() { /* ... */ }
function Grade() { /* ... */ } // 通过返回命名空间对象将API导出
return {
Subject: Subject,
Grade: Grade
};
}());

5. 函数属性、方法  

5.1 length属性

  函数的length属性是只读的,代表函数实参的数量,这里的参数指的是“形参”而非“实参”,也就是函数定义时给出的形参个数,通常也是在函数调用时期望传入的实参个数。

function check(args){
var actual = args.length;
var expected = args.callee.length;
if(actual !== expected){
throw Error("Expected "+expected+"args; got"+actual);
}
}
function f(x, y, z){
check(arguments);
return x+y+z;
}
console.log(f(1,2,3)); //

5.2 prototype属性

  每个函数都包含一个prototype属性,这个属性是指向一个对象的引用,这个对象称作“原型对象”。当将函数用做构造函数时,新创建的对象会从原型对象上继承属性。

5.3 bind()方法

  bind()方法:将函数绑定至某个对象。在函数f()上调用bind()方法并传入一个对象o作为参数时,这个方法返回一个新函数。调用新的函数将会把原始的函数f()当做o的方法来调用。传入新函数的任何实参都是传入原始函数。

function f(y) {return this.x + y;}
var o = {x:1};
var g = f.bind(o);
console.log(g(2)); // 

5.4 toString()方法

  toString方法返回函数的源码。

function f() {
a();
b();
c();
} console.log(f.toString());
// function f() {
// a();
// b();
// c();
// }