理解函数调用

隐式的函数参数 this 和 arguments

  • 两者会被默默的传给函数,并且可以像函数体内显式声明的参数一样被正常访问。
  • 参数 this 表示被调用函数的上下文对象
  • arguments参数表示函数调用过程中传递的所有参数

4.1 使用隐式函数参数

  • 函数调用时还会传两个隐式参数:arguments 和 this

4.1.1 arguments 参数

  • arguments参数是传递给函数的所有参数的集合。无论是否有明确定义对应的形参,通过它都可以访问到函数的所有参数。借此可以实现JavaScript并不支持的重载特性,而且可以实现接收参数数量可变的可变函数。

  • arguments 对象有一个名为 length 的属性,表示实参的确切个数。通过数组索引的方式可以获取单个参数的值。如:arguments[2] 将获取第三个参数。

    function whatever(a, b, c){
          assert(a === 1, 'The value of a is 1');
          assert(b === 2, 'The value of b is 2');
          assert(c === 3, 'The value of c is 3');
    
          assert(arguments.length === 5,
            'We’ve passed in 5 parameters');
    
          assert(arguments[0] === a,
            'The first argument is assigned to a');
          assert(arguments[1] === b,
            'The second argument is assigned to b');
          assert(arguments[2] === c,
            'The third argument is assigned to c');
    
          assert(arguments[3] === 4,
            'We can access the fourth argument');
          assert(arguments[4] === 5,
            'We can access the fifth argument');
        }
        
        whatever(1,2,3,4,5);
    
  • 可以通过 arguments.length 属性来获取传递给函数的实际参数的个数。

  • 通过数组下标的方式可以访问到 arguments 参数中的每个参数值。注意:这里包括没有和函数形参相关联的剩余参数。

  • arguments 是一个伪数组。不能使用数组的方法。

  • 使用arguments 参数对所有函数参数执行操作

    function sum() {
          var sum = 0;
          for(var i = 0; i < arguments.length; i++){
            sum += arguments[i];
          }
          return sum;
        }
    
        assert(sum(1, 2) === 3, "We can add two numbers");
        assert(sum(1, 2, 3) === 6, "We can add three numbers");
        assert(sum(1, 2, 3, 4) === 10, "We can add four numbers");
    
  • arguments对象作为函数参数的别名

    • arguments 参数有一个特性:他可以作为函数参数的别名。例如为arguments[0]赋一个值,那么会改变第一个参数的值。

      function infiltrate(person) {
            assert(person === 'gardener',
              'The person is a gardener');
            assert(arguments[0] === 'gardener',
              'The first argument is a gardener');
      
            arguments[0] = 'ninja';
      
            assert(person === 'ninja',
              'The person is a ninja now');
            assert(arguments[0] === 'ninja',
              'The first argument is a ninja');
      
            person = 'gardener';
      
            assert(person === 'gardener',
              'The person is a gardener once more');
            assert(arguments[0] === 'gardener',
              'The first argument is a gardener again');
          }
      
          infiltrate("gardener");
      
    • 避免使用别名

      • 将 arguments 对象作为函数参数的别名使用会影响代码的可读性,因此在严格模式中将无法再使用它。
      • 严格模式
        • 严格模式是ES5中引入的特性,它可以改变JavaScript引擎的默认行为并执行更加严格的语法检查,一些在普通模式下的静默错误会在严格模式下抛出异常。在严格模式下,部分语言特性会被改变,甚至完全禁用一些不安全的语言特性,其中arguments别名在严格模式下将无法使用。
    • 使用严格模式避免使用 arguments 别名

      "use strict";
      
          function infiltrate(person){
            assert(person === 'gardener',
              'The person is a gardener');
            assert(arguments[0] === 'gardener',
              'The first argument is a gardener');
      
            arguments[0] = 'ninja';
      
            assert(arguments[0] === 'ninja',
              'The first argument is now a ninja');
      
            assert(person === 'gardener',
              'The person is still a gardener');
          }
      
          infiltrate("gardener");
      
      • 第一行代码是一个简单的字符串。告诉JavaScript引擎,下面的代码在严格模式下执行。

4.1.2 this 参数: 函数上下文

  • this 参数是面向对象 JavaScript 编程的一个重要组成部分,代表函数调用相关联的对象。因此,通常称之为函数上下文。
  • 函数上下文是来自面向对象语言(如 Java)的一个概念。在这些语言中,this通常指向定义当前方法的类的实例。
  • 在JavaScript中,将一个函数作为一个方法调用仅仅是函数调用的一种方式。事实上,this 参数的指向不仅是由定义函数的方式和位置决定的,同时还严重受到函数调用方式的影响。真正理解 this 参数 是面向对象JavaScript编程的基础。

4.2 函数调用

  • 可以通过4种方式调用一个函数,每种方式之间有一些细微差别。
    • 作为一个函数——skulk()直接被调用
    • 作为一个方法——ninja.skulk(),关联在一个对象上,实现面向对象编程
    • 作为一个构造函数——new ninja(),实例化一个新的对象
    • 通过函数的apply或者call方法——skulk.apply(ninja)或者skulk.call(ninja)
      • 除了call 和 apply 的方式外,函数调用的操作符都是函数表达式之后加一对圆括号。

4.2.1 作为函数直接被调用

  • 如果一个函数没有作为方法、构造函数或者通过apply和call调用的话,就称之为作为函数直接调用。

  • 通过()运算符调用一个函数,且被执行的函数表达式不是作为一个对象的属性存在时,就属于这种调用类型。

  • 当执行的函数表达式是一个对象的属性时:

  • //函数定义作为函数被调用
    function ninja() {}
    ninja ();
    //函数表达式作为函数被调用
    var samurai = function() {};
    samurai ();
    //会被立即调用的函数表达式,作为函数调用
    (function(){})()
    
  • 以这种方式调用时,函数上下文(this关键字的值)有两种可能性:在非严格模式下,它将是全局上下文(window对象),而在严格模式下。它将是undefined;

    //非严格模式下的函数
    //以window对象作为函数上下文
    function ninja () {
      return this;
    }
    
    //严格模式下的函数
    //其函数上下文为undefined
    function samurai () {
      'use strict';
      return this;
    }
    

4.2.2 作为方法被调用

  • 当一个函数被赋值给一个对象的属性,并且通过对象属性引用的方式调用函数时,函数会作为对象的方法被调用。

    var ninja = {};
    ninja.skulk = function(){};
    ninja.skulk();
    
    • 这种情况下的函数被称为方法。
    • 当函数作为某个对象的方法被调用时,该对象会成为函数的上下文,并且在函数内部可以通过参数访问到。
  • 函数作为函数调用和方法调用的异同点。

    //返回函数上下文,从而能从函数外面检查函数上下文
    function whatsMyContext() {
      return this;
    }
    
    //作为函数被调用并将其上下文设置为window
    assert(whatsMyContext() === window, "function call on window");
    
    //变量getMyThis得到了函数whatsMyContext的引用
    var getMyThis = whatsMyContext;
    assert(getMyThis() === window, 'Another function call in window');
    
    //创建于一个对象ninja1,其属性getMythis得到了函数whatsMyContext的引用
    var ninja1 = {
      getMyThis: whatsMyContext,
    }
    //使用ninjal对象的方法getMyThis调用函数,函数的上下文就变成了ninjal了,这就是面向对象。
    assert(ninjal.getMyThis() === ninja1, 'working with 1st ninja');
    
    //创建于一个对象ninja2,其属性getMythis得到了函数whatsMyContext的引用
    var ninja2 = {
      getMyThis: whatsMyContext,
    }
    
    //使用ninja2对象的方法getMyThis调用函数,函数的上下文就变成了ninja2了
    assert(ninja2.getMyThis() === ninja2, 'working with 1st ninja');
    
    • whatsMyContext函数的唯一功能就是返回它的函数上下文,这样就可以在函数外部看到调用的函数的上下文。
  • 注意:将函数作为方法调用对于实现JavaScript面向对象编程至关重要。可以通过this在任何方法中引用该方法的”宿主对象“。

4.2.3 作为构造函数调用

  • 构造函数的目的是创建一个新对象,并进行初始化设置,然后将其作为构造函数的返回值。

  • 构造函数的声明和其他函数类似,通过使用函数声明和函数表达式可以很容易的构造新的对象。唯一的例外是箭头函数,但最主要的区别是调用函数的方式。

  • 要通过构造函数的方式调用,需在函数调用之前使用关键字new。

  • 构造函数的强大功能
    function Ninja() {
          this.skulk = function() {
            return this;
          };
        
    
    var ninja1 = new Ninja();
    var ninja2 = new Ninja();
    
    assert(ninja1.skulk() === ninja1,
    "The 1st ninja is skulking");
    assert(ninja2.skulk() === ninja2,
    "The 2nd ninja is skulking");
    
    
    • 构造函数创建一个对象,并在该对象也就是函数上下文上添加一个属性skulk。这个skulk方法再次返回函数上下文,从而让我们在外部检测函数上下文。
    • 通过new关键字调用构造函数创建两个新对象,变量ninja1变量ninja2分别引用了这个两个新对象。
    • 检测已创建对象中的skulk方法。每个方法都应该返回自身已创建的对象。
  • 在这个例子中,创建了一个名为Ninja的函数作为构造函数。当通过new关键字调用时会创建一个空的对象实例,并将其作为函数函数上下文(this参数)传递给函数。构造函数中在该对象上创建了一个名为skulk的属性并赋值为一个函数,使得该函数成为新创建对象的一个方法。

  • 调用构造函数时会触发一系列特殊操作:

    1. 创建一个新的空对象,在内存(堆)中开辟一片新空间。

    2. 该对象作为this参数传递给构造函数,从而称为构造函数的函数上下文。

    3. 新构造的对象作为new运算符的返回值。

      function Ninja() {
            this.skulk = function() {
              return this;
      };
      var ninja1 = new Ninja();
      
      
      • 使用new关键字来构造函数,从而创建一个新的空对象
      • 新的空对象被设置为该函数的上下文(this)
      • 为该对象增加一个新的方法,也就是这里的skulk方法
      • 新构造的对象作为函数的返回值。
  • 构造函数的目的是创建一个新对象,并进行初始化设置,然后将其作为构造函数的返回值。任何有悖于这两点的情况都不适合作为构造函数。

  • 构造函数返回值
    function Ninja() {
        this.skulk = function () {
          return true;
    	};
        return 1;
    }
    assert(Ninja() === 1,
        "Return value honored when not called as a constructor");
    
    var ninja = new Ninja();
    
    assert(typeof ninja === "object",
    "Object returned when called as a constructor");
    assert(typeof ninja.skulk === "function",
    "ninja object has a skulk method");
    
    
    • 定义一个叫做Ninja的构造函数
    • 构造函数返回一个确定的原始类型值,即数字1
    • 该函数以函数的形式被调用,返回值为数字1
    • 通过new关键字以构造函数形式调用Ninja
    • 测试结果表明,返回值1被忽略了,new关键字将被初始化好的新对象返回
  • 一个构造函数返回另一个对象

    //显式返回对象值的构造函数
    var puppet = {
        rules: false
    };
    
    function Emperor() {
       this.rules = true;
       return puppet;
    }
    
    var emperor = new Emperor();
    
    assert(emperor === puppet,
    "The emperor is merely a puppet!");
    assert(emperor.rules === false,
    "The puppet does not know how to rule!");
    
    
    • 创建一个全局对象,该对象的rules属性设置为false
    • 尽管初始化了传入的this对象,但是依旧返回puppet全局对象
    • 作为构造函数调用该函数
    • 测试表明,变量emperor的值为构造函数返回的对象,而不是new表达式所返回的对象
  • 这个示例首先创建一个全局对象,通过puppet引用它,并将其包含的rules属性设置为false。

  • 然后定义一个Emperor函数,它会为新构造的对象添加一个rules属性并设置为true。此外,Emperor还有一个特殊点,他返回了puppet对象。

  • 之后通过new关键字将Emperor作为构造函数调用

  • 测试结果表明,puppet对象最终作为构造函数调用的返回值,而且在构造函数中对函数上下文的操作都是无效的。 最终返回的是puppet。

  • 结论

    • 如果构造函数返回一个对象,则该对象将作为整个表达式的返回值,而传入构造函数的this将被丢弃
    • 但是,如果构造函数返回的是非对象类型,则忽略返回值,返回新创建的对象
编写构造函数的注意事项
  • 构造函数的目的是根据初始条件对函数调用创建的新对象进行初始化。虽然这些函数可以被”正常“调用,或者被赋值为对象属性从而作为方法调用,但这样做没有太大意义。
  • 构造函数通常以不同于普通函数的方式编码和使用,并且只有作为构造函数调用时才有意义,因此出现了命名约定来区分构造函数和普通的函数及方法。
  • 函数和方法的命名通常以描述其行为的动词开头,且第一个字母小写。
  • 构造函数通常以描述所构造对象的名词命名,并以大写字母开头。
  • 通过构造函数我们可以更优雅的创建多个遵循相同模式的对象,而无需一次次重复相同的代码。通用代码只需要作为构造函数的主体写一次即可。

4.2.4使用call和apply方法调用

  • 不同类型函数调用之间的主要区别在于:最终作为函数上下文传递给执行函数的对象不同。

    • 对于方法而言,即为方法所在对象。
    • 对于顶级函数而言是window对象或者undefined(取决于是否处于严格模式下)。
    • 对于构造函数而言是一个新创建的对象实例。
  • 为函数绑定特定的上下文

    <button id="test">Click Me!</button>
    <script>
        function Button(){
          this.clicked = false;
          this.click = function() {
            this.clicked = true;
            assert(button.clicked,"The button has been clicked");
          }
        }
        var button = new Button();
        var elem = document.getElementById("test");
        elem.addEventListener("click", button.click);
     </script>
    
    
    • 如何解决单击事件里this的指向问题?
    • this.click的this指向实例对象 里面的this.clicked指向点击元素
  • 使用apply和call方法
    • 函数作为第一类对象(函数是由内置的Function构造函数所创建),函数可以像其他对象类型一样拥有属性,也包括方法。

    • 使用 apply 方法调用函数需要为其传入两个参数:

      • 作为函数上下文的对象
      • 一个数组
    • call的方法使用类似,不同点在call是直接以参数列表的形式作为第二参数,而不再以数组传递

      function juggle() {
            var result = 0;
            for (var n = 0; n < arguments.length; n++) {
              result += arguments[n];
            }
            this.result = result;
          }
      
          var ninja1 = {};
          var ninja2 = {};
      
          juggle.apply(ninja1,[1,2,3,4]);
          juggle.call(ninja2, 5,6,7,8);
      
          assert(ninja1.result === 10, "juggled via apply");
          assert(ninja2.result === 26, "juggled via call");
      
      
      • 在这个例子中,定义了一个名为 juggle 的函数,作用是将所有的参数加在一起并存储在函数上下文的result属性中(通过this关键字引用)。
      • 设置两个参数对象 ninja1 和 ninja2,使用这个两个对象作为函数的上下文,将第一个对象连同一个参数数组一起传递给函数的apply方法,将第二个对象连同一个参数列表一起传递给函数的call方法
      • apply和call之间唯一不同之处在于如何传递参数。
        • 在使用apply的情况下,使用参数数组
        • 在使用call的情况下,在函数上下文之后依次列出调用参数
  • 强制指定回调函数的函数上下文
    • 将函数上下文强制设置为指定的对象

    • 实现forEach 迭代方法展示如何设置函数上下文

      function forEach(list, callback) {
          for (var n = 0; n < list.length; n++) {
            callback.call(list[n], n);
          }
      }
      
      var weapons = [{ type:'shuriken'}, 
                     { type:'katana'},
                     { type:'nunchucks'}];
      
      forEach(weapons, function(index){
            assert(this === weapons[index],
                  "Got the expected object of " + weapons[index].type);
      });
      
      
      • 迭代函数接收需要遍历的目标对象数组作为第一个参数,回调函数作为第二个参数。迭代函数遍历数组,对每个数组元素执行回调函数。
      • 使用call方法调用回调函数,将当前遍历到的元素作为第一个参数,循环索引作为第二个参数,使得当前元素作为函数上下文,循环索引作为回调函数的参数。
      • 换行测试时,设置一个简单的数组 weapons ,然后调用forEach函数,传入数组及回调函数。

4.3 解决函数上下文的问题

4.3.1 使用箭头函数绕过函数上下文

  • 箭头函数作为回调函数还有一个更优秀的特性

    • 箭头函数没有单独的this值

    • 箭头函数的this与声明所在的上下文相同。

      function Button() {
            this.clicked = false;
            this.click = () => {
              this.clicked = true;
              assert(button.clicked, "The button has been clicked");
            }
          }
          var button = new Button();
          var elem = document.getElementById("test");
          elem.addEventListener("click", button.click);
      
      
      • 调用箭头函数不会隐式的传入this参数,而是从定义时的函数继承上下文。在本例中,箭头函数在构造函数内部,this指向新创建的对象本身,因此无论何时调用click函数,this都指向新创建的实例对象。
  • 警告:箭头函数和对象字面量
    • 由于this值是在箭头函数创建时确定的,所以会导致一些看似奇怪的行为。因为只有一个按钮,因此可以假设不需要构造函数。直接使用对象字面量。

      //箭头函数与对象字面量
          assert(this === window, "this == window");
          var button = {
            clicked: false,
            click: () => {
              this.clicked = true;
              assert(button.clicked,"The button has been clicked");
              assert(this === window, "In arrow function this == window");
              assert(window.clicked, "clicked is stored in window");
            }
          };
      
          var elem = document.getElementById("test");
          elem.addEventListener("click", button.click);
      
      
      • 在全局代码中编写如下代码确认this的值

        • assert(this === window, “this == window”);
      • 在button对象字面量中click属性时箭头函数

        • var button = {
                clicked: false,
                click: () => {
                  this.clicked = true;
                  assert(button.clicked,"The button has been clicked");
                  assert(this === window, "In arrow function this == window");
                  assert(window.clicked, "clicked is stored in window");
                }
              };
          
          
      • 箭头函数在创建时确定了this的指向。由于click箭头函数是作为对象字面量的属性定义的,对象字面量在全局代码中定义,因此,箭头函数内部的this值与全局代码中的this值相同,指向window对象。

      • 如果忘记了箭头函数的副作用可能会导致一些bug,需特别小心

4.3.2 使用bind方法

//在事件处理中绑定指定上下文
<button id="test">Click Me!</button>
  <script type="text/javascript">
    var button = {
      clicked: false,
      click: function(){
        this.clicked = true;
        assert(button.clicked,"The button has been clicked");
      }
    };
    var elem = document.getElementById("test");
    elem.addEventListener("click", button.click.bind(button));

    var boundFunction = button.click.bind(button);
    assert(boundFunction !== button.click,
           "Calling bind creates a completyl new function");
  </script>

  • 所有函数均可访问bind方法,可以创建并返回一个新函数,并绑定在传入的对象上(本例中绑定带button对象上)。不管如何调用该函数,this均被设置为对象本身。被绑定的函数与原始函数行为一致,函数体一致。

  • 无论何时点击按钮,都将调用绑定的函数,函数的上下文是button对象。

  • 从最后一句断言可以看出,调用bind方法不会修改原始函数,而是创建了一个全新的函数:

    var boundFunction = button.click.bind(button);
        assert(boundFunction !== button.click,
               "Calling bind creates a completyl new function");
    
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值