体验函数的上下文
<script>
var obj = {
name: '小明',
age: 21,
a: 1,
b: 2,
addsum: function () {
console.log(this.a + this.b);
console.log(this === obj);
}
};
// 如果此时让你回答上述的代码的最终结果为多少
// 答案为不知道,因为不知道此对象的addsum方法的调用形式是什么,即不知道该函数的上下文
// 调用函数方法1:
obj.addsum();
// 此时结果为3, 因为对象直接打点调用函数,该函数内的this就是obj,结果也为true
// 调用函数方法2:
var addsum = obj.addsum;
// 将对象的方法单独再传给一个全局变量
addsum();
// 结果为nan,false
// 因为此时this === windows,该函数的上下文为windows,全局变量都为windows的属性,但是没有a,b这两个全局变量输出为nan
</script>
注意此时js语句内最后的两行代码。为什么将obj.addsum函数单拎出来赋值给addsum变量,在此调用结果就为NaN了呢?不应该还是3嘛?这就涉及到函数上下文的问题了。函数的上下文规则总结五种,下面进行归纳总结。
上下文规则一
规则一:函数通过对象打点调用运行,函数的上下文就是此对象。即this === 打点的对象。
<script>
// 定义一个函数,返回值为一个对象
function outer() {
var a = 11;
var b = 22;
return {
a: 33,
b: 66,
fn: function () {
console.log(this.a + this.b);
}
};
};
// 调用函数fn
outer().fn(); /* 结果为99 */
// 分析
// 此函数经过outer()调用后会返回一个对象,再进行打点调用fn()方法,此时fn函数的上下文为outer()函数返回的此对象,this === 此对象,结果为99
该js语句内先定义了一个函数outer(),函数会返回一个对象,对象内还有一个函数fn。当outer().fn(),调用后,该函数先执行outer()函数,会立刻返回一个
{
a: 33,
b: 66,
fn: function () {
console.log(this.a + this.b);
}对象。然后此对象fn(),也就是对象打点调用函数,此时函数fn的上下文就是该对象,此时this===该返回的对象,接着进行函数执行语句是console.log(this.a + this.b),也就是按照规则一,进行33 + 66 的运算,最后输出99。
上下文规则二
规则二:函数通过圆括号“()”调用运行,函数的上下文就是window对象。即this === window。
<script>
// 定义全局函数
function fn() {
return this.a + this.b;
}
// 定义全局变量
var a = 11;
var b = 22;
// 定义对象
var obj = {
a: 33,
b: fn(),
c: fn
};
// 调用函数
var result = obj.c();
console.log(result);
// 结果为66
// 分析
// 当obj.c()时,obj就是函数fn的上下文,this===obj.执行后为33+fn(),此时fn的调用方法为加圆括号调用,函数的上下文为window,此时全局变量为window的属性,11+22=33,所有obj.b=33,最后结果为66
</script>
该示例先定义了一个全局函数fn,函数内是返回两个值的和,但此时不知道函数fn的上下文是谁,结果为“不知道”。接着定义了两个全局变量a,b,这里需要了解的是全局变量都是window的属性。接着定义了对象obj,对象obj.b的值是函数fn的运行值,对象obj.c的值是fn函数,在内存中指向全局函数fn的地址,即两者公用一块地址。接着通过obj.c()来调用函数fn,此时函数fn的上下文就是obj,this === obj。所以在函数fn内“this.a” = 33,“this.b” = fn()。fn()明显是一个函数的执行结果。注意此时fn是通过圆括号“()”调用的,那么他的上下文就是window,this === window。他访问的是全局变量a,b,再次进行相加得到b = 33,那么回到最初的结果33 + 33 = 66。再将66赋值给result,最后输出。
上下文规则三
规则三:函数通过数组下标调用运行,函数的上下文就是该数组。即this === 数组
<script>
// 定义一个函数
function fn() {
arguments[3]();
};
// 调用函数
fn(1, 2, 3, function () {
console.log(this[1]);
});
// 结果为2
// 分析
// 函数fn的功能:调用执行传入函数fn的下标为3的实参,此时fn内的四个参数为一组类数组,所以this===该组数组,那么就会输出2
</script>
该示例先定义了一个全局函数fn,该函数的功能是实现传入的函数fn()实参数组下标为3的语句功能。接着调用函数fn()。此时传入的函数fn()实参数组为[
1, 2, 3, function () {
console.log(this[1]);
}
]其中下标为3的实参为一个函数,那么显然是要实现该函数的功能,但是是通过数组来调用的,那么此时该函数的上下文就是该实参数组,this = 实参数组,而数组下标为1的值是2,所以最后结果为2。
上下文规则四
规则四:IIFE函数,也就是立即执行函数,这种函数的上下文为windo,this===window。
<script>
// 定义全局变量
var a = 33;
// 定义对象
var obj = {
a: 66,
fn: (function () {
var a = this.a;
return function () {
console.log(a + this.a);
}
})()
};
// 调用函数
obj.fn();
// 结果为99
// 分析
// fn的属性值是一个IIFE函数,也就是立即执行函数,这种函数的上下文为windo,this===window,此时a = 33,接着会返回一个函数给fn,而该函数会因为闭包的性质记住a,接着通过obj打点调用fn,obj成为该函数的上下文,此时this === obj,所以this.a = 66,最后结果为99
</script>
该示例先定义了一个全局变量a。接着定义了对象obj,此对象的obj.fn值是一个IIFE函数,会立即执行,且IIFE函数执行值是返回一个函数,也就是obj.fn的值其实就是该返回的函数,此函数是实现两个值的和。接着obj.fn()打点调用了函数fn,fn内IIFE函数会立即执行,此时fn的IIFE函数的上下文是window,即this === window, 会找全局变量。执行第一条语句“var a = this.a;”,由于this === window,所以this.a = 33,接着返回一个
function () {
console.log(a + this.a);
}函数,此函数内的a因为闭包性质,会记录a的值。那么this.a的值为什么?this等于什么?因为是对象obj打点调用的函数,所以此函数的上下文就是对象obj,this === obj,那么this.a=66。最后相加输出99。
上下文规则五
规则五:函数通过定时器/延时器来调用,函数的上下文为window对象,this === window。
<script>
// 定义对象
var obj = {
a: 33,
b: 66,
fn: function () {
console.log(this.a + this.b);
}
};
// 定义全局变量
var a = 1;
var b = 2;
// 调用函数
setTimeout(obj.fn, 1000);
// 结果为3
setTimeout(function () {
obj.fn();
}, 1000);
// 结果为99
// 分析
// 第一种调用:因为函数是由定时器来直接调用的,所以函数的上下文为window,this === window,接着寻找全局变量,从而输出结果为3
// 第二种调用:因为函数是由定时器来间接调用的,实际上定时器是调用的外部的包裹obj.fn()的函数,此时fn的上下文为obj,this===obj,结果为99
</script>
该示例首先定义了一个对象obj,其中obj.fn是一个函数,此函数的功能是打印两个数的和。接着定义了两个全局变量a,b。接着通过延时器obj.fn来直接调用函数,此时函数的上下文就是window对象,this === window所以会寻找全局变量a,b,所以最后会输出3。注意我们再次用延时器调用此函数,但是在延时器内再定义一个函数,而函数内才调用函数fn,此时得到的结果不为3。这是为什么呢?这是因为对于obj.fn()他是在函数体内的,而延时器是间接来通过他调用函数fn的,而obj.fn()的形式符合规则一对象打点调用,函数的上下文就是对象obj,所以会输出99。
上下文规则六
规则六:DOM事件处理函数,函数的上下文就是绑定的DOM元素。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box {
width: 200px;
height: 200px;
float: left;
margin-right: 10px;
border: 1px solid black;
}
</style>
</head>
<body>
<div class="box" id="box1"></div>
<div class="box" id="box2"></div>
<div class="box" id="box3"></div>
<script>
// 获取元素
var box1 = document.getElementById('box1');
var box2 = document.getElementById('box2');
var box3 = document.getElementById('box3');
// 定义事件处理函数
function colortored() {
// 备份上下文
var self = this;
setTimeout(function () {
self.style.backgroundColor = 'red';
}, 2000);
};
// 调用函数
box1.onclick = colortored;
box2.onclick = colortored;
box3.onclick = colortored;
// 结果是点哪个盒子哪个盒子两秒后变红
// 分析
// 当通过打点事件监听调用函数,谁打点,谁就是this,此时定义一个self变量来保存当前的上下文,也是为了解决由于延时器调用函数,从而改变上下文的问题。
</script>
</body>
</html>
此示例是在网页上点击哪个盒子,哪个盒子两秒后变红,而且要使用同一个事件监听函数。为了实现使用同一个事件监听函数,我们需要先定义一个函数用于封装,函数内设置一个延时器,延时器内再设置事件监听函数,函数的作用是使盒子背景变红。但此时你就要注意到该事件监听函数不是直接通过延时器来调用的,所以直接写this.style.backgroundColor = 'red'是不对的。最后给每个盒子绑定监听来调用函数。那么此时的函数的上下文就是绑定的盒子,为了实现this.style.backgroundColor = 'red',而不受规则五的影响,我们要添加“var self = this;”语句,来备份一下此时的上下文,再在事件监听函数内书写self.style.backgroundColor = 'red';同样实现了规则五的作用,此时的this === self === 绑定的盒子。
指定函数的上下文
指定函数的上下文需要用到call()方法或apply()方法。
<script>
// 定义对象
var xiaoming = {
c: 100,
m: 100,
e: 100
}
// 定义函数
function fn() {
console.log(this.c + this.m + this.e);
}
// 调用函数
fn.apply(xiaoming); /* fn打点调用apply是将xiaoming指定为该函数的上下文 */
fn.call(xiaoming); /* fn打点调用call是将xiaoming指定为该函数的上下文 */
// 但是两者有区别
// 当还有函数还有要传入的参数时,call是在,小明后以逗号隔开的方式写入;apply是以数组的形式写入其中
</script>
此示例定义了一个对象xiaoming,定义了函数fn。通过函数打点调用apply/call方法的同时,会将call/apply()方法内的参数传入到函数内,该参数就是指定函数的上下文,本示例中传入了参数“xiaoming”,即让对象xiaoming成为函数fn的上下文,函数的this === xiaoming,接着再进行输出,不管是apply()方法还是call()方法都是输出一样的结果,但是两者还是有区别的。
call方法与apply方法的不同
两者的不同就在于传入方法内的参数形式不同。
对于call()方法,当需要传给函数的参数除了上下文后,如果还有其他的参数,需要以逗号","隔开的形式写入。
对于apply()方法,当需要传给函数的参数除了上下文后,如果还有其他的参数,需要以数组的形式写入。
<script>
// 定义函数
function fun1(a, b) {
console.log(a + b);
};
// 定义函数
function fun2() {
fun1.apply(this, arguments);
}
// fun2函数的作用是调用函数fun1
// 调用函数
fun2(33, 66);
// 当函数fun2被调用后,此时会将函数fun2的上下文window指定给函数fun1,并且因为传入的还有arguments,类数组对象,也就是一个数组,所以在这里必须用到apply方法,33,66以数组的形式进入到函数fun1中,得出结果99
</script>
该示例先定义了一个函数fun1,此功能是输出两个参数的和。接着定义了函数fun2,此函数的功能是调用函数fun1,并将给函数fun1添加上下文,此时还不知道this等于什么?第二个参数是一个类数组对象,也就是传入的参数以数组的形式传入。接着用圆括号的形式调用函数fun2,此时符合规则二,此时this === window,将window对象作为上下文,传给函数fun1,同时传入的还有一个arguments类数组对象,伪数组,也是数组。那么就需要用到apply()方法,进而得到99的值。