this相关题目

285 阅读13分钟

关于 this 的题目

参考文献

一、默认绑定

没有点this就是window(在严格模式下("use strict")没有点thisundefined,也称为默认绑定)

1. 绑定到window

var a = 10;
function foo () {
  console.log(this.a)
}
foo();

我们知道在使用var创建变量的时候(不在函数里),会把创建的变量绑定到window上,所以此时awindow下的属性。

而函数foo也是window下的属性。

因此上面的代码其实就相当于是这样:

window.a = 10;
function foo() {
  console.log(this.a)
}
window.foo(); // 10

2. 严格模式undefined

第一题开启严格模式:

"use strict";
var a = 10;
function foo () {
  console.log('this1', this)
  console.log(window.a)
  console.log(this.a)
}
console.log(window.foo)
console.log('this2', this)
foo();

需要注意的点:

  • 开启了严格模式,只是说使得函数内的this指向undefined,它并不会改变全局中this的指向。因此this1中打印的是undefined,而this2还是window对象。
  • 另外,它也不会阻止a被绑定到window对象上。

所以最后的执行结果:

f foo() {...}
'this2' Window{...}
'this1' undefined
10
Uncaught TypeError: Cannot read property 'a' of undefined

3. let和const不会绑定到window

let a = 10
const b = 20

function foo () {
  console.log(this.a)
  console.log(this.b)
}
foo();
console.log(window.a)

如果把var改成了let 或者 const,变量是不会被绑定到window上的,所以此时会打印出三个undefined

// ans
undefined
undefined
undefined

4. 函数作用域

var a = 1
function foo () {
  var a = 2
  console.log(this)
  console.log(this.a)
}
foo()

foo()函数内的this指向的是window,因为是window调用的foo

但是打印出的this.a呢?注意,是this.a,不是a,因此是window下的a

并且由于函数作用域的原因我们知道window下的a还是1

因此答案为:

// ans
Window{...}
1

5. 闭包?nope

// 修改第四题
var a = 1
function foo () {
  var a = 2
  function inner () { 
    console.log(this.a)
  }
  inner()
}
foo()

这里问你的是this.a,而在inner中,this指向的还是window

// ans
1

二、隐式绑定

this 永远指向最后调用它的那个对象

谁最后调用的函数,函数内的this指向的就是谁(不考虑箭头函数)。

1. 正常隐式绑定

function foo () {
  console.log(this.a)
}
var obj = { a: 1, foo }
var a = 2
obj.foo()

var obj = { foo }就相当于是var obj = { foo: foo }

函数foo()虽然是定义在window下,但是我在obj对象中引用了它,并将它重新赋值到obj.foo上。

这段代码就相当于是这样:

var obj = {
  a: 1,
  foo: function () {
    console.log(this.a)
  }
}
var a = 2
obj.foo() // 1

2. 隐式绑定丢失

有两种情况容易发生隐式丢失问题:

  • 使用另一个变量来给函数取别名
  • 将函数作为参数传递时会被隐式赋值,回调函数丢失this绑定

(1)使用另一个变量给函数取别名

使用另一个变量来给函数取别名会发生隐式丢失。

function foo () {
  console.log(this.a)
};
var obj = { a: 1, foo };
var a = 2;
var foo2 = obj.foo;
obj.foo(); // 这里的this指向obj 1
foo2(); // 它打印出的是window下的a。2

这是因为虽然foo2指向的是obj.foo函数,不过调用它的却是window对象,所以它里面this的指向是为window

(2)

function foo () {
  console.log(this.a)
};
var obj = { a: 1, foo };
var a = 2;
var foo2 = obj.foo;
var obj2 = { a: 3, foo2: obj.foo }
obj.foo(); // 1
foo2(); // 2
obj2.foo2(); //3
  • obj.foo()中的this指向调用者obj
  • foo2()发生了隐式丢失,调用者是window,使得foo()中的this指向window
  • foo3()发生了隐式丢失,调用者是obj2,使得foo()中的this指向obj2

(3)函数作参数

如果你把一个函数当成参数传递时,也会被隐式赋值,发生意想不到的问题。

function foo () {
  console.log(this.a)
}
function doFoo (fn) {
  console.log(this)
  fn()
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)

这里我们将obj.foo当成参数传递到doFoo函数中,在传递的过程中,obj.foo()函数内的this发生了改变,指向了window

注意,我这里说的是obj.foo()函数,而不是说doFoo()doFoo()函数内的this本来就是指向window的,因为这里是window调用的它。

但是你不要以为是doFoo()函数内的this影响了obj.foo()

(4)

现在我们不用window调用doFoo,而是放在对象obj2里,用obj2调用:

function foo () {
  console.log(this.a)
}
function doFoo (fn) {
  console.log(this)
  fn()
}
var obj = { a: 1, foo }
var a = 2
var obj2 = { a: 3, doFoo }
obj2.doFoo(obj.foo)

现在调用obj2.doFoo()函数,里面的this指向的应该是obj2,因为是obj2调用的它。

但是obj.foo()打印出来的a依然是2,也就是window下的。

// ans
Oject>{ a:3, doFoo: f ...}
2

,如果你把一个函数当成参数传递到另一个函数的时候,也会发生隐式丢失的问题,且与包裹着它的函数的this指向无关。在非严格模式下,会把该函数的this绑定到window上,严格模式下绑定到undefined。

当严格模式下:

"use strict"
function foo () {
  console.log(this.a)
}
function doFoo (fn) {
  console.log(this)
  fn()
}
var obj = { a: 1, foo }
var a = 2
var obj2 = { a: 3, doFoo }
obj2.doFoo(obj.foo)
// ans
{ a:3, doFoo: f }
Uncaught TypeError: Cannot read property 'a' of undefined

3. 显示绑定

就是强行使用某些方法,改变函数内this的指向。

通过call()、apply()或者bind()方法直接指定this的绑定对象, 如foo.call(obj)

这里有几个知识点需要注意:

  • 使用.call()或者.apply()的函数是会直接执行的
  • bind()是创建一个新的函数,需要手动调用才会执行
  • .call().apply()用法基本类似,不过call接收若干个参数,而apply接收的是一个数组

1. 显示绑定基本使用

function foo () {
  console.log(this.a)
}
var obj = { a: 1 }
var a = 2
foo() // 2
foo.call(obj) // 1
foo.apply(obj) // 1
foo.bind(obj) // 仅仅是使用bind创建了一个新的函数,且这个新函数也没用别的变量接收并调用,因此并不会执行。

如果call、apply、bind接收到的第一个参数是空或者null、undefined的话,则会忽略这个参数。

function foo () {
  console.log(this.a)
}
var a = 2
foo.call() // 2
foo.call(null) // 2
foo.call(undefined) // 2

2. setTimeout隐式赋值

var obj1 = {
  a: 1
}
var obj2 = {
  a: 2,
  foo1: function () {
    console.log(this.a)
  },
  foo2: function () {
    setTimeout(function () {
      console.log(this)
      console.log(this.a)
    }, 0)
  }
}
var a = 3
obj2.foo1() 
obj2.foo2() 

对于setTimeout中的函数,这里存在隐式绑定的隐式丢失,也就是当我们将函数作为参数传递时会被隐式赋值,回调函数丢失this绑定,因此这时候setTimeout中的函数内的this是指向window的。

// ans
2
Window{...}
3

3. setTimeout+显式绑定

面对上面👆这种情况我们就可以使用call、apply 或者bind来改变函数中this的指向,使它绑定到obj1上,从而打印出1

var obj1 = {
  a: 1
}
var obj2 = {
  a: 2,
  foo1: function () {
    console.log(this.a)
  },
  foo2: function () {
    setTimeout(function () {
      console.log(this)
      console.log(this.a)
    }.call(obj1), 0)
  }
}
var a = 3
obj2.foo1()
obj2.foo2()
// ans
2
{ a: 1 }
1

并且这里使用.bind()也是可以的,因为定时器里的函数在时间到了之后本就是会自动执行的。

4. 函数嵌套包裹

var obj1 = {
  a: 1
}
var obj2 = {
  a: 2,
  foo1: function () {
    console.log(this.a)
  },
  foo2: function () {
    function inner () {
      console.log(this)
      console.log(this.a)
    }
    inner()
  }
}
var a = 3
obj2.foo1()
obj2.foo2()

调用inner函数的依然是window,所以结果为:

2
Window{...}
3

如果给inner()函数显式绑定的话:

inner.call(obj1)

结果为

2
{ a: 1 }
1

5. .call()在不同位置

function foo () {
  console.log(this.a)
}
var obj = { a: 1 }
var a = 2
foo()
foo.call(obj)
foo().call(obj)
// ans
2
1
2
Uncaught TypeError: Cannot read property 'call' of undefined
  • foo().call(obj)开始会执行foo()函数,打印出2,但是会对foo()函数的返回值执行.call(obj)操作,可是我们可以看到foo()函数的返回值是undefined,因此就会报错了。

6. 带返回值的函数调用call

既然刚刚是因为函数没有返回值才报的错,那我现在给它加上返回值看看:

function foo () {
  console.log(this.a)
  return function () {
    console.log(this.a)
  }
}
var obj = { a: 1 }
var a = 2
foo()
foo.call(obj)
foo().call(obj)
2
1
2
1
  • 第一个数字2自然是foo()输出的,虽然foo()函数也返回了一个匿名函数,但是并没有调用它呀,只有写成foo()(),这样才算是调用匿名函数。

  • 第二个数字1foo.call(obj)输出的,由于.call()是紧跟着foo的,所以改变的是foo()this的指向,并且.call()是会使函数立即执行的,因此打印出1,同理,它也没有调用返回的函数。

  • 第三个数字2foo().call(obj)先执行foo()时打印出来的,此时foo()this还是指向window

  • 在执行完foo()之后,会返回一个匿名函数,并且后面使用了.call(obj)来改变这个匿名函数的this指向并调用了它,所以输出了1

7. 把call换成bind

先来回忆一下它们的区别:call是会直接执行函数的,bind是返回一个新函数,但不会执行。

function foo () {
  console.log(this.a)
  return function () {
    console.log(this.a)
  }
}
var obj = { a: 1 }
var a = 2
foo() // 2
foo.bind(obj) // 不执行,它返回一个新函数
foo().bind(obj) // 只执行前面的foo()函数,后面的bind(obj)是将foo()返回的匿名函数显式绑定this,没有调用

8. 立即执行函数

function foo () {
  console.log(this.a)
  return function () {
    console.log(this.a)
  }
}
var obj = { a: 1 }
var a = 2
foo.call(obj)()

就像是这道题,foo()函数内的this虽然指定了是为obj,但是调用最后调用匿名函数的却是window

所以结果为:

1
2

9. 对象+函数返回

var obj = {
  a: 'obj',
  foo: function () {
    console.log('foo:', this.a)
    return function () {
      console.log('inner:', this.a)
    }
  }
}
var a = 'window'
var obj2 = { a: 'obj2' }
obj.foo()() // foo: obj inner: window
obj.foo.call(obj2)() // foo: obj2 inner: window
obj.foo().call(obj2) // foo: obj innder: obj2

10. 添加参数

(这题感觉有点。。。)

var obj = {
  a: 1,
  foo: function (b) {
    b = b || this.a
    return function (c) {
      console.log(this.a + b + c)
    }
  }
}
var a = 2
var obj2 = { a: 3 }
obj.foo(a).call(obj2, 1)
obj.foo.call(obj2)(1)
// ans
6
6

开始调用obj.foo(a)2传入foo函数并赋值给型参b,并且由于闭包的原因,使得匿名函数内能访问到b,之后调用匿名函数的时候,用call()改变了this的指向,使得匿名函数内this.a3,并传入最后一个参数1,所以第一行输出的应该是3 + 2 + 1,也就是6

而第二行,obj.foo.call(obj2)这里是将foo函数内的this指向了obj2,同时并没有传递任何参数,所以b开始是undefined的,但是又因为有一句b = b || this.a,使得b变为了3;同时最后一段代码(1),是在调用匿名函数,且和这个匿名函数内的this应该是指向window的,因此输出也为3+2+1,为6

这里显式绑定省略一些题目。。感觉不是很必要。。

小总结

  • this 永远指向最后调用它的那个对象

  • 匿名函数的this永远指向window

  • 使用.call()或者.apply()的函数是会直接执行的

  • bind()是创建一个新的函数,需要手动调用才会执行

  • 如果call、apply、bind接收到的第一个参数是空或者null、undefined的话,则会忽略这个参数

  • forEach、map、filter函数的第二个参数也是能显式绑定this

三、new 绑定

1. 基础用法

使用new来调用Person,构造了一个新对象person1并把它(person1)绑定到Person调用中的this

function Person (name) {
  this.name = name
}
var name = 'window'
var person1 = new Person('LinDaiDai')
console.log(person1.name) // 'LinDaiDai'

2. 构造函数添加方法

function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  }
  this.foo2 = function () {
    return function () {
      console.log(this.name)
    }
  }
}
var person1 = new Person('person1')
person1.foo1() // 'person1'
person1.foo2()() // ''
  • 第二个this.name打印的应该就是window下的name了,但是这里window对象中并不存在name属性,所以打印出的是空。

3. new对象属性是函数

var name = 'window'
function Person (name) {
  this.name = name
  this.foo = function () {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var person2 = {
  name: 'person2',
  foo: function() {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
} 
var person1 = new Person('person1')
person1.foo()() // 'person1' 'window'
person2.foo()() // 'person2' 'window'

在这道题中,person1.fooperson2就没有什么区别。

4. new结合call

var name = 'window'
function Person (name) {
  this.name = name
  this.foo = function () {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.foo.call(person2)() // 'person2' 'window'
person1.foo().call(person2) // 'person1' 'person2'

四、箭头函数绑定

this 永远指向最后调用它的那个对象

但是对于箭头函数就不是这样咯,它里面的this是由外层作用域来决定的,且指向函数定义时的this而非执行时

它里面的this是由外层作用域来决定的啥意思呢?来看看这句话:

箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined。

1. 字面量对象中一层箭头函数

var obj = {
  name: 'obj',
  foo1: () => {
    console.log(this.name)
  },
  foo2: function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  }
}
var name = 'window'
obj.foo1() // 'window'
obj.foo2()() // 'obj' 'obj'
  • 对于obj.foo1()函数的调用,它的外层作用域是window对象obj当然不属于作用域了(我们知道作用域只有全局作用域window局部作用域函数)。所以会打印出window

  • obj.foo2()(),首先会执行obj.foo2(),这不是个箭头函数,所以它里面的this是调用它的obj对象,因此打印出obj,而返回的匿名函数是一个箭头函数,它的this由外层作用域决定,那也就是函数foo2咯,那也就是它的this会和foo2函数里的this一样,就也打印出了obj

2. 字面量对象中普通函数与箭头函数

var name = 'window'
var obj1 = {
	name: 'obj1',
	foo: function () {
		console.log(this.name)
	}
}
var obj2 = {
	name: 'obj2',
	foo: () => {
		console.log(this.name)
	}
}
obj1.foo() 'obj1'
obj2.foo() 'window'
  • 不使用箭头函数的obj1.foo()是由obj1调用的,所以this.nameobj1
  • 使用箭头函数的obj2.foo()的外层作用域是window,所以this.namewindow

3. 字面量中普通箭头函数嵌套

var name = 'window'
var obj1 = {
  name: 'obj1',
  foo: function () {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var obj2 = {
  name: 'obj2',
  foo: function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  }
}
var obj3 = {
  name: 'obj3',
  foo: () => {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var obj4 = {
  name: 'obj4',
  foo: () => {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  }
}
obj1.foo()() // 'obj1' 'window'
obj2.foo()() // 'obj2' 'obj2' (外层是普通函数,内层是箭头函数,都打印出obj2)
obj3.foo()() // 'window' 'window'
obj4.foo()() // 'window' 'window'

4. 构造函数对象中一层箭头函数

var name = 'window'
function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  }
  this.foo2 = () => {
    console.log(this.name)
  }
}
var person2 = {
  name: 'person2',
  foo2: () => {
    console.log(this.name)
  }
}
var person1 = new Person('person1')
person1.foo1() // 'person1'
person1.foo2() // 'person1' (箭头函数外层作用域是Person,通过new改变了this指向)
person2.foo2() // 'window'

5. 构造函数对象中函数嵌套

var name = 'window'
function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  } // 两层普通函数
  this.foo2 = function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  } // 普通函数包裹箭头函数
  this.foo3 = () => {
    console.log(this.name)
    return function () {
      console.log(this.name)
    } // 普通函数由最后调用者决定
  } // 箭头函数包裹普通函数
  this.foo4 = () => {
    console.log(this.name)
    return () => {
      console.log(this.name)
    } // 箭头函数都由外层作用域决定,也就是person1
  } // 箭头函数嵌套
}
var person1 = new Person('person1')
person1.foo1()() // 'person1' 'window'
person1.foo2()() // 'person1' 'person1'
person1.foo3()() // 'person1' 'window'
person1.foo4()() // 'person1' 'person1'

6. 箭头函数结合.call

箭头函数的this无法通过bind、call、apply直接修改,但是可以通过改变作用域中this的指向来间接修改。

var name = 'window'
var obj1 = {
  name: 'obj1',
  foo1: function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    } // 箭头函数和外层作用域中this相同
  },
  foo2: () => {
    console.log(this.name)
    return function () {
      console.log(this.name)
    } // 返回的普通函数可以通过.call修改this指向
  } // 第一层箭头函数无法通过.call改变this,对象不具有作用域,所以this指向全局作用域window
}
var obj2 = {
  name: 'obj2'
}
obj1.foo1.call(obj2)() // 'obj2' 'obj2'
obj1.foo1().call(obj2) // 'obj1' 'obj1' (使用了.call想要修改this的指向,但是并不能成功,因此.call(obj2)对箭头函数无效,还是打印出obj1)
obj1.foo2.call(obj2)() // 'window' 'window'
obj1.foo2().call(obj2) // 'window' 'obj2'

小总结

来总结一下箭头函数需要注意的点吧:

  • 它里面的this是由外层作用域来决定的,且指向函数定义时的this而非执行时
  • 字面量创建的对象,作用域是window,如果里面有箭头函数属性的话,this指向的是window
  • 构造函数创建的对象,作用域是可以理解为是这个构造函数,且这个构造函数的this是指向新建的对象的,因此this指向这个对象。
  • 箭头函数的this是无法通过bind、call、apply直接修改,但是可以通过改变作用域中this的指向来间接修改。

优点

  • 箭头函数写代码拥有更加简洁的语法(当然也有人认为这是缺点)
  • this由外层作用域决定,所以在某些场合我们不需要写类似const that = this这样的代码

五、一些手写题

1. 手写一个new

function Person (name) {
    this.name = name
}
Person.prototype.eat = function () {
    console.log('Eating')
}
var curry = new Person('Curry')
console.log(curry)
curry.eat()

2. 手写一个call

// ES6
Function.prototype.call3 = function(context) {
    context = (context !== null && context !== undefined) ? Object(context) : window
    var fn = Symbol()
    context[fn] = this
    let args = [...arguments].slice(1)
    let result = context[fn](...args)
    delete context[fn]
    return result
}