函数调用_强制指定回调函数的函数上下文
<!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"