学了JavaScript将近一年了,对于this的掌握却还是不太熟悉,甚至有时候都不知道它到底指向哪里,在看了《你不知道的JavaScript上》中关于this的指向的解析,对this的指向认识清晰了不少,接着又在网上查阅了一些资料,写下这篇博客来分享我对this的指向的理解。如果有什么不对请各位大佬指出我的错误。
在此之前我对this的理解是函数作用域中指向该函数,在对象中使用就指向该对象,但实际上,this真正的指向需要知道它所使用的位置,即它的调用位置。下面介绍this的四条绑定规则。(这四条规则的优先级是默认绑定<隐式绑定<显示绑定<new绑定,这里就不做优先级比较,下面介绍完后各位可以尝试优先级是否如上所说)
默认绑定
默认绑定在平时的使用中非常常见,我的理解是this的调用的位置在全局运行环境中,而全局环境下的this会指向全局对象(在使用Web Worker加载的文件中,this指向Web Worker对象),如下面这段代码。
function fn(){
var a=1;
console.log(this.a);
}
var a=2;
fn()
// 2
上面这行代码,可能会让人误以为会打印出1,但实际上却是打印出了2。这是因为this对象是在全局作用域下调用的,所以实际上调用了全局作用域下的变量a。要注意的是,如果在严格模式下的话,this会指向undefined,所以上面的代码在严格模式中是会报错的。
function fn(){
'use strict'
var a=1;
console.log(this.a);
}
var a=2;
fn()
// VM2278:4 Uncaught TypeError: Cannot read property 'a' of undefined at fn
在判断为不是其他几种绑定形式时,就可以视为是默认绑定了。
隐式绑定
隐式绑定考虑的是调用位置是否有上下文对象,或者是否被某个对象拥有或包含。
(上下文对象包含变量对象,作用域链和this指针,如果调用位置有上下文对象的话,this指向其this指针指向的位置)
隐式绑定主要有下面几种情况,作为对象时的方法,原型链上的调用,getter和setter中的this以及作为一个DOM事件处理函数。
作为对象的方法
var obj={
a:1,
fn(){
console.log(this.a);
}
}
var a=2;
obj.fn()
// 1
这里要注意的是,最后的结果之所以是1,是因为调用时是对象obj调用了fn方法,并非是因为方法定义在obj对象中,可以用下面的代码来看。
var obj={
a:1,
fn(){
console.log(this.a);
}
}
var obj1={
a:3,
fn:obj.fn
}
obj1.fn();
// 3
在这段代码中可以看到,虽然方法fn定义在对象obj里面,但是是用obj1来调用方法fn的,所以最后this指向的是调用该方法的对象obj1。
如果在使用对象链式调用方法时,方法中的this指向链的最后一个对象。如下代码
function fn() {
console.log(this.a);
}
var obj1 = {
a: 1,
fn: fn
}
var obj2 = {
a: 2,
obj1: obj1
}
console.log(obj2.obj1.fn());
// 1
可以看到,最后的this是指向对象链最后的obj1。若obj1中没有属性a的话,最后的结果会返回undefined。
原型链上的调用
原型链上的this指向的判断也是一样的,关键在于this是在哪里调用的,看看下面这段代码
function fn() {
console.log(this.a);
}
var obj1 = {
a: 1,
fn: fn
}
var obj2 = Object.create(obj1);
obj2.a = 2;
console.log(obj2.fn());
//2
在这段代码中,方法fn虽然定义在对象obj1上,但是调用时是在obj2对象上调用的,所以调用的是obj2对象上的a。而这样实际上和上面的对象的调用是一样的,不一样的地方在于,如果这里的对象obj2没有属性a的话,会沿着原型链寻找属性a,所以下面这段代码最后会打印出1。
function fn() {
console.log(this.a);
}
var obj1 = {
a: 1,
fn: fn
}
var obj2 = Object.create(obj1);
console.log(obj2.fn());
// 1
getter和setter中的this
如果将this写在getter和setter的方法中的话,this会指向getter和setter方法所属的对象。
var obj = {
n: 1,
get num() {
return this.n;
}
}
console.log(obj.num);
// 1
如上面代码,对象obj获取其属性num,调用了其getter方法,所以this指向了对象obj,最后返回了对象obj的属性n的值。setter也是同样的道理。
var obj = {
n: 1,
set num(val) {
this.n = val;
}
}
console.log(obj.n);
// 1
obj.num = 10;
console.log(obj.n);
// 10
DOM事件处理函数
在DOM事件处理函数上的this对象指向触发该事件的元素。
<body>
<p id="dom">DOM</p>
</body>
<script>
var p = document.getElementById('dom');
function fn() {
console.log(this.innerHTML);
}
p.onclick = fn;
// DOM
</script>
上面代码中,点击了p标签后就会在控制台打印出DOM,这是因为this指向了p。
关于隐式绑定,我的理解是在使用时没有明确指出this指向哪个对象,通过在某个对象上调用函数,将该调用函数中的this指向做出调用这个操作的对象。
而隐式调用在某些情况下会出现隐式丢失的情况,即失去了对对象的绑定。如下代码
function fn() {
console.log(this.a);
}
var obj = {
a: 1,
fn: fn
}
var f = obj.fn;
var a = 2;
f();
// 2
这里的f=obj.fn,看起来好像是在obj对象中调用该方法的,但仔细看一下就会发现,这条语句是将该函数fn赋值给变量f,即这里的f实际上严格等于fn,即是在全局环境下调用了fn,所以这其实是默认绑定的情况。setTimeout第一个参数传入函数名时也会出现这种情况。
function fn() {
console.log(this.a);
}
var a = 1;
var obj = {
a: 2,
fn: fn
}
setTimeout(obj.fn, 100)
// 1
//等同于
setTimeout(function fn(){
console.log(this.a);
},100)
显式绑定
显式绑定,顾名思义就是显式地将绑定写出来,与隐式绑定不同,显式绑定会明确地将要绑定的对象写出来。显式绑定主要和call(),apply()和bind()三个方法密切相关,而事实上这三个方法的第一个参数就是要来绑定this对象的。
不考虑其后面的参数,这三个方法在this绑定上是一样的,我们就call方法来进行解析。
function fn() {
console.log(this.a);
}
var obj = {
a: 1
}
var a = 2;
fn.call(obj);
// 2
如上代码,使用call方法显式地将this对象指向对象obj,所以最后打印出的是obj对象的属性a的值。像这种绑定的方式称为硬绑定,硬绑定的缺点在于会降低函数的灵活性,要绑定的对象必须显式地写出来,而且使用硬绑定之后就没法再使用隐式绑定和硬绑定来修改this对象的指向了。
function fn() {
console.log(this.a);
}
var obj = {
a: 1
}
var a = 2;
var obj1 = {
a: 3,
fn: fn.call(obj)
}
obj1.fn;// 尝试使用obj1隐式绑定
// 1 还是原来的硬绑定
function fn1() {
fn.call(obj);
}
fn1.call(window);// 尝试使重用硬绑定绑定到全局
// 1 还是原来的硬绑定
对于这些硬绑定的缺点,JavaScript有软绑定来解决这些问题,下面用《你不知道的JavaScript上》里面的部分代码来解析
下面是软绑定softBind()的方法
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
var curried= Array.prototype.slice.call(arguments, 1);//获取绑定对象外的参数
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?obj : this,
//检查是否为undefined或者全局对象(默认绑定),是则将默认对象obj绑定到this,否则不修改this
curried.concat.apply(curried, arguments)
);
};
bound.prototype = Object.create(fn.prototype);
return bound;
};
}
软绑定关键在于若调用this时绑定到全局或者undefined(默认绑定),则将其绑定到之前传入的默认绑定对象,若不是默认绑定的情况,则不修改this,即使this绑定到新的对象上。
function foo() {
console.log(this.name);
}
var obj1 = { name: "obj1" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind(obj1);
fooOBJ();// obj1 使用软绑定绑定,因为当前this为默认绑定的情况,所以this指向了传入的对象obj1
obj2.foo = foo.softBind(obj1);
obj2.foo();// obj2 使用隐式绑定成功修改this绑定的对象
fooOBJ.call(obj3);// obj3 使用硬绑定成功修改this绑定的对象
setTimeout(obj2.foo, 1000);// obj1 与上面的fooOBJ()一样,这里等同于默认绑定的情况,所以this指向了传入的对象obj1
new绑定
new绑定出现在使用new操作符构造对象的时候,它会将函数中的this指向构建的新对象。
function fn(val) {
this.a = val;
}
var obj = new fn(1);
console.log(obj.a);
上面代码中,方法fn将传入的参数作为this指向的对象的属性a的值,而通过new运算符的构建,使this对象指向obj,所以obj的属性a被传入值1。
判断this指向
综上所述,绑定this的指向就分为以下几步
1.判断是否是new运算符创建的,如果是的话则指向新创建的对象
2.判断是否是显式绑定的(apply,call,bind或其他API调用的“上下文”),如果是则指向指定的对象
3.判断是否为隐式绑定的(在某个上下文对象中调用),如果是则指向该上下文对象
4.如果以上情况都不是的话,则为默认绑定,严格模式下指向undefined,非严格模式下指向全局
除了上面这几步外,还有例外情况,即ES6中的箭头函数,箭头函数比起普通函数更容易写,但存在的问题是箭头函数内的this会由外部的作用域来决定。
function fn() {
return (a) => {
console.log(this.a);// this继承自fn的this
}
}
var obj1 = {
a: 1
}
var obj2 = {
a: 2
}
var o = fn.call(obj1);
o.call(obj2);
// 1
上面代码中,fn返回的箭头函数中的this实际上继承自fn中的this,所以在后面的代码中,fn绑定obj1,返回的箭头函数绑定obj2,最后打印出的是1,就是因为实际上指向的是fn绑定的对象。
以上即为我对JavaScript的this对象指向的理解,希望对大家有所帮助,也希望有大佬能指出我的不足。