看看是否真的懂了this的指向问题

178 阅读7分钟

假设你对this指向已经有了一定的了解,但是遇到this的问题还会不清楚具体指向,那么可以看看这篇文章,里面每道题都会做了具体解释

前置知识

1.this指向分别有

  • 默认绑定 fn()。此时this指向全局对象

  • 隐式绑定 obj.fn()。此时是通过obj对象来调用fn,因此fn里的this指向obj

  • 显示绑定。显示绑定包括硬绑定和api调用的上下文

    • 硬绑定。通过fn.call(obj)、fn.apply(obj)、fn.bind(obj)将fn里的this指向obj
    • api调用的上下文。[1,2].forEach(foo,obj)此时foo里的this指向obj
  • new绑定。不像Java,JavaScript中没有构造函数,任何函数都可以执行new关键字,因此JavaScript中只有构造函数调用,即任何函数都可以使用new。先了解一下new fn()干了什么

    1. 创造一个全新的对象
    2. 这个新对象会执行原型链接
    3. 这个新对象会绑定到函数调用的this
    4. 如果fn返回的是基础数据类型或者不返回,那么new fn得到的就是我们前三步创建的对象。如果函数返回的是引用数据类型,那么就返回new fn()得到的就是fn函数返回的对象,即new操作符无效了
  • 软绑定。通过硬绑定之后就无法通过隐式绑定或者显示绑定修改this指向。可以给默认绑定指定一个全局对象和underfined以外的值来实现硬绑定的效果。同时保留隐式绑定和显示绑定修改this的能力。软绑定的具体实现有很多种,本文不给予讨论。

  1. 优先级
  • 什么是优先级?即当函数调用时可以应用以上多条规则时,优先使用哪一条
    • new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
  1. 判断this指向的法则
    不要在意函数定义在哪里,也不要在意函数通过传值给了哪个变量,只需要关注函数的真实调用位置找到函数的真实调用位置,然后应用以上的法则

  2. 如果函数作为某个对象的值,那么这个对象只是存储了这个函数的地址,在进行赋值传递时也只是传递这个地址

题目演练+解析

var a = 'ikun'
let b = 'jntm'
function foo() {
	console.log(this.a) // ikun
	console.log(this.b) // undefined
}

foo()//这里是foo的调用位置,符合默认绑定,因此this指向全局对象

这里的this指向全局对象毫无疑问。this.b输出underfined的原因是:通过var声明的全局变量,会被绑定到全局对象中,比如浏览器运运行的话会绑定到window。因此this.a相当于window.a,自然可以输出。但是b是通过let声明的,let声明的变量属于当前代码块,但是当前代码块并不属于全局对象,window.b自然是underfined

如果这段代码在node环境下执行,this.a也是underfined。因为在node环境下var声明的变量不会被绑定到全局对象中。

var a = 'ikun'

const obj ={
	a: 'jntm',
	foo: function() {
        console.log(this.a) // jntm
		function fn() {
			console.log(this.a) // ikun
		}
             return fn()
	}
}

obj.foo()

执行obj.foo()时,这是一个隐式调用,因此foo函数里的this指向obj。foo返回的fn(),这里是默认调用,this执行window。

function fn(){
	console.log(this.a)
}

const obj1 = {
	a: 'ikun',
	foo: fn
}

const obj2 = {
	a: 'jntm',
	o: obj1
}

obj2.o.foo() // ikun

隐式绑定,在多个对象隐式绑定时,只有最后一层,也就是o会影响调用位置,即foo的this指向离它最近的o
4.

var a = 'ikun'

const obj = {
	a: 'jntm',
	foo: function() {
		(function() {
			console.log(this) // window
			console.log(this.a) // ikun
		})()
	}
}

obj.foo()

执行obj.foo()之后会执行一个立刻执行函数,立刻执行函数this只会指向window。可以理解成一个没有名字的函数默认调用了

var bar = 'ikun'
const foo = {
	bar: 'jntm',
	fn: function() {
		console.log(this)
		console.log(this.bar)
	}
}
var fn1 = foo.fn
fn1()

运用法则!找到函数的真实执行位置,var fn1 = foo.fn这一步实际上只是将foo这个对象里的fn属性传给了fn1,这里传递的只是一个地址,并没有发生调用。真正调用的是下一行fn1(),这是一个默认绑定,所以this指向全局,输出的是ikun

const obj1 = {
	text: 1,
	fn: function(){
		return this.text
	}
}

const obj2 = {
	text: 2,
	fn: function(){
		return obj1.fn()
	}
}

const obj3 = {
	text: 3,
	fn: function(){
		var fn1 = obj1.fn
		return fn1()
	}
}

console.log(obj1.fn())
console.log(obj2.fn())
console.log(obj3.fn())

同样的道理,找到真实调用位置。

  • obj1.fn()很明显是隐式绑定,fn的this指向obj1,输出1
  • obj2.fn()是隐式绑定,此时obj2里的fn的this指向obj2。这里的fn返回obj1.fn(),这个obj.fn()是真正的调用位置,用的是隐式绑定,obj的fn指向fn,输出1
  • obj3.fn()是隐式绑定,因此此时obj3.fn的this指向obj3.执行到var fn1 = obj1.fn时,仅仅是将obj1.fn的函数地址传递给了fn1,并没有发生函数的调用,因此此时this指向还不明确,下一行执行fn1()时,才是函数的真实调用位置,此时是默认绑定,this指向全局,输出underfined
const obj1 = {
	text: 1,
	fn: function(){
		return this.text
	}
}

const obj2 = {
	text: 2,
	fn: obj1.fn
}

obj2.fn()

还是找到函数的真实调用位置。此时obj.fn的值只是存了obj1.fn这个函数的地址,不要被迷惑到了。实际调用还是obj2.fn(),因此fn的this还是指向obj2,输出2

var a = 'ikun'
function foo() {
	console.log(this.a)
}

function foo2() {
        console.log(this) //这里的this指向obj
	foo() // 这里是默认绑定
}

const obj = {
	a: 1,
	foo3: foo2
}

obj.foo3() // ikun

obj.foo3()是隐式绑定,此时执行的真正内容是foo2,foo2的this指向obj。foo2执行到foo()时,这是默认绑定,this指向全局变量,因此这里输出ikun

const obj = {
	name: 'ikun',
	foo: function() {
		console.log(this.name)
	}
}

const obj2 = {
	name: 'tkl'
}
obj.foo.call(obj2)

真实调用的语句很明显是obj.foo.call(obj2),此时既有隐式绑定,又有显示绑定,则用上优先级的知识,foo函数的this指向显示绑定的obj2,输出tkl

var a= 'ikun'
function fn() {
	console.log(this.a)
}

fn.call(null) // ikun

显示绑定,在非严格模式下,显示绑定的参数为null或者undefined时,会替换成全局对象。原始值也会被转化为对象(new String将字符串转化为对象)

var obj = {
	name: 'ikun',
	foo: function() {
		console.log(this.name)
	}
}

var obj2 = {
	name: 'tkl'
}

var obj3 = {
	name: 'hcy'
}
obj.foo.bind(obj2).bind(obj3)()

有限显示绑定,显示绑定第一个之后,再显示绑定就不生效了,因此this指向obj2,输出tkl

function Foo() {
	this.name = 'ikun'
	this.age = 66
	const obj = {
		name: 'tkl'
	}
	return obj
}

const foo = new Foo()
console.log(foo.name) // tkl
console.log(foo.age) // undefined

这里foo使用了new绑定,返回obj对象类型,则new绑定失效,此时foo里只有name属性

function Foo() {
	this.name = 'ikun'
	this.age = 66
	const obj = {
		name: 'tkl'
	}
	return name
}

const foo = new Foo()
console.log(foo.name) // ikun
console.log(foo.age) // 66

new绑定返回name,new绑定生效foo含有name和age属性

  1. 箭头函数
const obj = {
	name: 'ikun',
	foo: () => {
		console.log(this)
	}
}

obj.foo()

箭头函数不适用以上绑定规则,他的this取决于外面一层作用域,执行foo函数输出this,foo的上一层作用域是全局作用域,自然指向window(对象obj没有作用域)

var obj = {
   say: function() {
     var f1 = () =>  {
       console.log("1111", this);
     }
     f1();
   },
   pro: {
     getPro:() =>  {
        console.log(this);
     }
   }
}
var o = obj.say;
o(); // window
obj.say(); // obj
obj.pro.getPro(); // window

箭头函数只需要判断外层作用域的this指向即可。

  • var o = obj.say;这里是将obj.say这个函数存储的地址赋值给了o,然后执行o,相当于默认绑定,因此o这个函数里的this指向window,当o继续执行里面的f1时,箭头函数的this是外面一层作用域的this,即window
  • obj.say()这里是调用位置,因此是这里的say方法的this指向obj,所以执行到f1时取外面一层作用域的this指向,即obj
  • obj.pro.getPro()这个隐式绑定,getPro这个方法指向最近的对象,即pro,但是这是箭头函数,箭头函数没有this,而是用外面一层作用域的this,外面一层作用域自然是window
window.number = 2
var obj = {
    number: 3,
    db1: (function () {
        console.log(this)
        this.number *= 4
        return function () {
            console.log(this)
            this.number *= 5
        }
    })()
}
var db1 = obj.db1
db1()
obj.db1()
console.log(obj.number)     // 15
console.log(window.number)  // 40

定义时,obj.db1的属性值是IIFE函数,即js引擎在解析obj时,就已经执行了这段函数并返回一个函数 function () {             console.log(this)             this.number *= 5         }。而作为IIFE函数的this自然指向window,因此此时window.number * 4 = 8。

var db1 = obj.db1这里是将function () {             console.log(this)             this.number *= 5         }的地址值传递给db1,但这里并没有执行,函数是在执行的时候才会有this指向,真正执行的代码是下一行db1()。db1()很明显就是一个默认绑定,那么this肯定指向window,因此window.number * 8 = 40

obj.db1()明显是隐式绑定,this自然指向obj,所以obj.number = 15

总结

this指向问题 = 真正调用位置 + 优先级规则