09. JS 函数

概念

语法

理解参数

  • arguments

  • arguments.callee

  • rest 参数

带有返回值的函数(return)

立即执行函数

递归

举例

一、概念

  • 函数就是对象,函数名是指针。每一个函数都是Function类型的实例,而且和其他引用类型一样具有属性和方法

  • 通过函数可以封装任意多条语句,而且可以在任何地方、任何时候调用执行

  • 函数使用function关键字来声明,后面跟一组参数以及函数体

二、语法

1. 函数的定义:

  • 函数声明

    • function 函数名(形式参数){函数体}

    • 重要特征是函数声明提升(function declaration hoisting),在执行代码之前会先读取函数声明。这就意味着可以把函数声明放在调用它的语句后面

  • 函数表达式

    • var functionName = function(形式参数){函数体}

    • var functionName(变量声明),然后 = function(arg0,...){} 赋值,整体就是个表达式。

    • 这种情况下创建的函数叫做匿名函数(anonymous function),因为 function 关键字后面没有标识符。

    • 匿名函数的 name 属性是空字符串(匿名函数有时候也叫拉姆达函数)

2. 函数的调用:函数名(参数);

3. 实列:

function sayHi(name,message){
	alert("Hello "+name+","+message);
}
sayHi("John","how are you?");//"John,how are you?"

三、理解参数

  • ECMAScript 中所有函数的参数都是按值传递的

  • JS 不介意传递进来多少个参数,也不在乎传递进来参数的数据类型

  • 传递进来的参数(实参)个数可以和定义的参数(形参)个数不相同

  • JS 中的参数在内部是用一个数组来表示的。函数接收的始终都是这个数组,不关心数组中包括那些参数

  • 实际上,在函数体内可以通过arguments对象来访问这个参数数组,从而获取传递给函数的每一个参数

  • 命名的参数只提供便利,但不是必须的

  • arguments对象可以与命名参数一起使用

  • arguments的值永远与对应命名参数的值保持同步

// 形式参数(形参)
function sum(a,b){  // 相当于 var a,b
    var c = a + b;
    document.write(c);
}

// 实际参数(实参) -- 调用函数的时候需要代入的实际参数
sum(1,2)    // 3

1. 访问变量有按值和按引用两种方式,而参数只能按值传递

  • 参数传递基本类型的值:被传递的值会被复制给一个局部变量(即命名参数,或者用 ECMAScript 的概念来说,就是 arguments 对象中的一个元素
function addTen(num){
    num += 10;  //不会影响函数外部的 count 变量
    return num; //参数 num 与变量 count 互不相识
}
var count = 20;
var result = addTen(count);
console.log(count); //20 没有变化
console.log(result);    //30
  • 参数传递引用类型的值: 会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部
function setName(obj){
    obj.name = "Nicholas",
    obj = new Object(),
    obj.name = "Greg"
}

var person = new Object();
setName(person);
console.log(person.name);   //"Nicholas"

2. arguments(内置的对象)

  • 在JS函数中,会自动创建一个名为arguments的内部变量,将所有参数的地址保存到其中,arguments类似数组对象,可以通过它来获取函数调用时所传递的参数(实参数列)。
function sum(a,b){  // a,b 就是形参

    // arguments -- [1,2]  实参列表
    // 相当于 var a = 1 , var b = 2

    console.log(sum.length); //形参的长度   //1
    console.log(arguments.length); //实参的长度 //3
}
//实际界参数 -- 实参
sum(1,2);
  • 形参和 arguments 虽然是改变哪一方另一方就会改变,但是并不是相等的,而是互相映射的关系。

  • 实参刚开始有什么,arguments 里面就只会什么

function sum(a,b){
    b = 2;
    console.log(arguments[1]);  // undefined 
    
    // arguments 里面只有实参 arguments[0] = 1
    // 这时候形参 b 只能作为变量使用。
}

sum(1);

  • 示例:
//不定参数求和

function  sum(){
    var result = 0;
    for(var i = 0; i < arguments.length; i++){
        result += arguments[i]; 
    }
    
    console.log(result);
}

sum(1,2,3,4,5);
  • 通过这种方式你可以很方便的找到最大的一个参数的值

  • 它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数(实参)。

  • arguments对象是所有(非箭头)函数中都可用的局部变量

  • 你可以使用arguments对象在函数中引用函数的参数

  • 此对象包含传递给函数的每个参数的条目,第一个条目的索引从0开始

  • 如果一个函数传递了三个参数,你可以以如下方式引用他们:

arguments[0]
arguments[1]
arguments[2]

参数也可以被设置:
arguments[1] = 'new value';
  • arguments对象不是一个数组

  • 它类似于数组,但除了 length 属性和索引元素之外没有任何数组的属性。

function foo(x){
	console.log('x = ' + x); // x = 10
	for(var i=0;i<arguments.length;i++){
		console.log('arg' + i + '=' + arguments[i]); 
		// arg0 = 10
		// arg1 = 20
		// arg2 = 30
	}
}
foo(10,20,30);

3. arguments 中的 callee,指向函数自身引用

function test() {
    console.log(arguments.callee);//打印函数test
    console.log(arguments.callee == test);//true
}
test();
// 求1到100的阶乘,arguments.callee 就是找到自己的引用
var num = (function (n) {
    if(n == 1){
        return 1;
    }
    return n * arguments.callee(n - 1);
}(100))
function test() {
    conso.log(arguments.callee);//打印test函数
    function demo () {
        conso.log(arguments.callee);//打印demo函数
    }
    demo();
}
test();
// 就是说在哪个函数里面的arguments.callee就指向那个函数的引用。

4. ES6标准引入了rest参数:

function foo(a,b, ...rest){
    console.log('a = '+ a);
    console.log('b = '+ b);
    console.log(rest);
}

foo(1,2,3,4,5);

//a = 1
//b = 2
//(3) [3, 4, 5]
  • rest参数只能写在最后,前面用 ... 标识,从运行结果可知,传入的参数先绑定ab,多余的参数以数组形式交给变量rest,所以,不再需要arguments我们就获取了全部参数。

  • 如果传入的参数连正常定义的参数都没填满,也不要紧,rest参数会接收一个空数组(注意不是 undefined)

function foo(a,b, ...rest){
    console.log('a = '+ a);
    console.log('b = '+ b);
    console.log(rest);
}

foo(1);

//a = 1
//b = undefined
//[]

四、带有返回值的函数(return)

  • 函数在定义时不必指定是否返回值

  • 位于return语句之后的任何代码都永远不会执行

  • 一个函数中也可以包含多个return语句

  • return语句也可以不带有任何返回值。在这种情况下,函数在停止执行后将返回undefined值。这种方法一般用在需要停止函数执行而又不需要返回值的情况下

  • 整个JavaScript 并不会停止执行,仅仅是函数。JavaScript 将继续执行代码,从调用函数的地方。
  • 您可以使返回值基于传递到函数中的参数:

function myFunction(a,b){
    return a*b;
    console.log(a); // 是不会执行的,在上面的 return 已经终止了函数。
}
var myObject = myFunction(4,3); 
console.log(myObject);
//12
  • 仅仅希望退出函数时 ,也可使用 return 语句。返回值是可选的:
function myFunction(a,b){
	if(a>b){
		return;     //a>b 所以直接就退出了
	}
	var x = a+b;    
	return x;
}

var myObject = myFunction(4,3);
console.log(myObject);
//undefined
function myFunction(a,b){
	if(a>b){
		return; //a<b 所以跳过
	}
	var x = a+b;
	return x;
}

var myObject = myFunction(1,3);
console.log(myObject);
//4

注意: 函数中参数和返回值不只是数字,还可以是字符串等其它类型

function compare(a,b){
    if(a>b){
        return a;
    }else if(a<b){
        return b;
    }else{
        return "两数相等"
    }
}

console.log("5 和 4 的较大值是:"+compare(5,4));
console.log("6 和 3 的较大值是:"+compare(6,3));
console.log("88 和 88 的较大值是:"+compare(88,88));

/*
5 和 4 的较大值是:5
6 和 3 的较大值是:6
88 和 88 的较大值是:两数相等
*/

五、立即执行函数

  • 有些函数只会被执行一次就不再调用它了,所以它就一直在等待被调用,占用了系统资源。所以就用立即执行函数的让它执行一次就被销毁

  • 针对初始化功能的函数(用完就销毁)

(function abc(){    // 因为执行完成后,就会销毁根本用不到函数名,所以可以不用写函数名。
    var a = 123;
    var b = 234;
    console.log(a+b);
}()) // 357  执行完成后就会销毁

console.log(abc);   // 会报错,因为 abc 已经销毁了。

--------------------------------

(function(a,b,c){   // 也可以使用参数
    console.log(a + b + c*2);
}(1,2,3))   // 9

--------------------------------------------

//接收返回值

var num = (function(a,b,c){  
    var d = a + b + c;
    return d;
}(1,2,3))   

console.log(d); // 6

只有表达式才能被执行符号()执行,执行完成后就会销毁函数,就类似于立即执行函数。


// 报错 --> 这是个函数声明,不是表达式不能直接用执行符号。
function test(){
    var a = 123;
}()     

// 这样是可以
function test(){
    var a = 123;
}
test(); 

// 这个是函数表达式所以也可以执行,但是执行完成后 test 就会被销毁。
// 这样写就成了立即执行函数。
var test = function(){  
    var a = 123;
    console.log(a);
}()

六、递归

  • 在程序中函数直接或间接调用自己

  • 跳出结构,有了跳出才有结果

function foo(){
    ...foo(...)...
}
  • 找规律
  • 找出口
function jc(n){
    if(n == 1 || n == 0){ // 一定要设置出口,如果没有就会无限死循环(用已知的当出口)。
       return 1;
    }

    return n * jc(n - 1);   // n! = n * (n -1)! --> 规律

    // 没有出口
    // jc(3) ==> 3 * jc(2);
    // jc(2) ==> 3 * jc(1);
    // jc(1) ==> 3 * jc(0);
    // jc(0) ==> 3 * jc(-1);  ....  没有出口就会一直的循环

    // 有出口
    // jc(3) ==> 3 * jc(2); ==> jc(3) ==> 3 * 3 // 从下面慢慢返回。
    // jc(2) ==> 3 * jc(1); ==> 设置了一个出口 1! = 1 ==> jc(2) ==> 3 * 1
    // 先执行的最后执行完,因为是从出口一点一点返回的。

}

jc(5);  // 120 5的阶乘
jc(10); // 3628800 10的阶乘
  • 定义阶乘函数一般都要用到递归算法;如上面的代码所示,在函数有名字,而且名字以后也不会变的情况下,这样定义没有问题。

  • 但问题是这个函数的执行与函数名紧紧耦合在了一起。

  • 为了消除这种紧密耦合的现象,可以在非严格模式下使用 arguments.callee()

var trueFactorial = function factorial(num){
    if(num <= 1){
        return 1;
    }else{
        return num * arguments.callee(num - 1)
    }
};

var factorial = function(){
    return 0;
};

console.log(trueFactorial(5));  //120
console.log(factorial(5));  //0
  • 在这个重写后的factorial()函数的函数体内,没有再引用函数名 factorial。这样,无论引用函数时使用的是什么名字,都可以保证正常完成递归调用。

  • 但在严格模式下,不能通过脚本访问 arguments.callee,访问这个属性会导致错误。不过,可以使用命名函数表达式来达成相同的结果 .

var factorial = (function f(num){
    if(num <= 1){
        return 1;
    }else{
        return num * f(num -1);
    }
});

console.log(factorial(5));  //120

算法核心:

  • 在有限次可预见性结果中,找到结果与上一次结果之间的关系。

  • f(n)f(n-1)的关系有时候很简单,如同走楼梯,状态单一;又有时如同细胞分裂,多种状态组合影响结果。

  • 关键在于梳理清楚本次结果和上一次结果的关系有哪些方面或是因素。

  • 在草稿纸上写出前几次的结果,或者画图,这样更容易找到规律,这种规律实际上就是递归方程。

  • 在算法的分析中,当一个算法中包含递归调用时,其时间复杂度的分析会转化成为一个递归方程的求解。

七、举例

1. 没有形参的情况下使用 arguments 实现累加

function totalizer(){
    var sum = 0;

    for(var i = 0;i < arguments.length;i++){
        sum += arguments[i];
    }
    console.log(sum);
}

totalizer(1,2,3,4,5);   // 15

2. 定义一组函数,输入 1-3 的数字,逆转并输出汉字形式。

function reverse(){
    var num = window.prompt('input');   // 输入进来的是字符串类型
    var str = '';
    for(var i = num.length-1; i >= 0; i--){ // 字符串的长度 num.length。
                                            // 字符串用索引值表示每个位置。 num[0],num[1],num[2]
        // str += num[i];  反向取值并连接成新的字符串。
        str += transfer(num[i]);    // 每个位置都要遍历一次函数 transfer。
    }
    document.write(str);
}

function transfer(target){
    switch(target){
        case '1' :
            return "一"
        case '2' :
            return "二"
        case '3' :
            return "三"
    }
}

reverse();

// 输入 123
// 输出 三二一

3. 求1-100的和

function sum(n){
    if(n==1){
        return 1;   // 临界
    }    
    return sum(n-1) + n;    // sum(n) == sum(n-1) + n 递推关系
}

console.log(sum(100));  // 5050

4. 求 1,3,5,7,9,...第n项的结果和前n项和,序号从1开始 (奇数)

function foo(n){
    if(n == 1) return 1;
    return foo(n-1) + 2;    // foo(n) = foo(n-1)+2
}

function sum(n){
    if(n == 1) return 1;
    return foo(n) + sum(n-1);   // sum(n) = foo(n) + sum(n-1)
}

console.log(foo(10));   // 19 // 第10项
console.log(sum(12));   // 144

5. 数列 1,1,2,4,7,11,16...求第 n 项,求前n项和

  • 假设已知函数 foo(n) 为第n项

  • 递归关系

  • 从第 0 项开始计算

    • 第 0 项, 1 => foo(0) + 0 = foo(1)
    • 第 1 项, 2 => foo(1) + 1 = foo(2)
    • 第 2 项, 3 => foo(2) + 2 = foo(3)
    • ...
    • 第 n-1 项, n => foo(n-1) + (n-1) = foo(n)
    • foo(n) = foo(n-1) + (n-1);

0  1  2  3  4  5   6 
1  1  2  4  7  11  16

  • 从第 1 项开始计算

    • 第 1 项, 2 => fn( 1 ) + 0 = fn( 2 )
    • 第 2 项, 3 => fn( 2 ) + 1 = fn( 3 )
    • 第 3 项, 4 => fn( 3 ) + 2 = fn( 4 )
    • ...
    • foo(n) = fn(n-1) + (n - 2)

1  2  3  4  5  6   7
1  1  2  4  7  11  16

  1. 递归体
function foo(n){
    return foo(n-1)+(n-1);
}
  1. 临界条件
  • foo(0) == 1;
  • foo(1) == 1;
  1. 递归函数
function foo(n){
    if(n == 0) return 1;
    return foo(n-1) + (n -1);
}
foo(6); //16

//0  1  2  3  4  5   6
//1, 1, 2, 4, 7, 11, 16,
  1. 前n项的和

    • 假设已知函数 sum(n)为前n项和
    • 递归关系
      • sum(n) = foo(n) + sum(n-1);
    • 递归体
    function sum(n){
        return foo(n) + sum(n-1);
    }
    
    • 临界条件
      • sum(0) = 1;
    • 递归函数
function sum(n){
    if(n == 0) return 1;
    return foo(n) + sum(n-1);
}
  1. 完整函数
function foo(n){
    if(n == 0) return 1;
    return foo(n-1) + (n -1);
}

function sum(n){
    if(n == 0) return 1;
    return foo(n) + sum(n-1);
}

sum(6); //42

//1+1+2+4+7+11+16=42

6. n 的阶乘

// 递归
var n = parseInt(window.prompt("input"));

function jc(n){
    if(n == 1 || n ==0){ // 一定要设置出口,如果没有就会无限死循环。
       return 1;
    }
    return n * jc(n - 1);
}
document.write(jc(n)); 

7. Fibonacci数列-斐波那契数列(1,1,2,3,5,8,13,21,34,55,89...求第 n 项)

  1. 假设已知 fib(n) 为第 n 项

  2. 递归关系

    fib(n) = fib(n-1) + fib(n-2)

  3. 递归体

    function fib(n){
        return fib(n-1)+fib(n-2);
    }
    
  4. 临界条件

    • fib(1) == 1
    • fib(2) == 1
  5. 递归函数

function fib(n){
  if(n<=2){
    return 1;
  }
  return fib(n-1) + fib(n-2);  
}
console.log(fib(7));    //13
  1. 循环实现(从第 1 项开始计算)
var fib = function (n){
  var a1=1,a2=1,a3=0;
  if(n<=2){
    return 1;
  }
  for(var i = 1;i < n-1;i++){
    a3 = a1 + a2;
    a1 = a2;
    a2 = a3;
  }
  return a3;  
}
console.log(fib(5));    //13

8. 求幂

  • 求幂就是求某一个数几次方2*2 2 的平方(2的2次方)
  • 求 n 的 m 次方
    • 最终要得到一个函数 power( n, m )
    • n 的 m 次方就是 m 个 n 相乘 即 n 乘以 (m-1) 个 n 相乘
  1. 假设已知函数 power(n,m) 为 n 的 m 次幂

  2. 递归关系

    • power(n,m-1) * n
  3. 递归体

    function power(n,m){
        return power(n,m-1) * n;
    }
    
  4. 临界条件

    • m == 1 ,return n
    • m == 0 ,reutnr 1
  5. 递归函数

function power(n,m){
    if(m == 1) return n;
    return power(n,m-1) * n;
}

console.log(power(3,4));    // 81

9.综合

var x = 1;
if(function f(){}){ // 有括号里面就会变成表达式,它就不是函数定义,f 就消失了。
    x += typeof f;  // f 因为已经消失所以没有定义,而 typeof 没有定义的会输出 'undefined'
}
console.log(x); // 1undefined

10. 求一个方法,求一个字符串的字节长度。(提示:字符串有一个方法 chaCodeAt(); 一个中文占两个字节,一个英文占一个字节。)

  • 定义和用法:charCodeAt() 方法可以返回指定位置的字符的 Unicode 编码。这个返回值是 0 - 65535 之间的整数。(当返回值是 <= 255 时,为英文。当返回值 > 255 时为中文)
// 第一种

function bytesLength(str){
    var sum = 0;
    var c;
    for(var i = 0;i < str.length; i ++){
        if(str.charCodeAt(i) <= 255){
            c = 1;
        }else if(str.charCodeAt(i) > 255){
            c = 2;
        }
        
        sum += c;
    }
    console.log(sum);
}

bytesLength('java');  //4
bytesLength('构造函数');  //8

// 第二种

function bytesLength(str){
    var count = str.length;
    for(var i = 0;i < str.length; i ++){
        if(str.charCodeAt(i) > 255){
           count ++;
        }
    }
    return count;
}

bytesLength('abcd函数');    // 8

11. 参数列表传递

function foo(){
    bar.apply(null, arguments);
}
function bar(){
    console.log(arguments);
}
foo(1,2,3,4,5);
// [1,2,3,4,5]
posted @ 2019-07-16 14:24  胤小飞  阅读(96)  评论(0)    收藏  举报