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.

           

  • 相关阅读:
    Hadoop之HDFS的热备份
    Hadoop之联邦HDFS
    Hadoop之HDFS如何保证高可用
    Hadoop之HDFS的读流程
    Hadoop之HDFS数据写流程和写失败的情况
    Hadoop之HDFS的block、packet、chunk
    Hadoop之HDFS的元数据冷备份(CheckPoint)
    当ZooKeeper作为Dubbo的注册中心的时候,是怎么工作的?
    Dubbo
    mysql磁盘满了如何恢复
  • 原文地址:https://www.cnblogs.com/SamWeb/p/13194676.html
Copyright © 2011-2022 走看看