JS函数

250 阅读11分钟

函数

1.什么是函数

从一个案例来看

假如我们要分别计算1到10、5到12、14到35的整数和

//1到10
let sum = 0;

for(let i = 1; i <= 10; i++){
    sum += i;
}
console.log(sum);


//5到12
for(let i = 5; i <= 12; i++){
    sum += i;
}
console.log(sum);

//14-35
for(let i = 14; i <= 35; i++){
    sum += i;
}
console.log(sum);

//重复率太高  我们可以对代码进行封装  当想调用的时候进行调用即可

wxG1jU.png

函数就如同上面的机器这个神奇的机器,送入机器两个数字,它将产生两个数字之间所有整数的和。

定义以及作用

函数就是语句的封装,可以让这些代码方便地被复用

函数具有“一次定义,多次调用”的优点

使用函数,可以简化代码,让代码更具有可读性

2.函数的定义

和变量类似,函数必须先定义然后才能使用

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

1.function 命令

使用function关键字定义函数,function是“功能”的意思,表示这里面定义的一个功能。

wOuQHO.png

声明一个没有参数的函数

//函数就是一组语句的集合
function fun(){
	alert("我是一个函数");
}

// 定义函数 , 定义函数的时候不会直接执行
function fun() {
    console.log("云牧Dsb");
    console.log("人见人爱");
}

// 函数必须等到调用的时候才能被执行
fun();
fun();
fun();


//也可以充当一个事件函数  事件触发时才执行  不能加()
document.onclick = fun;

2.函数表达式

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

wOu7G9.png

let fn = function() {
  console.log("云牧DSB");
};

//这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式

//因为赋值语句的等号右侧只能放表达式。

//采用函数表达式声明函数时,function命令后面不带有函数名。

//如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。

3.Function 构造函数

let add = new Function('x','y','console(x+y)');

//可以传递任意数量的参数给Function构造函数

//只有最后一个参数会被当做函数体

//如果只有一个参数,该参数就是函数体。
let foo = new Function('console.log("云牧DSB");');

// 等同于
function foo() {
    console.log("云牧DSB");
}

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

函数的调用

执行函数体中的所有语句,就称为“调用函数”

调用函数非常简单,只需在函数名字后书写圆括号对即可

语句的执行顺序

function fun() {
    console.log("云牧Dsb");
    console.log("人见人爱");
}
//函数不调用就不会执行

console.log(1);
console.log(2);
fun();//调用的时候主程序的执行权移交给函数      函数体内的所有语句执行完毕 语句执行权交换给主程序
console.log(3);
console.log(4);

函数声明的提升

和变量声明提升类似,函数声明也可以被提升

fun();

function fun() {
    console.log("云牧Dsb");
    console.log("人见人爱");//在预解析阶段会被提升  所以函数调用正常执行
}

函数表达式不能提升

fun();  //报错

let fun = function () {
    console.log("云牧Dsb");
    console.log("人见人爱");
};

函数的重复声明

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

function fn() {
  console.log(1);
}

f() // 2

function fn() {
    console.log(2);
}

3.函数的参数

参数是函数内的一些待定值,在调用函数时,必须传入这些参数的具体值

函数的参数可多可少,函数可以没有参数,也可以有多个参数,多个参数需要使用逗号隔开

function fn(x){//形参:函数定义的时候,圆括号中定义形式参数  参数是什么类型的没写,不需要指定类型:
    //相当于  let x = 5;
	console.log(x);
}

fn(5);//实参:函数调用的时候,传入的参数  传进去是什么类型  x就是什么类型
function add (x , y){  
	let sum = x + y;
	console.log("两个数字的和是" + sum);
}

add(3 , 6);  //数字3传给x  数字6传给y

形参和实参不统一的情况

函数参数不是必需的,JavaScript 允许省略参数。运行时无论提供多少个参数(或者不提供参数),JavaScript 都不会报错

如果实参多于形参

function add (x , y){  
	let sum = x + y;
	console.log("两个数字的和是" + sum);
}

add(3 , 6 , 9);  //没有形参来接收9

形参多于实参

function add (x , y , z){  
	let sum = x + y + z;
    console.log(x , y , z);  // 3  6  undefined  c没有收到实际参数  值为undefined
	console.log("三个数字的和是" + sum); //NaN进行任何运算结果都是NaN
}

add(3 , 6);  

解决本节课开头的案例

function calcSumFromAtoB(a, b) {
    let sum = 0;

    for (let i = a; i <= b; i++) {
        sum += i;
    }
    console.log("从" + a + "到" + b + "的所有的整数和是" + sum);
}

calcSumFromAtoB(3 , 7); //25

系统自带函数

document.getElementById( "box" )   // "box" 为实参
alert( "123" )   // "123" 为实参
console.log( "abc" )    // "abc" 为实参

arguments 对象

由于 JavaScript 允许函数有不定数目的参数,

所以需要一种机制,可以在函数体内部读取所有参数。这就是arguments对象的由来。

arguments对象包含了函数运行时的接收到所有实参的集合(数组形式)

说明:

​ 1、arguments对象 只是与数组类似,并不是Array的实例。(类数组对象)

​ 2、[] 语法访问它的每一个元素。

​ 3、length属性确定传递参数的个数。

function fn() {
	console.log(arguments);
	console.log(arguments.length); //5
	console.log(arguments[0]); //666  索引是从0开始的正整数
}
fn(666 , 4 , 8 , 9 , 19);
function fn(n1 , n2) {
    arguments[0] = 100; //严格模式无法修改
    console.log(n1);  //200
}
fn(-10, 30); //100

实现传入任意一组数的相加的和

// 不管用户传入多个实际参数,永远能够计算他们的和
function fn() {
    let sum = 0;
    for (let i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
    console.log(sum);
}
fn(1, 2, 3, 7); //13

使用arguments对象获取到函数参数(任意一组数)的平均值,并且把得到的值转成整数,输出出来

function getAvg() {
    let sum = 0,
        len = arguments.length;

    for (var i = 0; i < len; i++) {
        sum += arguments[i];
    }
    return sum / len;
}

var avg = getAvg(1, 2, 3, 4, 5, 8);

console.log(parseInt(avg));

4.函数的返回值

函数体内可以使用return关键字表示“函数的返回值”

return :立即终止函数的执行,并返回指定的返回值

函数不指定返回值默认返回 undefined

function add(n1, n2) {
    return n1 + n2;   //函数的返回值
}

let result = add(3 , 5); // 这个值可以被变量接收

alert(result);
console.log(result);
document.write(result);
//对这个返回值可以进行任何的操作

调用一个有返回值的函数,可以被当做一个普通值,从而可以出现在任何可以书写值的地方

function add(n1, n2) {
    return n1 + n2; 
}

let result = add(3, 5) * add(1 , 4);  //两个值 8 * 5 

console.log(result);
function add(n1, n2) {
    return n1 + n2;
}

let result = add(6 , add(3 , 1)); //10  先执行内层 再执行外层  
								//我们将一个执行语句返回的值当做另一个函数执行的参数  
console.log(result);

遇见return即退出函数

调用函数时,一旦遇见return语句则会立即退出函数,将执行权交还给调用者

function fn(){
    console.log("A");
    return "B";
    console.log("C");  //return后面的语句不会输出
}

console.log(1111);
let result = fn();
console.log(result);
console.log(2222);

结合if语句的时候,往往不需要写else分支了

题目

请编写一个函数,判断一个数字是否是偶数

function checkEven(n){
    if(n % 2 == 0) return true;  
     return false;
}

let result = checkEven(6);
console.log(result);//true

5.函数算法题

题目1

题目:喇叭花数是这样的三位数:其每一位数字的阶乘之和恰好等于它本身。即abc = a!+b! +c!,其中

abc表示一个三位数。试寻找所有喇叭花数。

思路:将计算某个数字的阶乘封装成函数,这样可以让问题简化。

<script>
      //计算一个数字的阶乘
      function factorial(n) {
        // 累乘器
        let result = 1;
        for (let i = 1; i <= n; i++) {
          result *= i;
        }
        return result;
      }

      // let n = factorial(4);
      // console.log(n);

      // 穷举法  从100到999寻找喇叭花数
      for(let i = 100; i <= 999; i++){

        // 把数字i变为字符串
        let i_str  = i.toString();
        // a b c 分别代表百位,十位,个位
        let a = Number(i_str.charAt(0));
        let b = Number(i_str.charAt(1));
        let c = Number(i_str.charAt(2));

        //根据喇叭花数的条件进行判断
        if(factorial(a) + factorial(b) + factorial(c) == i){  
          console.log(i + "是喇叭花数");
        }
      }
</script>

题目2

Javascript内置的sort方法

数组排序可以使用sort()方法 , 这个方法的参数又是一个函数

这个函数中的a、b分别表示数组中靠前和靠后的项

如果需要将它们交换位置,则返回任意正数;否则就返回负数。

let arr = [66, 33, 55, 77, 99];

arr.sort(function (a, b) {
    if (a > b) {
        return 1;  //靠前的项大于靠后的则需要交换位置   
    } else {
        return -1;
    }
});

console.log(arr);

对这个进行简写

//会使得原数组变成升序
arr.sort(function (a, b) {
    return a - b;
});

arr.sort(function(a , b){ return  b - a })//会使得原数组变成降序

console.log(arr);

6.应用函数简化编程

我们现在做一个程序,输出2~100的所有质数。

所谓的质数,就是只有1、自己两个约数,没有其他约数。

要把一个复杂的问题,拆分为一个个小问题。

约数个数函数 → 判断质数函数 → 高层业务

//约数个数函数:能够传入一个数字,吐出来它约数的个数
function approximateNumber(a) {
    //计算a这个数字约数的个数
    let count = 0;
    
    for (let i = 1; i <= a; i++) {
        if (a % i == 0) {
            count++;
        }
    }
    
    return count; //返回这个数字的约数的个数
}
//如果一个函数的名字取is 判断是否是质数
//就暗示了将返回布尔值,要么是true要么是false。是通常做法,不是规定
//接收一个参数m,返回是否是质数true或者false
  function isZhishu(m) {
        if (approximateNumber(m) == 2) return true;
        return false;
      }

for (let i = 1; i <= 100; i++) {
    if (isZhishu(i)) {
        console.log(i);
    }
}

7.递归

wvlGUP.md.png

函数的内部语句可以调用这个函数自身,从而发起对函数的一次迭代。在新的迭代中,又会执行调用函数自身的语句,从而又产生一次迭代。当函数执行到某一次时,不再进行新的迭代,函数被一层一层返回,函数被递归。

递归是一种较为高级的编程技巧,它把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。

用求4的阶乘举例

4! ===> 4 * 3! ===> 3 * 2! ===> 2 * 1!  ===> 1  //1的阶乘是1 2的阶乘2 3的阶乘是6 4的阶乘是24

0iyLTg.png

递归的要素

边界条件 : 确定递归到何时终止 也称为递归出口

递归模式: 大问题是如何分解为小问题的 , 也称为递归体

<script>
      // 书写一个函数  这个函数内部自己会调用自己 从而形成递归
      function factorial(n) {
        // 函数的功能是计算n的阶乘    n! 不就是n * (n - 1)!吗?

        // 这个就是递归的出口,如果计算1的阶乘,可以不用递归了,直接告诉你答案就是1
        if (n == 1) return 1;
          
        // 如果询问的不是1的阶乘 就返回n * (n-1)!
        return n * factorial(n - 1);
      }

      let result = factorial(6);
      alert(result);
    </script>

递归算法题

斐波那契数列

斐波那契数列是这样的数列:1、1、2、3、5、8、13、21,

数列下标为0和1的项的值都是1,从下标为2的项开始,每项等于前面两项的和

<script>
      // 编写一个函数  这个函数的功能就是返回斐波那契数列下标为n的那项的值
      function fib(n) {
        // 数列下标为0和1的项 值是1
        if (n == 0 || n == 1) return 1;

        // 斐波那契数列的本质特征就是每一项都等于前面两项的和
        return fib(n - 1) + fib(n - 2);
      }

      let result = fib(4);
      alert(result); //5
    
    //书写一个循环语句  计算斐波那契数列前15项
    for(let i = 0; i < 15; i++){
        console.log(fib(i));
    }
    </script>

实现深克隆

JavaScript中的数据类型有两类:基本类型和引用类型值

基本类型类型值和引用类型值的区别

基本数据类型直接按值存在栈中.

引用数据类型的数据存在堆内存中,但是数据指针是存放在栈内存中

访问引用数据时,先从栈内存中获取指针,通过指针在堆内存中找到数据

wvJUne.png

基本数据(值)类型的 赋值

let a = 100;
let b = 100;
console.log(a === b);  //true
//基础类型比较数据类型和值相同与否


//可以理解为复制一个文本
let a = 100;
let b = a;
b = 200;
console.log(a);//a=100  将a的值拷贝一份赋值给b,此时b改变,a不会改变

wvvens.png

复杂数据(引用)类型赋值

var xm = {
    age:18,
    weight:"80kg"
}
var xh = {
    age:18,
    weight:"80kg"
}
console.log(a === b);//false 指向不同的内存的地址  相当于双胞胎


//可以理解为发送一个文本的快捷方式到桌面
let a = { age:20 }
let b = a;  //将 a 的内存地址给 b 此时ab指向同一个内存地址
console.log(a === b); //true
b.age=21;
console.log(a.age)//a=21  

wvv6ud.png

我们数组也是如此

// 准备原数组
let arr1 = [11, 22, 33, 44];

// 测试一下这样的语句是否能克隆数组
let arr2 = arr1;

arr1.push(55);
console.log(arr2); 
console.log(arr1 === arr2); //true arr1和arr2本质是同一个数组

总结:

基础数据类型赋值时给值 , 引用数据类型赋值时给地址

为什么值类型可以直接复制值?引用类型就必须复制地址呢?

原因可能在性能存储方面的问题.

值类型因为只有值,直接赋值没什么问题,占用内存比较少.

引用类型JSON对象可能会非常大,导致不好管理.而且直接赋值可能会导致速度比较慢.

数组的浅克隆

浅克隆:只克隆数组的第一层,如果是多维数组,或者数组中的项是其他引用类型值,则不克隆其他层

 <script>
      // 准备原数组
      let arr1 = [11, 22, 33, 44];

      let result = [];
	//遍历原数组,将遍历到的项都推入到结果数组中
      for (let i = 0; i < arr1.length; i++) {
        result.push(arr1[i]);
      }
	
      //输出结果数组
      console.log(result);
      //测试是否实现了克隆 就是说本质上是内存中不同的数组了
      console.log(arr1 === result); //false

      arr1.push(55);

      console.log(result);
     
     //为什么说这样是浅克隆  如果里面有一项或者多项又是数组  则会藕断丝连
    </script>
let arr1 = [11, 22, 33, 44, [55, 66]];

let result = [];

//遍历原数组,将遍历到的项都推入到结果数组中
for (let i = 0; i < arr1.length; i++) {
    result.push(arr1[i]);
}
//输出结果数组
console.log(result);

//测试是否实现了克隆 就是说本质上是内存中不同的数组了
console.log(arr1 === result);
//测试这样的的克隆是浅克隆  藕断丝连

console.log(arr1[4] === result[4]);//true

arr1[4].push(88);
console.log(result);  //原数组第四项直接将引入地址推入了result 导致原数组第四项藕断丝连

数组的深克隆

深克隆:克隆数组的所有层,要使用递归思想整体思路和浅克隆类似,但稍微进行一些改动,

如果遍历到的项是基本类型值,则直接推入结果数组,

如果遍历到的项是又是数组,则重复执行浅克隆的操作。

数组的检测

数组用typeof检测结果是object

Array.isArray()方法可以用来检测数组

Array.isArray方法返回一个布尔值,表示参数是否为数组。

可以弥补typeof运算符的不足。

console.log(typeof [1, 2]); //object

console.log(typeof { a: 1, b: 2 }); //object  typeof运算符只能显示数组的类型是Object

console.log(Array.isArray([1, 2])); //true  而Array.isArray方法可以识别数组。

console.log(Array.isArray({ a: 1, b: 2 })); //false

console.log(Array.isArray([])); //true
数组的push方法

用于在数组的末端添加一个或多个元素,并返回添加新元素后的数组长度。

注意,该方法会改变原数组

let colors = ["yellow", "green"];
let len = colors.push("pink", "red");
console.log(colors);
console.log(len); //4
 <script>
      // 准备原数组
      let arr = [11, 22, 33, 44, [55, 66]];

      // 函数  这个函数会被递归
      function deepClone(arr) {
        // 结果数组  "每一层" 都有一个结果数组
        let result = [];

        // 遍历数组的每一项
        for (let i = 0; i < arr.length; i++) {
          // 类型判断,如果遍历到的项是数组
          if (Array.isArray(arr[i])) {
            //递归
            result.push(deepClone(arr[i]));  //返回值是一个数组推入当前的结果数组之中成为这一项
          } else {
            // 如果遍历到的项不是数组,是基本类型值,则直接推入到结果数组中
            // 相当于是递归的出口
             result.push(arr[i]);
          }
        }
        //返回结果数组
        return result;
      }

      // 测试一下
      let newArr = deepClone(arr);

      console.log(newArr);

      // 是否藕断丝连
      console.log(arr[4] === newArr[4]); //false

      arr[4].push(99);

      console.log(newArr);
</script>