javascript(函数)

function 

函数是一段可以重复调用的代码块。接收相应的参数并可以返回对应的值

函数的声明

  javascript 有三种声明函数的方法。function 命令函数表达式Function构造器。

    (1)function 命令

     function命令声明的代码区块,就是一个函数。function命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。     

        function add(x, y) {
            console.log(x + y)
        }
        add(3,4);//7

    上面的代码命名了一个add函数,输出两个数字相加的和,以后使用add()这种形式,就可以调用相应的代码。这叫做函数的声明(Function Declaration)。

   (2)函数表达式

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

        let add=function (x, y) {
            console.log(x + y)
        };
        add(3,4);

      这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。

      采用函数表达式声明函数时,function命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。    

        let add=function addAdd(x, y) {
            console.log(addAdd)
        };
        console.log(addAdd) //报错  未定义
        add(3,4);// 输出函数本身

     上面代码在函数表达式中,加入了函数名addAdd。这个addAdd只在函数体内部可用,指代函数表达式本身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(函数内部报错时,函数名存在时报错时错误信息中会包含函数名,函数名不存在时会指向引用的值的名称)。因此,下面的形式声明函数也非常常见。 

  

   

        let add = function add(x, y) {
            console.log(x + y)
        };
        add(3, 4);

    需要注意的是,函数的表达式需要在语句的结尾加上分号,表示语句结束。而函数的声明在结尾的大括号后面不用加分号。总的来说,这两种声明函数的方式,差别很细微,可以近似认为是等价的。

    (3) Function 构造器     

        let add=new Function(
            'x','y','console.log(x+y)'
        )
        add(3,4);//7
        
        let say=new Function(
            'console.log("hello")'
        )
        say();//hello

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

      Function构造函数可以不使用new命令,返回结果完全一样。 总的来说,这种声明函数的方式非常不直观,几乎无人使用。

函数的重复声明

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

        function add(x,y){
            console.log(x+y);
        }
        add(3,4);//12
        
        function add(x,y){
            console.log(x*y)
        }
        add(3,4);//12

  上面代码中,后一次的函数声明覆盖了前面一次。而且,由于函数名的提升(参见下文),前一次声明在任何时候都是无效的,这一点要特别注意。

圆括号运算符,return 语句和递归

  调用函数时,要使用圆括号运算符。圆括号之中,可以加入函数的参数。

        function add(x,y){
            return x+y
       console.log('result')//不会执行 } let result
=add(3,4); console.log(result)//7 

  上面代码中,函数名后面紧跟一对圆括号,就会调用这个函数。 函数体内部的return语句,表示返回。JavaScript 引擎遇到return语句,就直接返回return后面的那个表达式的值,后面即使还有语句,也不会得到执行。也就是说,return语句所带的那个表达式,就是函数的返回值。return语句不是必需的,如果没有的话,该函数就不返回任何值,或者说返回undefined。

  函数可以调用自身,这就是递归(recursion)。下面就是通过递归,计算数字的阶乘 

        function fact(i) {
            if (i == 1) {
                return 1
            }
            return i * fact(i - 1)
        }
        console.log(fact(3)) //6

  上面的代码就是

fact(3)==>3*fact(3-1)==>3*fact(2)
fact(2)==>2*fact(2-1)==>3*fact(1)
fact(1)==>1
fact(3)==>3*2*1==>6

第一等公民

  一等公民:一般来说,如果某程序设计语言中的一个值可以作为参数传递,可以从子程序中返回,可以赋值给变量

  二等公民:可以作为参数传递,但是不能从子程序中返回,也不能赋给变量

  三等公民:它的值连作为参数传递都不行  

  JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。 由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民

  

        function add(x, y) {
            console.log(x + y);
        }

        // 将函数赋值给一个变量
        let operator = add;
        operator(3, 4); //7


        // 将函数作为参数和返回值
        function addOperat(fn) {
            return fn;
        }
        addOperat(add)(3, 4) //7

函数名的变量提升

  JavaScript 引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部、(另外两种声明是不会存在这种情况的)所以,下面的代码不会报错。 

        let operator = add;
        operator(3, 4); //7
        function add(x, y) {
            console.log(x + y);
        }

下面这两种情况都会报错 

  

        // add 未定义
        add();
        let add=function(){
            console.log(1)
        }    

  

        // add 不是一个函数
        let add;
        add();
        add=function(){
            console.log(1)
        }

  因此,如果同时采用function命令和赋值语句声明同一个函数,最后总是采用赋值语句的定义。因为函数表达式声明的函数不会存在函数名的变量提升  

        // 这里不使用 let f
        var f=function(){
            console.log(1)
        };
        function f(){
            console.log(2)
        }
        f();//1

函数的属性和方法

  name属性

    获取函数的原始名称

        function f(){}
        console.log(f.name);//f
        
        let f2=function (){};
        console.log(f2.name);//f2
        
        let f4=function f3(){};
        console.log(f4.name);//f3

  length属性

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

    

        function f(){}
        console.log(f.length);//0
        
        let f2=function (a,b){};
        console.log(f2.length);//2
        
        let f4=function f3(a){};
        console.log(f4.length);//1

  toString()

    输出函数的内容(一切内容、包括注释)

    

        function add(x,y){
            let a=x;
            let b=y;
            console.log(x+y)
        }
        console.log(add.toString())// 输出函数本省
        console.log(Math.ceil.toString())
        //对于那些原生的函数,toString()方法返回function (){[native code]}

函数作用域

  作用域(scope)指的是变量存在的范围。在 ES5 的规范中,JavaScript 只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。ES6 又新增了块级作用域。

  全局作用域,在任何地方都可以访问

        let age='18';
        function getAge(){
            console.log(age)
        }
        console.log(age);//18
        getAge();// 18  age全局作用域 函数内部也可访问

  函数作用域,只在函数内部可以访问

  

        function getAge(){
            var age='qzy';
            console.log(age)
        }
        getAge()//qzy
        console.log(age)// age未定义 函数内部定义的变量 只有函数内部可以访问

  块级作用域 es6 新增的 let 声明变量 

        {
            let age='25'
        }
        console.log(age) // age未定义

  函数内部定义的变量,会在该作用域内覆盖同名全局变量。

        let age=18;
        function getAge(){
            let age=25
            console.log(age)
        }
        console.log(age);//18
        getAge();//25

  注意,对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。 

        if(true){
            var age=25;
        }
        console.log(age);//25

函数内部的变量提升

  与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。

  

        function ageLevel(age){
            if(age>100){
                var level='高手'
            }
            console.log(level)
        }
        ageLevel(101);//高手
        
        // 等价于
        function ageLevelEqual(age){
            var level;
            if(age>100){
                level='高手'
            }
            console.log(level)
        }
        ageLevelEqual(101);//高手

函数本身的作用域

  函数本身也是一个值,也有自己的作用域它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关  

        let age=25;
        function getAge(){
            console.log(age)
        }
        
        function setAge(){
            let age=26;
            getAge();
        }
        
        setAge();//25

  getAge函数是在最外层声明的,所以它绑定的作用域是最外层,在函数内部变量age不会影响到其绑定的最外层的值

 

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

        let age = 25;

        function setAge() {
            let age = 26;

            function getAge() {
                console.log(age)
            }
            // 将声明的函数返回
            return getAge
        }
        setAge()();// 26

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

函数的参数

  函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结果,这种外部数据就叫参数。函数参数不是必需的,JavaScript 允许省略参数。

        function square(x){
            return x*x
        }
        
        console.log(square(2));//4
        console.log(square(3));//9

  函数的参数传递方式分为两种:传值传递、传址传递

    传值传递:函数参数如果是原始类型的值(数值、字符串、布尔值),则采用这种方式,此时在函数体内修改参数值,不会影响到函数外部    

        let age=25;
        function setAge(age){
            age=26;
        }
        console.log(age);//25

    传址传递:如果函数参数是复合类型的值(数组、对象、其他函数),则采用这种方式,传入函数的原始值的地址,此时在函数体内修改参数值,会影响到函数外部(有特殊的)  

        let person={
            age:25
        };
        function setPerson(person){
            person.age=26;
        }
        setPerson(person);
        console.log(person.age);//26

    person 为一个对象 在函数背部修改其中的属性,外面的变量中的值也做相应的改变

 

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

        let person={
            age:25
        };
        function setPerson(person){
            person={
                age:26
            };
        }
        setPerson(person);
        console.log(person.age);//25

    在函数f内部,参数对象person被整个替换成另一个值。这时不会影响到原始值。这是因为,形式参数person的值本来是是参数person的地址,重新对person赋值导致person指向另一个地址,保存在原地址上的值当然不受影响。

同名参数

  如果有同名的参数,则取最后出现的那个值。 

        function add(a,a){
            console.log(a)
        }
        add(1);// undefined 形参为两个 同名取最后一个 但是未传递 所以未定义 
        add(1,2);// 2
        add(1,2,3);// 2

arguments 对象

  由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是arguments对象的由来。 arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。  

        function add(one){
            let len=arguments.length;// 获取传入的参数个数
            for(let i=0;i<len;i++){
                console.log(arguments[i])
            }
        }
        add(1,2,3,4,5)

  注:arguments对象可以在运行时修改,修改后对应的形参就是修改后的值;但是在开启严格模式下可以修改,但是不会影响到形参, 

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

        console.log(f(1, 1)) // 2

  与数组的关系,需要注意的是,虽然arguments很像数组,但它是一个对象。数组专有的方法(比如slice和forEach),不能在arguments对象上直接使用。 如果要让arguments对象使用数组方法,真正的解决方法是将arguments转为真正的数组。下面是两种常用的转换方法:slice方法和逐一填入新数组。

        function add(one){
            let args = Array.prototype.slice.call(arguments);
            args.forEach(list=>{
                console.log(list)
            })
        }
        add(1,2,3,4,5) 
        function add(one) {
            let args = [];
            for (let i = 0; i < arguments.length; i++) {
                args.push(arguments[i]);
            }
            args.forEach((list)=>{
                console.log(list)
            })
        }
        add(1, 2, 3, 4, 5)

  都可以使用forEach 输出所以的参数值

callee 属性

  arguments对象带有一个callee属性,返回它所对应的原函数,可以通过arguments.callee,达到调用函数自身的目的。这个属性在严格模式里面是禁用的,因此不建议使用 

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

闭包

  闭包(closure)是 JavaScript 语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 理解闭包,首先必须理解变量作用域。前面提到,JavaScript 有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。

  函数内部可以读取函数外部的变量,但是函数外部是不能读取到函数内部的变量的,这个在上面已经实验过了

  如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数  

        function person(){
            let age=18;
            function getAge(){
                return age
            }
            return getAge
        }
        let p=person();// 接收getAge
        let pAge=p();
        console.log(pAge);// 18

  上面的代码 我们在person内部定义了一个age字段 ,但是我们想在外部取不到这个值,这个时候我们可以在函数内部定义一个函数,这个函数在person 函数内部,是可以取到person内部的值的,然后我们再把函数暴露出来,就可以获取到这个值了,那此时getAge函数就是闭包,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

  闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果  

        function createIn(count){
            return function (){
                console.log(++count);
            }
        }
        
        let result=createIn(0);
        result();//1
        result()//2
        result()//3

 

  上面代码中,count是函数createIn的内部变量。通过闭包,count的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包result使得函数createIn的内部环境一直存在。所以,闭包可以看作是函数内部作用域的一个接口。

  为什么会这样呢?原因就在于result始终在内存中,而result的存在依赖于createIn,因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收

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

        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);
        console.log(p1.getAge()) // 25

  上面代码中,函数Person的内部变量_age,通过闭包getAge和setAge,变成了返回对象p1的私有变量。

  注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题

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

  在 JavaScript 中,圆括号()是一种运算符,跟在函数名之后,表示调用该函数。比如,print()就表示调用print函数。 有时,我们需要在定义函数之后,立即调用该函数。这时,你不能在函数的定义之后加上圆括号,这会产生语法错误。

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

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

        // 语句
        function f() {}

        // 表达式
        var f = function f() {}

  为了避免解析上的歧义,JavaScript 引擎规定,如果function关键字出现在行首,一律解释成语句。那不是的就是函数表达式喽。因此,JavaScript 引擎看到行首是function关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错了。

  下面函数表达式后面跟上圆括号是可以立即执行的。

        // 表达式 直接输出1
        var f = function () {
            console.log(1)
        }()

  解决方法就是不要让function出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面。

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

  上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。 注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错 

        (function (){console.log(1)})()//1
        (function (){console.log(2)}())//2
        // Uncaught TypeError: (intermediate value)(...) is not a function

  上面代码的两行之间没有分号,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参数

  推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,只要让引擎认为它是表达式即可。

        let i =function (){console.log(1)}();//1
        
        true && function (){console.log(2)}();//2
        
        +function (){console.log(3)}();//3

  通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。

eval 命令

  eval命令接受一个字符串作为参数,并将这个字符串当作语句执行。如果参数字符串无法当作语句运行,那么就会报错。放在eval中的字符串,应该有独自存在的意义,不能用来与eval以外的命令配合使用。举例来说,下面的代码将会报错。如果eval的参数不是字符串,那么会原样返回。

        eval('var a=1');
        console.log(a);//1
        console.log(eval(123));//123
     eval('3x');//报错
     eval('return')//报错

  eval没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题 

        var a=0;
        eval('var a=1');
        console.log(a);//1

  为了防止这种风险,JavaScript 规定,如果使用严格模式,eval内部声明的变量,不会影响到外部作用域。

       'use strict'
        var a=0;
        eval('var a=1');
        console.log(a);//0

  如果我不声明,直接修改a,可以看到还是可以修改成功的。

       'use strict'
       var a=0;
       eval('a=1');
       console.log(a);//1

  上面代码中,严格模式下,eval内部还是改写了外部变量,可见安全风险依然存在。通常情况下,eval最常见的场合是解析 JSON 数据的字符串,不过正确的做法应该是使用原生的JSON.parse方法。

        let str = '{"name": "qzy", "age": 25}';
        let obj = eval('('+str+')');//let obj=JSON.parse(str);
        console.log(obj); // Object {name: "hanzichi", age: 10}

  eval 解析时为什么要加圆括号呢 ?

        let obj='{}';
        console.log(eval(obj));//undefined
        console.log(eval('('+obj+')'));//{}

  上面说过eval 会执行输入的字符串,obj是以{开头、}结尾的。那js 引擎就会把这当成代码块,执行里面的语句。

eval 的别名调用

  前面说过eval不利于引擎优化执行速度。更麻烦的是,还有下面这种情况,引擎在静态代码分析的阶段,根本无法分辨执行的是eval。

        "use strict"
        let my=eval;
        my('var a=1');
        console.log(a);//1

  为了保证eval的别名不影响代码优化,JavaScript 的标准规定,凡是使用别名执行eval,eval内部一律是全局作用域。详细的看下一篇对象的讲解 

        var a = 1;

        function f() {
            var a = 2;
            var e = eval;
            e('console.log(a)');
        }

        f() // 1

  就一句话,没事别用eval。

  

 

posted @ 2019-04-02 21:32  善良的小邱子  阅读(930)  评论(0)    收藏  举报