zoukankan      html  css  js  c++  java
  • JS中的函数

      JS中的函数是一等公民,也就是说,它和其它对象或值地位相同,没有区别,其它对象或值怎么用,函数就可以怎么用。其他对象或值怎么用呢?以对象为例,它可以通过字面量进行创建,可以赋值一个变量,可以做为参数传递给函数,同时也可以被函数返回,最后,它的属性还可以动态创建和赋值。

    ({name: 'sam'}) // 通过对象字面量创建
    const obj = { name: 'sam'}; // 赋值给一个变量
    fetch('url', obj); // 作为参数
    function returnObj() { // 作为返回值
        return { name: 'sam'};
    }
    obj.job = "web" // 动态创建属性

      函数可以做同样的事情

    function add(a, b) { // 函数字面量
        return a + b;
    }
    let substract = (a, b) => a - b; // 函数赋值给变量
    [1, 2, 3].sort((a, b) => a - b); // 函数做为参数进行传递
    function curry(cb) { // 返回一个函数
        return cb;
    }
    add.id = 'sum';  // 给函数动态创建一个属性并赋值

      函数就是对象(一个值),对象能做什么,函数就能做什么,它拥有对象的一切能力,函数可以被看成对象。函数作为对象,给它添加属性,可以实现有趣的功能,比如给它加一个id属性,可以确保函数的唯一,不会重复地向一个对象中添加相同的函数,再比如函数有一个name 属性,可以知道哪个函数被调用了,有利于debug。最后,如果一个函数要进行大量的计算,比如阶乘,可以给函数添加一个属性,把以前的计算结果保存起来,提高性能。

    function factoral(n) {
        if(!factoral.result) { // result 保存计算结果
            factoral.result = {};
        }
        if (factoral.result[n] !== undefined) { // 如果以前计算过,直接返回结果
            return factoral.result[n];
        } 
    
        if (n === 1) {
            return 1;
        } else {
            return factoral.result[n] =  n * factoral(n - 1);
        }
    }
    console.log(factoral(5));

      当然,函数除了是对象之外,还有一个重要的特点,就是它可以被调用,实现某些功能,它是一个可以被调用的对象。但函数的调用在JS中也比较复杂,有四种不同方式,

      1,作为函数调用,就是最普通的调用方式,函数名加上(), 有可能还要加上参数。比如:Number("2")

      2,作为对象的属性调用,有的也称为方法调用。对象的属性是一个函数,就可以使用对象.属性进行调用. console.log('hello');

      3,作为构造函数调用,使用new 加上函数名。new Promise(function(){})

      4,通过函数拥有的方法call() 和apply() 调用. [].slice.call();

      为什么有这么多的调用方式呢?不就是调用一个函数吗?原因在于函数中有一个隐式的this参数,不管你用不用,this 存在每一个函数中。this 呢,又比较特别,只有在函数的调用的时候,才能知道它是什么,四种不同的调用方式,就是四种不同的this 值。

      作为函数调用,在非严格模式下,this的值是全局对象global, 具体到浏览器中,是window 对象。

    function sayThis() {
        console.log(this === window);
    }
    sayThis();

      在严格模式下,this的值是undefined。严格模式,就是整个js文件或函数体中使用""use strict";

    function sayThis() {
        "use strict";
        console.log(this === undefined);
    }
    sayThis();

      作为对象的属性调用, this的值是拥有这个属性的对象。对象的属性值可以是一个函数。

    function sayThis() {
        "use strict";
        console.log(this === obj);
    }
    const obj = {
        sayThis:sayThis
    }
    
    obj.sayThis();

      作为构造函数调用,就是调用函数的时候前面加上new, 它创建了全新的对象,this 指向这个对象

    function sayThis() {
        "use strict";
        console.log(this === {});
    }
    new sayThis();//{}

       call() 和apply(), 他们第一个参数就是this, 函数调用的时候,直接指定this. 函数中的this就等行call()或apply() 第一个对数。

    function sayThis() {
        "use strict";
        console.log(this === obj);
    }
    
    const obj = {
        name: 'sam'
    }
    
    sayThis.call(obj); // obj 

      如果函数中没有this, 使用最普通的调用方式就可以了,没有必要使用复杂的调用了。没有this,函数就没有运行时要决定的变量this,函数怎么写的,调用的时候就怎么执行,使用哪种方式调用,结果都是一致的。解释一下作为构造函数的调用。在JS中,没有构造函数一说,有的只是普通的函数。函数(箭头函数除外)前面都可以加上new 时进行调用。

    function print() {
        console.log('hello');
    }
    
    const any = new print(); // hello
    console.log(any); // {}

      使用new调用函数时,它先创建了一个对象,如果函数中有this, 就把对象赋值给this, 然后执行函数体。如果没有this 呢,就是执行函数体,执行完函数体后,就把对象返回。new的调用,就是创建对象,执行函数体,返回对象。那如果我们函数中直接返回一个值呢,比如返回一个1,使用new 调用会怎么样?

    function print() {
        console.log('hello');
        return 1;
    }
      const any = new print();
      console.log(any);

      没有什么变化,返回的1被舍弃了,使用new调用返回原始类型的值的函数,这个原始值会被舍弃,new调用返回的还是new创建的对象。如果new调用的是返回对象的函数呢?

    function print() {
        console.log('hello');
        return [1, 2];
    }
    
    const any = new print();
    console.log(any); // [1,2]

      new调用返回的是函数的返回值,new创建的对象被舍弃了。JS中函数,绝大多数都可以在调用的时候,前面加上new, 但函数样式不同,返回的值也不同,所以如果真的要让函数作为构造函数进行使用,就要遵循一定的规范,函数中使用this, 不要有返回值,使用默认返回值,函数名最好首字母大写。

    function Person(name, job) {
        this.name = name;
        this.job = job;
    }
    
    const person = new Person('sam', 'web');
    console.log(person); 

       再说一下方法调用的一个问题,this的丢失。

    const obj = {
        name: 'sam',
        sayName() {
            return this.name
        }
    }
    
    const anotherfun = obj.sayName;
    console.log(anotherfun()); // undefined

      obj 对象有一个sayName() 方法,简单地返回对象的name. 把obj.sayName 赋值给另一个变量,然后进行调用,可以发现并没有返回对象的name值。为什么呢?obj的sayName 属性,它并不是真正拥有函数,而是一个引用,指向函数。当我把obj.sayName赋值给一个变量的时候,赋值的也是引用,也就是说anotherfun 也指向了obj.sayName 指向的函数,相当于

    const anotherfun = function(){
        return this.name
    };

      anotherfun函数进行调用的时候,也是最普通的调用方法,加(), 所以函数中的this指向了window,antherfun返回的值是window 对象中的name. 这也印证了,只有在调用的时候,才能决定this是什么。要想快速的知道this 具体的指向,就要准确的定位到函数是什么地方调用的,函数的调用点。 解决这个问题的办法,使用bind()。函数有一个bind方法,接受的第一个参数就是this, 用来指定函数中的this,返回一个函数,那么返回的函数中, this是固定的。再赋值给其它变量时,里面的this 就不会动态变化了。

    const anotherfun = obj.sayName.bind(obj);
    console.log(anotherfun()); // 'sam'

      this的丢失还有一种情况,对象的属性值是一个包含函数的函数,内部的函数中的this并不会继承外部函数中的this

    const obj = {
        name: 'sam',
        sayName() {
            (function() {
                console.log(this) // window
                console.log(this.name)
            })()
        }
    }
    
    obj.sayName();

      内部的函数调用也相当于函数的普通调用,this指向了window. 解决这个问题的办法是箭头函数。箭头函数没有自己的this, 它内部的this继承自外围作用域,并且this的值(指向)是在它定义的时候,就已经确定了,就像使用了bind方法,而不是使用动态绑定。箭头函数就是 参数列表 => 函数体;如(a,b) => a+b; 它是一个匿名函数表达式,要把它赋值给一个变量引用,才能对它进行调用

    let sum = (a,b) => a+b; 
    sum(1, 2)

      简单解释一下,箭头函数接受两个参数a,b  返回 a + b的值。函数体如果是一句表达式,默认会返回表达式的值,这也是没有写return a +b 的原因,这里要注意一点,如果返回一个对象,这个对象要用() 括起来。

    let obj = name =>({name:name}) // 如果不写外面的括号,{} 就会被当做块级作用域

      箭头函数还有其他变体

    let hello = () => console.log('hello'); // 箭头函数没有参数,直接用一个括号表示
    let add10 = num => num +10; // 箭头函数只有一个参数num,通常直接写这个参数,不用括号括起来。
    // 函数体是一段可以执行的语名块, 需要用{}把语名块包起来,如果语句块执行完毕,还要返回值,那就要在语句块的末尾显示调用return
    let amount = n => {
        let sum =0;
        for(let i=0; i<=n; i++){
            sum = sum + i;
        }
        return sum;
    }

      箭头函数没有自己的this,但可以在它里面使用this,这时this的指向就继承自外围作用域。this 存在两个地方,一个是函数中,一个是全局对象window。继承自外围作用域就是说箭头函数中的this使用的要么是它的父函数或祖先函数中的this,要么是window对象。this会顺着函数的作用域链向上进行查找,直到找到包含它的一个函数,然后使用该函数中的this,如果找不到,那就是window对象。举个例子

    const object = {
        f1: function(){
            console.log(this);
            const f2 = () => console.log(this); 
    f2(); } }
    object.f1(); // f1 函数内部的this全都指向 object

      f2箭头函数的this,向上找,找到了包含它的函数f1,那就使用f1中的this,f2 中的this 和f1 中的this 保持一致。再改一下,把f1 也改成箭头函数,

    const object = {
        f1: () => {
            console.log(this);
            var f2 = () => console.log(this); 
            f2();
            setTimeout(f2, 1000);
        }
    }
    object.f1();  // f1 函数内部的this全都指向window

      this 指向了window, 按照 object.f1() 的调用方式,f1 函数中的this 应该指向object.其实不是,箭头函数的this 是在它定义的时候,就已经确定了,就像使用了bind方法,而不是动态绑定了, 当箭头函数调用的时候,真正要确定的是它在定义的时候,它所能向上寻找到的包含它的最外围的函数中this. 我们再来分析一下,f2 向上找f1,  f1 也是箭头函数,它还要向上找,但你发现包含f1的函数没有了,只有全局对象window了,this 指向了window,  箭头函数在调用的时候,它真正确定的是包含f1函数的函数中this 的指向, 如果没有包含函数,就是window全局对象了。用箭头函数来解决this 丢失的问题,就是函数的属性值是普通函数,属性值函数中的所有函数都用箭头函数,普通函数包含箭头函数

    const obj = {
        name: 'sam',
        sayName() {
            (() => {
                console.log(this) // window
                console.log(this.name)
            })()
        }
    }
    
    obj.sayName();

      函数调用的时候,如果返回一个函数,那就有可能涉及到另外一个问题----闭包。闭包,最常见的就是一个函数包含另外一个函数,内部的函数可以访问外部函数中的变量,纵然外部函数消失了。举例

    function outer() {
        let a = 10;
    
        function inner() {
            let b = 20;
            console.log(a + b);
        }
    
        return inner;
    }
    let inner = outer();
    inner(); // 30
      当outer()函数调用完之后,按理说它里面的变量a就消失了,但返回的inner函数仍然可以引用a, 外部函数中的变量a并没有消失,内部函数把外部函数变量包起来,保留下来,这就是闭包。为什么呢?因为js是词法作用域,词法作用域有两个方面的意思:
      1,写代码的时候,变量的作用域就已经确定了。变量写在了什么地方,它的作用域就在什么地方。 在上面的例子中,变量b的作用域只在inner中,变量a 和 inner函数的作用域在outer函数中,
      2,变量的解析,当一个变量在它所在的作用域中,找不到时,它会向上,到包含作用域中查找,直到找到,或找到全局作用域,没有找到。比如b, 先在inner函数作用域中查找,找不到,再到包含inner 函数的outer函数作用域中查找,找到了,就不向上查找了。
      在我们书写代码的时候,创建了一个作用域链, inner函数不仅包含自己的作用域中的变量,还要包含函数中它所用到的变量。当函数在执行时候,它会按照既定的作用域链进行查找和解析,所以在返回inner 函数的时候,外部的a 不会消失,因为inner函数作用域中包含它。

      闭包在JS中是天然存在的,因为JS中的函数是值,可以包含在另外一个函数中,也可以被返回。再者,JS是词法作用域

       ES6函数增强

      声明函数的时候,给形参赋一个值,这个值就是参数的默认值。调用函数时,如果没有进行相应的实参传递,参数就会使用默认值。

    // num2拥有默认参数值5,如果没有给num2形参传值,它的取值将会是5 
    function sum (num1, num2 = 5) {
        return num1 + num2;
    }
    
    console.log(sum(1)) // 6 调用sum函数时, 只传递了一个参数1,所以函数中num1 =1, num2就会使用默认参数值5, 1+5 =6;
    console.log(sum(1,2)) // 3 函数调用时,我们传递了两个参数,所以默认参数值不起作用, 函数使用我们传递过去的参数 1+2 =3

      这里所说的‘值’是广义的值,不仅仅是指像5这样的简单值,它可以是任意的js表达式,甚至是函数的调用

    function getValue(value) {
         return value + 5;
    }
    // 函数参数是第一个参数的值。
    function add(first, second = getValue(first)) {
        return first + second;
    }
    console.log(add(1, 1)); // 2
    console.log(add(1)); // 7

      正如你看到的那样,默认参数为函数时,这个函数的调用是惰性的,如add(1,1)传递了两个参数,函数就不会调用。add(1)只传递一个参数, 这个函数才会调用。其次,getValue函数可以把函数的第一个参数first 作为自己的参数。但是这里有一个小细节要注意,函数的形参也有了自己的作用域,形参的作用域只是把函数声明()中的参数包起来,如果在参数默认值中去解析一变量,它先从形参作用域中进行查找,如果没有找到,它再从函数外面的作用域中进行查找。add 函数的参数的声明就像下面一样

    let first;
    let second = getValue(first);·

      getValue中的first参数,正好在形参作用域中找到了,所以没有问题,如果写反了,function add( second = getValue(first), first) {}, 函数的参数声明就变成了

    let second = getValue(first);
    let first;

      first 变量还没有声明,就使用了,造成了暂存死区。再来看一个例子,

    let w = 1, z = 2;
    
    function foo( x = w + 1, y = x + 1, z = z + 1 ) {
        console.log( x, y, z );
    }
    foo(); // ReferenceError

      先执行w + 1,  就会从形参作用域中找w,没有找到,就到函数外面去找,正好找到了w=1, 那x就赋值为2,再执行y=x + 1, 这时从形参作用域中找到了x,y变成了3,最后是z = z + 1,先执行z+1,在形参作用域找到了z,它就不会从外面的作用域去找了,但是z声明在后,z+1 就变成了引用一个未声明的变量,造成了暂存死区

      当参数拥有默认值以后,它影响了argumets 对象。我们都知道,每一个函数内部都有一个arguments 对象,保存函数调用时传递过去的参数,第一个参数对应的就是arguments[0], 第二个参数对应的就是arguments[1]. 像上面的sum 函数, num1 == argument[0]; 但有了默认参数值,这种对于关系打破了. sum(1) 调用sum 函数的时候,我们只传递了一个值1,也就意味着arguments[1] 的值是undefined, 但是它对应的num2 形参,num2 参数由于默认值的存在,这里取5.  arguments[1]  就不等于num2 了。还有一点就是,arguments 只是保存了传递过去的值,如果在函数内部 参数的值有更改,那么arguments 也不会实时反应这种变化,还是上面的sum(1) 调用,arguments[0] 永远等于1。 初始的时候,num1 == arguments[0]; 但如果在函数体中 num1 重新赋值为2, arguments[0] 就不等于num1 了。  

    function sum (num1, num2 = 5) {
        console.log(arguments.length); // 1, 只传递了一个参数
        console.log(num1 === arguments[0]); // true  初始时相等
        console.log(num2 === arguments[1]); // false 只传一个参数,arguments[1] 是undefined, num2 取默认值5
        num1 = 2;
        console.log(num1 === arguments[0]); // false arguments只保存调用时的初值。
        return num1 + num2;
    }
    
    console.log(sum(1));

      记住一点就可以了, arguments 对象只保存调用函数时传递过去的参数的初始值。不太理解也没有关系,arguments 对象几乎用不到了,因为ES6 提供了更好的参数保存方式(剩余参数rest),下面会介绍。

      剩余参数 (rest)

      当我们调用函数的时候,我们可以传递任意数量的实参给函数,如果函数形参的数量少于实参的数量,我们就只能通过函数内部的arguments 获取多余的实参。ES6 提供了一个更简单的方法来获取这些多余的参数,就是剩余参数。我们在声明函数的时候,在一个参数的前面加上..., 这个参数就变成了一个数组,它会把多余的参数收集到它里面,变成它的元素。

    let sum = (obj, ...rest) => {
        console.log(rest)  // [2,3,4,5]
    } 
    
    sum({a:1},2,3,4,5)

      上面代码中的rest就是一个剩余参数,它把2,3,4,5 收集起来,变成了它的元素,它本身是一个数组。

      注意:一个函数中只能有一个剩余参数,且它必须放到所有参数最后,这很好理解,因为,它把所有参数都收集到一起了,一个就足够了,如果它后面还有参数,这些参数也获取不到数据了,所以也就没有必要设置参数了。

      扩展操作符(...)

      扩展操作符,把一个可迭代对象(如数组)扩展给一个一个的单体。

    let array = [1,2,3,4,5]; 
    console.log(...array)  // 1 2 3 4 5

      js 中的函数有两个内部的方法, [[Call]]  和[[Construct]] , 当我们调用函数的时候,没有使用new, 那[[Call]] 方法就会被调用,执行函数体。当调用函数的时候前面加了一个new, 那[[Construct]]  方法就会被调用,生成一个对象,调用函数,给对象赋值,并返回对象。当然并不是每一个函数都有[[Construct]] 方法,比如箭头函数就没有,所以箭头函数就不能使用new 进行调用。那怎么决定函数是用那种方法呢?

      ES6 增加了一个new.target,  如果一个函数通过new 调用,它内部会获取一个new.target 的元属性,它指向的就是我们的构造函数。 当然,如果这个函错误地通过一般函数调用,new.target 就是undefined. 这样我们就可以轻松地判断一个函数是不是通过new进行调用,从而避免了构造函数用普通方式进行调用产生的错误。

    function Person(name) {
        if (typeof new.target !== "undefined") {
            this.name = name;
        } else {
            console.error("You must use new with Person.")
        }
    }
    var person = new Person("Nicholas");
    var notAPerson = Person.call(person, "Michael"); // You must use new with Person.

           

  • 相关阅读:
    自然语言交流系统 phxnet团队 创新实训 项目博客 (十一)
    install ubuntu on Android mobile phone
    Mac OS, Mac OSX 与Darwin
    About darwin OS
    自然语言交流系统 phxnet团队 创新实训 项目博客 (十)
    Linux下编译安装qemu和libvirt
    libvirt(virsh命令总结)
    深入浅出 kvm qemu libvirt
    自然语言交流系统 phxnet团队 创新实训 项目博客 (九)
    自然语言交流系统 phxnet团队 创新实训 项目博客 (八)
  • 原文地址:https://www.cnblogs.com/SamWeb/p/13194676.html
Copyright © 2011-2022 走看看