this指向

this的指向

this绑定规则一共有5种:

  • 默认绑定(严格/非严格模式)
  • 隐式绑定
  • 显式绑定
  • new绑定
  • 箭头函数绑定

普通方法的this指向(默认绑定 严格/非严格模式)

1
2
3
4
5
6
7
// 非严格模式
var name = 'window';
var doSth = function(){
console.log(this.name);
}
doSth(); // 'window'
虽然本例中`window.doSth`确实等于`doSth``name`等于`window.name`。上面代码中这是因为在`ES5`中,全局变量是挂载在顶层对象(浏览器是`window`)中。 事实上,并不是如此。
1
2
3
4
5
6
7
// 非严格模式
let name2 = 'window2';
let doSth2 = function(){
console.log(this === window);
console.log(this.name2);
}
doSth2() // true, undefined

这个例子中let没有给顶层对象中(浏览器是window)添加属性,window.name2和window.doSth都是undefined

严格模式中,普通函数中的this则表现不同,表现为undefined

1
2
3
4
5
6
7
8
// 严格模式
'use strict'
var name = 'window';
var doSth = function(){
console.log(typeof this === 'undefined');
console.log(this.name);
}
doSth(); // true,// 报错,因为this是undefined

对象方法的this指向(隐式绑定)

this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象

  • 如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window
  • 如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象
  • 如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
var person = {
name: "axuebin",
getName: function(){
return this.name;
}
}
console.log(person.getName()); // axuebin

// 需要注意1:
var name = "xb";
var person = {
name: "axuebin",
getName: function(){
return this.name;
}
}
var getName = person.getName;
console.log(getName()); // xb,this指向的是调用它的对象

// 需要注意2:在函数执行过程中,this一旦被确定,就不可更改了。
var a = 10;
var obj = {
a: 20
}
function fn() {
this = obj; // 这句话试图修改this,运行后会报错
console.log(this.a);
}
fn();

// 需要注意3:this的上一级对象为b,b内部并没有a变量的定义,所以输出undefined
var o = {
a:10,
b:{
fn:function(){
console.log(this.a); //undefined
}
}
}
o.b.fn();

构造函数的this指向(new绑定)

1
2
3
4
5
6
7
function Foo(name,age){
this.name=name;
this.age=age;
}
var foo=new Foo("Tom",14);
foo.name;//Tom
foo.age;//14

构造函数的 this 指向实例的,因为 new 的步骤

  • 创建一个新的空对象
  • 新对象的 __proto__ 属性指向构造函数的原型对象
  • 把构造函数的this设置为新创建的对象
  • 执行构造函数的代码
  • 构造函数没有return语句,则将该新创建的对象返回
图片

new 的模拟实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function myNew(Func, ...args) {
if(typeof Func !== 'function'){
return new TypeError('myNew function the first param must be a function');
}
// 1.创建一个新对象
const obj = {}
// 2.新对象原型指向构造函数原型对象
obj.__proto__ = Func.prototype
// 3.将构建函数的this指向新对象
let result = Func.apply(obj, args)
// 4.根据返回值判断
let isObject = typeof result === 'object' && result !== null
let isFunction = typeof result === 'function'
if(isObject || isFunction) return result
return obj
}
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function () {
console.log(this.name)
}

let p = myNew(Person, "huihui", 123)
console.log(p) // Person {name: "huihui", age: 123}
p.say() // huihui
优化版

__proto__在IE浏览器中不支持,使用 Object.create(),作用如下

1
2
3
4
let x = { name: "lsh" };
Object.create(x);

{}.__proto__ = x; 让空对象的 __proto__ 指向 x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function myNew(Func, ...args) {
if(typeof Func !== 'function'){
return new TypeError('myNew function the first param must be a function');
}
// const obj = {}
// obj.__proto__ = Func.prototype
// 创建一个Func的实例对象(实例.____proto____ = 类.prototype)
let obj = Object.create(Func.prototype);

let result = Func.apply(obj, args)
let isObject = typeof result === 'object' && result !== null
let isFunction = typeof result === 'function'
if(isObject || isFunction) return result
return obj
}
Object.create() 的模拟
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function newCreate(prototype, propertiesObject) {
if (typeof prototype !== 'object' && typeof prototype !== 'function') {
throw TypeError('Object prototype may only be an Object or null: ' + prototype)
}
function F() { }
F.prototype = prototype
const o = new F()

if (propertiesObject !== undefined) {
Object.keys(propertiesObject).forEach(prop => {
let desc = propertiesObject[prop]
if (typeof desc !== 'object' || desc === null) {
throw TypeError('Object prorotype may only be an Object: ' + desc)
} else {
Object.defineProperty(o, prop, desc)
}
})
}

return o
}

原型链中的this指向

1
2
3
4
5
6
7
8
function Student(name){
this.name = name;
}
var s1 = new Student('若川');
Student.prototype.doSth = function(){
console.log(this.name);
}
s1.doSth(); // '若川'

会发现这个似曾相识。这就是对象上的方法调用模式。自然是指向生成的新对象。 如果该对象继承自其它对象。同样会通过原型链查找。 上面代码使用 ES6class写法则是:

1
2
3
4
5
6
7
8
9
10
class Student{
constructor(name){
this.name = name;
}
doSth(){
console.log(this.name);
}
}
let s1 = new Student('若川');
s1.doSth();

当this碰到return

如果返回值是一个对象({},[],RegExp, Date, Function),那么this指向的就是那个返回的对象,如果返回值不是一个对象(基本类型)那么this还是指向函数的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Foo(name,age){
this.name=name;
this.age=age;
return {name:"Jeff"}
}
var foo=new Foo("Tom",14);
foo.name;//Jeff

// 返回基本类型
function fn()
{
this.user = 'xxx';
return 1;
}
var a = new fn;
console.log(a.user); //xxx


// 注意的是null虽然也是对象,但是此时new仍然指向实例对象
function fn()
{
this.user = 'xxx';
return null;
}
var a = new fn;
console.log(a.user); //xxx

箭头函数的this指向(箭头函数绑定)

  • 箭头函数本身没有thissuperargumentsnew.target绑定

  • 不能使用new来调用

  • 没有原型对象

  • 不可以改变this的绑定

  • 形参名称不能重复

箭头函数中没有this绑定,必须通过查找作用域链来决定其值。 如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this,否则this的值则被设置为全局对象。在定义函数的时候绑定,而不是在执行函数的时候绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var name = 'window';
var student = {
name: '若川',
doSth: function(){
// var self = this;
var arrowDoSth = () => {
// console.log(self.name);
console.log(this.name);
}
arrowDoSth();
},
arrowDoSth2: () => {
console.log(this.name);
}
}
student.doSth(); // '若川'
student.arrowDoSth2(); // 'window'

其实就是相当于箭头函数外的this是缓存的该箭头函数上层的普通函数的this。如果没有普通函数,则是全局对象(浏览器中则是window)。 也就是说无法通过callapplybind绑定箭头函数的this(它自身没有this)。而callapplybind可以绑定缓存箭头函数上层的普通函数的this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var student = {
name: '若川',
doSth: function(){
console.log(this.name);
return () => {
console.log('arrowFn:', this.name);
}
}
}
var person = {
name: 'person',
}
student.doSth().call(person); // '若川' 'arrowFn:' '若川'
student.doSth.call(person)(); // 'person' 'arrowFn:' 'person'

call、apply、bind 的 this 指向(显示绑定)

在另一篇文章中

DOM事件处理函数的this指向

addEventerListener、attachEvent、onclick。this 指向触发事件的元素,也就是始事件处理程序所绑定到的DOM节点。

1
2
3
4
5
var ele = document.getElementById("id");
ele.addEventListener("click",function(e){
console.log(this);
console.log(this === e.target); // true
})

HTML标签内联事件处理函数的this指向

this 指向所在的DOM元素。第一个是button本身,所以是true,第二个是window。这里跟严格模式没有关系。

1
2
<button class="btn1" onclick="console.log(this === document.querySelector('.btn1'))">点我呀</button>
<button onclick="console.log((function(){return this})());">再点我呀</button>

jQuery的this指向

在许多情况下JQuery的 this 都指向DOM元素节点

1
2
3
$(".btn").on("click",function(){
console.log(this);
});

优先级

new 调用 > call、apply、bind 调用 > 对象上的函数调用 > 普通函数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var name = 'window';
var person = {
name: 'person',
}
var doSth = function(){
console.log(this.name);
return function(){
console.log('return:', this.name);
}
}
var Student = {
name: '若川',
doSth: doSth,
}
// 普通函数调用
doSth(); // window
// 对象上的函数调用
Student.doSth(); // '若川'
// call、apply 调用
Student.doSth.call(person); // 'person'
new Student.doSth.call(person); // Uncaught TypeError: Student.doSth.call is not a constructor

new apply、call 无法一起使用,这是因为函数内部有两个不同的方法:[[Call]][[Constructor]]。 当使用普通函数调用时,[[Call]]会被执行。当使用构造函数调用时,[[Constructor]]会被执行。callapplybind和箭头函数内部没有[[Constructor]]方法。

下面比较 new 和 call、apply、bind 调用。在《你不知道的JavaScript》是对比bindnew,引用了mdnbindployfill实现,new调用时bind之后的函数,会忽略bind绑定的第一个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(something) {
this.a = something;
}

var obj1 = {};

var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2

var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.log( baz.a ); // 3

bar 被绑定到 obj1 上,但是 new bar(3) 并没有像我们预计的那样把 obj1.a 修改为3。但是,new 修改了绑定调用 bar() 中的 this。

总结

如果要判断一个函数的this绑定,就需要找到这个函数的直接调用位置。然后可以顺序按照下面四条规则来判断this的绑定对象:

  1. 普通函数调用: 在严格模式下绑定到 undefined,否则绑定到全局对象。
  2. 对象上的函数调用:绑定到调用的那个对象
  3. new调用:绑定到新创建的对象(实例),注意:显示return函数或对象,返回值不是新创建的对象,而是显式返回的函数或对象。
  4. callapplybind调用:绑定到指定的对象。(严格模式下,绑定到指定的第一个参数。非严格模式下,nullundefined,指向全局对象(浏览器中是window),其余值指向被new Object()包装的对象。)
  5. 箭头函数不使用上面的绑定规则,根据外层作用域来决定this,继承外层非箭头函数调用的this绑定。
  6. DOM事件处理函数:一般指向绑定事件的DOM元素,但有些情况绑定到全局对象(比如IE6~IE8attachEvent
  7. HTML标签内联事件处理函数:指向所在的dom元素
  8. JQuery:在许多情况下都指向DOM元素节点

参考文章

JavaScript基础心法——this

JavaScript深入之从ECMAScript规范解读this

面试官问:JS的this指向https://juejin.cn/post/6844903746984476686#heading-5)