1.什么是this&&为什么要用this
1.1 什么是this
当前执行上下文(global、function 或 eval)的一个属性,在非严格模式下,总是指向一个对象,在严格模式下可以是任意值。
1.2 为什么要用this
先看下列代码
function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call( this );
console.log( greeting );
}
var me = { name: "Kyle" };
var you = { name: "Reader" };
identify.call( me ); // KYLE
identify.call( you ); // READER
speak.call( me ); // Hello, 我是 KYLE
speak.call( you ); // Hello, 我是 READER
如果不使用 this,需要给 identify() 和 speak() 显式传入一个上下文对象
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify( context );
console.log( greeting );
}
identify( you ); // READER
speak( me ); //hello, 我是 KYLE
this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计 得更加简洁并且易于复用。随着你的使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样。当我们介绍对象和原型时,你就会明白函数可以自动引用合适的上下文对象 有多重要。
2.误解
2.1 指向自身
人们很容易把 this 理解成指向函数自身,这个推断从英语的语法角度来说是说得通的。 那么为什么需要从函数内部引用函数自身呢? 常见的原因是 递归 或者 可以写一个在第一次被调用后自己解除绑定的事件处理器。 JavaScript 的新手开发者通常会认为,既然函数看作一个对象(JavaScript 中的所有函数都 是对象),那就可以在调用函数时存储状态(属性的值)。这是可行的,有些时候也确实有用, 但是有的时候,让大家看到 this 并不像我们所想的那样指向函数 本身。
看一段代码
function foo(num) {
console.log( "foo: " + num ); // 记录 foo 被调用的次数
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 0 -- Whats up?
console.log 语句产生了 4 条输出,证明 foo(..) 确实被调用了 4 次,但是 foo.count 仍然 是 0。显然从字面意思来理解 this 是错误的。
==执行 foo.count = 0 时,的确向函数对象 foo 添加了一个属性 count。但是函数内部代码 this.count 中的 this 并不是指向那个函数对象,所以虽然属性名相同,根对象却并不相同,困惑随之产生。==
大家一定会问 “如果我增加的 count 属性和预期的不一样,那我增加的是哪个 count ?”
实际上,如果深入探索的话,就会发现这段代码在 无意中创建了一个全局变量 count,它的值为 NaN。 当然, 如果他发现了这个奇怪的结果,那一定会接着问:“为什么它是全局的,为什么它的值是 NaN 而不是其他更合适的值?”
2.2 它的作用域
第二种常见的误解是,this 指向函数的作用域。这个问题有点复杂,因为在某种情况下它 是正确的,但是在其他情况下它却是错误的。
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); // ReferenceError: a is not defined
虽然这段代码看起来好像是我们故意写出来的例子,但是实 际上它出自一个公共社区中互助论坛的精华代码。这段代码非常完美(同时也令人伤感) 地展示了 this 多么容易误导人。
首先,这段代码试图通过 this.bar()来引用 bar() 函数。这是绝对不可能成功的,我们之后会解释原因。调用 bar() 最自然的方法是省略前面的 this,直接使用词法引用标识符。
此外,编写这段代码的开发者还试图 使用 this 联通 foo() 和 bar() 的词法作用域,从而让 bar() 可以访问 foo() 作用域里的变量 a,这是不可能实现的。你不能使用 this 来引用一 个词法作用域内部的东西。
每当你想要把 this 和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的。
[排除了一些错误理解之后,我们来看看 this 到底是一种什么样的机制。]
之前我们说过 this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包 含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的 其中一个属性,会在函数执行的过程中用到。3.this的全面解析
3.1 绑定规则
全局上下文
无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象。
// 在浏览器中, window 对象同时也是全局对象:
console.log(this === window); // true
a = 37;
console.log(window.a); // 37
this.b = "MDN";
console.log(window.b) // "MDN"
console.log(b) // "MDN"
函数上下文
在函数内部,this的值取决于函数被调用的方式。为了能够一眼看出this指向的是什么,我们首先需要知道this的绑定规则有哪些?
3.1.1 默认绑定
默认绑定,在不能应用其它绑定规则时使用的默认规则,通常是独立函数调用。
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
函数调用时应用了 this 的默认绑定,因此 this 指向全局对象。
那么我们怎么知道这里应用了默认绑定呢?可以通过分析调用位置来看看 foo() 是如何调 用的。在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用 默认绑定,无法应用其他规则。
3.1.2 隐式绑定
函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的形式为 XXX.fun().我们来看一段代码:
function sayHi(){
console.log('Hello,', this.name);
}
var person = {
name: 'YvetteLau',
sayHi: sayHi
}
var name = 'Wiliam';
person.sayHi();
sayHi 函数声明在外部,严格来说并不属于 person,但是在调用 sayHi 时,调用位置会使用 person 的上下文来引用函数,隐式绑定会把函数调用中的 this (即此例sayHi函数中的this)绑定到这个上下文对象
需要注意的是:对象属性链中只有最后一层会影响到调用位置。
Grand.Father.Brother.sayHi(); // 不管有多少层,在判断this的时候,我们只关注最后一层,即此处的Brother。
隐式绑定有一个大陷阱: ==( 绑定丢失 )==
function sayHi(){
console.log('Hello,', this.name);
}
var person = {
name: 'YvetteLau',
sayHi: sayHi
}
var name = 'Wiliam';
var Hi = person.sayHi;
Hi(); //Hello,Wiliam.
这是为什么呢,Hi直接指向了sayHi的引用,在调用的时候,跟person没有半毛钱的关系,针对此类问题,我建议大家只需牢牢记住这个格式:XXX.fn();fn()前如果什么都没有,那么肯定不是隐式绑定。
看一个例子
function sayHi(){
console.log('Hello,', this.name);
}
var person1 = {
name: 'YvetteLau',
sayHi: function(){
setTimeout(function(){
console.log('Hello,',this.name);
})
}
}
var person2 = {
name: 'Christina',
sayHi: sayHi
}
var name='Wiliam';
person1.sayHi(); // 1
setTimeout(person2.sayHi,100); // 2
setTimeout(function(){
person2.sayHi(); // 3
},200);
结果为
Hello, Wiliam // 1
Hello, Wiliam // 2
Hello, Christina // 3
第一条输出很容易理解,setTimeout的回调函数中,this使用的是默认绑定,非严格模式下,执行的是全局对象
第二条输出是不是有点迷惑了?说好XXX.fun()的时候,fun中的this指向的是XXX呢,为什么这次却不是这样了!Why? 其实这里我们可以这样理解: setTimeout(fn,delay){ fn(); },相当于是将person2.sayHi赋值给了一个变量,最后执行了变量,这个时候,sayHi中的this显然和person2就没有关系了。
第三条虽然也是在setTimeout的回调中,但是我们可以看出,这是执行的是person2.sayHi()使用的是隐式绑定,因此这是this指向的是person2,跟当前的作用域没有任何关系。
如果没有特殊指向,setInterval和setTimeout的回调函数中this的指向都是window。这是因为JS的定时器方法是定义在window下的。
3.1.3 显式绑定
call,apply和bind的第一个参数,就是对应函数的this所指向的对象。call和apply的作用一样,只是传参方式不同。call和apply都会执行对应的函数,bind方法不会,而是返回一个改变了上下文的函数。显式绑定比较好理解,就是通过call,apply,bind的方式,显式的指定this所指向的对象。
那么,使用了显式绑定,是不是意味着不会出现隐式绑定所遇到的绑定丢失呢?显然不是这样的,不信,继续往下看。
function sayHi(){
console.log('Hello,', this.name);
}
var person = {
name: 'YvetteLau',
sayHi: sayHi
}
var name = 'Wiliam';
var Hi = function(fn) {
fn();
}
Hi.call(person, person.sayHi);
复制代码输出的结果是 Hello, Wiliam. 原因很简单,Hi.call(person, person.sayHi)的确是将this绑定到Hi中的this了。但是在执行fn的时候,相当于直接调用了sayHi方法(记住: person.sayHi已经被赋值给fn了,隐式绑定也丢了),没有指定this的值,对应的是默认绑定。 现在,我们希望绑定不会丢失,要怎么做?很简单,调用fn的时候,也给它硬绑定。
function sayHi(){
console.log('Hello,', this.name);
}
var person = {
name: 'YvetteLau',
sayHi: sayHi
}
var name = 'Wiliam';
var Hi = function(fn) {
fn.call(this);
}
Hi.call(person, person.sayHi);
复制代码此时,输出的结果为: Hello, YvetteLau,因为person被绑定到Hi函数中的this上,fn又将这个对象绑定给了sayHi的函数。这时,sayHi中的this指向的就是person对象。
3.1.4 new绑定
使用new来调用函数,会自动执行下面的操作:
1.创建一个空对象,构造函数中的this指向这个空对象
2.这个新对象被执行 [[原型]] 连接
3.执行构造函数方法,属性和方法被添加到this引用的对象中
4.如果构造函数中没有返回其它对象,那么返回this,即创建的这个的新对象,否则,返回构造函数中返回的对象。
function _new() {
// 1、创建一个新对象
let target = {};
let [constructor, ...args] = [...arguments]; // 第一个参数是构造函数
// 2、原型链连接
target.__proto__ = constructor.prototype;
// 3、将构造函数的属性和方法添加到这个新的空对象上。
let result = constructor.apply(target, args);
if (result && (typeof result == "object" || typeof result == "function")) {
// 如果构造函数返回的结果是一个对象,就返回这个对象
return result
}
// 如果构造函数返回的不是一个对象,就返回创建的新对象。
return target
}
如下
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定;
3.2 绑定优先级
new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
3.3 箭头函数
箭头函数是ES6中新增的,它和普通函数有一些区别,箭头函数没有自己的this,它的this继承于外层代码库中的this。箭头函数在使用时,需要注意以下几点:
- 函数体内的this对象,继承的是外层代码块的this。
- 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
- 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
- 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
- 箭头函数没有自己的this,所以不能用call()、apply()、bind()这些方法去改变this的指向。
下面是 Babel 转箭头函数产生的 ES5 代码,就能清楚地说明this的指向
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
上面代码中,转换后的 ES5 版本清楚地说明了,==箭头函数里面根本没有自己的this,而是引用外层的this。==
3.4 原型链中的this
对于在对象原型链上某处定义的方法,个人理解类似于隐式调用。如果该方法存在于一个对象的原型链上,那么 this 指向的是调用这个方法的对象,就像该方法就在这个对象上一样。
var o = {
f: function() {
return this.a + this.b;
}
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
console.log(p.f()); // 5
在这个例子中,对象 p 没有属于它自己的 f 属性,它的 f 属性继承自它的原型。虽然最终是在 o 中找到 f 属性的,这并没有关系;查找过程首先从 p.f 的引用开始,所以函数中的 this 指向p。也就是说,因为f是作为p的方法调用的,所以它的this指向了p。这是 JavaScript 的原型继承中的一个有趣的特性。
4.总结
4.1 如何准确判断this指向的是什么?
==(如果是箭头函数,箭头函数的this继承的包裹它的第一个普通函数的指向。)==
- 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。
var bar = new foo()
- 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。
var bar = foo.call(obj2)
- 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。
var bar = obj1.foo()
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到 全局对象。
var bar = foo()
-
如果把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
-
当函数被用作事件处理函数时,它的 this 指向触发事件的元素 (一些浏览器在使用非 addEventListener 的函数动态地添加监听函数时不遵守这个约定)。
// 被调用时,将关联的元素变成蓝色
function bluify(e){
console.log(this === e.currentTarget); // 总是 true
// 当 currentTarget 和 target 是同一个对象时为 true
console.log(this === e.target);
this.style.backgroundColor = '#A5D9F3';
}
// 获取文档中的所有元素的列表
var elements = document.getElementsByTagName('*');
// 将bluify作为元素的点击监听函数,当元素被点击时,就会变成蓝色
for(var i=0 ; i<elements.length ; i++){
elements[i].addEventListener('click', bluify, false);
}
- 当代码被内联 on-event 处理函数 (en-US) 调用时,它的this指向监听器所在的DOM元素
<button onclick="alert(this);">
Show this
</button>
参考:
嗨,你真的懂this吗
你不知道的JavaScript(上)