在Javascript中,this 关键字是一个非常重要的概念,this这个关键字可以说是很常见也用的很多,说它简单也很简单,说它难也很难。我们经常会用到this,也经常会因为this头疼,是一个经常被误解和误用的概念,为什么呢,因为有时候我们不知道this到底指的是什么?怎么用?
在 JavaScript 中,this 的值在函数被调用时确定,而不是在函数被创建时确定。这使得 this 在 JavaScript 中的行为与其他一些语言中的类似关键字(如 Python 的 self 或 Java 的 this)有所不同。本文将从全局作用域或函数外部、普通函数调用、对象的方法、构造函数、事件处理函数、箭头函数几个方面来剖析JavaScript中的this。
一、抛砖引玉
先看一段代码:
var name = "前端技术营";
var obj = {name: "张三",foo: function() {console.log(this.name);}
};
var foo = obj.foo;
obj.foo(); // 张三
foo(); // 前端技术营
可以看到上面代码中,obj.foo() 和 foo() 都指向同一个函数,但是执行结果却不一样;产生这种差异的原因,就在于函数体内部使用了 this 关键字。
在《JavaScript高级程序设计》一书中是这样说的,this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常纸箱window。
所以上面的问题,对obj.foo() 来说, foo 运行在 obj 环境中,所以 this 指向 obj ;对于 foo() 来说, foo 运行在全局环境下,所以在非严格模式下 this 指向 window ,所以导致了两者运行的结果不同。看到这有的人可能就有疑问了,函数的运行环境是如何判定的?为什么 obj.foo() 就是在 obj 环境,为何 var foo = obj.foo; foo() 就在全局环境执行了?插个眼,继续往下看,就明白这个问题了!
二、为什么需要this
先看下面代码:
function foo() {console.log(this.name)
}var bar = {name: '张三',foo: foo
}
var baz = {name: '李四',foo: foo
}
bar.foo(); // 张三
baz.foo(); // 李四
Javascript 引擎在处理上面代码时,会在堆内存中,生成两个对象,然后把这两个对象在内存中的地址分别赋值给变量bar和baz。在读取 this.name 时,需要先从变量bar和baz拿到地址,然后再分别从对应地址中拿到对象,再返回它的 name 属性。
对象的属性是一个函数,当引擎遇到对象属性是函数的情况,会将函数单独保存在堆中,然后再将函数的地址赋值给对象属性,而 Javascript 是允许在函数体内引用当前环境的其他变量。那么问题来了,函数可以在不同的运行环境执行,所以我们就需要一种机制,能够在函数内获得当前运行环境,foo只定义了一次,却可以被不同的对象引用,实现了代码共享,由此诞生了 this,它的设计目的就是指向函数运行时所在的环境。
那么,如何正确的在代码中判定this所指向的环境呢?
三、全局作用域中
在全局作用域代码中this 是不变的,this始终是全局对象本身,即window。
var a = '张三';
this.b = '李四';
window.c = '王五';console.log(this.a); // 张三
console.log(b); // 李四
console.log(this.c); // 王五console.log(this === window); // true
运行以上代码发现,this === window为true,也就是说在全局作用域中this就是全局对象window,所以上述 a ,b ,c 都相当于在全局对象上添加相应的属性。
var a = 1;
var b = function () {return "function1";
}
console.log(window.a); //1
console.log(window.b); //ƒ (){ return "function1"; }
console.log(window.a === a); //true
console.log(window.b === b); //true
在全局对象上定义的变量可以直接访问。
window.aa = 2;
this.bb = function () {return "function2";
}
console.log(aa); //2
console.log(bb); //ƒ (){ return "function2"; }
四、函数中的this
在函数中使用this,才是令我们最容易困惑的,这里我们主要是对函数代码中的this进行分析。
这里再次强调一下,this的指向在函数创建的时候是决定不了的,而是在进入当前执行上下文时确定的,也就是在函数执行时并且是执行前确定的。但是同一个函数,作用域中的this指向可能完全不同,但是不管怎样,函数在运行时的this的指向是不变的,而且不能被赋值。
函数中this的指向丰富的多,它可以是全局对象、当前对象、或者是任意对象,当然这取决于函数的调用方式。在JavaScript中函数的调用方式有一下几种方式:作为函数调用、作为对象属性调用、作为构造函数调用、使用apply或call调用。下面我们将按照这几种调用方式一一讨论this的含义。
4.1 作为函数调用
看如下代码:
function foo() {var name = '张三';console.log(this.name); // undefinedconsole.log(this); // Window
}foo();
按照我们上面说的this最终指向的是调用它的对象,这里的函数foo实际是被Window对象所点出来的,下面的代码就可以证明。
function foo() {var name = '张三';console.log(this.name); // undefinedconsole.log(this); // Window
}window.foo();
和上面代码一样,其实alert也是window的一个属性,也是window点出来的。
function foo() {function bar() {this.name = '张三';console.log(this === window); // true}bar()
}
foo();
console.log(name); // 张三
上述代码中,在函数内部的函数独立调用,此时this还是被绑定到了window。
在严格模式下,不能将全局对象 window 作为默认绑定,此时 this 会绑定到 undefined ,但是在严格模式下调用函数则不会影响默认绑定。
function foo() {"use strict";console.log(this===window); // falseconsole.log(this===undefined); // true
}
foo();"use strict"
function foo() {var name = "张三";console.log(this.name);
};foo();
// Uncaught TypeError: Cannot read property ‘name’ of undefined at foo
加了"use strict"之后,和上面一样的代码运行就会报错,在严格模式下,不能将全局对象 window 作为默认绑定。
var name = '张三';function foo() {console.log(this.name); // 张三console.log(this === window); // true
};(() => {"use strict"foo();
})();
看上面代码,在foo() 前加了"use strict",运行并没有报错,依然打印出了结果,可见在严格模式下调用函数则不会影响默认绑定。
小结:当函数作为独立函数被调用时,内部this被默认绑定为(指向)全局对象window,但是在严格模式下会有区别,在严格模式下this被绑定为undefined。
4.2 作为对象属性调用
先看一段代码:
var obj = {name: "张三",foo: function() {console.log(this.name); //张三}
}
obj.foo();
根据this最终指向调用它的对象可知,这里的this指向的是对象obj,因为调用这个foo是通过obj.foo()执行的,那自然指向就是对象o。
是不是感觉自己懂了?别急,再看看下边的代码。
var obj = {name: "张三",foo: function() {console.log(this.name); // 张三}
}
window.obj.foo();
先解释一下window.obj.foo(),window是js中的全局对象,我们创建的变量实际上是给window添加属性,所以这里可以用window点obj对象。
再看这段代码和上面的那段代码几乎是一样的,但是这里的this为什么不是指向window?如果按照上面的理论,最终this指向的是调用它的对象,那这个理论还成不成立呢,我们先接着再看下面一段代码。
var obj = {name: '张三',bar: {name: '李四',foo: function() {console.log(this.name); // 李四}}
}
obj.bar.foo();
这里同样也是对象obj点出来的,但是同样this并没有执行它,那你肯定会说我一开始说的那些不就都是错误的吗?其实也不是,只是一开始说的不准确,接下来补充一句话,我相信你就可以彻底的理解this的指向的问题。
1、如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window,这里需要说明的是在js的严格版中this指向的不是window。
2、如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。
3、如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象。
var obj = {name: '张三',bar: {// name: '李四',foo: function() {console.log(this.name); // undefined}}
}
obj.bar.foo();
这段代码尽管对象bar中没有属性name,这个this指向的也是对象bar,因为this只会指向它的上一级对象,不管这个对象中有没有this要的属性。
var obj = {name: '张三',bar: {age: 18,foo: function() {console.log(this.age); // undefinedconsole.log(this); // window}}
}var fn = obj.bar.foo;
fn();
解释:obj.bar.foo方法声明部分,只需要理解为在堆内存中开辟了一块空间,并由obj.bar.foo持有这块内存空间的引用,由于函数尚未执行,因此还没有确定this。将obj.foo赋值给bar,也就是将函数的引用拷贝一份给了bar,bar独立调用,因此this指向window。this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的。
4.3 使用apply或call调用
apply和call为函数原型上的方法。它可以更改函数内部this的指向。
var name = '前端技术营';function foo() {console.log(this.name);
}
var obj1 = {name: '张三'
}
var obj2 = {name: '李四'
}
var obj3 = {name: '王五'
}
// this指向window,打印“前端技术营”
foo();
// this指向 obj1,打印“张三”
foo.apply(obj1);
// this指向 obj2,打印“李四”
foo.call(obj2);
// this指向 obj3,打印“王五”
foo.call(obj3);
当函数foo 作为独立函数调用时,this被绑定到了全局对象window,当使用bind、call或者apply方法调用时,this 被分别绑定到了不同的对象。
call和apply 的功能一样,唯一不同的是传给函数的参数的方式,第一个参数是this指向的新对象,从第二个参数开始,apply传数组,这个数组包含函数所需要的参数,apply只支持传入一个数组,哪怕是一个参数也要是数组形式,最终调用函数时候这个数组会拆分成一个个参数分别传入;call 直接传参数,多个参数逗号分割。
var obj1 = {name: '张三'
}
var obj2 = {name: '李四'
}function foo(arg1, arg2) {console.log(this);console.log(arg1 + arg2);
};foo.call(obj1, 1, 2);
// {name: '张三'}
// 3foo.apply(obj2, [1, 2]);
// {name: '李四'}
// 3
还有一个bind方法。bind方法和call使用方式一样,作用也一样,不一样的是实现方式,call和apply传参结束后直接执行函数,而bind只是更改this值和给函数传参,函数并不执行,所以bind可以作为事件的处理函数去使用。
var name = '前端技术营';function foo() {console.log(this.name);
}
var obj = {name: '张三'
}foo.bind(obj);
console.log(foo.bind(obj))
// ƒ foo() { console.log(this.name); }foo.bind(obj)(); // 张三
function add(a, b){return a + b
}
function sub(a, b){return a - b
}
add.bind(sub, 5, 3)(); // 8
4.4 作为构造函数调用
var name = '前端技术营';function Foo(){this.name = "张三";
}
var baz = new Foo();
console.log(baz.name); // 张三
这里之所以对象baz可以点出函数Foo里面的name是因为new关键字可以改变this的指向,将这个this指向对象baz(因为用了new关键字就是创建一个对象实例),这里用变量baz创建了一个Foo的实例(相当于复制了一份Foo到对象baz里面),此时仅仅只是创建,并没有执行,而调用这个函数Foo的是对象baz,那么this指向的自然是对象baz。那么为什么对象baz中会有name,因为你已经复制了一份Foo函数到对象baz中,用了new关键字就等同于复制了一份。
4.5 总结
当我们要判断当前函数内部的this绑定,可以依照下面的原则:
(1)函数是否在是通过 new 操作符调用?如果是,this 绑定为新创建的对象。
var bar = new foo(); // this指向bar
(2)函数是否通过call或者apply调用?如果是,this 绑定为指定的对象
foo.call(obj1); // this指向obj1
foo.apply(obj2); // this指向obj2
(3)函数是否通过 对象 . 方法调用?如果是,this 绑定为当前对象
obj.foo(); // this指向obj
(4)函数是否独立调用?如果是,this 绑定为全局对象。
foo(); // this指向window