函数的上下文
概述
在函数体的语句中,会出现this
这个词,this
就是函数的上下文
函数中this
是谁,就说明函数的上下文是谁
函数中的this
是谁,要看是如何调用的,因为this
不是一成不变的
比如我们看下面的例子
var obj = {
a: 100,
fun: function() {
console.log(this.a);
}
};
我们此时在obj
对象中定义了一个fun
函数,就是obj
的属性
现在如果直接对象打点调用
obj.fun();
此时会弹出100
,说明上下文就是对象本身
如果此时我们将整个方法进行一次赋值
var obj = {
a: 100,
fun: function() {
console.log(this.a);
}
};
var f = obj.fun;
f();
页面中就会弹出undefined
,因为此时this
的上下文不是obj
了,而是window
规则1:直接圆括号执行,上下文是window
对象
什么叫做直接圆括号调用,就是没有对象打点执行,不是方括号枚举执行,通常是从数组、对象中提取出来后单独执行的
var obj = {
a: 100,
fun: function() {
alert(this.a);
}
};
var f = obj.fun;
f();
f()
就是直接圆括号执行,因为这个f
是从obj
提取出来的
- 直接圆括号执行的
this
指向的是window
对象 - 需要注意的是
js
中全局变量都是window
对象的属性 - 还需要注意的是IIFE也属于直接圆括号调用的范畴,里面的
this
都是window
对象
var a = 300;
var obj = {
a: 100,
b: (function() {
console.log(this.a)
})()
};
弹出的内容是300,一位IIFE的this
指向的是window
小题目
企业面试题
var xiaohong = {
name: '小红',
age: 25,
sayHello: (function() {
console.log(this.age)
return this.age >= 18 ? '女士' : '女生'
})()
}
console.log(`大家好,我叫${xiaohong.name},我是一个${xiaohong.sayHello}`);
答案是女生,这道题的重点是IIFE里面的this
,上面我们说过了IIFE里面的this
指向的是window
,所以此时IIFE里面的this.age
是undefined
,由于undefined >= 18
结果是false
,三元表达式走后面的“女生”
小题目
var obj = {
a: 100,
fun: function() {
var a = 200;
console.log(this.a);
}
}
var a = 300;
var f = obj.fun;
f();
答案是300,切记this
指向谁一定要看调用,此时我们发现,调用是圆括号直接执行的。所以我们就知道了,内部的this
就是window
,所以obj
里面的所有的a
都是障眼法。由于全局变量都是window
对象的属性,所以var a = 300
就是window.a = 300
,此时弹出的结果就是300
规则2:从对象中调用或者数组中枚举执行的函数,上下文就是这个对象或者数组
先补充点函数知识
函数的length
指的是函数的形参列表长度
function fun(a, b, c, d, e, f) {
}
console.log(fun.length);
函数的实参是一个arguments
对象
function fun(a, b, c, d, e, f) {
console.log(arguments)
}
fun(1, 2, 3, '你好', '哈哈');
每一个函数都有一个属性是arguments
,值是一个类数组对象
什么是类数组对象?
类数组对象和数组很像,本质是对象,拥有数组的length
属性,有对应的下标索引值。函数的arguments
或者我们document.getXX
获取DOM的时候返回对象类型都是类数组对象,因为这些对象虽然看似数组,但是没有数组的能力,不能进行push
等等操作
我们知道函数中this
是上下文,需要看如何调用,如果想表达函数自己,用arguments.callee
function fun() {
console.log(arguments.callee == fun)
}
fun();
小题目
function fun1(a, b, c) {
arguments[0]();
}
function fun2(a, b, c, d, e) {
console.log(this.length);
}
fun1(fun2, 9, 2, 4, 2, 34, 234);
此时this
是看谁调用的。fun1
在调用时,fun1
调用的时候执行了函数arguments[0]
,因为arguments
是fun1
的实参列表,所以第0项就是fun2
函数,所以符合规则2;fun2
函数中的this
指的就是fun1
函数的arguments
类数组对象,所以length
就是7
小题目
此时我们把上面的题目升级
function fun1(a, b, c) {
arguments[0](1, 2, 3, 4, 5, 6);
}
function fun2(a, b, c, d, e) {
console.log(this.length);
console.log(arguments.length);
console.log(arguments.callee.length);
console.log(this.callee.length);
}
fun1(fun2, 9, 2, 4, 2, 34, 234);
解析:通过分析知道了fun2
中的this
指的是fun1
函数,所以此时this.length
指的就是fun1
的arguments
类数组对象(因为是类数组枚举执行的符合规则2)
arguments
本身是fun2
函数自己的实参列表,所以长度是6(调用的时候传了1~6的参数)
我们知道arguments.callee
是fun2
函数自己,所以length
就是形参列表为5
this.callee.length
指的就是fun1
的形参列表为3
小题目
var m = 2;
var obj = {
fun1: function() {
return this.fun2();
},
fun2: fun2,
m: 4
};
function fun2() {
return this.m;
}
console.log(obj.fun1());
题目的核心就是上下文的传递
小题目
综合前面规则1和规则2出的面试题
var num = 1;
var obj = {
num: 2,
fun: (function() {
var num = 3;
this.num += 4;
return function() {
this.num *= 5;
num *= 6;
console.log(num);
}
})()
};
obj.fun();
obj.fun();
console.log(num);
console.log(obj.num);
var f1 = obj.fun;
f1();
console.log(num);
console.log(obj.num);
var f2 = obj.fun;
f2();
console.log(num);
小题目
var length = 1;
var obj = {
length: 10,
b: [{
length: 20,
fun: function() {
console.log(this.length);
}
}]
};
var arr = [obj, obj.b, obj.b[0], obj.b[0].fun];
arr[0].b[0].fun();
arr[1][0].fun();
arr[2].fun();
arr[3]();
规则3:定时器直接调用,上下文是window
对象
var a = 100;
function fun() {
console.log(this.a++);
}
setInterval(fun, 1000);
需要注意的是定时器调用和定时器内部调用是有区别的
下面代码是定时器在调用obj.fun
函数,所以调用者是定时器
var obj = {
a: 300,
fun: function() {
console.log(this.a++);
}
}
var a = 100;
setInterval(obj.fun, 1000);
下面的代码本质是obj
在调用函数,所以上下文是obj
var obj = {
a: 300,
fun: function() {
console.log(this.a++);
}
}
var a = 100;
setInterval(function() {
obj.fun();
}, 1000);
规则4:DOM事件中的this
,指的是触发事件的这个DOM元素
// 首先生成四个div
for (var i = 0; i < 4; i++) {
var div = document.createElement('div');
div.style.width = '100px';
div.style.height = '100px';
div.style.backgroundColor = 'white';
div.style.display = 'inline-block';
div.style.marginRight = '16px';
div.style.border = '1px solid black';
div.id = ('box' + i);
document.body.appendChild(div);
}
// 点击div修改颜色
var box0 = document.getElementById('box0');
var box1 = document.getElementById('box1');
var box2 = document.getElementById('box2');
var box3 = document.getElementById('box3');
function changeColor() {
this.style.backgroundColor = 'purple';
}
box0.onclick = changeColor;
box1.onclick = changeColor;
box2.onclick = changeColor;
box3.onclick = changeColor;
规则5:call()
和apply()
可以设置函数的上下文
函数的上下文主要是看谁在调用,但是我们可以通过call()
和apply()
区设置函数的上下文
call()
和apply()
本质就是调用函数的同时,指定上下文
比如我们有一个changeSex
的函数,它的作用是修改sex
的属性
此时有一个xiaohong
对象,sex
为女
此时我们调用这个changeSex
函数,强行将函数的上下文绑定为xiaohong
function changeSex() {
if (this.sex == '男') {
this.sex = '女';
} else {
this.sex = '男';
}
console.log(this);
}
var xiaohong = {
name: '小红',
sex: '女'
}
changeSex.call(xiaohong);
console.log(xiaohong.sex);
此时小红对象被修改为了男
apply
函数也有同样的功能
changeSex.apply(xiaohong);
规范
函数.call(带有上下文的内容);
函数.apply(带有上下文的内容);
函数的上下文就是带有上下文的内容
需要注意的是call()
和apply()
的本质核心是有区别的:主要是语法上的区别。call
是接收参数,apply
是接收数组
call
方法要求,所有的参数在上下文对象后面一一罗列
function person(name, age, height, weight) {
this.name = name;
this.age = age;
this.height = height;
this.weight = weight;
};
var xiaoming = {
name: '小明',
age: 3,
height: 60,
weight: 15
};
person.call(xiaoming, '小明', 23, 183, 65);
此时我们换成apply
,apply
要求所有的参数必须规整到一个数组中
person.apply(xiaoming, ['小明', 30, 183, 75]);
apply
本质上只要求两个参数,第二个参数是一个数组集合
在使用结果上两种方式都是一样的
小题目
function fun1() {
fun2.apply(obj, arguments)
}
function fun2(a, b, c) {
console.log(obj);
console.log(a);
console.log(b);
console.log(c);
}
var obj = {
name: '小明',
sex: '男'
}
fun1('香蕉', '葡萄', '梨子');
此时你会发现apply
有一个功能是将第二个数组参数进行解构,变成一个个的罗列参数,比如我们传进arguments
是一个类数组对象,但是我们在fun2
函数接收的a,b,c
形参中进行了解构,也就是分别变成了 香蕉、葡萄、梨子
小题目
此时我们想根据apply
的特点出一个思考题,利用Math
方法进行数组的最大值查找
Math.max.apply(null, [789, 2342, 123, 2134, 2345, 22])
我们知道apply
第二个参数是数组,但是apply
有能力给解构,所以我们可以利用这个特点求数组的最大或最小值
上面的题目是企业中经常遇到的面试题,一定不要只写循环遍历求最大最小值
企业面试题
题目1
var a = 1;
function fn() {
this.a++;
a += 10;
var a = 8;
}
fn()
console.log(a)
答:结果是2,因为fn
执行的时候内部的this
指的是window
也就是全局的a=1
,所以this.a++
等于2;函数内部的a += 10
本质上是undefined += 10
结果为NaN
,因为后面有个var a = 8
;因为变量声明提升的原因造成的
题目2
var length = 5;
var arr = [fn1, fn2];
function fn1() {
return this.length;
}
function fn2() {
return this[0];
}
var a = arr[0]();
var b = arr[1]()();
console.log(a);
console.log(b);
因为a
此时的this
是数组枚举执行,符合规则2,也就是上下文是arr
数组,所以length
是2
b
其实最后返回的就是一个函数,函数直接圆括号执行此时上下文是window
所以是5
题目3
var number = 2;
var obj = {
number: 3,
fn1: (function() {
this.number *= 2;
number = number * 3;
var number = 2;
return function() {
this.number *= 4;
number *= 5;
console.log(number);
}
})(),
fn2: function() {
this.number *= 2;
}
}
var fn1 = obj.fn1;
console.log(number);
fn1();
obj.fn1();
obj.fn2();
console.log(window.number);
console.log(obj.number);
题目4
var length = 5;
function getLength() {
return this.length;
}
function foo() {
this.length = 1;
return (function() {
var length = 2;
return {
length: function(a, b, c) {
return this.arr.length;
},
arr: [1, 2, 3, 4],
info: function() {
return getLength.call(this.length);
}
}
})();
}
var result = foo().info();
console.log(result);
答案是3,因为result
是一个对象,这个对象打点调用info
,info
函数中执行了getLength
函数,并且将上下文一同绑定了,绑定的是对象的length
属性,该属性值是一个函数,所以getLength
函数在执行时候的this
就是函数,this.length
也就是该函数的length
结果为3