最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 函数的深入浅出和各种函数说法

    正文概述 掘金(阿离王)   2020-11-27   418

    函数的深入浅出

    JavaScript 函数是被设计为执行特定任务的代码块。

    JavaScript 函数会在某代码调用它时被执行。

    通俗来讲:函数就是把多行代码包起来成代码块,可以通过调用函数来重复使用代码块。(也就是把重复的代码封装起来,通过调用函数执行代码块)

    JavaScript 函数语法

    JavaScript 函数通过 function 关键词进行定义,其后是函数名和括号 ()和花括号{}。

    function 函数名(){} 
    

    函数名可包含字母数字下划线美元符号规则与变量名相同)。

    圆括号可包括由逗号分隔的参数:

    (参数 1, 参数 2, ...)
    

    由函数执行的代码被放置在花括号中:{}

    function (参数 1,funName 参数 2, 参数 3) {
        要执行的代码
    }
    

    在函数中,参数是局部变量。

    函数声明

    常用定义一个函数的方式有两种

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

    这里给大家讲讲函数声明提升,和变量的提升

    函数声明提升与变量提升

    JS在编译阶段,函数声明和变量声明都会被先处理置于执行环境的顶部,且赋值会被留在原地,这个过程称之为提升。

    举个简单例子:

    console.log(fn);
    console.log(i);
    var i = 1;
    function fn () {
      console.log(2)
    }
    

    实际上代码顺序是这样的:

     var fn = function () {
      console.log(2)
    }
    var i;
    console.log(fn);
    console.log(i);
    i = 1;
    

    变量提升

    变量声明在编译阶段被处理,而变量赋值则留在原地等待执行。

    console.log(i);   // undefined
    var i = 1;
    console.log(i);   // 1
    

    相当于:

    var i;
    console.log(i);   // 由于i只是声明未赋值,输出undefined
    i = 1;
    console.log(i)    // i已赋值,输出1
    

    一道测试题

     var age = 10;
      function person () {
          age = 100;
          console.log(age);  // 100
          var age;
          console.log(age)  // 100
      }
      person();
      console.log(age);   // 10
    

    这里可以在页面上设置断点调试看看 局部作用域的age 和 全局作用域的age

    函数提升

    js解析器在解析时对函数声明与函数表达式有着不同的优先级,实际上编译阶段函数声明会先于变量被提升,并使其在执行任何代码之前可访问,函数表达式实际上是变量声明的一种,因此函数声明提升优于函数表达式

    console.log(fn(1));    // 1
    function fn (a) {
        return a;
    }
    

    如上面的代码,由于函数声明被置于执行环境顶部,即使调用函数的代码在声明函数之前也可以正确访问。再看函数表达式的例子:

    console.log(fn(1));
    var fn = function (a) {
        return a;
    }
    // fn is not a function
    

    相当于

    var fn;
    console.log(fn(1));
    fn = function (a) {
      return a;
    }
    // fn is not a function
    

    上面的例子之所以报错,是因为变量fn声明后还未对函数引用(fn还没赋值)。

    另外函数声明提升不会被变量声明覆盖,但会被变量赋值覆盖。

    变量未赋值的例子:

    function fn() {
    	console.log(1);
    }
    var fn;
    console.log(fn);    // 由于后一个fn只声明未负值,因此输出的是函数fn
    

    变量赋值的例子:

    function fn(){
        console.log(1);
      }
      var fn = function () {
          console.log(2)
      };
      fn();    // 2
    

    相当于

    function fn(){
    	console.log(1);
    }
    var fn;
    fn = function () {
    	console.log(2)
    };
    fn();    // 2(因为声明的函数fn被后一个已引用函数的变量fn所覆盖,因此输出2)
    

    再来点例子

    fn();
    var fn = function () {
    	console.log(1);
    }
    fn();
    function fn () {
    	console.log(2);
    }
    var fn;
    fn();
    // 依次输出2,1,1
    

    局部 JavaScript 变量

    在 JavaScript 函数内部声明的变量(使用 var)是局部变量,所以只能在函数内部访问它。(该变量的作用域是局部的)。

    您可以在不同的函数中使用名称相同的局部变量,因为只有声明过该变量的函数才能识别出该变量。

    只要函数运行完毕,本地变量就会被删除。

    全局 JavaScript 变量

    在函数外声明的变量是全局变量,网页上的所有脚本和函数都能访问它。

    变量的作用域

    变量在哪里声明,则那里就是它的有效区域

    var scope = "global";		//定义全局变量
    function checkscope(){		
    	var scope = "local";		//定义局部变量
    	console.log(scope);			
    }
    checkscope();				          //=>'local'		改变的只是局部变量scope
    console.log(scope);						//=>'global'		全局变量scope并没有改变
    

    在函数体内的参数使用var定义的变量都是属于局部变量,一定要善于使用局部变量,防止污染全局变量

    变量的声明提前

    用了var定义的变量或函数的参数,都会被提前到函数体的顶部进行声明(但不涉及赋值,具体赋值还是需要运行到对应行数)

    var scope = "global";
    function f() {
    	console.log(scope); // =>undefined 			输出"undefined",而不是"global" 
    	var scope = "local"; // 变量 在这里 赋 初始 值, 但 变量 本身 在函数体内任何地方均是有定义的
    	console.log(scope);	 // =>"local"
    }
    

    这个特性跟全局变量的声明提前其实是一致的,所以编程时,要有个好习惯,尽量把函数所需要用到的变量在函数顶部声明好,这样可以使源代码非常真实的反映函数的变量作用域

    JavaScript 变量的生存期

    JavaScript 变量的生命期从它们被声明的时间开始。

    局部变量会在函数运行以后被删除。

    全局变量会在页面关闭后被删除。

    向未声明的 JavaScript 变量分配值

    如果您把值赋给尚未声明的变量,该变量将被自动作为 window 的一个属性。

    这条语句:

    carname="Volvo";
    

    将声明 window 的一个属性 carname。

    非严格模式下给未声明变量赋值创建的全局变量,是全局对象的可配置属性,可以删除。

    var a = 1;
    function fn () {
        b = 2;
    }
    fn();
    console.log(a);
    console.log(b);
    console.log(this.a);
    console.log(this.b);
    console.log(this === window);
    console.log(delete a); // false 无法删除
    console.log(a); //1
    console.log(delete b); // false 无法删除
    console.log(b); //1
    

    函数调用

    函数可以通过其名字加上括号进行调用,有参数的就括号里面写参数,多个参数就英文逗号隔开

    function fn(a, b){
      console.log(a);
      console.log(b);
    }
    
    //通过函数名加()调用函数
    fn();
    fn(1);
    fn(1, 2);
    

    为何使用函数?

    您能够对代码进行复用:只要定义一次代码,就可以多次使用它。

    您能够多次向同一函数传递不同的参数,以产生不同的结果。

    function fn (a) {
      var num = 0;
      for (var i = 0; i < a; i ++) {
        num += i;
      }
      console.log(num);
      return num;
    }
    fn(10);
    

    细心的同学就看到我上面的例子函数里面,写了个return num; 这个return是什么呢?

    return 语句

    当 JavaScript 到达 return 语句,函数将停止执行。(函数体内没有写return的时候,默认在函数体最后有个return;)

    函数通常会计算出返回值。这个返回值会返回给调用者

    return 语句 只能 在 函数 体内 出现, 如果不 是的 话 会 报 语法 错误。 当 执行 到 return 语句 的 时候, 函数 终止 执行, 并 返回 expression 的 值 给 调用 程序。

    看下面例子 ,计算两个数的乘积,并返回结果:

    var x = myFunction(7, 8);        // 调用函数,返回值被赋值给 x
    
    console.log(x);
    
    function myFunction(a, b) {
        return a * b;                // 函数返回 a 和 b 的乘积
    }
    

    JavaScript 函数参数

    javascript函数的参数与大多数其他语言的函数的参数有所不同。函数不介意传递进来多少个参数,也不在乎传进来的参数是什么数据类型,甚至可以不传参数。

    Javascript的参数分为实参形参两种类型:

    实参:从字面意义我们可以理解为“实际存在的参数”,是在函数调用时传给函数的变量,该变量在函数执行时必须存在。实参可以为变量、常量、函数、表达式等。

    形参:从字面意义我们可以理解为“形式上存在的参数”,由此我们可以看出它并不是真实存在的参数,又称为虚拟变量。它在函数定义时使用,作用为接收函数调用时的实参。

    在JavaScript中实参与形参数量并不需要像JAVA一样必须在数量上严格保持一致,具有很大的灵活性。如下:

    function test(str1, str2, str3) {
        console.log(str1);
        console.log(str2);
        console.log(str3);
    }
    test();                             // str1: undefined, str2: undefined, str3: undefined
    test('hello');                      // str1: 'hello', str2: undefined, str3: undefined
    test('hello', 'world');             // str1: 'hello', str2: 'world', str3: undefined
    test('hello', 'world', '!');        // str1: 'hello', str2: 'world', str3: '!'
    

    在JavaScript代码运行过程中,形参的作用为接收实参,它们两个分别位于不同的内存地址中,大致可以分为两种情况:

    1. 实参原始值。当实参为原始值时,此时形参实参拷贝。因此,函数体内形参值的改变并不会影响实参
    function test(str) {
        str = 'chinese';
        return str;
    }
    const str1 = 'china';
    const str2 = test(str1);
    console.log(str1);      // china
    console.log(str2);      // chinese
    
    1. 实参引用值。当实参为引用值时,此时形参为实参内存地址的拷贝。因此,函数体内形参值的变化一定情况下将会影响实参
    function test(obj) {
        // 形参obj的值实际上为实参obj的内存引用,及形参与实参同时指向同一个内存地址。
        obj.name = 'typeScript';    // 此时改变的为形参与实参同时指向的那个内存地址中的值
                                    // 所以此时也导致实参的name属性发生了变化
        obj = {                     // 此时对形参obj进行重新赋值,给予了它一个新的内存地址
            name: 'react',          // 从此之后的形参将于实参完全解绑,两者之前不再存在联系
            star: 13000,
        }
        obj.star = 20000;           // 所以这里仅仅是改变了形参的star属性
        return obj;
    }
    const obj1 = {
        name: 'javaScript',
        star: 100000,
    }
    const obj2 = test(obj1);
    console.log(obj1);      // name: 'typeScript', star: 100000
    console.log(obj2);      // name: 'react', star: 20000
    

    同名形参

    在非严格模式下,函数中可以出现同名形参,且只能访问最后出现的该名称的形参。

    function add(x,x,x){
      return x;
    }
    console.log(add(1,2,3)); //3
    

    而在严格模式下,出现同名形参会抛出语法错误

    function add(x,x,x){
    'use strict';
    return x;
    }
    console.log(add(1,2,3));//SyntaxError: Duplicate parameter name not allowed in this context
    

    参数个数

    当实参比函数声明指定的形参个数要少,剩下的形参都将设置为undefined值

    function add(x, y){
      console.log(x, y); //1 undefined
    }
    add(1);
    

    常常使用逻辑或运算符给省略的参数设置一个合理的默认值

    function add(x, y){
      y = y || 2;
      console.log(x, y); //1 2
    }
    add(1);
    

    更多设置默认参数的方法:ES5和ES6中对函数设置默认参数的方法总结

    当实参比形参个数要多时,剩下的实参没有办法直接获得,需要使用即将提到的arguments对象

    arguments对象

    javascript中的参数在内部是用一个数组来表示的。函数接收到的始终都是这个数组,而不关心数组中包含哪些参数。在函数体内可以通过arguments对象来访问这个参数数组,从而获取传递给函数的每一个参数。arguments对象并不是Array的实例,它是一个类数组对象,可以使用方括号语法访问它的每一个元素

    function add(x){
      console.log(arguments[0], arguments[1], arguments[2]) //1 2 3
      return x + 1;
    }
    console.log(add(1,2,3)); //2
    

    arguments对象的length属性显示实参的个数,函数的length属性显示形参的个数

    function add(x, y){
      console.log(arguments.length); //3
      return x + 1;
    }
    add(1, 2, 3);
    console.log(add.length); //2
    

    形参只是提供便利,但不是必需的

    function add(){
      return arguments[0] + arguments[1];
    }
    console.log(add(1, 2)); //3
    

    当形参与实参的个数相同时,arguments对象的值和对应形参的值保持同步

    function test(num1, num2){
      console.log(num1, arguments[0]); //1 1
      arguments[0] = 2;
      console.log(num1, arguments[0]); //2 2
      num1 = 10;
      console.log(num1, arguments[0]); //10 10
    }
    test(1);
    

    [注意]虽然命名参数和对应arguments对象的值相同,但并不是说读取两个值会访问相同的内存空间。它们的内存空间是独立的,但值是同步的

    但在严格模式下,arguments对象的值和形参的值是独立的

    function test(num1, num2){
      'use strict';
      console.log(num1, arguments[0]); //1 1
      arguments[0] = 2;
      console.log(num1, arguments[0]); //1 2
      num1 = 10;
      console.log(num1, arguments[0]); //10 2
    }
    test(1);
    

    当形参并没有对应的实参时,arguments对象的值与形参的值并不对应

    function test(num1,num2){
      console.log(num1, arguments[0]); //undefined,undefined
      num1 = 10;
      arguments[0] = 5;
      console.log(num1, arguments[0]); //10,5
    }
    test(); 
    

    arguments对象的属性callee

    arguments对象有一个名为callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数

    下面是经典的阶乘函数

    function factorial(num){
      if(num <=1){
        return 1;
      }else{
        return num* factorial(num-1);
      }
    } 
    console.log(factorial(5)); //120
    

    但是,上面这个函数的执行与函数名紧紧耦合在了一起,可以使用arguments.callee可以消除函数解耦

    function factorial(num){
      if(num <= 1){
        return 1;
      }else{
        return num * arguments.callee(num - 1);
      }
    } 
    console.log(factorial(5)); //120
    

    但在严格模式下,访问这个属性会抛出TypeError错误

    function factorial(num){
      'use strict';
      if(num <=1){
       return 1;
      }else{
        return num * arguments.callee(num - 1);
      }
    } 
    //TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
    console.log(factorial(5));
    

    对象参数

    当一个函数包含超过3个形参时,要记住调用函数中实参的正确顺序实在让人头疼

    通过键/值对的形式来传入参数,这样参数的顺序就无关紧要了。定义函数的时候,传入的实参都写入一个单独的对象之中,在调用的时候传入一个对象,对象中的键/值对是真正需要的实参数据

    function fn(args){
      args.a && console.log(args.a);
      args.b && console.log(args.b);
      args.c && console.log(args.c);
    }
    fn({
      a: 1,
      b: [1,2]
    });
    

    函数重载

    javascript函数不能像传统意义上那样实现重载。而在其他语言中,可以为一个函数编写两个定义,只要这两个定义的签名(接受的参数的类型和数量)不同即可

    javascript函数没有签名,因为其参数是由包含0或多个值的数组来表示的。而没有函数签名,真正的重载是不可能做到的

    //后面的声明覆盖了前面的声明
    function addSomeNumber(num){
      return num + 100;
    }
    function addSomeNumber(num){
      return num + 200;
    }
    var result = addSomeNumber(100); //300
    

    只能通过检查传入函数中参数的类型和数量并作出不同的反应,来模仿方法的重载

    function doAdd(){
      if(arguments.length == 1){
        alert(arguments[0] + 10);
      }else if(arguments.length == 2){
        alert(arguments[0] + arguments[1]);
      }
    }
    doAdd(10); //20
    doAdd(30, 20); //50
    

    es6函数的扩展

    函数参数的默认值

    在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。

    function log(x, y) {
      y = y || 'World';
      console.log(x, y);
    }
    
    log('Hello') // Hello World
    log('Hello', 'China') // Hello China
    log('Hello', '') // Hello World
    

    上面代码检查函数log的参数y有没有赋值,如果没有,则指定默认值为World。这种写法的缺点在于,如果参数y赋值了,但是对应的布尔值为false,则该赋值不起作用。就像上面代码的最后一行,参数y等于空字符,结果被改为默认值。

    为了避免这个问题,通常需要先判断一下参数y是否被赋值,如果没有,再等于默认值。

    if (typeof y === 'undefined') {
      y = 'World';
    }
    

    ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。

    function log(x, y = 'World') {
      console.log(x, y);
    }
    
    log('Hello') // Hello World
    log('Hello', 'China') // Hello China
    log('Hello', '') // Hello
    

    可以看到,ES6的写法比ES5简洁许多,而且非常自然。下面是另一个例子。

    function Point(x = 0, y = 0) {
      this.x = x;
      this.y = y;
    }
    
    var p = new Point();
    p // { x: 0, y: 0 }
    

    除了简洁,ES6的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。

    参数变量是默认声明的,所以不能用let或const再次声明。

    function foo(x = 5) {
      let x = 1; // error
      const x = 2; // error
    }
    

    上面代码中,参数变量x是默认声明的,在函数体中,不能用let或const再次声明,否则会报错。

    与解构赋值默认值结合使用

    参数默认值可以与解构赋值的默认值,结合起来使用。

    function foo({x, y = 5}) {
      console.log(x, y);
    }
    
    foo({}) // undefined, 5
    foo({x: 1}) // 1, 5
    foo({x: 1, y: 2}) // 1, 2
    foo() // TypeError: Cannot read property 'x' of undefined
    

    上面代码使用了对象的解构赋值默认值,而没有使用函数参数的默认值。只有当函数foo的参数是一个对象时,变量x和y才会通过解构赋值而生成。如果函数foo调用时参数不是对象,变量x和y就不会生成,从而报错。如果参数对象没有y属性,y的默认值5才会生效。

    下面是另一个对象的解构赋值默认值的例子。

    function fetch(url, { body = '', method = 'GET', headers = {} }) {
      console.log(method);
    }
    
    fetch('http://example.com', {})
    // "GET"
    
    fetch('http://example.com')
    // 报错
    

    上面代码中,如果函数fetch的第二个参数是一个对象,就可以为它的三个属性设置默认值。

    上面的写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。

    function fetch(url, { method = 'GET' } = {}) {
      console.log(method);
    }
    
    fetch('http://example.com')
    // "GET"
    

    上面代码中,函数fetch没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量method才会取到默认值GET。

    参数默认值的位置

    通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。

    // 例一
    function f(x = 1, y) {
      return [x, y];
    }
    
    f() // [1, undefined]
    f(2) // [2, undefined])
    f(, 1) // 报错
    f(undefined, 1) // [1, 1]
    
    // 例二
    function f(x, y = 5, z) {
      return [x, y, z];
    }
    
    f() // [undefined, 5, undefined]
    f(1) // [1, 5, undefined]
    f(1, ,2) // 报错
    f(1, undefined, 2) // [1, 5, 2]
    

    上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined。

    如果传入undefined,将触发该参数等于默认值,null则没有这个效果。

    function foo(x = 5, y = 6) {
      console.log(x, y);
    }
    
    foo(undefined, null)
    // 5 null
    

    上面代码中,x参数对应undefined,结果触发了默认值,y参数等于null,就没有触发默认值。

    函数的length属性

    指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。

    (function (a) {}).length // 1
    (function (a = 5) {}).length // 0
    (function (a, b, c = 5) {}).length // 2
    

    上面代码中,length属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了3个参数,其中有一个参数c指定了默认值,因此length属性等于3减去1,最后得到2。

    这是因为length属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,rest参数也不会计入length属性。

    (function(...args) {}).length // 0
    

    如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。

    (function (a = 0, b, c) {}).length // 0
    (function (a, b = 1, c) {}).length // 1
    

    rest参数

    ES6引入rest参数(形式为“...变量名”),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

    function add(...values) {
      let sum = 0;
    
      for (var val of values) {
        sum += val;
      }
    
      return sum;
    }
    
    add(2, 5, 3) // 10
    

    上面代码的add函数是一个求和函数,利用rest参数,可以向该函数传入任意数目的参数。

    rest参数中的变量代表一个数组,所以数组特有的方法都可以用于这个变量。下面是一个利用rest参数改写数组push方法的例子。

    function push(array, ...items) {
      items.forEach(function(item) {
        array.push(item);
        console.log(item);
      });
    }
    
    var a = [];
    push(a, 1, 2, 3)
    console.log(a);
    

    注意,rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

    // 报错
    function f(a, ...b, c) {
      // ...
    }
    

    函数的length属性,不包括rest参数。

    (function(a) {}).length  // 1
    (function(...a) {}).length  // 0
    (function(a, ...b) {}).length  // 1
    

    匿名函数

    没有函数名的函数,叫做匿名函数

    setTimeout(function(){
        console.log('我是匿名函数');
    }, 1000);
    

    上面例子中的 function(){}, 就叫做匿名函数

    回调函数

    setTimeout(function(){
        console.log('我是匿名函数');
    }, 1000);
    

    上面例子中的 function(){}, 就叫做匿名函数, 也可以叫回调函数,就是一个函数当作参数传入,给别人函数内来调用,也就是回调函数

    立即执行函数

    声明了函数后加括号,就立即执行

    (function(){ console.log('我是立即执行函数') })();
    

    递归函数

    自己调用自己,就是递归函数了

    let num = 0;
    function add(){
        console.log(num++);
        add();
    }
    

    箭头函数

    let fn = () => { console.log('我是箭头函数') }
    fn();
    let fn1 = res => console.log(res);
    fn1(1)
    

    起源地下载网 » 函数的深入浅出和各种函数说法

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元