JavaScript this关键字
发布于 2016-10-19 16:08 阅读数 190
本文必须得到作者授权后,方可转载,摘要引流随意。
By 依韵 , From https://blog.cdswyda.com/post/20161019
涵义
this
关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。
首先,this
总是返回一个对象,简单说,就是返回属性或方法“当前执行环境”的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | var showName = function() {
console.log('My name is', this.name);
};
var zs = {
name: 'Zhang San',
describe: showName
},
ls = {
name: 'Li Si',
describe: showName
};
zs.describe(); // My name is Zhang San
ls.describe(); // My name is Li Si
showName(); // My name is
name = 'window'; // 等价于 window.name = 'window'; 和this.name = 'window'; 因为此时window===this
showName(); // My name is window
|
上面代码中定义了showName
方法,将在控制台输出"My name is "并拼接上this.name
,并将这个方法赋给了zs和li这两个对象的describe
方法。
当通过这两个对象调用describe
方法时,分别输出zs和ls的name属性。
直接在全局环境下调用showName
方法并没有报错,但也没有输出任何内容。不报错的原因是此时的this
指的是浏览器的window对象,window对象有name属性。没有输出内容的原因是window.name
初始值是一个空字符串。
我们给name
属性赋值为"window"后再次执行showName
方法时,将输出: My name is window
以上示例中实际都是执行的showName
方法,但是由于环境不同,输出的结果也不同,根本原因是不同情况下的this是不一样的。
- zs.describe(); this === zs
- ls.describe(); this === ls
- showName(); this === window
再看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <input type="button" name="按钮1" onclick="showName()" value="按钮1">
<input id="btn1" type="button" name="按钮2" value="按钮2">
<input id="btn2" type="button" name="按钮3" value="按钮3">
<script>
window.name = 'window';
var showName = function() {
console.log('My name is', this.name);
};
var btn1 = document.getElementById('btn1');
btn1.onclick = showName;
var btn2 = document.getElementById('btn2');
btn2.addEventListener('click', showName, false);
</script>
|
点击三个按钮,控制台输出结果分别是什么呢?
第一个为:My name is window 第二个为:My name is 按钮2 ,第三个为My name is 按钮3
这是为什么呢?这个和绑定事件的机制有关系。第一种形式是HTML事件,onclick="showName()"
表示在点击时执行showName
方法,此时执行环境为全局环境,this为window,所以输出window。
第二种和第三种分别为DOM0级事件
和DOM2级事件
,其实质是给点击事件指定了一个回调函数,其为showName
。在点击事件的回调函数中,this是指当前这个dom元素,因此输出的值为这两个按钮的name
属性。
使用场合
this
的使用是很广泛的,其作用也非常强大。我们可以将this
的使用归为一下几类。
构造函数
在构造函数中,this
的出现频率是非常高的,它指的是实例对象。
1 2 3 4 5 6 7 8 9 | function Person(name, gender) {
this.name = name;
this.gender = gender;
}
zs = new Person( 'zs' , 'male' );
// {
// name: "zs",
// gender: "male"
// }
|
上面使用构造函数产生实例对象时,两个参数赋值给了实例对象就是通过this来完成的。
1 2 3 4 5 6 7 8 9 10 | function Person(name, gender) {
this.name = name;
this.gender = gender;
}
Person.prototype.showSelf = function() {
return this;
}
zs = new Person('zs', 'male'); // {name: "zs", gender: "male"}
zs.showSelf(); // {name: "zs", gender: "male"}
zs === zs.showSelf(); // true
|
上面代码通过showSelf
方法返回了构造函数里的this
,它的输出内容和实例对象一致。也是和实例对象严格相等的,可以说明,构造函数中的this是指代实例对象。
对象的方法
在对象里面定义的方法中也经常看到this
的身影,那么此时的this
指的又是什么呢?
大多数情况下,this
指的是当前的这个对象,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 | var box = {
id: +new Date(),
name: 'noName',
setName: function(name) {
this.name = name;
},
getName:function(){
return this.name;
}
}
box.getName(); // "noName"
box.setName('box1');
box; // {id: 1476846238291, name: "box1"}
|
上面代码中在通过box对象来调用setName
和getName
方法的情况下,this
指的就是当前的这个对象,此处为box,也正是由于这种情况下的this
指的是当前对象,我们才能通过这两个方法对box的name属性进行读写。
但是只有box.getName()
和box.setName('box1')
这样使用时,this
才指向当前对象。请看下面例子:
当将一个对象的一个方法赋给另一个对象时,this
的指向也会改变。
1 2 3 4 5 6 7 8 9 10 11 | var box = {
name: 'box',
getName: function() {
return this.name;
}
};
var bag = {
name: 'bag'
};
bag.getName = box.getName;
bag.getName(); // "bag"
|
虽然bag.getName
实际是对box.getName
的一个引用,由于运行时使用的是bag.getName()
,此时是在bag对象下运行的,this也就指的是bag了。
再看一点奇怪的:
1 2 3 | // 注意 box.getName 没有括号
(false || box.getName)(); // window
(false ? alert : box.getName)(); // window
|
上面这两种情况下,输出的都不再是box对象的name属性,而是window(之前设置了window.name='window')。表示此时方法内部的this
指向的是浏览器顶层对象window
。
可以这么理解:
box对象指向了一个地址M1
, box.getName
作为box的一个方法,但本身也是对象,它自己也有一个地址M2
,只有通过box.getName()
调用时,是从M1
中调用M2
,所以this
指向的是box。上面两种情况都是直接拿到M2
来调用,此时和M1
已经没有任何关系了,this
的指向当前代码块所在的对象。
全局环境
在全局环境中使用this,在浏览器中,指的就是顶层对象window
。
1 2 3 4 5 6 7 | console.log(this === window); // true
function thisIs() {
console.log(this === window);
}
thisIs(); // true
|
上面代码说明,不管this
是写在全局环境下,还是一个函数作用域内,只要是在全局环境下运行,this
的指向都是顶层对象window
。
Node
在Node中,this
的指向又分成两种情况。全局环境中,this
指向全局对象global
;模块环境中,this
指向module.exports
。
ES6箭头函数
ES6中新增的箭头函数里面所使用的this
和之前介绍的情况都不一样了,在箭头函数中this
不随其运行环境的改变而改变,而是在声明箭头函数时,就已经固定下来了。箭头函数中this
的指向就是声明箭头函数是所在的对象。
先看一个常规的例子:
1 2 3 4 5 6 7 8 9 | function foo() {
setTimeout(function() {
console.log('name:', this.name);
}, 100);
}
foo(); // name: window
foo.call({ name: 'an obj' }); // name: window
|
定义一个函数foo
内部使用定时器调用一个匿名函数,此时函数有多层了,this
的指向应该是全局对象window
,输出结果证明了这一点。使用foo.call
结果也相同的原因是,call替换的是foo函数内的this指向,而输出的this是在定时器的回调中的,故结果依然是window
。
我们再看一下箭头函数中这一点的表现:
1 2 3 4 5 6 7 8 9 10 | // ES6箭头函数
function arrow_foo() {
setTimeout(() => {
console.log('name:', this.name);
}, 100);
}
arrow_foo(); // name: window
arrow_foo.call({ name: 'an obj' }); // name: an obj
|
我们发现结果,居然和上面不一样了。为什么呢?我们将其转化成ES5的结果来看一下,上面代码转化后的结果是这样的:
1 2 3 4 5 6 7 8 | function arrow_foo() {
var $__1 = this;
setTimeout(function() {
console.log('name:', $__1.name);
}, 100);
}
arrow_foo();
arrow_foo.call({name: 'an obj'});
|
看一下转换后的结果,原因就一目了然了,箭头函数中this直接固定成了其定义时所在的对象,此处为foo
。实际在箭头函数中的所有this都是一个对象,这个对象就是其定义时所在对象的this
,上面转换后的结果中在foo中首先使用一个变量记录下this,而在箭头函数中的this被替换成了之前存储this的那个变量。
因此直接运行时,this
是指全局对象,而使用call
时,将foo
内的this
替换成了指定的对象{name: 'an obj'}
,从而输出的上面的结果。
使用注意点
避免多层this
由于this
的指向是不确定的,所以切勿在函数中包含多层的this。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | var box = {
name: 'box',
size: {
width: 300,
height: 300
},
show: function() {
console.log('name', this.name);
(function() {
console.log('size', this.size);
})();
}
};
box.show();
// name box
// size undefined
|
我们本意是想在show
方法内部输出name
,并输出size
,但是结果却并不是想要的这样,这是因为在立即执行的函数内部,this
的执行不再是box
对象而变成了顶层对象window,因此第二行输出为undefined
。
解决方法为,在外层用一个变量记录下this,在要使用的地方使用那个变量。
将上例进行改写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | var box = {
name: 'box',
size: {
width: 300,
height: 300
},
show: function() {
console.log('name', this.name);
var that = this;
(function() {
console.log('size', that.size);
})();
}
};
box.show();
// name box
// size Object {width: 300, height: 300}
|
这样就能得到我们想要的正确结果了。
还用一种做法是JavaScript
提供的严格模式use strict
,如果函数内部的this直接指向了顶层对象会直接报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | var box = {
name: 'box',
size: {
width: 300,
height: 300
},
show: function() {
'use strict'
console.log('name', this.name);
(function() {
console.log('size', this.size);
})();
}
};
box.show();
// Uncaught TypeError: Cannot read property 'size' of undefined(…)
|
避免在回调函数中使用this
通常回调函数中的this
都有其特定的,如果在回调函数中使用this
,应该需要了解其含义,否则可能出现意料之外的结果。
事件处理函数作为一种特殊的回调函数,其this
是指当前的DOM对象
,最开始的例子已经说明了这个问题。
回调函数本身是一个函数,其作为另一个函数的参数传递进去,然后在那个函数内部执行,这本身已经构成了多层this
,此时this
的指向是不确定的,需要慎用。
绑定this的方法
this
的动态性给JavaScript
带来了很大的灵活性,但是前面所描述的内容中也表现出了其不确定性,因此有时希望能够将this
固定下来。
function.prototype.call()
使用函数的call
方法,可以指定函数内部this
的指向,使其在指定的作用域中运行。
1 2 3 4 5 6 7 8 | var obj = {};
var f = function () {
return this;
};
f() === this; // true this === window
f.call(obj) === obj; // true
|
上面代码中,在全局环境运行函数f时,this
指向全局环境;call
方法可以改变this
的指向,指定this
指向对象obj
,然后在对象obj
的作用域中运行函数f
。
call
方法的第一个参数为一个对象,其表示要为函数所指定的运行上下文环境的对象(当指定为undefined
或null
是默认传入window
),之后的参数依次作为原函数的参数。
function.prototype.apply()
使用函数的apply
方法同样可以指定函数运行的环境,作用和call
相同,使用方法也类似,都是第一个参数传入要指定的上下文对象。不同点在于,apply
方法最多接收两个参数,第二个参数为一个数组(无论原函数需要的参数是何种类型,此数组中的每个元素将依次传递给原函数),表示传递给原函数的参数,而call
可以接收多个参数,从第二个参数开始,之后的所有参数都传递给原函数。
由于apply
第二个参数接收的是数组,其有很多巧用。由于此文重点是描述this
关键字,就不再赘述了。
function.prototype.bind()
ES5中有bind
这样一个方法,也可以指定函数的运行环境,但是和call
、apply
有所不同,bind
方法可接收一个参数,用于指定函数运行的上下文环境,返回一个函数作为绑定指定上下文环境后的新函数。
这样bind
和call
、apply
的区别就出来了:前者是根据指定的上下文环境返回一个新函数,而后两者是使用指定的上下文坏境运行原函数。
其实bind
和jQuery.proxy()
类似,虽然没有后者处理多种情况,但作为JavaScript
原生方法,更轻量、高效。
用本文最开始的例子来演示此方法的使用,某对象下有某方法,我们要将此对象这个方法作为作为一个事件处理函数,但不希望方法内部的this
被改变:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <input id="btn1" type="button" name="按钮1" value="张三的名字是">
<input id="btn2" type="button" name="按钮2" value="张三的名字是">
<script>
window.name = 'window';
var showName = function() {
console.log(this.name);
};
var zs = {
name: '张三'
};
var btn1 = document.getElementById('btn1');
btn1.addEventListener('click', showName, false);
var btn2 = document.getElementById('btn2');
btn2.addEventListener('click', showName.bind(zs), false);
</script>
|
这样点击第二个按钮,将可以正确输出张三的名字。
bind
第一个参数为一个对象,为undefined
或null
是默认传入window
。
除此之外,bind还可以接收额外参数,用于在生成新函数时,从原函数的第一个参数开始替换一部分参数(和jQuery.proxy()
类似)。比如原函数要接收两个参数,使用bind产生新函数时,除了第一个参数的外,可以再传入一个参数,此参数将替换原函数的第一个参数,这样生成的新函数就只用接收一个参数了,详情见jQuery 工具方法简析中jQuery.proxy( function, context [, additionalArguments ] )
。