zoukankan      html  css  js  c++  java
  • js你不是的那些基础问题-函数

    1 概述

    1.1 函数的声明

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

      (1)function 命令

    function命令声明的代码区块,就是一个函数。function命令后面是函数名,

    函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。

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

      上面的代码命名了一个print函数,以后使用print()这种形式,

      就可以调用相应的代码。这叫做函数的声明(Function Declaration)。

      (2)函数表达式

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

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

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

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

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

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

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

      上面代码在函数表达式中,加入了函数名x。这个x只在函数体内部可用,指代函数表达式本身,

      其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,

      二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。

      因此,下面的形式声明函数也非常常见。

    var f = function f() {};
    

      需要注意的是,函数的表达式需要在语句的结尾加上分号,表示语句结束。

      而函数的声明在结尾的大括号后面不用加分号。

      总的来说,这两种声明函数的方式,差别很细微,可以近似认为是等价的。

     

      (3)Function 构造函数

      第三种声明函数的方式是Function构造函数。

    var add = new Function(
      'x',
      'y',
      'return x + y'
    );
    
    // 等同于
    function add(x, y) {
      return x + y;
    }
    

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

      其他参数都是add函数的参数。

      你可以传递任意数量的参数给Function构造函数,只有最后一个参数会被当做函数体,

      如果只有一个参数,该参数就是函数体。

    var foo = new Function(
      'return "hello world";'
    );
    
    // 等同于
    function foo() {
      return 'hello world';
    } 

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

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

     1.2 第一等公民

    JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。

    凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,

    也可以当作参数传入其他函数,或者作为函数的结果返回。

    函数只是一个可以执行的值,此外并无特殊之处。

    1.3 函数名的提升

       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 f = function () {
      console.log('1');
    }
    
    function f() {
      console.log('2');
    }
    
    f() // 1
    

    2 函数的属性和方法

    2.1 name 属性

       函数的name属性返回函数的名字。

    function f1() {}
    f1.name // "f1"
    

      如果是通过变量赋值定义的函数,那么name属性返回变量名。

    var f2 = function () {};
    f2.name // "f2"
    

      但是,上面这种情况,只有在变量的值是一个匿名函数时才是如此。

      如果变量的值是一个具名函数,那么name属性返回function关键字之后的那个函数名。

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

      上面代码中,f3.name返回函数表达式的名字。注意,真正的函数名还是f3

      而myName这个名字只在函数体内部可用。

      name属性的一个用处,就是获取参数函数的名字。

    var myFunc = function () {};
    
    function test(f) {
      console.log(f.name);
    }
    
    test(myFunc) // myFunc
    

      上面代码中,函数test内部通过name属性,就可以知道传入的参数是什么函数。

    2.2 length 属性

      函数的length属性返回函数预期传入的参数个数,即函数定义之中的参数个数。

    function f(a, b) {}
    f.length // 2
    

      上面代码定义了空函数f,它的length属性就是定义时的参数个数。

      不管调用时输入了多少个参数,length属性始终等于2。

      length属性提供了一种机制,判断定义时和调用时参数的差异,

      以便实现面向对象编程的“方法重载”(overload)。

    2.3 toString()

      函数的toString方法返回一个字符串,内容是函数的源码。

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

      对于那些原生的函数,toString()方法返回function (){[native code]}

    Math.sqrt.toString()
    // "function sqrt() { [native code] }"
    

      上面代码中,Math.sqrt是 JavaScript 引擎提供的原生函数,toString()方法就返回原生代码的提示。

      函数内部的注释也可以返回。

    function f() {/*
      这是一个
      多行注释
    */}
    
    f.toString()
    // "function f(){/*
    //   这是一个
    //   多行注释
    // */}"
    

      利用这一点,可以变相实现多行字符串。

    var multiline = function (fn) {
      var arr = fn.toString().split('
    ');
      return arr.slice(1, arr.length - 1).join('
    ');
    };
    
    function f() {/*
      这是一个
      多行注释
    */}
    
    multiline(f);
    // " 这是一个
    //   多行注释"
    

    3 函数作用域

    3.1 函数本身的作用域

      函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,

      就是其声明时所在的作用域,与其运行时所在的作用域无关。

     

    var a = 1;
    var x = function () {
      console.log(a);
    };
    
    function f() {
      var a = 2;
      x();
    }
    
    f() // 1
    

     

      上面代码中,函数x是在函数f的外部声明的,所以它的作用域绑定外层,

      内部变量a不会到函数f体内取值,所以输出1,而不是2

      总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。

      很容易犯错的一点是,如果函数A调用函数B,却没考虑到函数B不会引用函数A的内部变量。

    var x = function () {
      console.log(a);
    };
    
    function y(f) {
      var a = 2;
      f();
    }
    
    y(x)
    // ReferenceError: a is not defined
    

      同样的,函数体内部声明的函数,作用域绑定函数体内部。

    function foo() {
      var x = 1;
      function bar() {
        console.log(x);
      }
      return bar;
    }
    
    var x = 2;
    var f = foo();
    f() // 1
    

      上面代码中,函数foo内部声明了一个函数barbar的作用域绑定foo

      当我们在foo外部取出bar执行时,变量x指向的是foo内部的x,而不是foo外部的x

      正是这种机制,构成了下文要讲解的“闭包”现象。

    4. 参数

     

    4.1 传递方式

    函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。

    这意味着,在函数体内修改参数值,不会影响到函数外部。

    var p = 2;
    
    function f(p) {
      p = 3;
    }
    f(p);
    
    p // 2
    

      但是,如果函数参数是复合类型的值(数组、对象、其他函数),

      传递方式是传址传递(pass by reference)。

      也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。

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

      注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。

    var obj = [1, 2, 3];
    
    function f(o) {
      o = [2, 3, 4];
    }
    f(obj);
    
    obj // [1, 2, 3]
    

    4.2 arguments 对象

      由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,

      可以在函数体内部读取所有参数。这就是arguments对象的由来。

      正常模式下,arguments对象可以在运行时修改。

    var f = function(a, b) {
      arguments[0] = 3;
      arguments[1] = 2;
      return a + b;
    }
    
    f(1, 1) // 5
    

      严格模式下,arguments对象与函数参数不具有联动关系。

      也就是说,修改arguments对象不会影响到实际的函数参数。

    var f = function(a, b) {
      'use strict'; // 开启严格模式
      arguments[0] = 3;
      arguments[1] = 2;
      return a + b;
    }
    
    f(1, 1) // 2
    

      callee 属性

      arguments对象带有一个callee属性,返回它所对应的原函数。

    var f = function () {
      console.log(arguments.callee === f);
    }
    
    f() // true
    

      可以通过arguments.callee,达到调用函数自身的目的。这个属性在严格模式里面是禁用的,因此不建议使用。

    函数的其他知识点

    5.1 闭包

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

      上面代码中,函数f2就在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。

      但是反过来就不行,f2内部的局部变量,对f1就是不可见的。

      这就是 JavaScript 语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。

      所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

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

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

      闭包就是函数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的内部环境,一直存在。

      所以,闭包可以看作是函数内部作用域的一个接口。

     

      为什么会这样呢?原因就在于inc始终在内存中,而inc的存在依赖于createIncrementor

      因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。

     

      闭包的另一个用处,是封装对象的私有属性和私有方法。

    function Person(name) {
      var _age;
      function setAge(n) {
        _age = n;
      }
      function getAge() {
        return _age;
      }
    
      return {
        name: name,
        getAge: getAge,
        setAge: setAge
      };
    }
    
    var p1 = Person('张三');
    p1.setAge(25);
    p1.getAge() // 25
    

      注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,

      所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。

    5.2 立即调用的函数表达式(IIFE)

      我们需要在定义函数之后,立即调用该函数。这时,

      你不能在函数的定义之后加上圆括号,这会产生语法错误。

    function(){ /* code */ }();
    // SyntaxError: Unexpected token (
    

      产生这个错误的原因是,function这个关键字即可以当作语句,也可以当作表达式。

       为了避免解析上的歧义,JavaScript 引擎规定,如果function关键字出现在行首,一律解释成语句。

      因此,JavaScript 引擎看到行首是function关键字之后,认为这一段都是函数的定义,

      不应该以圆括号结尾,所以就报错了

      解决方法就是不要让function出现在行首,让引擎将其理解成一个表达式。

      最简单的处理,就是将其放在一个圆括号里面。

    (function(){ /* code */ }());
    // 或者
    (function(){ /* code */ })();
    

      上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,

      而不是函数定义语句,所以就避免了错误。

      这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。

      注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错。

     

      通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。

      它的目的有两个:一是不必为函数命名,避免了污染全局变量;

      二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。

    // 写法一
    var tmp = newData;
    processData(tmp);
    storeData(tmp);
    
    // 写法二
    (function () {
      var tmp = newData;
      processData(tmp);
      storeData(tmp);
    }());
    

      上面代码中,写法二比写法一更好,因为完全避免了污染全局变量。

    6 eval 命令

      eval命令接受一个字符串作为参数,并将这个字符串当作语句执行。

    eval('var a = 1;');
    a // 1
    

      如果参数字符串无法当作语句运行,那么就会报错。

    eval('3x') // Uncaught SyntaxError: Invalid or unexpected token
    

      放在eval中的字符串,应该有独自存在的意义,不能用来与eval以外的命令配合使用。

      举例来说,下面的代码将会报错。

    eval('return;'); // Uncaught SyntaxError: Illegal return statement
    

      如果eval的参数不是字符串,那么会原样返回。

    eval(123) // 123
    

      eval没有自己的作用域,都在当前作用域内执行,

      因此可能会修改当前作用域的变量的值,造成安全问题。

    var a = 1;
    eval('a = 2');
    
    a // 2
    

      上面代码中,eval命令修改了外部变量a的值。由于这个原因,eval有安全风险。

      为了防止这种风险,JavaScript 规定,如果使用严格模式,

      eval内部声明的变量,不会影响到外部作用域。

    (function f() {
      'use strict';
      eval('var foo = 123');
      console.log(foo);  // ReferenceError: foo is not defined
    })()
    

      总之,eval的本质是在当前作用域之中,注入代码。

      由于安全风险和不利于 JavaScript 引擎优化执行速度,所以一般不推荐使用。

      通常情况下,eval最常见的场合是解析 JSON 数据的字符串,

      不过正确的做法应该是使用原生的JSON.parse方法。

     

     

    文章内容转自 阮一峰老师 JavaScript教程 https://wangdoc.com/javascript/index.html

  • 相关阅读:
    目标检测应用化之web页面(YOLO、SSD等)
    传统候选区域提取方法
    非极大值抑制(Non-Maximum Suppression,NMS)
    Darknet windows移植(YOLO v2)
    线性判别分析 LDA
    SVM 支持向量机
    特征-相似度衡量
    布隆过滤器 Bloom Filter
    聚类算法
    图论--最大流
  • 原文地址:https://www.cnblogs.com/WernerWu/p/11345261.html
Copyright © 2011-2022 走看看