函数调用_强制指定回调函数的函数上下文

<!DOCTYPE html>
<html lang="en">
<head>  
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用forEach迭代方法展示如何设置函数上下文</title>
    <script src="../unitl/test.js"></script>
    <style>
        #results li.pass {color:green;}
        #results li.fail {color:red;}
    </style>
</head>
<body>
    <ul id="results"></ul>
</body>
<script>

   // 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。确保每个元素的上下文正确。
    forEach(weapons,function(index){

        assert(this===weapons[index],"Got the expected value of " + weapons[index].type);

    });


</script>
</html>
    

迭代函数接受需要遍历的目标对象数组作为第一个参数,回调函数作为第二个参数。迭代函数遍历数组,对每个数组元素执行回调函数:

        function forEach(list,callback) {

        for(var n=0; n<list.length; n++) {

            callback.call(list[n],n);

        }

    }

使用call方法调用回调函数,将当前遍历到的元素作为第一个参数,循环索引作为第二个参数,使得当前元素作为函数上下文,循环所引作为回调函数的参数。
执行测试时,设置一个简单的数组weapons,然后调用forEach函数,传入数组及回调函数:

      //调用迭代函数forEach。确保每个元素的上下文正确。
    forEach(weapons,function(index){

        assert(this===weapons[index],"Got the expected value of " + weapons[index].type);

    });

从下图可以看到,函数运行得很完美

apply和call的功能类似,但问题是在二者如何选中?答案与许多其他问题的答案是相似的:选择任意可以精简代码的方法。更实际的答案是选择与现有参数相匹配的方法。如果有一组无关的值,则直接使用call方法。若已有参数是数组类型,apply方法是
更加选择。

解决函数上下文的问题

箭头函数作为回调函数还有一个更优秀的特性:箭头函数没有单独的this值。箭头函数的this和声明所在的上下文的相同。

<!DOCTYPE html>
<html lang="en">
<head>  
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用forEach迭代方法展示如何设置函数上下文</title>
    <script src="../unitl/test.js"></script>
    <style>
        #results li.pass {color:green;}
        #results li.fail {color:red;}
    </style>
</head>
<body>
    <button id="test">Click me </button>
    <ul id="results"></ul>
</body>
<script>


    // Button构造函数用于创建保存按钮的状态的对象
    function Button () {
        this.clicked = false;
        // 声明用于处于点击事件的箭头函数。因为click是对象的方法。我们在函数内部使用this获得对象的引用
        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);


</script>
</html>       

代码运行后的结果如下图所示。

可以看出一切正常。按钮对象保持单击状态,在button构造函数内部使用箭头函数创建时间的处理方法如下:

      function Button () {
        this.clicked = false;
        // 声明用于处于点击事件的箭头函数。因为click是对象的方法。我们在函数内部使用this获得对象的引用
        this.click = ()=>{

            this.clicked = true;
            //在函数内,验证点击后按钮的状态发生改变
            assert(button.clicked,"The button has been clicked");
        }

    }

调用箭头函数时,不会隐式传入this参数,而是从定义时的函数继承上下文。在本例中,箭头函数在构造函数内部,this指向新创建的对象本身,因此无论何时调用click函数,this都将指向新创建的button对象。

警告:箭头函数和对象字面量

由于this值在箭头函数创建时确定的,所以会导致一些看似奇怪的行为。回到按钮单击示例中,因为只是一个按钮,因此可以假设不需要构造函数。直接使用对象字面量。如下面例子所示。

      <!DOCTYPE html>
<html lang="en">
<head>  
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用forEach迭代方法展示如何设置函数上下文</title>
    <script src="../unitl/test.js"></script>
    <style>
        #results li.pass {color:green;}
        #results li.fail {color:red;}
    </style>
</head>
<body>
    <button id="test">Click me </button>
    <ul id="results"></ul>
</body>
<script>

    //全局代码中的this指向全局window对象
    assert(this===window,"this===window");



    //使用对象字面量定义button
    var button = {
        clicked:false,
        //箭头函数是对象字面量的属性。
        click:()=>{
            this.clicked = true,
            //验证是否单击按钮
            assert(button.clicked,"The button has been clicked");
            //箭头函数中的this指向全局window对象
            assert(this===window,"In arrow function this === window");
            //Clicked属性存储在window对象上
            assert(window.clicked,"clicked is stored in window");
        }
    }

    var elem = document.getElementById("test");
    elem.addEventListener("click",button.click);

</script>
</html>
    

运行程序我们将会感到失望,因为button对象无法跟踪clicked状态。如下图所示

在代码中定义一些断言会有帮助。例如,在全局代码中编写如下代码确认this的值。

      assert(this===window,"this===window");

断言通过,因此可以确定全局代码中的this指向全局window对象。

      //使用对象字面量定义button
    var button = {
        clicked:false,
        //箭头函数是对象字面量的属性。
        click:()=>{
            this.clicked = true,
            //验证是否单击按钮
            assert(button.clicked,"The button has been clicked");
            //箭头函数中的this指向全局window对象
            assert(this===window,"In arrow function this === window");
            //Clicked属性存储在window对象上
            assert(window.clicked,"clicked is stored in window");
        }
    }

回顾一下规则,箭头函数在创建时确定了this的指向。由于click的函数是作为对象字面量定义的,对象字面量在全局代码中定义,因此,箭头函数内部this值与全局代码的this值相同。代码清单中中第一句断言:

      assert(this===window,"this===window")

可以看出全局代码的this指向全局window对象。因此,clicked属性被定义在另外window对象上,而不再button对象上。最后断言可以确定clicked属性赋值在window对象上:

      assert(window.clicked,"Clicked is stored in window");

如果忘记箭头函数的副作用可能会导致一些bug,需要特别小心!
已经看到箭头函数可以规避函数上下文的问题,继续看另一种解决方案。

使用bind方法

函数还可访问bind方法创建新函数。无论使用哪种方法调用,bind方法创建的新函数与原始函数的函数体相同,新函数被绑定到指定的对象上。再次回顾按钮单击事件处理的问题。

<!DOCTYPE html>
<html lang="en">
<head>  
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用forEach迭代方法展示如何设置函数上下文</title>
    <script src="../unitl/test.js"></script>
    <style>
        #results li.pass {color:green;}
        #results li.fail {color:red;}
    </style>
</head>
<body>
    <button id="test">Click me </button>
    <ul id="results"></ul>
</body>
<script>




    //使用对象字面量定义button
    var button = {
        clicked:false,
        //箭头函数是对象字面量的属性。
        click:function() {
            this.clicked = true;
            assert(button.clicked,"The button has been clicked");
        }
    }

    var elem = document.getElementById("test");

    //使用bind函数创建新函数
    elem.addEventListener("click",button.click.bind(button));


    var boundFunction = button.click.bind(button);
    assert(boundFunction != button.click,"Calling bind create s a completly new Function");

</script>
</html>
      

所有函数均可访问bind方法,可以创建并返回一个新函数,并绑定在传入的对象上(在本例中,绑定在button对象上)。不管如何调用该函数,this均被设置为对象本身。
被绑定的函数与原始函数行为一致,函数体一致。
无论何时单击按钮,都将调用绑定的函数,函数的上下文是button对象。
从示例代码中的最后一局断言可以看出,调用bind方法不会修改原始函数,而是创建了一个全新的函数

      var boundFunction = button.click.bind(button);
      assert(boundFunction != button.click,"Calling bind creates a completly new function");

以上我们完成了对函数上下文的研究。

对象字面量就是创建对象的一种简单容易阅读的方法。如下创建了一个对象

     var obj = {
        a:'add',
        b:'bro',
        c:'chris'
    }
 obj.a  //add
 obj['a']//"add"
    
posted @ 2020-12-17 14:31  yongjar  阅读(268)  评论(0编辑  收藏  举报