js中有关this指向问题

394 阅读5分钟

概念(this是什么)

this是在函数在被调用的时候才发生的绑定,即this具体指向什么,在于函数是怎么调用的,而非函数自身,也非函数的词法作用域

this的四种绑定(优先级从低到高)

  • 默认绑定
  • 隐士绑定
  • 显示绑定
  • new 绑定

默认绑定

没有其他绑定规则时存在的默认规则即默认绑定

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

因为foo是直接调用的,没有其他绑定规则,将foo绑定在了全局对象this上,即this.a 就是全局变量中的a

注意:在严格模式下(strict mode),全局对象将无法使用默认绑定,即执行会报undefined的错误
function foo() { 
    "use strict";
   console.log( this.a );
}

var a = 2; 
foo(); // Uncaught TypeError: Cannot read property 'a' of undefined

隐式绑定

除了直接调用函数之外,函数的调用是在某个对象上触发的,即调用位置存在上下文对象

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

在这段代码中foo 被当做引用属性,添加到obj对象上

获取obj属性obj.foo -> 根据引用找到foo函数,执行调用

所以在这里对foo的调用存在上下文对象obj,进行了隐式绑定(即将this绑定在了obj上),因此打印出来的结果为3.

隐式调用链

function foo(){
    console.log(this.a)
}
var a = 2;
var obj1 ={
    a:4,
    foo:foo
}
var obj2 = {
    a:3,
    obj1:obj1
}

obj2.obj1.foo()  // 4

先获取obj2.obj1 -> 通过引用获取到obj1对象,再访问 obj1.foo -> 最后执行foo函数调用

这里调用链不只一层,存在obj1、obj2两个对象,那么隐式绑定具体会绑哪个对象。这里原则是获取最后一层调用的上下文对象,即obj1,所以结果显然是4(obj1.a)。

隐式丢失(函数别名)

注意:这里存在一个陷阱,大家在分析调用过程时,要特别小心
function foo(){
    console.log(this.a)
}

var a = 2;
var obj = {
    a:3,
    foo:foo
}
var bar = obj.foo;
bar();  //2

为什么会这样,obj.foo 赋值给bar,那调用bar()为什么没有触发隐式绑定,使用的是默认绑定呢。

在这里,obj.foo是引用属性,赋值给bar 的实际是foo函数(bar指向foo函数自身)

那么,实际的调用关系是:通过bar找到foo函数,进行调用。整个调用过程并没有obj的参数,所以是默认绑定,全局属性a。

#####隐式丢失(回调函数)

function foo(){
    console.log(this.a)
}
var a = 2;
var obj = {
    a:3;
    foo:foo
}
setTimeout(obj.foo,100) // 2


同样的道理,虽然参传是obj.foo,因为是引用关系,所以传参实际上传的就是foo对象本身的引用。

对于setTimeout的调用,还是 setTimeout -> 获取参数中foo的引用参数 -> 执行 foo 函数,中间没有obj的参与。这里依旧进行的是默认绑定。

显式绑定

相对隐式绑定,this的值会在调用过程中发生变化,但我们的需求是绑定指定的对象,这时就用到了显示绑定

显示绑定主要是通过改变对象的prototype关联对象

可以通过这两个方法call(…)或apply(…)来实现(大多数函数及自己创建的函数默认都提供这两个方法)。

call与apply是同样的作用,区别只是其他参数的设置上
function foo (){
    console.log(this.a)
}

var a = 2;

var obj1 = {
    a:3,
}
var obj2 = {
    a:4
}
foo.call(obj1) // 3
foo.call(obj2) // 4


这里因为显示的申明了要绑定的对象,所以this就被绑定到了obj上,打印的结果自然就是obj1.a 和obj2.a。

硬绑定
function foo(){
    console.log(this.a)
}

var a = 2;

var obj1 = {
    a:3
}
var obj2 = {
    a:4
}
var bar = function(){
    foo.call(obj1)
}
setTimeout(bar,100) // 3

bar.call(obj2) // 3

这里需要注意下,虽然bar被显示绑定到obj2上,对于bar,function(){…} 中的this确实被绑定到了obj2

而foo因为通过foo.call( obj1 )已经显示绑定了obj1,所以在foo函数内,this指向的是obj1,不会因为bar函数内指向obj2而改变自身。所以打印的是obj1.a(即3)。

new绑定

js中的new操作符,和其他语言中(如JAVA)的new机制是不一样的。js中,它就是一个普通函数调用,只是被new修饰了而已

使用new来调用函数,会自动执行如下操作 1.如果函数没有返回其他对象,那么new表达式的函数调用就会自动返回这个新对象

function foo(a){
    this.a = a;
}
var a = 2;

var bar1 = new foo(3);

console.log(bar1.a); //3

var bar2 = new foo(4);

console.log(bar2.a) //4


因为每次调用生成的是全新的对象,该对象又会自动绑定到this上,所以答案显而易见。

绑定规则优先级

上面也说过,这里在重复一下。优先级是这样的,以按照下面的顺序来进行判断:

数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。

数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是 指定的对象。

数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。

果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到 全局对象。

规则例外

在显示绑定中,对于null和undefined的绑定将不会生效

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


这种情况主要是用在不关心this的具体绑定对象(用来忽略this),而传入null实际上会进行默认绑定,导致函数中可能会使用到全局变量,与预期不符。

所以对于要忽略this的情况,可以传入一个空对象ø,该对象通过Object.create(null)创建。

这里不用{}的原因是,ø是真正意义上的空对象,它不创建Object.prototype委托,{}和普通对象一样,有原型链委托关系。

箭头函数


var foo = ()=>{
    console.log(this.a)
}
var a = 2;
var obj = {
    a:3,
    foo:foo
}
obj.foo() // 2
foo.call(obj) // 箭头函数中绑定不会生效

箭头函数的this绑定只取决于外层(函数或全局)的作用域,对于前面的4种绑定规则是不会生效的。

它也是作为this机制的一种替换,解决之前this绑定过程各种规则带来的复杂性。