JavaScript 函数内部this的各种指向
目录:
1、引言
this
是js中的一个关键字,随着函数使用场合的不同,this的值也会发生变化。当我们在不使用箭头函数
、call()
方法、apply()
方法、bind()
方法的情况下,在调用函数的时候,这些函数内部this
的指向是不确定的。调用函数方式的不同,决定了this指向的不同
。
但是,总有一个原则,那就是this总是指向调用函数的那个对象
。
2、code例子
2.1 普通函数
普通模式:
//this最终指向的是调用它的对象,这里的函数fun实际是被Window对象所点出来的
function fun() {
console.log(this);
};
fun();//输出window
//写成Window.fun() 也是一样的!
严格模式:
// 开启严格模式
"use strict";
function fun() {
console.log(this);
};
fun();//输出undefinde
// 因为从严格意义上来讲它是自己调用的,而不是window
2.2 匿名函数
普通模式:
//this最终指向的是调用它的对象,这里的函数fun实际是被Window对象所点出来的
let fun = function() {
console.log(this);
};
fun();//输出window
//写成Window.fun() 也是一样的!
严格模式:
// 开启严格模式
"use strict";
let fun = function() {
console.log(this);
};
fun();//输出undefinde
// 因为从严格意义上来讲它是自己调用的,而不是window
2.3 构造函数
// 构造函数
function Person(name) {
// 实例属性
this.name = name;
console.log(this);
}
// 原型函数
Person.prototype.speak = function() {
console.log(this);
};
// 实例对象,调用构造函数
let son = new Person("儿子");
// 调用原型方法
son.speak();
// 输出Person,即实例对象
2.4 对象方法调用
// 构造函数
function Person(name) {
// 实例属性
this.name = name;
// 实例方法
this.speak = function() {
console.log(this);
}
}
// 实例对象,调用构造函数
let son = new Person("儿子");
// 调用实例方法
son.speak();
// 输出Person,即实例对象
2.5 事件绑定函数
<body>
<div></div>
<script>
var box = document.querySelector("div");
box.addEventListener("click", function() {
console.log(this);
})
// 输出div标签,因为是这个事件绑定在div身上,同时也是div触发的它
</script>
</body>
2.6 定时器函数
window.setTimeout(function() {
// 输出window,因为不管是炸弹定时器还是循环执行计时器都是由window点出来的,有时候我们只是省略的window而已
console.log(this);
}, 1000)
2.7 立即执行函数
普通模式:
(function fun() {
// 输出window,因为子调用函数其实与普通函数的原理是一样的,只不过它是直接调用了而已
console.log(this);
})()
严格模式:
// 开启严格模式
"use strict";
(function fun() {
// 输出undefined,因为严格来讲,它是自己调用的,而并非window
console.log(this);
})()
3、总结
调用方式 | this指向 |
---|---|
普通函数 | 严格模式下是undefined,正常模式是Window |
匿名函数 | 严格模式下是undefined,正常模式是Window |
构造函数 | 实例对象 |
对象方法调用 | 该方法所属的对象 |
事件绑定方法 | 当前事件所绑定的对象 |
定时器函数 | Window |
立即执行函数(自调用函数) | 严格模式下是undefined,正常模式是Window |
注意:
- 如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window,这里需要说明的是在js的严格版中this指向的不是window。
- 如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。
- 如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象。
4、改变函数内部 this 的指向
每个函数或者方法都默认拥有三个非继承而来的方法:call()
、apply()
和bind()
。这三个方法的作用都是一样的。即:在特定的作用域中调用函数,等于设置函数体内的this
的指向,以扩充函数赖以运行的作用域。 一般来说,this
总是指向调用某个方法的对象,但是使用这三个方法后,就会改变this的指向。比如在ES5类的继承中,我们总是使用call()
方法来借调基类的构造函数来实现借调继承。请看下方code实例:
例1:
// window对象下的属性
window.number = 'one';
// document对象下的属性
document.number = 'two';
// 字面量方式创建对象,并添加一个属性
var s1 = {number: 'three' };
// 定义一个普通方法
function changeColor(){
//谁调用它,它就打印谁的numbe属性
console.log(this.number);
}
changeColor.apply(); //one (默认传参传递window对象)
changeColor.apply(window); //one
changeColor.apply(document); //two 将documnet文档对象作为参数传递
changeColor.apply(this); //one 在全局作用域下this就代表window
changeColor.apply(s1); //three
例2:ES5基于原型的组合继承
// 基类构造函数
function Father(name, age) {
// 实例属性定义在构造函数内部
this.name = name;
this.age = age;
// 判断原型中的该属性是否是函数类型的,也就是判断原型中是否有该方法
if ((typeof Father.prototype.speak) !== "function") {
// 方法定义在原型中
Father.prototype.speak = function() {
console.log(`我叫${this.name},今年${this.age}岁了`)
}
}
}
// 派生类构造函数
function Son(name, age, sex) {
// 调用基类的构造函数,相当于把基类中的属性添加到未来的派生来实例对象中
// 将Father基类中的this指向改变为son的实例,并将基类所需要的参数传递
Father.call(this, name, age);
// 实例对象属性
this.sex = sex;
}
// 修改派生类的原型,这样就可以继承基类原型中的方法
Son.prototype = new Father();
// 实例化son,并将基类所需要的参数一并传递
var son = new Son("小明", 20, "男");
console.log(son.name);
console.log(son.age)
son.speak();
5、异同点
共同的:都可以改变this
的指向
不同点:
1. `call()`和`apply()`都会调用函数,并且改变函数内部的`this`指向
2. `call()`和`apply()`传递的参数不一样,`call()`传递参数使用逗号隔开,而`apply()`使用数组传递。`bind()`与`call()`参数一致
3. `bind()`不会立马调用函数,而会创建一个**新的函数(称为绑定函数)**, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,提供一个给定的参数序列。光说相信大家肯定不会明白,那么就看下方code:
例1:
function f(y, z){
return this.x + y + z;
}
// 使用bind方法将{x:1}对象作为this传递,同时也传递2,没有立马调用,而是生成一个新的函数
var m = f.bind({x : 1}, 2);
// 调用函数并将最后一个参数传递
console.log(m(3));
分析:
1. `bind()`会把它的第一个实参绑定给f函数内的this,那么这里this的指向为`{x:1}`对象
2. 然后将第二个参数传递进去,对应f函数内部的y参数,这里并没有立马调用,而是生成了一个新的函数m,这就是`bind()`的特别之处
3. 最后调用新生成的m函数,并将最后一个参数3传递到函数f中,对应f函数内的参数z
4. 最后执行的结果为:1+2+3=6
5. 分步处理参数的过程其实是一个典型的函数柯里化的过程(Curry)
例2:
var a = document.write;
a('hello'); //Error
a.bind(document)('hello');//hello
a.call(document, 'hello');//hello
分析:
var a = document.write;
将文档对象的write
方法放入变量a中,但是放入变量a中,我们打印一下,看变成了什么
- 那么既然变成了
window.write()
,那么采用a('hello');
这样的方式调用,肯定抛异常,因为window对象下没有这个方法 a.bind(document)('hello')
===a.call(document, 'hello')
这两种调用方式相同,只是写法不同而已。先将document文档对象作为参数传递给bind()
,此时window.write()
的指向就变成了document.write()
,那么再次传递字符串参数就是作为参数传递到了document.write()
方法中,那么就写入到了文档对象中
6、应用场景
call()
:在ES5中经常做组合继承.apply()
:经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值bind()
:不想立马调用函数,并且想隐式创建一个新的函数